From 636478c622ec9f7f08db4fa2530435c5bfde0e7a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 08:30:19 +0100 Subject: [PATCH] fix: keep control ui slash commands browser-safe --- CHANGELOG.md | 1 + src/auto-reply/commands-registry.data.ts | 3 +- src/auto-reply/commands-registry.shared.ts | 25 ++++++++++++-- .../slash-commands.browser-import.test.ts | 34 +++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 ui/src/ui/chat/slash-commands.browser-import.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cdb2096585d..fac73260280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai - Discord/gateway: reconnect when the gateway socket closes while waiting for the shared IDENTIFY concurrency window, instead of silently skipping IDENTIFY and leaving the bot online but unresponsive. Fixes #74617. Thanks @zeeskdr-ai. - Telegram/startup: use the existing `getMe` request guard for the gateway bot probe instead of a fixed 2.5-second budget, and honor higher `timeoutSeconds` configs for slow Telegram API paths. Fixes #75783. Thanks @tankotan. +- Control UI/slash commands: keep fallback command metadata on a browser-safe registry path, so provider thinking runtime imports cannot blank the Web UI with `process is not defined`. Fixes #75987. Thanks @novkien. - Infer/media: report missing image-understanding and audio-transcription provider configuration for `image describe`, `image describe-many`, and `audio transcribe` instead of blaming the input path when no provider is available. Fixes #73569 and supersedes #73593, #74288, and #74495. Thanks @bittoby, @tmimmanuel, @Linux2010, and @vyctorbrzezowski. - Docs/health: clarify that session listing surfaces stored conversation rows rather than Discord/channel socket liveness, and point connectivity checks at channel status and health probes. Fixes #70420. Thanks @ashersoutherncities-art and @martingarramon. - WhatsApp/Cron: keep DM pairing-store approvals out of implicit cron and heartbeat recipient fallback, so scheduled automation only uses explicit targets, active configured recipients, or configured `allowFrom` entries. Fixes #62339. Thanks @kelvinisly-collab. diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 5c4b62516ec..3b9fa94f365 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -6,6 +6,7 @@ import { defineChatCommand, } from "./commands-registry.shared.js"; import type { ChatCommandDefinition } from "./commands-registry.types.js"; +import { listThinkingLevels } from "./thinking.js"; type ChannelPlugin = ReturnType[number]; @@ -28,7 +29,7 @@ let cachedRegistryVersion = -1; function buildChatCommands(): ChatCommandDefinition[] { const commands: ChatCommandDefinition[] = [ - ...buildBuiltinChatCommands(), + ...buildBuiltinChatCommands({ listThinkingLevels }), ...listLoadedChannelPlugins() .filter(supportsNativeCommands) .map((plugin) => defineDockCommand(plugin)), diff --git a/src/auto-reply/commands-registry.shared.ts b/src/auto-reply/commands-registry.shared.ts index 3410e9ee7b6..866d8d33d3d 100644 --- a/src/auto-reply/commands-registry.shared.ts +++ b/src/auto-reply/commands-registry.shared.ts @@ -2,11 +2,25 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { COMMAND_ARG_FORMATTERS } from "./commands-args.js"; import type { ChatCommandDefinition, + CommandArgChoiceContext, CommandCategory, CommandScope, CommandTier, } from "./commands-registry.types.js"; -import { listThinkingLevels } from "./thinking.js"; +import { BASE_THINKING_LEVELS, type ThinkLevel } from "./thinking.shared.js"; + +type ListThinkingLevels = ( + provider?: string | null, + model?: string | null, + catalog?: CommandArgChoiceContext["catalog"], +) => ThinkLevel[]; + +const BROWSER_SAFE_THINKING_LEVELS: ThinkLevel[] = [ + ...BASE_THINKING_LEVELS, + "xhigh", + "adaptive", + "max", +]; type DefineChatCommandInput = { key: string; @@ -121,7 +135,11 @@ export function assertCommandRegistry(commands: ChatCommandDefinition[]): void { } } -export function buildBuiltinChatCommands(): ChatCommandDefinition[] { +export function buildBuiltinChatCommands( + params: { listThinkingLevels?: ListThinkingLevels } = {}, +): ChatCommandDefinition[] { + const listThinkingLevelChoices = + params.listThinkingLevels ?? (() => BROWSER_SAFE_THINKING_LEVELS); const commands: ChatCommandDefinition[] = [ defineChatCommand({ key: "help", @@ -727,7 +745,8 @@ export function buildBuiltinChatCommands(): ChatCommandDefinition[] { name: "level", description: "Thinking level", type: "string", - choices: ({ provider, model, catalog }) => listThinkingLevels(provider, model, catalog), + choices: ({ provider, model, catalog }) => + listThinkingLevelChoices(provider, model, catalog), }, ], argsMenu: "auto", diff --git a/ui/src/ui/chat/slash-commands.browser-import.test.ts b/ui/src/ui/chat/slash-commands.browser-import.test.ts new file mode 100644 index 00000000000..830cae83d90 --- /dev/null +++ b/ui/src/ui/chat/slash-commands.browser-import.test.ts @@ -0,0 +1,34 @@ +// @vitest-environment node +import { readFile } from "node:fs/promises"; +import { describe, expect, it } from "vitest"; + +describe("slash command browser import", () => { + it("builds fallback commands from the browser-safe shared registry", async () => { + const mod = await import("./slash-commands.ts?browser-import"); + + expect(mod.SLASH_COMMANDS.find((command) => command.name === "think")).toMatchObject({ + name: "think", + category: "model", + }); + }); + + it("keeps provider thinking runtime out of the Control UI import path", async () => { + const slashCommands = await readFile(new URL("./slash-commands.ts", import.meta.url), "utf8"); + const sharedRegistry = await readFile( + new URL("../../../../src/auto-reply/commands-registry.shared.ts", import.meta.url), + "utf8", + ); + const serverRegistry = await readFile( + new URL("../../../../src/auto-reply/commands-registry.data.ts", import.meta.url), + "utf8", + ); + const mod = await import("./slash-commands.ts?browser-import"); + + expect(mod.SLASH_COMMANDS.some((command) => command.name === "think")).toBe(true); + expect(slashCommands).toContain("commands-registry.shared.js"); + expect(sharedRegistry).toContain("thinking.shared.js"); + expect(sharedRegistry).not.toContain("./thinking.js"); + expect(sharedRegistry).not.toContain("provider-thinking"); + expect(serverRegistry).toContain('from "./thinking.js"'); + }); +});