From 56d7b37c64fef5f5518d8cc5a1b133ed57b80679 Mon Sep 17 00:00:00 2001 From: musi Date: Tue, 11 Aug 2026 12:36:44 +0800 Subject: [PATCH] Support multiple WorkBuddy models and generic app paths --- packages/core/src/agents/codex/app-launch.ts | 40 ++++++++---- .../agents/codex-app-model-catalog.test.mjs | 63 +++++++++++++++++++ .../ui/src/pages/home/components/profiles.tsx | 15 +---- packages/ui/src/pages/home/shared/i18n.tsx | 2 + packages/ui/src/pages/home/shared/profiles.ts | 2 +- packages/ui/test/component/profiles.test.tsx | 24 ++++++- 6 files changed, 117 insertions(+), 29 deletions(-) diff --git a/packages/core/src/agents/codex/app-launch.ts b/packages/core/src/agents/codex/app-launch.ts index 4fd80e6b..26c9bf21 100644 --- a/packages/core/src/agents/codex/app-launch.ts +++ b/packages/core/src/agents/codex/app-launch.ts @@ -315,7 +315,7 @@ export function writeCodexCompatibleAppModelCatalog( export function writeWorkbuddyModelsConfig( workbuddyConfigDir: string, - profile: Pick, + profile: ProfileConfig, config?: AppConfig ): WorkbuddyModelsConfigWriteResult { const file = path.join(workbuddyConfigDir, "models.json"); @@ -341,21 +341,38 @@ function workbuddySelectedModel( } function workbuddyModelsConfig( - model: string, - profile: Pick, + defaultModel: string, + profile: ProfileConfig, + config?: AppConfig +): Record { + const allowedModels = profileAllowedModels(profile); + const catalogItems = codexCompatibleAppModelCatalog(config, defaultModel, "workbuddy", allowedModels).models; + const models = catalogItems.map((catalogItem) => + workbuddyModelConfig(catalogItem.slug || catalogItem.id || catalogItem.model, defaultModel, profile, catalogItem, config) + ); + if (models.length === 0) { + models.push(workbuddyModelConfig(defaultModel, defaultModel, profile, undefined, config)); + } + return { + availableModels: models.map((item) => String(item.id || "")), + models + }; +} + +function workbuddyModelConfig( + model: string, + defaultModel: string, + profile: Pick, + catalogItem: CodexModelCatalogItem | undefined, config?: AppConfig ): Record { - const catalogItem = codexCompatibleAppModelCatalog(config, model, "workbuddy") - .models.find((item) => item.slug === model || item.id === model || item.model === model); const vendor = workbuddyModelVendor(model, profile.providerName); - const displayName = profile.name?.trim() - ? `${profile.name.trim()} / ${model}` - : `${vendor} / ${model}`; + const displayName = model.includes("/") ? model : `${vendor} / ${model}`; const workbuddyModel: Record = { apiKey: "${CCR_PROFILE_API_KEY}", disabled: false, id: model, - isDefault: true, + isDefault: model === defaultModel, name: displayName, supportsImages: Boolean(catalogItem?.supports_image_detail_original), supportsReasoning: Boolean(catalogItem?.supports_reasoning_summaries), @@ -375,10 +392,7 @@ function workbuddyModelsConfig( supportedEfforts: reasoningEfforts }; } - return { - availableModels: [model], - models: [workbuddyModel] - }; + return workbuddyModel; } function workbuddyModelVendor(model: string, providerName?: string): string { diff --git a/packages/core/test/unit/agents/codex-app-model-catalog.test.mjs b/packages/core/test/unit/agents/codex-app-model-catalog.test.mjs index 85b256f2..3e787994 100644 --- a/packages/core/test/unit/agents/codex-app-model-catalog.test.mjs +++ b/packages/core/test/unit/agents/codex-app-model-catalog.test.mjs @@ -397,6 +397,69 @@ test("WorkBuddy AI app profile writes the virtual desktop auth session", () => { } }); +test("WorkBuddy AI app profile writes every allowed model to models.json", () => { + const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-models-")); + try { + const profile = { + agent: "workbuddy", + availableModels: ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"], + enabled: true, + id: "workbuddy-main", + model: "Codex API/gpt-5-codex", + name: "WorkBuddy Main", + providerId: "claude-code-router", + scope: "ccr", + surface: "app" + }; + + const result = writeCodexCompatibleAppModelCatalog(configDir, profile, { + Providers: [{ + models: ["gpt-5-codex", "gpt-5.1-codex", "gpt-4.1"], + name: "Codex API", + type: "openai_responses" + }] + }); + + const modelsConfig = JSON.parse(readFileSync(result.workbuddyModelsConfig.file, "utf8")); + assert.deepEqual(modelsConfig.availableModels, ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]); + assert.deepEqual(modelsConfig.models.map((item) => item.id), ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]); + assert.deepEqual(modelsConfig.models.map((item) => item.name), ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex"]); + assert.deepEqual(modelsConfig.models.map((item) => item.isDefault), [true, false]); + } finally { + rmSync(configDir, { force: true, recursive: true }); + } +}); + +test("WorkBuddy AI app profile writes every catalog model when the allowlist is unrestricted", () => { + const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-workbuddy-app-unrestricted-models-")); + try { + const profile = { + agent: "workbuddy", + enabled: true, + id: "workbuddy-main", + model: "Codex API/gpt-5-codex", + name: "WorkBuddy Main", + providerId: "claude-code-router", + scope: "ccr", + surface: "app" + }; + + const result = writeCodexCompatibleAppModelCatalog(configDir, profile, { + Providers: [{ + models: ["gpt-5-codex", "gpt-5.1-codex", "gpt-4.1"], + name: "Codex API", + type: "openai_responses" + }] + }); + + const modelsConfig = JSON.parse(readFileSync(result.workbuddyModelsConfig.file, "utf8")); + assert.deepEqual(modelsConfig.availableModels, ["Codex API/gpt-5-codex", "Codex API/gpt-5.1-codex", "Codex API/gpt-4.1"]); + assert.deepEqual(modelsConfig.models.map((item) => item.isDefault), [true, false, false]); + } finally { + rmSync(configDir, { force: true, recursive: true }); + } +}); + function withPlatform(platform, callback) { const descriptor = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { diff --git a/packages/ui/src/pages/home/components/profiles.tsx b/packages/ui/src/pages/home/components/profiles.tsx index 6d07890e..f6e3aede 100644 --- a/packages/ui/src/pages/home/components/profiles.tsx +++ b/packages/ui/src/pages/home/components/profiles.tsx @@ -1316,18 +1316,9 @@ function profileNumberDraftValid(value: string, min: number, max: number): boole return Number.isFinite(numeric) && numeric >= min && numeric <= max; } -function profileAppPathLabel(agent: ProfileConfig["agent"]): "CLAUDE_APP_PATH" | "CHATGPT_APP_PATH" | "OPENCODE_APP_PATH" | "WORKBUDDY_APP_PATH" | undefined { - if (agent === "claude-code") { - return "CLAUDE_APP_PATH"; - } - if (agent === "codex") { - return "CHATGPT_APP_PATH"; - } - if (agent === "opencode") { - return "OPENCODE_APP_PATH"; - } - if (agent === "workbuddy") { - return "WORKBUDDY_APP_PATH"; +function profileAppPathLabel(agent: ProfileConfig["agent"]): "APP_PATH" | undefined { + if (agent === "claude-code" || agent === "codex" || agent === "opencode" || agent === "workbuddy") { + return "APP_PATH"; } return undefined; } diff --git a/packages/ui/src/pages/home/shared/i18n.tsx b/packages/ui/src/pages/home/shared/i18n.tsx index 51bb6c4f..fa709e4c 100644 --- a/packages/ui/src/pages/home/shared/i18n.tsx +++ b/packages/ui/src/pages/home/shared/i18n.tsx @@ -605,6 +605,7 @@ export const appCopy: Record = { "Arguments": "Arguments", "Working directory": "Working directory", "API key env": "API key env", + "APP_PATH": "APP_PATH", "CLAUDE_APP_PATH": "CLAUDE_APP_PATH", "CHATGPT_APP_PATH": "CHATGPT_APP_PATH", "OPENCODE_APP_PATH": "OPENCODE_APP_PATH", @@ -1751,6 +1752,7 @@ export const appCopy: Record = { "Arguments": "参数", "Working directory": "工作目录", "API key env": "API Key 环境变量", + "APP_PATH": "APP_PATH", "CLAUDE_APP_PATH": "CLAUDE_APP_PATH", "CHATGPT_APP_PATH": "CHATGPT_APP_PATH", "OPENCODE_APP_PATH": "OPENCODE_APP_PATH", diff --git a/packages/ui/src/pages/home/shared/profiles.ts b/packages/ui/src/pages/home/shared/profiles.ts index 4b2989bd..2ab21781 100644 --- a/packages/ui/src/pages/home/shared/profiles.ts +++ b/packages/ui/src/pages/home/shared/profiles.ts @@ -1204,7 +1204,7 @@ export function profileSummaryItems( : []; const appPath = profile.appPath?.trim() || ""; const appPathSummaryItems = appPath && surface !== "cli" && profile.agent !== "zcode" - ? [{ label: t(profile.agent === "claude-code" ? "CLAUDE_APP_PATH" : profile.agent === "opencode" ? "OPENCODE_APP_PATH" : "CHATGPT_APP_PATH"), value: appPath }] + ? [{ label: t("APP_PATH"), value: appPath }] : []; const savedBot = profile.botConfigId ? config.botConfigs.find((item) => item.id === profile.botConfigId) diff --git a/packages/ui/test/component/profiles.test.tsx b/packages/ui/test/component/profiles.test.tsx index 6bfc980c..f29fbe5d 100644 --- a/packages/ui/test/component/profiles.test.tsx +++ b/packages/ui/test/component/profiles.test.tsx @@ -259,7 +259,7 @@ test("AddProfileForm keeps the default model locked in allowed model lists", () assert.doesNotMatch(otherLabel, /disabled=""/); }); -test("AddProfileForm places CLAUDE_APP_PATH below Bot settings", () => { +test("AddProfileForm places APP_PATH below Bot settings", () => { const config = appConfigFixture(); const html = renderToStaticMarkup( { /> ); const botIndex = html.indexOf(">Bot"); - const appPathIndex = html.indexOf("CLAUDE_APP_PATH"); + const appPathIndex = html.indexOf("APP_PATH"); const envIndex = html.indexOf("Environment variables"); assert.ok(botIndex >= 0); @@ -504,6 +504,23 @@ test("profileSummaryItems uses Pi-specific model labels", () => { assert.equal(items[0]?.label, "Pi model"); }); +test("profileSummaryItems uses a generic App path label", () => { + const config = appConfigFixture(); + const items = profileSummaryItems({ + agent: "workbuddy", + appPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron", + enabled: true, + id: "workbuddy-main", + model: "openai/gpt-5.2", + name: "Workbuddy Main", + scope: "ccr", + surface: "app" + }, config, (value) => value); + + assert.equal(items.find((item) => item.value.includes("WorkBuddy AI.app"))?.label, "APP_PATH"); + assert.doesNotMatch(items.map((item) => item.label).join(" "), /CHATGPT_APP_PATH|WORKBUDDY_APP_PATH/); +}); + test("profileSummaryItems omits disabled profile properties from cards", () => { const config = appConfigFixture(); const disabledItems = profileSummaryItems({ @@ -745,7 +762,8 @@ test("Workbuddy profiles support local App configuration", () => { ); assert.match(html, /App only/); assert.doesNotMatch(html, /CLI only/); - assert.match(html, /WORKBUDDY_APP_PATH/); + assert.match(html, /APP_PATH/); + assert.doesNotMatch(html, /WORKBUDDY_APP_PATH/); const profile = normalizeUnknownProfileItem({ agent: "work-buddy",