diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 61d2285ed34c..a0f0752645ff 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -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 diff --git a/docs/plugins/sdk-entrypoints.md b/docs/plugins/sdk-entrypoints.md index 8345c4957a7e..239d2bd917ef 100644 --- a/docs/plugins/sdk-entrypoints.md +++ b/docs/plugins/sdk-entrypoints.md @@ -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 diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index caf6da97657d..3ef6b7921281 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -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. diff --git a/src/plugins/api-builder.ts b/src/plugins/api-builder.ts index 546e31e384df..ddb195fbfd7d 100644 --- a/src/plugins/api-builder.ts +++ b/src/plugins/api-builder.ts @@ -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; diff --git a/src/plugins/captured-registration.test.ts b/src/plugins/captured-registration.test.ts index f7d93f79e273..5e5dbaaa5715 100644 --- a/src/plugins/captured-registration.test.ts +++ b/src/plugins/captured-registration.test.ts @@ -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 () => { diff --git a/src/plugins/captured-registration.ts b/src/plugins/captured-registration.ts index e462a25de4c9..10ab1853f1e0 100644 --- a/src/plugins/captured-registration.ts +++ b/src/plugins/captured-registration.ts @@ -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: { diff --git a/src/plugins/loader-cli-registry.ts b/src/plugins/loader-cli-registry.ts index ca040c39c317..9a5770565a8b 100644 --- a/src/plugins/loader-cli-registry.ts +++ b/src/plugins/loader-cli-registry.ts @@ -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 { @@ -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, ...(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; diff --git a/src/plugins/loader.cli-metadata.test.ts b/src/plugins/loader.cli-metadata.test.ts index 84dbe535e726..a8535007caf4 100644 --- a/src/plugins/loader.cli-metadata.test.ts +++ b/src/plugins/loader.cli-metadata.test.ts @@ -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(); diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index e7e04fcefb4a..a932b706e6a1 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -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(); diff --git a/src/plugins/setup-registry.ts b/src/plugins/setup-registry.ts index 91d0f1a9b4c6..f96b2af3f8db 100644 --- a/src/plugins/setup-registry.ts +++ b/src/plugins/setup-registry.ts @@ -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[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, diff --git a/ui/config/control-ui-boot-modules.json b/ui/config/control-ui-boot-modules.json index 5369ac2bbcc0..556dc0ae2fb2 100644 --- a/ui/config/control-ui-boot-modules.json +++ b/ui/config/control-ui-boot-modules.json @@ -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", diff --git a/ui/src/components/app-sidebar.ts b/ui/src/components/app-sidebar.ts index 16a2341ba86a..aa61f8ac8103 100644 --- a/ui/src/components/app-sidebar.ts +++ b/ui/src/components/app-sidebar.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 = 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(); } diff --git a/ui/src/components/lobster-pet-look.ts b/ui/src/components/lobster-pet-look.ts index c0ac7041c13e..7b704a6cda9b 100644 --- a/ui/src/components/lobster-pet-look.ts +++ b/ui/src/components/lobster-pet-look.ts @@ -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 = new Set(["retro", "goldenretro"]); const PALETTE_FRAME_CLASSES: Partial> = { diff --git a/ui/src/components/lobster-pet-plans.ts b/ui/src/components/lobster-pet-plans.ts index dfacd7125778..6365a962237e 100644 --- a/ui/src/components/lobster-pet-plans.ts +++ b/ui/src/components/lobster-pet-plans.ts @@ -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 }; diff --git a/ui/src/components/lobster-pet-variants.test.ts b/ui/src/components/lobster-pet-variants.test.ts index 1079db38763f..15917c42d1c0 100644 --- a/ui/src/components/lobster-pet-variants.test.ts +++ b/ui/src/components/lobster-pet-variants.test.ts @@ -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["palette"]["id"]; diff --git a/ui/src/components/lobster-pet.ts b/ui/src/components/lobster-pet.runtime.ts similarity index 97% rename from ui/src/components/lobster-pet.ts rename to ui/src/components/lobster-pet.runtime.ts index 6e23128ad121..a141922c315f 100644 --- a/ui/src/components/lobster-pet.ts +++ b/ui/src/components/lobster-pet.runtime.ts @@ -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) { + override willUpdate(changed: PropertyValues) { 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(); diff --git a/ui/src/components/lobster-pet.test.ts b/ui/src/components/lobster-pet.test.ts index ed1758780b6f..c62ea15e1ece 100644 --- a/ui/src/components/lobster-pet.test.ts +++ b/ui/src/components/lobster-pet.test.ts @@ -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; diff --git a/ui/src/components/viewer-facepile.ts b/ui/src/components/viewer-facepile.ts index 47a179bc77db..398aa81640e4 100644 --- a/ui/src/components/viewer-facepile.ts +++ b/ui/src/components/viewer-facepile.ts @@ -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; diff --git a/ui/src/pages/about/view.ts b/ui/src/pages/about/view.ts index 90c7e1d5e5e0..acfd17c3eca6 100644 --- a/ui/src/pages/about/view.ts +++ b/ui/src/pages/about/view.ts @@ -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, diff --git a/ui/src/pages/agents/memory/view.ts b/ui/src/pages/agents/memory/view.ts index 0210e6a55c47..113db658ab33 100644 --- a/ui/src/pages/agents/memory/view.ts +++ b/ui/src/pages/agents/memory/view.ts @@ -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"; diff --git a/ui/src/pages/config/memory-overview.test.ts b/ui/src/pages/config/memory-overview.test.ts index 921039ec25d0..671356d5c153 100644 --- a/ui/src/pages/config/memory-overview.test.ts +++ b/ui/src/pages/config/memory-overview.test.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(); +vi.mock("../../components/lobster-pet-look.ts", async (importOriginal) => { + const actual = await importOriginal(); + 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"), ), }; }); diff --git a/ui/src/pages/config/memory-overview.ts b/ui/src/pages/config/memory-overview.ts index 836403adb946..133f464053e5 100644 --- a/ui/src/pages/config/memory-overview.ts +++ b/ui/src/pages/config/memory-overview.ts @@ -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, diff --git a/ui/src/pages/config/view-appearance-preferences.ts b/ui/src/pages/config/view-appearance-preferences.ts index 48690550b5f7..143d238d8d8c 100644 --- a/ui/src/pages/config/view-appearance-preferences.ts +++ b/ui/src/pages/config/view-appearance-preferences.ts @@ -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, diff --git a/ui/src/pages/lobsterdex/lobsterdex-page.ts b/ui/src/pages/lobsterdex/lobsterdex-page.ts index 1ccef5c766c0..ab6ceef8704c 100644 --- a/ui/src/pages/lobsterdex/lobsterdex-page.ts +++ b/ui/src/pages/lobsterdex/lobsterdex-page.ts @@ -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"; diff --git a/ui/src/pages/lobsterdex/view.ts b/ui/src/pages/lobsterdex/view.ts index 5208aca972b4..cb0080d43585 100644 --- a/ui/src/pages/lobsterdex/view.ts +++ b/ui/src/pages/lobsterdex/view.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; diff --git a/ui/src/styles/lobster-pet.css b/ui/src/styles/lobster-pet.css index 55fa54900915..eb830178a163 100644 --- a/ui/src/styles/lobster-pet.css +++ b/ui/src/styles/lobster-pet.css @@ -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;