mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(plugins): fail closed when plugin runtime is unavailable during registration (#130597)
* fix(plugins): fail closed when plugin runtime is unavailable Core fabricated an empty object as `PluginRuntime` for the `cli-metadata` and `setup-only` registration passes, so any plugin touching `api.runtime` during `register()` died with an opaque `TypeError: Cannot read properties of undefined (reading 'openSyncKeyedStore')` that named neither the contract nor the mode. Replace those fabrications with one shared fail-closed runtime that throws a named, actionable error identifying the plugin and registration mode. Symbol reads stay inert so inspection and reflection cannot trigger the guard. Also resolve the lightweight `cli-metadata` entry beside the resolved plugin entry, not only at the package root. Published plugin packages emit it at `dist/cli-metadata.js`, so the probe always missed and the loader fell back to executing the full heavy entry for CLI metadata collection. Documents which registration modes expose a live `api.runtime`; the SDK reference previously claimed it was injected into every plugin. * fix(ui): restore sidebar lazy import boundaries Remove the redundant viewer-facepile dynamic import: sidebar consumers already register that element through the static graph. Import pet data and sprite helpers from their owning leaf modules, delete the registration module's re-export barrel, and make the element itself the lobster-pet.runtime.ts lazy boundary. Keep shared sprite styles with the look renderer so standalone pages do not need element registration. Regenerate the measured boot inventory with the canonical browser probe. Use Lit's typed lifecycle map to remove the obsolete assertion baseline entry and keep the now-private facepile variant type local. The full build has no INEFFECTIVE_DYNAMIC_IMPORT warnings. Relevant UI coverage passes (503 tests plus 5 Chromium E2E tests); the unchanged plugin runtime regression suites pass all 58 tests. The broad plugin suite still reports schema-version, doctor-closure, hook-process, and catalog failures.
This commit is contained in:
committed by
GitHub
parent
1cb914d6bf
commit
bbd66c475e
@@ -3561,7 +3561,7 @@ src/plugins/lazy-service-module.ts 1
|
||||
src/plugins/legacy-session-surfaces.ts 3
|
||||
src/plugins/loader-channel-runtime.ts 1
|
||||
src/plugins/loader-channel-setup.ts 17
|
||||
src/plugins/loader-cli-registry.ts 4
|
||||
src/plugins/loader-cli-registry.ts 2
|
||||
src/plugins/loader-discovery.ts 1
|
||||
src/plugins/loader-load-context.ts 5
|
||||
src/plugins/loader-module-runtime.ts 13
|
||||
@@ -3641,7 +3641,7 @@ src/plugins/sdk-alias.ts 3
|
||||
src/plugins/services.ts 1
|
||||
src/plugins/session-catalog-history-import.ts 2
|
||||
src/plugins/setup-registry-loader-state.ts 1
|
||||
src/plugins/setup-registry.ts 13
|
||||
src/plugins/setup-registry.ts 12
|
||||
src/plugins/slots.ts 3
|
||||
src/plugins/toggle-config.ts 3
|
||||
src/plugins/tool-descriptor-cache.ts 4
|
||||
@@ -3999,7 +3999,6 @@ ui/src/components/github-link-hovercard.runtime.ts 2
|
||||
ui/src/components/hub-tabs.ts 2
|
||||
ui/src/components/input-dialog.ts 1
|
||||
ui/src/components/lobster-dex.ts 2
|
||||
ui/src/components/lobster-pet.ts 1
|
||||
ui/src/components/login-gate.ts 3
|
||||
ui/src/components/markdown-assistant-transcript.ts 1
|
||||
ui/src/components/markdown-code-blocks.ts 1
|
||||
|
||||
@@ -391,14 +391,16 @@ limited to config-only routes or methods required by that setup flow.
|
||||
|
||||
`api.registrationMode` tells your plugin how it was loaded:
|
||||
|
||||
| Mode | When | What to register |
|
||||
| ------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `"full"` | Normal gateway startup | Everything |
|
||||
| `"discovery"` | Read-only capability discovery | Channel registration, static CLI descriptors, and inert providers; skip sockets, workers, clients, and services |
|
||||
| `"tool-discovery"` | Scoped load to list or run specific plugins' tools | Capability/tool registration only; no channel activation |
|
||||
| `"setup-only"` | Disabled/unconfigured channel | Channel registration only |
|
||||
| `"setup-runtime"` | Setup flow with runtime available | Channel registration plus only the lightweight runtime needed during setup |
|
||||
| `"cli-metadata"` | Root help / CLI metadata capture | CLI descriptors only |
|
||||
| Mode | When | Runtime | What to register |
|
||||
| ------------------ | -------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `"full"` | Normal gateway startup | Live | Everything |
|
||||
| `"discovery"` | Read-only capability discovery | Live | Channel registration, static CLI descriptors, and inert providers; skip sockets, workers, clients, and services |
|
||||
| `"tool-discovery"` | Scoped load to list or run specific plugins' tools | Live | Capability/tool registration only; no channel activation |
|
||||
| `"setup-only"` | Disabled/unconfigured channel | Unavailable | Channel registration only |
|
||||
| `"setup-runtime"` | Setup flow with runtime available | Live | Channel registration plus only the lightweight runtime needed during setup |
|
||||
| `"cli-metadata"` | Root help / CLI metadata capture | Unavailable | CLI descriptors only |
|
||||
|
||||
In `"cli-metadata"` and `"setup-only"` modes, accessing a runtime capability throws an error naming the plugin and mode. Defer runtime access out of `register()` or declare root commands in the manifest's `cliCommands` so CLI metadata can be collected without executing the plugin.
|
||||
|
||||
`defineChannelPluginEntry` handles this split automatically. If you use
|
||||
`definePluginEntry` directly for a channel, check mode yourself and remember
|
||||
|
||||
@@ -9,7 +9,7 @@ read_when:
|
||||
- You are implementing model-picker persistence in a channel plugin
|
||||
---
|
||||
|
||||
Reference for the `api.runtime` object injected into every plugin during registration. Use these helpers instead of importing host internals directly.
|
||||
Reference for the live `api.runtime` object available during `"full"`, `"discovery"`, `"tool-discovery"`, and `"setup-runtime"` registration. During `"cli-metadata"` and `"setup-only"` registration, runtime capabilities are intentionally unavailable: accessing one throws an error naming the plugin and mode. Defer runtime access out of `register()` or, for root CLI commands, declare `cliCommands` in the plugin manifest. Use runtime helpers instead of importing host internals directly.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Channel plugins" href="/plugins/sdk-channel-plugins">
|
||||
|
||||
@@ -186,6 +186,28 @@ const noopRegisterMemoryCorpusSupplement: OpenClawPluginApi["registerMemoryCorpu
|
||||
() => {};
|
||||
const noopOn: OpenClawPluginApi["on"] = () => {};
|
||||
|
||||
export function createUnavailableRuntime(
|
||||
registrationMode: "cli-metadata" | "setup-only",
|
||||
pluginId?: string,
|
||||
): PluginRuntime {
|
||||
const owner = pluginId ? `Plugin "${pluginId}"` : "Plugin";
|
||||
const guidance =
|
||||
registrationMode === "cli-metadata"
|
||||
? "Declare root commands in the manifest's cliCommands or defer runtime access out of register()."
|
||||
: "Defer runtime access out of register().";
|
||||
// SAFETY: String capabilities fail closed; symbols stay inert so reflection cannot trigger runtime errors.
|
||||
return new Proxy(Object.create(null) as PluginRuntime, {
|
||||
get(_target, property) {
|
||||
if (typeof property === "symbol") {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error(
|
||||
`${owner} runtime is intentionally unavailable during "${registrationMode}" registration. ${guidance}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi {
|
||||
const handlers = params.handlers ?? {};
|
||||
const registerCli = handlers.registerCli ?? noopRegisterCli;
|
||||
|
||||
@@ -4,6 +4,20 @@ import { capturePluginRegistration } from "./captured-registration.js";
|
||||
import type { AnyAgentTool, OpenClawPluginApi } from "./types.js";
|
||||
|
||||
describe("captured plugin registration", () => {
|
||||
it("rejects runtime access while capturing CLI metadata without activating the real runtime", () => {
|
||||
expect(() =>
|
||||
capturePluginRegistration({
|
||||
id: "captured-cli-plugin",
|
||||
registrationMode: "cli-metadata",
|
||||
register(api) {
|
||||
api.runtime.state.openSyncKeyedStore({ namespace: "example", maxEntries: 1 });
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
'Plugin "captured-cli-plugin" runtime is intentionally unavailable during "cli-metadata" registration.',
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves root machine-output metadata", () => {
|
||||
const machineOutput = ({ stdoutIsTTY }: { stdoutIsTTY: boolean }) => !stdoutIsTTY;
|
||||
const captured = capturePluginRegistration({
|
||||
@@ -156,6 +170,7 @@ describe("captured plugin registration", () => {
|
||||
expect(captured.textTransforms[0]?.input).toHaveLength(1);
|
||||
expect(captured.agentToolResultMiddlewares).toHaveLength(1);
|
||||
expect(captured.agentToolResultMiddlewares[0]?.runtimes).toEqual(["codex"]);
|
||||
expect(captured.api.runtime.version).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("enforces captured middleware runtime and tool scopes", async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
agentToolResultMiddlewareRegistrationCoversTool,
|
||||
normalizeAgentToolResultMiddlewareRuntimes,
|
||||
} from "./agent-tool-result-middleware.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { buildPluginApi, createUnavailableRuntime } from "./api-builder.js";
|
||||
import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js";
|
||||
import type { EmbeddingProviderAdapter } from "./embedding-providers.js";
|
||||
import type {
|
||||
@@ -137,6 +137,7 @@ export function createCapturedPluginRegistration(params?: {
|
||||
const pluginId = params?.id ?? "captured-plugin-registration";
|
||||
const pluginName = params?.name ?? "Captured Plugin Registration";
|
||||
const pluginSource = params?.source ?? "captured-plugin-registration";
|
||||
const registrationMode = params?.registrationMode ?? "full";
|
||||
const noopLogger = {
|
||||
info() {},
|
||||
warn() {},
|
||||
@@ -180,9 +181,12 @@ export function createCapturedPluginRegistration(params?: {
|
||||
id: pluginId,
|
||||
name: pluginName,
|
||||
source: pluginSource,
|
||||
registrationMode: params?.registrationMode ?? "full",
|
||||
registrationMode,
|
||||
config: params?.config ?? ({} as OpenClawConfig),
|
||||
runtime: createPluginRuntime(),
|
||||
runtime:
|
||||
registrationMode === "cli-metadata" || registrationMode === "setup-only"
|
||||
? createUnavailableRuntime(registrationMode, pluginId)
|
||||
: createPluginRuntime(),
|
||||
logger: noopLogger,
|
||||
resolvePath: (input) => input,
|
||||
handlers: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
import { describeRootFileOpenFailure, openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { buildPluginApi, createUnavailableRuntime } from "./api-builder.js";
|
||||
import {
|
||||
resolveEffectiveEnableState,
|
||||
resolveEffectivePluginActivationState,
|
||||
@@ -41,17 +41,9 @@ import { withProfile } from "./plugin-load-profile.js";
|
||||
import { normalizePluginPolicyId } from "./plugin-policy-id.js";
|
||||
import { createPluginIdScopeSet } from "./plugin-scope.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { hasKind, kindsEqual } from "./slots.js";
|
||||
import type { OpenClawPluginModule } from "./types.js";
|
||||
|
||||
const CLI_METADATA_ENTRY_BASENAMES = [
|
||||
"cli-metadata.ts",
|
||||
"cli-metadata.js",
|
||||
"cli-metadata.mjs",
|
||||
"cli-metadata.cjs",
|
||||
] as const;
|
||||
|
||||
export async function loadOpenClawPluginCliRegistry(
|
||||
options: PluginLoadOptions = {},
|
||||
): Promise<PluginRegistry> {
|
||||
@@ -64,7 +56,7 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
});
|
||||
const { registry, registerCli, rollbackPluginGlobalSideEffects } = createPluginRegistry({
|
||||
logger,
|
||||
runtime: {} as PluginRuntime,
|
||||
runtime: createUnavailableRuntime("cli-metadata"),
|
||||
coreGatewayHandlers: options.coreGatewayHandlers as Record<string, GatewayRequestHandler>,
|
||||
...(options.coreGatewayMethodNames !== undefined && {
|
||||
coreGatewayMethodNames: options.coreGatewayMethodNames,
|
||||
@@ -198,7 +190,7 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
pushPluginLoadError(`invalid config: ${validatedConfig.error.join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
|
||||
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir, candidate.source);
|
||||
const sourceForCliMetadata =
|
||||
candidate.origin === "bundled"
|
||||
? cliMetadataSource
|
||||
@@ -322,7 +314,7 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
registrationMode: "cli-metadata",
|
||||
config: context.cfg,
|
||||
pluginConfig: validatedConfig.value,
|
||||
runtime: {} as PluginRuntime,
|
||||
runtime: createUnavailableRuntime("cli-metadata", record.id),
|
||||
logger,
|
||||
resolvePath: (input) => resolveUserPath(input),
|
||||
handlers: {
|
||||
@@ -354,11 +346,13 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
return registry;
|
||||
}
|
||||
|
||||
function resolveCliMetadataEntrySource(rootDir: string): string | null {
|
||||
for (const basename of CLI_METADATA_ENTRY_BASENAMES) {
|
||||
const candidate = path.join(rootDir, basename);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
function resolveCliMetadataEntrySource(rootDir: string, source: string): string | null {
|
||||
for (const directory of new Set([rootDir, path.dirname(source)])) {
|
||||
for (const extension of [".ts", ".js", ".mjs", ".cjs"]) {
|
||||
const candidate = path.join(directory, `cli-metadata${extension}`);
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -79,6 +79,82 @@ describe("plugin loader CLI metadata", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects runtime access during CLI metadata registration with actionable plugin guidance", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "runtime-dependent",
|
||||
filename: "runtime-dependent.cjs",
|
||||
body: `module.exports = {
|
||||
id: "runtime-dependent",
|
||||
register(api) {
|
||||
api.runtime.state.openSyncKeyedStore({ namespace: "example", maxEntries: 1 });
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: [plugin.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pluginError = registry.plugins.find((entry) => entry.id === plugin.id)?.error;
|
||||
expect(pluginError).toContain('Plugin "runtime-dependent"');
|
||||
expect(pluginError).toContain('"cli-metadata" registration');
|
||||
expect(pluginError).toContain("runtime is intentionally unavailable");
|
||||
expect(pluginError).toContain("cliCommands");
|
||||
expect(pluginError).toContain("defer runtime access out of register()");
|
||||
expect(pluginError).not.toContain("Cannot read properties of undefined");
|
||||
});
|
||||
|
||||
it("loads packaged CLI metadata beside the resolved dist entry without evaluating the heavy entry", async () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makePluginLoaderTempDir();
|
||||
const distDir = path.join(pluginDir, "dist");
|
||||
const heavyMarker = path.join(pluginDir, "heavy-loaded.txt");
|
||||
fs.mkdirSync(distDir);
|
||||
const plugin = writePlugin({
|
||||
id: "packaged-cli-metadata",
|
||||
dir: pluginDir,
|
||||
filename: "dist/index.js",
|
||||
body: `require("node:fs").writeFileSync(${JSON.stringify(heavyMarker)}, "loaded");
|
||||
module.exports = { id: "packaged-cli-metadata", register() {} };`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "packaged-cli-metadata",
|
||||
openclaw: { extensions: ["./dist/index.js"] },
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(distDir, "cli-metadata.js"),
|
||||
`module.exports = {
|
||||
id: "packaged-cli-metadata",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [{ name: "packaged-light", description: "Light entry", hasSubcommands: false }],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
allow: [plugin.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(heavyMarker)).toBe(false);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("packaged-light");
|
||||
});
|
||||
|
||||
it("suppresses trust warning logs during CLI metadata loads", async () => {
|
||||
useNoBundledPlugins();
|
||||
const stateDir = makePluginLoaderTempDir();
|
||||
|
||||
@@ -983,6 +983,29 @@ describe("setup-registry module loader", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports unavailable setup runtime access with the plugin id and registration mode", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
writeSetupApiStub(pluginRoot);
|
||||
mockSinglePlugin({ id: "runtime-dependent-setup", rootDir: pluginRoot });
|
||||
mocks.createJiti.mockImplementation(() => () => ({
|
||||
default: {
|
||||
register(api: import("./types.js").OpenClawPluginApi) {
|
||||
api.runtime.state.openSyncKeyedStore({ namespace: "example", maxEntries: 1 });
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(resolvePluginSetupRegistry({ env: {} }).diagnostics).toMatchObject([
|
||||
{
|
||||
pluginId: "runtime-dependent-setup",
|
||||
code: "setup-registration-failed",
|
||||
message: expect.stringContaining(
|
||||
'Plugin "runtime-dependent-setup" runtime is intentionally unavailable during "setup-only" registration.',
|
||||
),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("publishes each plugin setup registration atomically on synchronous success", () => {
|
||||
const throwingRoot = makeTempDir();
|
||||
const healthyRoot = makeTempDir();
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@openclaw/normalization-core/string-normalization";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { buildPluginApi } from "./api-builder.js";
|
||||
import { buildPluginApi, createUnavailableRuntime } from "./api-builder.js";
|
||||
import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
||||
import { getCurrentPluginMetadataSnapshotState } from "./current-plugin-metadata-state.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from "./plugin-module-loader-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { resolvePreferredBundledRootArtifact } from "./plugin-runtime-artifact-selection.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
import { listSetupCliBackendIds, listSetupProviderIds } from "./setup-descriptors.js";
|
||||
import { pluginSetupRegistryLoaderState } from "./setup-registry-loader-state.js";
|
||||
import type {
|
||||
@@ -94,7 +93,6 @@ type SetupAutoEnableReason = {
|
||||
|
||||
type PluginApiBuildParams = Parameters<typeof buildPluginApi>[0];
|
||||
|
||||
const EMPTY_RUNTIME = {} as PluginRuntime;
|
||||
const NOOP_LOGGER: PluginLogger = {
|
||||
info() {},
|
||||
warn() {},
|
||||
@@ -312,7 +310,7 @@ function buildSetupPluginApi(params: {
|
||||
rootDir: params.record.rootDir,
|
||||
registrationMode: "setup-only",
|
||||
config: {} as OpenClawConfig,
|
||||
runtime: EMPTY_RUNTIME,
|
||||
runtime: createUnavailableRuntime("setup-only", params.record.id),
|
||||
logger: NOOP_LOGGER,
|
||||
resolvePath: (input) => input,
|
||||
handlers: params.handlers,
|
||||
|
||||
@@ -1208,7 +1208,7 @@
|
||||
"ui/src/components/lobster-pet-sprites-wild.ts",
|
||||
"ui/src/components/lobster-pet-sprites.ts",
|
||||
"ui/src/components/lobster-pet-traffic.ts",
|
||||
"ui/src/components/lobster-pet.ts",
|
||||
"ui/src/components/lobster-pet.runtime.ts",
|
||||
"ui/src/components/login-gate.ts",
|
||||
"ui/src/components/macos-titlebar-controls.ts",
|
||||
"ui/src/components/markdown-assistant-transcript.ts",
|
||||
@@ -1425,7 +1425,6 @@
|
||||
"ui/src/lib/sessions/session-group-catalog.ts",
|
||||
"ui/src/lib/sessions/session-key.ts",
|
||||
"ui/src/lib/sessions/session-mutations.ts",
|
||||
"ui/src/lib/sessions/session-placement-recovery-migration.runtime.ts",
|
||||
"ui/src/lib/sessions/session-placement-recovery-storage-key.ts",
|
||||
"ui/src/lib/sessions/session-placement-recovery.ts",
|
||||
"ui/src/lib/sessions/session-placement-startup.ts",
|
||||
|
||||
@@ -69,12 +69,7 @@ import { SessionOrganizerController } from "./session-organizer-controller.ts";
|
||||
import { SidebarMenusController } from "./sidebar-menus-controller.ts";
|
||||
// The shared loader retries transient chunk failures online; a deploy-pruned
|
||||
// chunk still stays off until reload when that retry fails, by design.
|
||||
const sidebarChromeImport = createIdleImport(() =>
|
||||
Promise.all([
|
||||
customElements.get("openclaw-lobster-pet") ? undefined : import("./lobster-pet.ts"),
|
||||
customElements.get("openclaw-viewer-facepile") ? undefined : import("./viewer-facepile.ts"),
|
||||
]),
|
||||
);
|
||||
const lobsterPetImport = createIdleImport(() => import("./lobster-pet.runtime.ts"));
|
||||
|
||||
class AppSidebar extends AppSidebarSessionNavigationElement implements SessionListHost {
|
||||
@state() override sidebarNarrationLines: ReadonlyMap<string, string> = new Map();
|
||||
@@ -333,7 +328,7 @@ class AppSidebar extends AppSidebarSessionNavigationElement implements SessionLi
|
||||
);
|
||||
// The decorative pet's large module stays out of startup and upgrades in place.
|
||||
// Its first visit is at least 15 seconds after load, so idle loading cannot miss one.
|
||||
sidebarChromeImport.schedule();
|
||||
lobsterPetImport.schedule();
|
||||
this.catalogRendererImport.schedule();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "../styles/lobster-pet.css";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { html, nothing, svg } from "lit";
|
||||
import { fnv1aUtf16 } from "../lib/fnv1a.ts";
|
||||
@@ -56,8 +57,6 @@ import {
|
||||
TAIL_FAN,
|
||||
} from "./lobster-pet-sprites.ts";
|
||||
|
||||
export { LOBSTER_PET_PALETTES } from "./lobster-pet-palettes.ts";
|
||||
|
||||
const RETRO_GEOMETRY_PALETTES: ReadonlySet<LobsterPetPaletteId> = new Set(["retro", "goldenretro"]);
|
||||
|
||||
const PALETTE_FRAME_CLASSES: Partial<Record<LobsterPetPaletteId, string>> = {
|
||||
|
||||
@@ -9,12 +9,12 @@ import type {
|
||||
LobsterRunOutcome,
|
||||
} from "./lobster-pet-contract.ts";
|
||||
import {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
lobsterPetName,
|
||||
mulberry32,
|
||||
SPOT_ZONES,
|
||||
} from "./lobster-pet-look.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "./lobster-pet-palettes.ts";
|
||||
|
||||
export { SPOT_ZONES };
|
||||
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { lobsterPetSeed } from "./lobster-pet-contract.ts";
|
||||
import { canonicalLobsterLook, createLobsterPetLook } from "./lobster-pet-look.ts";
|
||||
import { LOBSTER_PALETTE_LORE } from "./lobster-pet-lore.ts";
|
||||
import { LOBSTER_PALETTE_WEIGHTS } from "./lobster-pet-palettes.ts";
|
||||
import {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
createLobsterPetLook,
|
||||
lobsterPetSeed,
|
||||
moonPhaseFraction,
|
||||
} from "./lobster-pet.ts";
|
||||
import { moonPhaseFraction } from "./lobster-pet-moon.ts";
|
||||
import { LOBSTER_PALETTE_WEIGHTS, LOBSTER_PET_PALETTES } from "./lobster-pet-palettes.ts";
|
||||
|
||||
type LobsterPetPaletteId = ReturnType<typeof createLobsterPetLook>["palette"]["id"];
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
// Drawn in the smooth OpenClaw lobster style (see the dreams scene and
|
||||
// icons.lobster). Look and personality are seeded per session + page load so
|
||||
// every new session hatches a slightly different lobster.
|
||||
import "../styles/lobster-pet.css";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { LitElement, nothing } from "lit";
|
||||
import { LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { isLobsterDay } from "../../../src/shared/lobster-day.js";
|
||||
import { patchSettings } from "../app/settings.ts";
|
||||
@@ -21,24 +20,6 @@ import * as lobsterLook from "./lobster-pet-look.ts";
|
||||
import * as plans from "./lobster-pet-plans.ts";
|
||||
import { LobsterLedgeTraffic } from "./lobster-pet-traffic.ts";
|
||||
|
||||
export {
|
||||
lobsterPetSeed,
|
||||
resolveLobsterPetMode,
|
||||
resolveLobsterRunOutcome,
|
||||
type LobsterPetLook,
|
||||
type LobsterPetMode,
|
||||
type LobsterRunOutcome,
|
||||
} from "./lobster-pet-contract.ts";
|
||||
export {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
createLobsterPetLook,
|
||||
lobsterLookStyle,
|
||||
renderLobsterSvg,
|
||||
} from "./lobster-pet-look.ts";
|
||||
export { lobsterPaletteName } from "./lobster-pet-lore.ts";
|
||||
export { moonPhaseFraction } from "./lobster-pet-moon.ts";
|
||||
|
||||
class LobsterPet extends LitElement {
|
||||
override createRenderRoot() {
|
||||
return this;
|
||||
@@ -157,7 +138,7 @@ class LobsterPet extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
override willUpdate(changed: Map<PropertyKey, unknown>) {
|
||||
override willUpdate(changed: PropertyValues<this>) {
|
||||
const seedChanged = this.look === null || changed.has("seed");
|
||||
if (seedChanged) {
|
||||
this.look = lobsterLook.createLobsterPetLook(this.seed);
|
||||
@@ -196,7 +177,7 @@ class LobsterPet extends LitElement {
|
||||
this.outcomePresenceOwner = null;
|
||||
this.trackVigil();
|
||||
} else if (changed.has("mode")) {
|
||||
const previousMode = changed.get("mode") as contract.LobsterPetMode | undefined;
|
||||
const previousMode = changed.get("mode");
|
||||
const finished = previousMode === "busy" && this.mode === "idle";
|
||||
const presenceOwner = finished && this.vigil ? "vigil" : null;
|
||||
this.trackVigil();
|
||||
@@ -4,6 +4,13 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { render } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { getLobsterdex, getLobsterdexEntries } from "./lobster-dex.ts";
|
||||
import { resolveLobsterPetMode, resolveLobsterRunOutcome } from "./lobster-pet-contract.ts";
|
||||
import {
|
||||
canonicalLobsterLook,
|
||||
createLobsterPetLook,
|
||||
renderLobsterSvg,
|
||||
} from "./lobster-pet-look.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "./lobster-pet-palettes.ts";
|
||||
import {
|
||||
LOBSTER_BOTTLE_FORTUNES,
|
||||
pickLobsterEntrance,
|
||||
@@ -11,14 +18,7 @@ import {
|
||||
planLobsterPasser,
|
||||
resolveLobsterLoadIdentity,
|
||||
} from "./lobster-pet-plans.ts";
|
||||
import {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
createLobsterPetLook,
|
||||
renderLobsterSvg,
|
||||
resolveLobsterPetMode,
|
||||
resolveLobsterRunOutcome,
|
||||
} from "./lobster-pet.ts";
|
||||
import "./lobster-pet.runtime.ts";
|
||||
|
||||
type LobsterPetMode = ReturnType<typeof resolveLobsterPetMode>;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ function renderViewerAvatar(view: IdentityAvatarView) {
|
||||
return html`${renderIdentityAvatarImage({ view, fallbackSelector: ".viewer-avatar" })}${fallback}`;
|
||||
}
|
||||
|
||||
export type ViewerAvatarVariant = "session" | "footer" | "profile";
|
||||
type ViewerAvatarVariant = "session" | "footer" | "profile";
|
||||
|
||||
class ViewerAvatar extends OpenClawLightDomContentsElement {
|
||||
@property({ attribute: false }) user: PresenceViewer | null = null;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import "../../styles/lobster-pet.css";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import type { ControlUiBuildInfo } from "../../build-info.ts";
|
||||
import { icons } from "../../components/icons.ts";
|
||||
import {
|
||||
canonicalLobsterLook,
|
||||
LOBSTER_PET_PALETTES,
|
||||
lobsterLookStyle,
|
||||
renderLobsterSvg,
|
||||
} from "../../components/lobster-pet.ts";
|
||||
} from "../../components/lobster-pet-look.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "../../components/lobster-pet-palettes.ts";
|
||||
import {
|
||||
renderSettingsPage,
|
||||
renderSettingsRow,
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// Control UI view renders dreaming screen content.
|
||||
import "../../../styles/lobster-pet.css";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { parseDateStringTimestampMs } from "@openclaw/normalization-core/number-coercion";
|
||||
import { html, nothing } from "lit";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { renderHubTabs } from "../../../components/hub-tabs.ts";
|
||||
import {
|
||||
createLobsterPetLook,
|
||||
lobsterPetSeed,
|
||||
renderLobsterSvg,
|
||||
} from "../../../components/lobster-pet.ts";
|
||||
import { lobsterPetSeed } from "../../../components/lobster-pet-contract.ts";
|
||||
import { createLobsterPetLook, renderLobsterSvg } from "../../../components/lobster-pet-look.ts";
|
||||
import { toSanitizedMarkdownHtml } from "../../../components/markdown.ts";
|
||||
import "../../../components/modal-dialog.ts";
|
||||
import { t } from "../../../i18n/index.ts";
|
||||
|
||||
@@ -9,13 +9,14 @@ import type { DoctorMemoryStatusPayload } from "../../../../src/gateway/server-m
|
||||
// per-load salt, so the palette (and with it sprite geometry like the sleeping
|
||||
// eye peek) varies per test process. Pin a canonical look so pose assertions
|
||||
// stay deterministic.
|
||||
vi.mock("../../components/lobster-pet.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../components/lobster-pet.ts")>();
|
||||
vi.mock("../../components/lobster-pet-look.ts", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../components/lobster-pet-look.ts")>();
|
||||
const { LOBSTER_PET_PALETTES } = await import("../../components/lobster-pet-palettes.ts");
|
||||
return {
|
||||
...actual,
|
||||
createLobsterPetLook: () =>
|
||||
actual.canonicalLobsterLook(
|
||||
expectDefined(actual.LOBSTER_PET_PALETTES[0], "canonical lobster palette"),
|
||||
expectDefined(LOBSTER_PET_PALETTES[0], "canonical lobster palette"),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { html, nothing } from "lit";
|
||||
import type { DoctorMemoryStatusPayload } from "../../../../src/gateway/server-methods/doctor.ts";
|
||||
import { lobsterPetSeed } from "../../components/lobster-pet-contract.ts";
|
||||
import {
|
||||
createLobsterPetLook,
|
||||
lobsterLookStyle,
|
||||
lobsterPetSeed,
|
||||
renderLobsterSvg,
|
||||
} from "../../components/lobster-pet.ts";
|
||||
} from "../../components/lobster-pet-look.ts";
|
||||
import {
|
||||
renderSettingsNavRow,
|
||||
renderSettingsRow,
|
||||
|
||||
@@ -9,14 +9,13 @@ import {
|
||||
import { icons } from "../../components/icons.ts";
|
||||
import { getLobsterdexEntries } from "../../components/lobster-dex.ts";
|
||||
import { previewLobsterChirp } from "../../components/lobster-pet-audio.ts";
|
||||
import { LOBSTER_PALETTE_LORE } from "../../components/lobster-pet-lore.ts";
|
||||
import {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
lobsterLookStyle,
|
||||
lobsterPaletteName,
|
||||
renderLobsterSvg,
|
||||
} from "../../components/lobster-pet.ts";
|
||||
} from "../../components/lobster-pet-look.ts";
|
||||
import { LOBSTER_PALETTE_LORE, lobsterPaletteName } from "../../components/lobster-pet-lore.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "../../components/lobster-pet-palettes.ts";
|
||||
import "../../components/tooltip.ts";
|
||||
import {
|
||||
renderSettingsDefaultState,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { state } from "lit/decorators.js";
|
||||
import { titleForRoute } from "../../app-navigation.ts";
|
||||
import { getLobsterdexEntries } from "../../components/lobster-dex.ts";
|
||||
import type { LobsterPetPaletteId } from "../../components/lobster-pet-contract.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "../../components/lobster-pet.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "../../components/lobster-pet-palettes.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { copyToClipboard } from "../../lib/clipboard.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { icons } from "../../components/icons.ts";
|
||||
import type { LobsterPetPaletteId } from "../../components/lobster-pet-contract.ts";
|
||||
import { LOBSTER_PALETTE_LORE } from "../../components/lobster-pet-lore.ts";
|
||||
import {
|
||||
LOBSTER_PET_PALETTES,
|
||||
canonicalLobsterLook,
|
||||
lobsterLookStyle,
|
||||
lobsterPaletteName,
|
||||
renderLobsterSvg,
|
||||
} from "../../components/lobster-pet.ts";
|
||||
} from "../../components/lobster-pet-look.ts";
|
||||
import { LOBSTER_PALETTE_LORE, lobsterPaletteName } from "../../components/lobster-pet-lore.ts";
|
||||
import { LOBSTER_PET_PALETTES } from "../../components/lobster-pet-palettes.ts";
|
||||
import { i18n, t } from "../../i18n/index.ts";
|
||||
import "../../styles/lobster-pet.css";
|
||||
|
||||
type LobsterdexViewEntry = {
|
||||
firstSeenAt: number | null;
|
||||
|
||||
@@ -83,7 +83,7 @@ openclaw-lobster-pet[data-dex-complete]::after {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- Rare palette variants (weights + lore in lobster-pet.ts) ---- */
|
||||
/* ---- Rare palette variants (lobster-pet-palettes.ts + lobster-pet-lore.ts) ---- */
|
||||
|
||||
/* Ghost/albino: pale translucent shell, icy glints. */
|
||||
.lobster-pet--palette-ghost {
|
||||
@@ -555,7 +555,7 @@ openclaw-lobster-pet[data-dex-complete]::after {
|
||||
}
|
||||
}
|
||||
|
||||
/* Split two-tone: the right body half (drawn in lobster-pet.ts) plus the
|
||||
/* Split two-tone: the right body half (drawn in lobster-pet-look.ts) plus the
|
||||
right claw and antenna wear the second shell color. */
|
||||
.lobster-pet--palette-split {
|
||||
--lob-shell2: #46536b;
|
||||
|
||||
Reference in New Issue
Block a user