From 35f985baff9667645b05bcde1cf92b9ee18a181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99drzy=C5=84ski?= Date: Fri, 31 Jul 2026 17:37:04 +0200 Subject: [PATCH] Switch reasoning, mode, plan, and fast from the Command Center (#2274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch reasoning, mode, plan, and fast from the Command Center Extend the Command Center (Cmd-K) with selectors for a focused agent's Thinking (reasoning effort), access Mode, Plan mode, and Fast mode, mirroring the model switcher. Typing surfaces tiered " › " breadcrumb rows with icons and a check on the current value, for both a running agent and a new draft tab. Rows only appear while typing, so the default palette is unchanged. - Thinking and Mode are multi-option selects; Plan mode and Fast mode are On/Off toggles. - Plan mode is dual-natured: a permission mode for Claude/Copilot/OpenCode (excluded from the Mode list, given its own toggle) and the plan_mode feature for Codex. Fast mode drives the fast_mode feature (model-gated). - All logic lives in command-center/agent-setting-contributions.ts as pure, unit-tested builders, with a small icon helper and a hook that registers the running-agent selectors; the draft registers from its composer tab. No changes to the command palette component and no protocol changes. Co-Authored-By: Claude Opus 4.8 * Unify Command Center agent controls * refactor: clarify agent control ownership * refactor(app): tighten command center agent controls --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Mohamed Boudra --- .../e2e/command-center-agent-controls.spec.ts | 110 +++++ .../helpers/command-center-agent-controls.ts | 95 +++++ packages/app/e2e/helpers/command-center.ts | 5 + packages/app/e2e/helpers/seed-client.ts | 1 + packages/app/src/agent-controls/icons.ts | 65 +++ .../app/src/agent-controls/labels.test.ts | 32 ++ packages/app/src/agent-controls/labels.ts | 45 ++ .../app/src/agent-controls/policy.test.ts | 37 ++ packages/app/src/agent-controls/policy.ts | 19 + .../agent-control-contributions.test.ts | 400 ++++++++++++++++++ .../agent-control-contributions.ts | 319 ++++++++++++++ .../agent-control-registration.tsx | 156 +++++++ .../app/src/command-center/command-center.tsx | 21 +- packages/app/src/command-center/icon.tsx | 19 + .../model-contributions.test.tsx | 104 ----- .../src/command-center/model-contributions.ts | 51 --- .../app/src/command-center/provider-icon.tsx | 20 - .../schedules/schedule-form-sheet.tsx | 2 +- .../src/composer/agent-controls/control.tsx | 10 +- .../app/src/composer/agent-controls/index.tsx | 98 +++-- .../composer/agent-controls/mode-control.tsx | 68 +-- .../src/composer/agent-controls/utils.test.ts | 32 -- .../app/src/composer/agent-controls/utils.ts | 50 +-- .../app/src/composer/draft/workspace-tab.tsx | 66 +-- packages/app/src/i18n/resources.test.ts | 6 + packages/app/src/i18n/resources/ar.ts | 10 + packages/app/src/i18n/resources/en.ts | 10 + packages/app/src/i18n/resources/es.ts | 10 + packages/app/src/i18n/resources/fr.ts | 10 + packages/app/src/i18n/resources/ja.ts | 10 + packages/app/src/i18n/resources/pt-BR.ts | 10 + packages/app/src/i18n/resources/ru.ts | 10 + packages/app/src/i18n/resources/zh-CN.ts | 10 + .../app/src/schedules/schedule-form-model.ts | 2 +- 34 files changed, 1526 insertions(+), 387 deletions(-) create mode 100644 packages/app/e2e/command-center-agent-controls.spec.ts create mode 100644 packages/app/e2e/helpers/command-center-agent-controls.ts create mode 100644 packages/app/src/agent-controls/icons.ts create mode 100644 packages/app/src/agent-controls/labels.test.ts create mode 100644 packages/app/src/agent-controls/labels.ts create mode 100644 packages/app/src/agent-controls/policy.test.ts create mode 100644 packages/app/src/agent-controls/policy.ts create mode 100644 packages/app/src/command-center/agent-control-contributions.test.ts create mode 100644 packages/app/src/command-center/agent-control-contributions.ts create mode 100644 packages/app/src/command-center/agent-control-registration.tsx create mode 100644 packages/app/src/command-center/icon.tsx delete mode 100644 packages/app/src/command-center/model-contributions.test.tsx delete mode 100644 packages/app/src/command-center/model-contributions.ts delete mode 100644 packages/app/src/command-center/provider-icon.tsx diff --git a/packages/app/e2e/command-center-agent-controls.spec.ts b/packages/app/e2e/command-center-agent-controls.spec.ts new file mode 100644 index 000000000..77479aeff --- /dev/null +++ b/packages/app/e2e/command-center-agent-controls.spec.ts @@ -0,0 +1,110 @@ +import { test } from "./fixtures"; +import { closeCommandCenter, openCommandCenter } from "./helpers/command-center"; +import { + applyCommandCenterAgentControls, + chooseCommandCenterAgentControl, + expectCommandCenterAgentControlSelected, + expectFocusedAgentControls, + expectWorkspaceAgentConfiguration, + submitDraftAgent, + waitForDraftComposer, +} from "./helpers/command-center-agent-controls"; +import { clickNewChat, gotoWorkspace } from "./helpers/launcher"; +import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; +import { seedWorkspace } from "./helpers/seed-client"; +import { getServerId } from "./helpers/server-id"; + +const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; + +async function seedMockDraftPreferences(page: import("@playwright/test").Page): Promise { + await page.addInitScript( + ({ preferencesKey, serverId }) => { + localStorage.setItem( + preferencesKey, + JSON.stringify({ + serverId, + provider: "mock", + providerPreferences: { + mock: { + model: "five-minute-stream", + mode: "load-test", + }, + }, + }), + ); + }, + { preferencesKey: CREATE_AGENT_PREFERENCES_KEY, serverId: getServerId() }, + ); +} + +test.describe("Command Center agent controls", () => { + test.describe.configure({ timeout: 180_000 }); + + test("changes a running agent setting and preserves its selected row", async ({ page }) => { + const workspace = await seedMockAgentWorkspace({ + repoPrefix: "command-center-live-controls-", + title: "Command Center live controls", + model: "five-minute-stream", + }); + try { + // A stale runtime mode makes selecting the provider's supported mode + // exercise the live setting RPC instead of the selected-row no-op. + await workspace.client.setAgentMode(workspace.agentId, "legacy-mode"); + await openAgentRoute(page, { + workspaceId: workspace.workspaceId, + agentId: workspace.agentId, + }); + await openCommandCenter(page); + await chooseCommandCenterAgentControl({ + page, + query: "load test", + choice: "Mode › Load test", + }); + await expectWorkspaceAgentConfiguration(workspace, { + id: workspace.agentId, + provider: "mock", + model: "five-minute-stream", + modeId: "load-test", + }); + await openCommandCenter(page); + await expectCommandCenterAgentControlSelected({ + page, + query: "load test", + choice: "Mode › Load test", + }); + } finally { + await workspace.cleanup(); + } + }); + + test("applies draft model and setting choices to the created agent", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "command-center-draft-controls-" }); + await seedMockDraftPreferences(page); + + try { + await openDraftComposer(page, workspace.workspaceId); + await applyCommandCenterAgentControls(page, DRAFT_AGENT_CONTROL_CHOICES); + await closeCommandCenter(page); + await submitDraftAgent(page, "Create an agent with Command Center draft choices"); + await expectFocusedAgentControls(page, DRAFT_AGENT_CONTROL_CHOICES); + await expectWorkspaceAgentConfiguration(workspace, { + provider: "mock", + model: "ten-second-stream", + modeId: "load-test", + }); + } finally { + await workspace.cleanup(); + } + }); +}); + +const DRAFT_AGENT_CONTROL_CHOICES = [ + { query: "ten second stream", choice: "Model › Mock Load Test › Ten second stream" }, + { query: "load test", choice: "Mode › Load test" }, +] as const; + +async function openDraftComposer(page: import("@playwright/test").Page, workspaceId: string) { + await gotoWorkspace(page, workspaceId); + await clickNewChat(page); + await waitForDraftComposer(page); +} diff --git a/packages/app/e2e/helpers/command-center-agent-controls.ts b/packages/app/e2e/helpers/command-center-agent-controls.ts new file mode 100644 index 000000000..4b4cbfa4d --- /dev/null +++ b/packages/app/e2e/helpers/command-center-agent-controls.ts @@ -0,0 +1,95 @@ +import { expect, type Page } from "@playwright/test"; +import type { SeededWorkspace } from "./seed-client"; +import { openCommandCenter } from "./command-center"; + +export interface CommandCenterAgentControlChoice { + query: string; + choice: string; +} + +export async function chooseCommandCenterAgentControl(input: { + page: Page; + query: string; + choice: string; +}): Promise { + const panel = input.page.getByTestId("command-center-panel"); + await panel.getByRole("textbox").fill(input.query); + const choice = panel.getByRole("button", { name: input.choice }); + await expect(choice).toBeVisible({ timeout: 30_000 }); + await choice.click(); +} + +export async function expectCommandCenterAgentControlSelected(input: { + page: Page; + query: string; + choice: string; +}): Promise { + const panel = input.page.getByTestId("command-center-panel"); + await panel.getByRole("textbox").fill(input.query); + const choice = panel.getByRole("button", { name: input.choice }); + await expect(choice).toHaveRole("button"); + await expect(choice).toHaveAttribute("aria-pressed", "true"); +} + +export async function applyCommandCenterAgentControls( + page: Page, + choices: readonly CommandCenterAgentControlChoice[], +): Promise { + for (const choice of choices) { + await openCommandCenter(page); + await chooseCommandCenterAgentControl({ page, ...choice }); + } +} + +export async function expectFocusedAgentControls( + page: Page, + choices: readonly CommandCenterAgentControlChoice[], +): Promise { + await openCommandCenter(page); + for (const choice of choices) { + await expectCommandCenterAgentControlSelected({ page, ...choice }); + } +} + +export async function submitDraftAgent(page: Page, prompt: string): Promise { + const composer = page.getByRole("textbox", { name: "Message agent..." }).first(); + await expect(composer).toBeEditable({ timeout: 30_000 }); + await composer.fill(prompt); + await composer.press("Enter"); + await expect(page.getByText(prompt, { exact: true }).first()).toBeVisible({ timeout: 30_000 }); +} + +export async function waitForDraftComposer(page: Page): Promise { + await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeEditable({ + timeout: 30_000, + }); +} + +export async function expectWorkspaceAgentConfiguration( + workspace: Pick, + expected: { id?: string; provider: string; model: string | null; modeId: string | null }, +): Promise { + await expect + .poll( + async () => { + const entries = (await workspace.client.fetchAgents({ scope: "active" })).entries; + const agent = entries + .map((entry) => entry.agent) + .find( + (candidate) => + candidate.workspaceId === workspace.workspaceId && + (expected.id === undefined || candidate.id === expected.id), + ); + return agent + ? { + id: agent.id, + provider: agent.provider, + model: agent.model, + modeId: agent.currentModeId, + } + : null; + }, + { timeout: 30_000 }, + ) + .toMatchObject(expected); +} diff --git a/packages/app/e2e/helpers/command-center.ts b/packages/app/e2e/helpers/command-center.ts index 4a3a58b56..7677a1363 100644 --- a/packages/app/e2e/helpers/command-center.ts +++ b/packages/app/e2e/helpers/command-center.ts @@ -7,3 +7,8 @@ export async function openCommandCenter(page: Page): Promise { await expect(panel).toBeVisible({ timeout: 30_000 }); return panel; } + +export async function closeCommandCenter(page: Page): Promise { + await page.keyboard.press("Escape"); + await expect(page.getByTestId("command-center-panel")).not.toBeVisible(); +} diff --git a/packages/app/e2e/helpers/seed-client.ts b/packages/app/e2e/helpers/seed-client.ts index bcc7b4144..03ed14e2b 100644 --- a/packages/app/e2e/helpers/seed-client.ts +++ b/packages/app/e2e/helpers/seed-client.ts @@ -129,6 +129,7 @@ export interface SeedDaemonClient { }>; }>; updateAgent(agentId: string, updates: { name?: string }): Promise; + setAgentMode(agentId: string, modeId: string): Promise; waitForAgentUpsert( agentId: string, predicate: (snapshot: { status: string }) => boolean, diff --git a/packages/app/src/agent-controls/icons.ts b/packages/app/src/agent-controls/icons.ts new file mode 100644 index 000000000..06b275907 --- /dev/null +++ b/packages/app/src/agent-controls/icons.ts @@ -0,0 +1,65 @@ +import type { ComponentType } from "react"; +import { + Bot, + Brain, + ListTodo, + Settings2, + Shield, + ShieldAlert, + ShieldCheck, + ShieldEllipsis, + ShieldOff, + ShieldPlus, + ShieldQuestionMark, + Zap, +} from "lucide-react-native"; +import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; + +export interface AgentControlIconProps { + size: number; + color: string; +} + +export type AgentControlIcon = ComponentType; + +export const ThinkingIcon = Brain; +export const PlanModeIcon = ListTodo; + +const MODE_ICONS: Record = { + Bot, + Shield, + ShieldAlert, + ShieldCheck, + ShieldEllipsis, + ShieldOff, + ShieldPlus, + ShieldQuestionMark, +}; + +const FEATURE_ICONS: Record = { + "list-todo": ListTodo, + "shield-check": ShieldCheck, + zap: Zap, +}; + +export function getAgentModeIcon( + provider: string, + modeId: string, + providerDefinitions: AgentProviderDefinition[], +): AgentControlIcon { + const icon = getModeVisuals(provider, modeId, providerDefinitions)?.icon; + return (icon ? MODE_ICONS[icon] : undefined) ?? Bot; +} + +export function getAgentModeOptionIcon( + provider: string, + modeId: string, + providerDefinitions: AgentProviderDefinition[], +): AgentControlIcon | undefined { + const icon = getModeVisuals(provider, modeId, providerDefinitions)?.icon; + return icon ? MODE_ICONS[icon] : undefined; +} + +export function getAgentFeatureIcon(icon?: string): AgentControlIcon { + return (icon ? FEATURE_ICONS[icon] : undefined) ?? Settings2; +} diff --git a/packages/app/src/agent-controls/labels.test.ts b/packages/app/src/agent-controls/labels.test.ts new file mode 100644 index 000000000..4f1302fd1 --- /dev/null +++ b/packages/app/src/agent-controls/labels.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { formatAgentModeLabel, formatThinkingOptionLabel } from "./labels"; + +describe("formatAgentModeLabel", () => { + it("sentence-cases provider mode labels", () => { + expect(formatAgentModeLabel({ id: "plan", label: "Plan" })).toBe("Plan"); + expect(formatAgentModeLabel({ id: "full-access", label: "Full Access" })).toBe("Full access"); + expect(formatAgentModeLabel({ id: "auto-review", label: "Auto-review" })).toBe("Auto-review"); + expect(formatAgentModeLabel({ id: "read_only", label: "read_only" })).toBe("Read only"); + expect(formatAgentModeLabel({ id: "acceptEdits", label: "acceptEdits" })).toBe("Accept edits"); + }); + + it("splits compact mode ids when no provider label is available", () => { + expect(formatAgentModeLabel({ id: "auto-review" })).toBe("Auto review"); + }); +}); + +describe("formatThinkingOptionLabel", () => { + it("formats compact thinking option labels for display", () => { + expect(formatThinkingOptionLabel({ id: "none", label: "none" })).toBe("None"); + expect(formatThinkingOptionLabel({ id: "low", label: "low" })).toBe("Low"); + expect(formatThinkingOptionLabel({ id: "medium", label: "medium" })).toBe("Medium"); + expect(formatThinkingOptionLabel({ id: "high", label: "high" })).toBe("High"); + expect(formatThinkingOptionLabel({ id: "xhigh", label: "xhigh" })).toBe("Extra high"); + }); + + it("sentence-cases split provider labels", () => { + expect(formatThinkingOptionLabel({ id: "extra_high", label: "extra_high" })).toBe("Extra high"); + expect(formatThinkingOptionLabel({ id: "think-hard", label: "think-hard" })).toBe("Think hard"); + expect(formatThinkingOptionLabel({ id: "xhigh", label: "XHigh" })).toBe("Extra high"); + }); +}); diff --git a/packages/app/src/agent-controls/labels.ts b/packages/app/src/agent-controls/labels.ts new file mode 100644 index 000000000..ff32aaf4d --- /dev/null +++ b/packages/app/src/agent-controls/labels.ts @@ -0,0 +1,45 @@ +import { i18n } from "@/i18n/i18next"; + +interface ControlLabelInput { + id: string; + label?: string | null; +} + +function sentenceCase(value: string): string { + if (!value) { + return value; + } + return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); +} + +function splitCompactLabel(value: string, splitHyphen: boolean): string { + const separatorPattern = splitHyphen ? /[_-]+/g : /_+/g; + + return value + .replace(separatorPattern, " ") + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .replace(/\s+/g, " ") + .trim(); +} + +function formatControlLabel(option: ControlLabelInput, splitHyphen: boolean): string { + const rawLabel = (option.label ?? option.id).trim(); + return sentenceCase(splitCompactLabel(rawLabel, splitHyphen)); +} + +export function formatAgentModeLabel(mode: ControlLabelInput): string { + return formatControlLabel(mode, mode.label == null); +} + +export function formatThinkingOptionLabel(option: ControlLabelInput): string { + const rawLabel = (option.label ?? option.id).trim(); + const compactId = option.id.replace(/[\s_-]+/g, "").toLowerCase(); + const compactLabel = rawLabel.replace(/[\s_-]+/g, "").toLowerCase(); + + if (compactId === "xhigh" || compactLabel === "xhigh") { + return i18n.t("agentControls.thinking.extraHigh"); + } + + return formatControlLabel(option, true); +} diff --git a/packages/app/src/agent-controls/policy.test.ts b/packages/app/src/agent-controls/policy.test.ts new file mode 100644 index 000000000..588c2c9a7 --- /dev/null +++ b/packages/app/src/agent-controls/policy.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import type { AgentMode } from "@getpaseo/protocol/agent-types"; +import { isPlanningAgentMode, resolveNonPlanningModeId } from "./policy"; + +describe("isPlanningAgentMode", () => { + it("prefers planning metadata and recognizes existing provider ids", () => { + expect(isPlanningAgentMode({ id: "research", colorTier: "planning" })).toBe(true); + expect(isPlanningAgentMode({ id: "plan" })).toBe(true); + expect( + isPlanningAgentMode({ + id: "https://agentclientprotocol.com/protocol/session-modes#plan", + }), + ).toBe(true); + expect(isPlanningAgentMode({ id: "default", colorTier: "safe" })).toBe(false); + }); +}); + +describe("resolveNonPlanningModeId", () => { + const modes = [ + { id: "plan", label: "Plan", colorTier: "planning" }, + { id: "default", label: "Default", colorTier: "safe" }, + { id: "full", label: "Full", colorTier: "dangerous" }, + ] satisfies AgentMode[]; + + it("uses a non-planning provider default", () => { + expect(resolveNonPlanningModeId(modes, "full")).toBe("full"); + }); + + it("does not use a planning or stale provider default", () => { + expect(resolveNonPlanningModeId(modes, "plan")).toBe("default"); + expect(resolveNonPlanningModeId(modes, "deleted")).toBe("default"); + }); + + it("returns null when no non-planning mode exists", () => { + expect(resolveNonPlanningModeId([modes[0]], "plan")).toBeNull(); + }); +}); diff --git a/packages/app/src/agent-controls/policy.ts b/packages/app/src/agent-controls/policy.ts new file mode 100644 index 000000000..827dfadf5 --- /dev/null +++ b/packages/app/src/agent-controls/policy.ts @@ -0,0 +1,19 @@ +import type { AgentMode } from "@getpaseo/protocol/agent-types"; + +export const PLAN_MODE_FEATURE_ID = "plan_mode"; +export const FAST_MODE_FEATURE_ID = "fast_mode"; + +export function isPlanningAgentMode(mode: Pick): boolean { + return mode.colorTier === "planning" || mode.id === "plan" || mode.id.endsWith("#plan"); +} + +export function resolveNonPlanningModeId( + modes: readonly AgentMode[], + defaultModeId: string | null, +): string | null { + const defaultMode = modes.find((mode) => mode.id === defaultModeId); + if (defaultMode && !isPlanningAgentMode(defaultMode)) { + return defaultMode.id; + } + return modes.find((mode) => !isPlanningAgentMode(mode))?.id ?? null; +} diff --git a/packages/app/src/command-center/agent-control-contributions.test.ts b/packages/app/src/command-center/agent-control-contributions.test.ts new file mode 100644 index 000000000..6fe8f91bb --- /dev/null +++ b/packages/app/src/command-center/agent-control-contributions.test.ts @@ -0,0 +1,400 @@ +import { describe, expect, it } from "vitest"; +import type { AgentFeature, AgentMode, AgentSelectOption } from "@getpaseo/protocol/agent-types"; +import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import type { CommandCenterContribution } from "./contributions"; +import { + buildAgentControlContributions, + type AgentControlContributionSource, +} from "./agent-control-contributions"; + +function TestIcon() { + return null; +} + +const LABELS: AgentControlContributionSource["labels"] = { + model: "Model", + thinking: "Thinking", + mode: "Mode", + planMode: "Plan mode", + fast: "Fast", + on: "On", + off: "Off", + modelKeywords: "model switch", + thinkingKeywords: "reasoning effort", + modeKeywords: "access permission", + planKeywords: "plan planning", + fastKeywords: "fast speed", +}; + +const ICONS: AgentControlContributionSource["icons"] = { + provider: () => TestIcon, + thinking: TestIcon, + planMode: TestIcon, + mode: () => TestIcon, + feature: () => TestIcon, +}; + +function provider(input: { + id: string; + label: string; + state?: "models" | "error"; +}): ProviderSelectorProvider { + if (input.state === "error") { + return { + id: input.id, + label: input.label, + modelSelection: { kind: "error", message: "Unavailable" }, + }; + } + return { + id: input.id, + label: input.label, + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: `${input.id}:shared`, + provider: input.id, + providerLabel: input.label, + modelId: "shared", + modelLabel: `${input.label} model`, + description: undefined, + }, + ], + }, + }; +} + +function toggleFeature(id: string, value = false, icon?: string): AgentFeature { + return { type: "toggle", id, label: id, value, icon }; +} + +function makeSource( + overrides: { + provider?: string | null; + models?: Partial; + thinking?: Partial; + modes?: Partial>; + features?: Partial; + } = {}, +): AgentControlContributionSource { + return { + serverId: "host", + ownerKey: "agent", + provider: overrides.provider === undefined ? "claude" : overrides.provider, + labels: LABELS, + icons: ICONS, + models: { + providers: [], + selectedProvider: null, + selectedModelId: null, + select: () => undefined, + ...overrides.models, + }, + thinking: { + options: [], + selectedId: null, + select: () => undefined, + ...overrides.thinking, + }, + modes: { + options: [], + selectedId: null, + defaultModeId: null, + select: () => undefined, + ...overrides.modes, + }, + features: { + list: [], + set: () => undefined, + ...overrides.features, + }, + }; +} + +function selected(contribution: CommandCenterContribution): boolean { + return contribution.presentation.kind === "choice" ? contribution.presentation.selected : false; +} + +const THINKING_OPTIONS: AgentSelectOption[] = [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + { id: "xhigh", label: "Extra High" }, +]; + +const CLAUDE_MODES: AgentMode[] = [ + { id: "plan", label: "Plan Mode" }, + { id: "default", label: "Always Ask" }, + { id: "acceptEdits", label: "Accept File Edits" }, +]; + +describe("Command Center agent-control contributions", () => { + it("omits mode-derived choices when no mode control is available", () => { + const contributions = buildAgentControlContributions({ + ...makeSource({ + models: { providers: [provider({ id: "claude", label: "Claude" })] }, + features: { list: [toggleFeature("plan_mode", true)] }, + }), + modes: undefined, + }); + + const groups = contributions.map((contribution) => contribution.group); + expect(groups).toContain("models"); + expect(groups).toContain("plan-mode"); + expect(groups).not.toContain("modes"); + }); + + it("builds unique cross-provider model choices and skips unavailable or empty models", () => { + const selections: string[] = []; + const contributions = buildAgentControlContributions( + makeSource({ + provider: null, + models: { + providers: [ + provider({ id: "claude", label: "Claude" }), + provider({ id: "codex", label: "Codex" }), + provider({ id: "unavailable", label: "Unavailable", state: "error" }), + { + id: "empty", + label: "Empty", + modelSelection: { + kind: "models", + rows: [ + { + favoriteKey: "empty:", + provider: "empty", + providerLabel: "Empty", + modelId: "", + modelLabel: "Default", + description: undefined, + }, + ], + }, + }, + ], + selectedProvider: "codex", + selectedModelId: "shared", + select: (selectedProvider, modelId) => { + selections.push(`${selectedProvider}:${modelId}`); + }, + }, + }), + ); + + expect( + contributions.map((contribution) => ({ + id: contribution.id, + selected: selected(contribution), + })), + ).toEqual([ + { id: "models:claude:shared", selected: false }, + { id: "models:codex:shared", selected: true }, + ]); + expect(contributions[0].presentation).toMatchObject({ + path: ["Model", "Claude", "Claude model"], + testId: "command-center-model-host:claude:shared", + }); + + contributions[0].run(); + contributions[1].run(); + expect(selections).toEqual(["claude:shared"]); + }); + + it("publishes thinking choices only when selection is meaningful", () => { + const selections: string[] = []; + const contributions = buildAgentControlContributions( + makeSource({ + thinking: { + options: THINKING_OPTIONS, + selectedId: "high", + select: (id) => { + selections.push(id); + }, + }, + }), + ).filter((contribution) => contribution.group === "thinking"); + + expect(contributions.map((contribution) => contribution.id)).toEqual([ + "thinking:low", + "thinking:high", + "thinking:xhigh", + ]); + expect(contributions.map(selected)).toEqual([false, true, false]); + for (const contribution of contributions) contribution.run(); + expect(selections).toEqual(["low", "xhigh"]); + + const singleOption = buildAgentControlContributions( + makeSource({ + thinking: { + options: [{ id: "high", label: "High" }], + selectedId: "high", + }, + }), + ); + expect(singleOption.filter((contribution) => contribution.group === "thinking")).toEqual([]); + }); + + it("formats access modes, excludes planning modes, and marks the current mode", () => { + const selections: string[] = []; + const contributions = buildAgentControlContributions( + makeSource({ + modes: { + options: CLAUDE_MODES, + selectedId: "default", + defaultModeId: "default", + select: (id) => { + selections.push(id); + }, + }, + }), + ).filter((contribution) => contribution.group === "modes"); + + expect( + contributions.map((contribution) => ({ + id: contribution.id, + path: + contribution.presentation.kind === "choice" ? contribution.presentation.path : undefined, + selected: selected(contribution), + })), + ).toEqual([ + { id: "modes:default", path: ["Mode", "Always ask"], selected: true }, + { id: "modes:acceptEdits", path: ["Mode", "Accept file edits"], selected: false }, + ]); + contributions[1].run(); + expect(selections).toEqual(["acceptEdits"]); + }); + + it("toggles mode-based Plan through the plan mode and provider default", () => { + const selections: string[] = []; + const contributions = buildAgentControlContributions( + makeSource({ + modes: { + options: CLAUDE_MODES, + selectedId: "plan", + defaultModeId: "default", + select: (id) => { + selections.push(id); + }, + }, + }), + ).filter((contribution) => contribution.group === "plan-mode"); + + expect( + contributions.map((contribution) => ({ + id: contribution.id, + selected: selected(contribution), + })), + ).toEqual([ + { id: "plan-mode:on", selected: true }, + { id: "plan-mode:off", selected: false }, + ]); + contributions[1].run(); + expect(selections).toEqual(["default"]); + }); + + it("does not use a planning provider default as the Plan off target", () => { + const selections: string[] = []; + const contributions = buildAgentControlContributions( + makeSource({ + modes: { + options: [ + { id: "research", label: "Research", colorTier: "planning" }, + { id: "default", label: "Default", colorTier: "safe" }, + ], + selectedId: "research", + defaultModeId: "research", + select: (id) => { + selections.push(id); + }, + }, + }), + ).filter((contribution) => contribution.group === "plan-mode"); + + contributions[1].run(); + expect(selections).toEqual(["default"]); + }); + + it("prefers the plan_mode feature and reads its canonical value", () => { + const calls: [string, unknown][] = []; + const contributions = buildAgentControlContributions( + makeSource({ + modes: { + options: CLAUDE_MODES, + selectedId: "default", + defaultModeId: "default", + }, + features: { + list: [toggleFeature("plan_mode", true, "list-todo")], + set: (id, value) => { + calls.push([id, value]); + }, + }, + }), + ).filter((contribution) => contribution.group === "plan-mode"); + + expect(contributions.map(selected)).toEqual([true, false]); + contributions[1].run(); + expect(calls).toEqual([["plan_mode", false]]); + }); + + it("publishes Fast only for an available toggle and ignores unrelated features", () => { + const calls: [string, unknown][] = []; + const contributions = buildAgentControlContributions( + makeSource({ + features: { + list: [toggleFeature("fast_mode", false, "zap"), toggleFeature("auto_accept", true)], + set: (id, value) => { + calls.push([id, value]); + }, + }, + }), + ); + const fast = contributions.filter((contribution) => contribution.group === "fast-mode"); + + expect(fast.map(selected)).toEqual([false, true]); + expect(contributions.some((contribution) => contribution.id.includes("auto_accept"))).toBe( + false, + ); + fast[0].run(); + expect(calls).toEqual([["fast_mode", true]]); + + const unavailable = buildAgentControlContributions(makeSource()); + expect(unavailable.filter((contribution) => contribution.group === "fast-mode")).toEqual([]); + }); + + it("centralizes query-only group ordering between built-in result sections", () => { + const contributions = buildAgentControlContributions( + makeSource({ + models: { + providers: [provider({ id: "claude", label: "Claude" })], + selectedProvider: "claude", + selectedModelId: "shared", + }, + thinking: { options: THINKING_OPTIONS, selectedId: "high" }, + modes: { + options: CLAUDE_MODES, + selectedId: "default", + defaultModeId: "default", + }, + features: { list: [toggleFeature("fast_mode")] }, + }), + ); + const ranks = new Map(); + for (const contribution of contributions) { + ranks.set(contribution.group, contribution.groupRank); + expect(contribution.visibility).toBe("query"); + } + + expect([...ranks.entries()]).toEqual([ + ["models", 1], + ["thinking", 1.1], + ["modes", 1.2], + ["plan-mode", 1.3], + ["fast-mode", 1.4], + ]); + for (const rank of ranks.values()) expect(rank).toBeLessThan(2); + expect(new Set(contributions.map((contribution) => contribution.id)).size).toBe( + contributions.length, + ); + }); +}); diff --git a/packages/app/src/command-center/agent-control-contributions.ts b/packages/app/src/command-center/agent-control-contributions.ts new file mode 100644 index 000000000..d8f31523e --- /dev/null +++ b/packages/app/src/command-center/agent-control-contributions.ts @@ -0,0 +1,319 @@ +import type { + AgentFeature, + AgentMode, + AgentProvider, + AgentSelectOption, +} from "@getpaseo/protocol/agent-types"; +import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import { formatAgentModeLabel, formatThinkingOptionLabel } from "@/agent-controls/labels"; +import { + FAST_MODE_FEATURE_ID, + isPlanningAgentMode, + PLAN_MODE_FEATURE_ID, + resolveNonPlanningModeId, +} from "@/agent-controls/policy"; +import type { CommandCenterContribution, CommandCenterIcon } from "./contributions"; + +const GROUP_RANK = { + models: 1, + thinking: 1.1, + modes: 1.2, + planMode: 1.3, + fastMode: 1.4, +} as const; + +export interface AgentControlContributionLabels { + model: string; + thinking: string; + mode: string; + planMode: string; + fast: string; + on: string; + off: string; + modelKeywords: string; + thinkingKeywords: string; + modeKeywords: string; + planKeywords: string; + fastKeywords: string; +} + +export function buildAgentControlContributionLabels( + t: (key: string) => string, +): AgentControlContributionLabels { + return { + model: t("shell.commandCenter.modelGroupLabel"), + thinking: t("shell.commandCenter.thinkingGroupLabel"), + mode: t("shell.commandCenter.modeGroupLabel"), + planMode: t("shell.commandCenter.planModeGroupLabel"), + fast: t("shell.commandCenter.fastModeGroupLabel"), + on: t("shell.commandCenter.settingOn"), + off: t("shell.commandCenter.settingOff"), + modelKeywords: t("shell.commandCenter.modelSearchKeywords"), + thinkingKeywords: t("shell.commandCenter.thinkingSearchKeywords"), + modeKeywords: t("shell.commandCenter.modeSearchKeywords"), + planKeywords: t("shell.commandCenter.planModeSearchKeywords"), + fastKeywords: t("shell.commandCenter.fastModeSearchKeywords"), + }; +} + +export interface AgentControlContributionIcons { + provider(provider: AgentProvider): CommandCenterIcon; + thinking: CommandCenterIcon; + planMode: CommandCenterIcon; + mode(modeId: string): CommandCenterIcon; + feature(feature: AgentFeature): CommandCenterIcon; +} + +interface CommandCenterChoice { + id: string; + path: readonly string[]; + keywords?: readonly string[]; + icon?: CommandCenterIcon; + selected: boolean; + testId?: string; + select(): void | Promise; +} + +interface CommandCenterChoiceGroup { + id: string; + rank: number; + label: string; + keywords: readonly string[]; + choices: readonly CommandCenterChoice[]; +} + +export interface AgentControlContributionSource { + serverId: string; + ownerKey: string; + provider: string | null; + labels: AgentControlContributionLabels; + icons: AgentControlContributionIcons; + models: { + providers: readonly ProviderSelectorProvider[]; + selectedProvider: string | null; + selectedModelId: string | null; + select(provider: AgentProvider, modelId: string): void | Promise; + }; + thinking: { + options: readonly AgentSelectOption[]; + selectedId: string | null; + select(optionId: string): void | Promise; + }; + modes?: { + options: readonly AgentMode[]; + selectedId: string | null; + defaultModeId: string | null; + select(modeId: string): void | Promise; + }; + features: { + list: readonly AgentFeature[]; + set(featureId: string, value: unknown): void | Promise; + }; +} + +function buildContributions( + groups: readonly CommandCenterChoiceGroup[], +): CommandCenterContribution[] { + const contributions: CommandCenterContribution[] = []; + for (const group of groups) { + for (const [rank, choice] of group.choices.entries()) { + contributions.push({ + id: `${group.id}:${choice.id}`, + group: group.id, + groupRank: group.rank, + rank, + keywords: [...group.keywords, ...(choice.keywords ?? [])], + visibility: "query", + run: () => { + if (!choice.selected) return choice.select(); + }, + presentation: { + kind: "choice", + path: [group.label, ...choice.path], + icon: choice.icon, + selected: choice.selected, + testId: choice.testId, + }, + }); + } + } + return contributions; +} + +function buildModelGroup(source: AgentControlContributionSource): CommandCenterChoiceGroup { + const choices: CommandCenterChoice[] = []; + for (const provider of source.models.providers) { + if (provider.modelSelection.kind !== "models") continue; + const agentProvider = provider.id; + const icon = source.icons.provider(agentProvider); + for (const model of provider.modelSelection.rows) { + if (!model.modelId) continue; + const modelId = model.modelId; + choices.push({ + id: `${provider.id}:${modelId}`, + path: [provider.label, model.modelLabel], + keywords: [modelId], + icon, + selected: + source.models.selectedProvider === provider.id && + source.models.selectedModelId === modelId, + testId: `command-center-model-${source.serverId}:${provider.id}:${modelId}`, + select: () => source.models.select(agentProvider, modelId), + }); + } + } + return { + id: "models", + rank: GROUP_RANK.models, + label: source.labels.model, + keywords: [source.labels.modelKeywords], + choices, + }; +} + +function buildThinkingGroup(source: AgentControlContributionSource): CommandCenterChoiceGroup { + const choices = + source.thinking.options.length > 1 + ? source.thinking.options.map( + (option): CommandCenterChoice => ({ + id: option.id, + path: [formatThinkingOptionLabel(option)], + icon: source.icons.thinking, + selected: option.id === source.thinking.selectedId, + testId: `command-center-thinking-${source.serverId}:${source.ownerKey}:${option.id}`, + select: () => source.thinking.select(option.id), + }), + ) + : []; + return { + id: "thinking", + rank: GROUP_RANK.thinking, + label: source.labels.thinking, + keywords: [source.labels.thinkingKeywords], + choices, + }; +} + +function buildModeGroup(source: AgentControlContributionSource): CommandCenterChoiceGroup { + const modes = source.modes; + const choices = modes + ? modes.options + .filter((mode) => !isPlanningAgentMode(mode)) + .map( + (mode): CommandCenterChoice => ({ + id: mode.id, + path: [formatAgentModeLabel(mode)], + icon: source.icons.mode(mode.id), + selected: mode.id === modes.selectedId, + testId: `command-center-mode-${source.serverId}:${source.ownerKey}:${mode.id}`, + select: () => modes.select(mode.id), + }), + ) + : []; + return { + id: "modes", + rank: GROUP_RANK.modes, + label: source.labels.mode, + keywords: [source.labels.modeKeywords], + choices, + }; +} + +function toggleChoices(input: { + source: AgentControlContributionSource; + key: "plan" | "fast"; + icon: CommandCenterIcon; + isOn: boolean; + turnOn(): void | Promise; + turnOff(): void | Promise; +}): CommandCenterChoice[] { + const states = [ + { id: "on", label: input.source.labels.on, selected: input.isOn, select: input.turnOn }, + { id: "off", label: input.source.labels.off, selected: !input.isOn, select: input.turnOff }, + ]; + return states.map((state) => ({ + id: state.id, + path: [state.label], + icon: input.icon, + selected: state.selected, + testId: `command-center-${input.key}-${input.source.serverId}:${input.source.ownerKey}:${state.id}`, + select: state.select, + })); +} + +function buildPlanModeGroup(source: AgentControlContributionSource): CommandCenterChoiceGroup { + const planFeature = source.features.list.find( + (feature) => feature.id === PLAN_MODE_FEATURE_ID && feature.type === "toggle", + ); + let choices: CommandCenterChoice[] = []; + if (planFeature?.type === "toggle") { + choices = toggleChoices({ + source, + key: "plan", + icon: source.icons.feature(planFeature), + isOn: planFeature.value, + turnOn: () => source.features.set(PLAN_MODE_FEATURE_ID, true), + turnOff: () => source.features.set(PLAN_MODE_FEATURE_ID, false), + }); + } else if (source.modes) { + const modes = source.modes; + const planMode = modes.options.find(isPlanningAgentMode); + const offModeId = resolveNonPlanningModeId(modes.options, modes.defaultModeId); + if (planMode && offModeId) { + choices = toggleChoices({ + source, + key: "plan", + icon: source.icons.planMode, + isOn: modes.selectedId === planMode.id, + turnOn: () => modes.select(planMode.id), + turnOff: () => modes.select(offModeId), + }); + } + } + return { + id: "plan-mode", + rank: GROUP_RANK.planMode, + label: source.labels.planMode, + keywords: [source.labels.planKeywords], + choices, + }; +} + +function buildFastModeGroup(source: AgentControlContributionSource): CommandCenterChoiceGroup { + const fastFeature = source.features.list.find( + (feature) => feature.id === FAST_MODE_FEATURE_ID && feature.type === "toggle", + ); + const choices = + fastFeature?.type === "toggle" + ? toggleChoices({ + source, + key: "fast", + icon: source.icons.feature(fastFeature), + isOn: fastFeature.value, + turnOn: () => source.features.set(FAST_MODE_FEATURE_ID, true), + turnOff: () => source.features.set(FAST_MODE_FEATURE_ID, false), + }) + : []; + return { + id: "fast-mode", + rank: GROUP_RANK.fastMode, + label: source.labels.fast, + keywords: [source.labels.fastKeywords], + choices, + }; +} + +export function buildAgentControlContributions( + source: AgentControlContributionSource, +): CommandCenterContribution[] { + const groups = [buildModelGroup(source)]; + if (source.provider) { + groups.push( + buildThinkingGroup(source), + buildModeGroup(source), + buildPlanModeGroup(source), + buildFastModeGroup(source), + ); + } + return buildContributions(groups); +} diff --git a/packages/app/src/command-center/agent-control-registration.tsx b/packages/app/src/command-center/agent-control-registration.tsx new file mode 100644 index 000000000..ecc7e8985 --- /dev/null +++ b/packages/app/src/command-center/agent-control-registration.tsx @@ -0,0 +1,156 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import type { + AgentFeature, + AgentMode, + AgentProvider, + AgentSelectOption, +} from "@getpaseo/protocol/agent-types"; +import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; +import { + getAgentFeatureIcon, + getAgentModeIcon, + PlanModeIcon, + ThinkingIcon, +} from "@/agent-controls/icons"; +import { getProviderIcon } from "@/components/provider-icons"; +import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import { + buildAgentControlContributions, + buildAgentControlContributionLabels, + type AgentControlContributionSource, +} from "./agent-control-contributions"; +import { getCommandCenterIcon } from "./icon"; +import { useCommandCenterActions } from "./provider"; + +export interface AgentControlCommandCenterSource { + serverId: string; + ownerKey: string; + provider: string | null | undefined; + providerDefinitions: AgentProviderDefinition[]; + models: { + providers: readonly ProviderSelectorProvider[]; + selectedProvider: string | null | undefined; + selectedModelId: string | null | undefined; + select(provider: AgentProvider, modelId: string): void | Promise; + }; + thinking: { + options: readonly AgentSelectOption[] | null | undefined; + selectedId: string | null | undefined; + select(optionId: string): void | Promise; + }; + modes?: { + options: readonly AgentMode[]; + selectedId: string | null | undefined; + select(modeId: string): void | Promise; + }; + features: { + list: readonly AgentFeature[] | undefined; + set?: (featureId: string, value: unknown) => void | Promise; + }; +} + +function ignoreAgentFeatureChange(): void { + return; +} + +function resolveDefaultModeId( + provider: string | null | undefined, + providerDefinitions: AgentProviderDefinition[], +): string | null { + return ( + providerDefinitions.find((definition) => definition.id === provider)?.defaultModeId ?? null + ); +} + +function buildModeContributionSource( + options: readonly AgentMode[] | undefined, + selectedId: string | null | undefined, + select: ((modeId: string) => void | Promise) | undefined, + defaultModeId: string | null, +): Pick { + if (!options || !select) return {}; + return { + modes: { + options, + selectedId: selectedId ?? null, + defaultModeId, + select, + }, + }; +} + +export function useAgentControlCommandCenterActions(input: { + sourceId: string; + enabled: boolean; + controls: AgentControlCommandCenterSource; +}): void { + const { t } = useTranslation(); + const { controls } = input; + const { features, models, modes, thinking } = controls; + const actions = useMemo( + () => + buildAgentControlContributions({ + serverId: controls.serverId, + ownerKey: controls.ownerKey, + provider: controls.provider ?? null, + labels: buildAgentControlContributionLabels(t), + icons: { + provider: (provider) => getCommandCenterIcon(getProviderIcon(provider)), + thinking: getCommandCenterIcon(ThinkingIcon), + planMode: getCommandCenterIcon(PlanModeIcon), + mode: (modeId) => + getCommandCenterIcon( + getAgentModeIcon(controls.provider ?? "", modeId, controls.providerDefinitions), + ), + feature: (feature) => getCommandCenterIcon(getAgentFeatureIcon(feature.icon)), + }, + models: { + providers: models.providers, + selectedProvider: models.selectedProvider ?? null, + selectedModelId: models.selectedModelId ?? null, + select: models.select, + }, + thinking: { + options: thinking.options ?? [], + selectedId: thinking.selectedId ?? null, + select: thinking.select, + }, + ...buildModeContributionSource( + modes?.options, + modes?.selectedId, + modes?.select, + resolveDefaultModeId(controls.provider, controls.providerDefinitions), + ), + features: { + list: features.set ? (features.list ?? []) : [], + set: features.set ?? ignoreAgentFeatureChange, + }, + }), + [ + controls.ownerKey, + controls.provider, + controls.providerDefinitions, + controls.serverId, + features.list, + features.set, + models.providers, + models.select, + models.selectedModelId, + models.selectedProvider, + modes?.options, + modes?.select, + modes?.selectedId, + thinking.options, + thinking.select, + thinking.selectedId, + t, + ], + ); + + useCommandCenterActions({ + sourceId: input.sourceId, + enabled: input.enabled, + actions, + }); +} diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx index f66cbf254..51b94eeca 100644 --- a/packages/app/src/command-center/command-center.tsx +++ b/packages/app/src/command-center/command-center.tsx @@ -401,6 +401,15 @@ interface ResultRowProps { const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRowProps) { const press = useCallback(() => onSelect(result), [onSelect, result]); + const choice = + result.kind === "contribution" && result.contribution.presentation.kind === "choice" + ? result.contribution.presentation + : null; + const accessibilityLabel = choice?.path.join(" › "); + const accessibilityState = useMemo( + () => (isNative && choice ? { selected: choice.selected } : undefined), + [choice], + ); const style = useCallback( ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.row, @@ -415,7 +424,15 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo [active, result], ); return ( - + ); @@ -497,7 +514,7 @@ function ResultContent({ result }: { result: CommandCenterResult }) { ); } return ( - + {Icon ? ( diff --git a/packages/app/src/command-center/icon.tsx b/packages/app/src/command-center/icon.tsx new file mode 100644 index 000000000..569e3575f --- /dev/null +++ b/packages/app/src/command-center/icon.tsx @@ -0,0 +1,19 @@ +import { withUnistyles } from "react-native-unistyles"; +import type { AgentControlIcon } from "@/agent-controls/icons"; +import type { CommandCenterIcon, CommandCenterIconProps } from "./contributions"; + +const commandCenterIcons = new Map(); + +export function getCommandCenterIcon(Component: AgentControlIcon): CommandCenterIcon { + const cached = commandCenterIcons.get(Component); + if (cached) return cached; + + const ThemedIcon = withUnistyles(Component, (theme) => ({ + color: theme.colors.foregroundMuted, + })); + function CommandCenterIconAdapter({ size }: CommandCenterIconProps) { + return ; + } + commandCenterIcons.set(Component, CommandCenterIconAdapter); + return CommandCenterIconAdapter; +} diff --git a/packages/app/src/command-center/model-contributions.test.tsx b/packages/app/src/command-center/model-contributions.test.tsx deleted file mode 100644 index 6e38b33e1..000000000 --- a/packages/app/src/command-center/model-contributions.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; -import { buildModelChoiceContributions } from "./model-contributions"; - -function TestIcon() { - return null; -} - -function provider(input: { - id: string; - label: string; - state: "models" | "error"; -}): ProviderSelectorProvider { - if (input.state === "error") { - return { - id: input.id, - label: input.label, - modelSelection: { kind: "error", message: "Unavailable" }, - }; - } - return { - id: input.id, - label: input.label, - modelSelection: { - kind: "models", - rows: [ - { - favoriteKey: `${input.id}:model`, - provider: input.id, - providerLabel: input.label, - modelId: "model", - modelLabel: `${input.label} model`, - description: undefined, - }, - ], - }, - }; -} - -describe("Command Center model choices", () => { - it("publishes selectable providers and executes the draft owner's command", () => { - const selections: string[] = []; - const choices = buildModelChoiceContributions({ - serverId: "host", - providers: [ - provider({ id: "claude", label: "Claude", state: "models" }), - provider({ id: "codex", label: "Codex", state: "models" }), - ], - selectedProvider: "codex", - selectedModelId: "model", - groupLabel: "Model", - searchKeywords: "model switch", - getIcon: () => TestIcon, - select: (selectedProvider, modelId) => selections.push(`${selectedProvider}:${modelId}`), - }); - - expect( - choices.map((choice) => ({ - id: choice.id, - selected: choice.presentation.kind === "choice" ? choice.presentation.selected : false, - })), - ).toEqual([ - { id: "host:claude:model", selected: false }, - { id: "host:codex:model", selected: true }, - ]); - choices[0].run(); - choices[1].run(); - expect(selections).toEqual(["claude:model"]); - }); - - it("does not publish unavailable providers or no-op default rows", () => { - const choices = buildModelChoiceContributions({ - serverId: "host", - providers: [ - provider({ id: "unavailable", label: "Unavailable", state: "error" }), - { - id: "empty", - label: "Empty", - modelSelection: { - kind: "models", - rows: [ - { - favoriteKey: "empty:", - provider: "empty", - providerLabel: "Empty", - modelId: "", - modelLabel: "Default", - description: undefined, - }, - ], - }, - }, - ], - selectedProvider: null, - selectedModelId: null, - groupLabel: "Model", - searchKeywords: "model switch", - getIcon: () => TestIcon, - select: () => undefined, - }); - - expect(choices).toEqual([]); - }); -}); diff --git a/packages/app/src/command-center/model-contributions.ts b/packages/app/src/command-center/model-contributions.ts deleted file mode 100644 index 182a528c9..000000000 --- a/packages/app/src/command-center/model-contributions.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AgentProvider } from "@getpaseo/protocol/agent-types"; -import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; -import type { CommandCenterContribution, CommandCenterIcon } from "./contributions"; - -interface ModelChoiceInput { - serverId: string; - providers: readonly ProviderSelectorProvider[]; - selectedProvider: string | null; - selectedModelId: string | null; - groupLabel: string; - searchKeywords: string; - getIcon(provider: AgentProvider): CommandCenterIcon; - select(provider: AgentProvider, modelId: string): void; -} - -export function buildModelChoiceContributions( - input: ModelChoiceInput, -): CommandCenterContribution[] { - const contributions: CommandCenterContribution[] = []; - let rank = 0; - for (const provider of input.providers) { - if (provider.modelSelection.kind !== "models") continue; - const agentProvider = provider.id; - const icon = input.getIcon(agentProvider); - for (const model of provider.modelSelection.rows) { - if (!model.modelId) continue; - const modelId = model.modelId; - const selected = input.selectedProvider === provider.id && input.selectedModelId === modelId; - contributions.push({ - id: `${input.serverId}:${provider.id}:${modelId}`, - group: "models", - groupRank: 1, - rank, - keywords: [modelId, input.searchKeywords], - visibility: "query", - run: () => { - if (!selected) input.select(agentProvider, modelId); - }, - presentation: { - kind: "choice", - path: [input.groupLabel, provider.label, model.modelLabel], - icon, - selected, - testId: `command-center-model-${input.serverId}:${provider.id}:${modelId}`, - }, - }); - rank += 1; - } - } - return contributions; -} diff --git a/packages/app/src/command-center/provider-icon.tsx b/packages/app/src/command-center/provider-icon.tsx deleted file mode 100644 index 245d5de27..000000000 --- a/packages/app/src/command-center/provider-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { withUnistyles } from "react-native-unistyles"; -import type { AgentProvider } from "@getpaseo/protocol/agent-types"; -import { getProviderIcon } from "@/components/provider-icons"; -import type { CommandCenterIcon, CommandCenterIconProps } from "./contributions"; - -const commandCenterProviderIcons = new Map(); - -export function getCommandCenterProviderIcon(provider: AgentProvider): CommandCenterIcon { - const cached = commandCenterProviderIcons.get(provider); - if (cached) return cached; - - const ProviderIcon = withUnistyles(getProviderIcon(provider), (theme) => ({ - color: theme.colors.foregroundMuted, - })); - function CommandCenterProviderIcon({ size }: CommandCenterIconProps) { - return ; - } - commandCenterProviderIcons.set(provider, CommandCenterProviderIcon); - return CommandCenterProviderIcon; -} diff --git a/packages/app/src/components/schedules/schedule-form-sheet.tsx b/packages/app/src/components/schedules/schedule-form-sheet.tsx index febfbf6ca..7931b9548 100644 --- a/packages/app/src/components/schedules/schedule-form-sheet.tsx +++ b/packages/app/src/components/schedules/schedule-form-sheet.tsx @@ -33,7 +33,7 @@ import { type SelectFieldOption, type SelectFieldRenderOptionInput, } from "@/components/ui/select-field"; -import { formatThinkingOptionLabel } from "@/composer/agent-controls/utils"; +import { formatThinkingOptionLabel } from "@/agent-controls/labels"; import { mergeProviderPreferences, useFormPreferences, diff --git a/packages/app/src/composer/agent-controls/control.tsx b/packages/app/src/composer/agent-controls/control.tsx index f8d23390f..2077c86ab 100644 --- a/packages/app/src/composer/agent-controls/control.tsx +++ b/packages/app/src/composer/agent-controls/control.tsx @@ -1,16 +1,10 @@ -import { forwardRef, useCallback, type ComponentType } from "react"; +import { forwardRef, useCallback } from "react"; import { Text, View, type PressableStateCallbackType } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { useComposerControlLayout } from "@/composer/agent-controls/layout-context"; import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph"; - -export interface AgentControlIconProps { - size?: number; - color?: string; -} - -export type AgentControlIcon = ComponentType; +import type { AgentControlIcon } from "@/agent-controls/icons"; interface AgentControlTriggerProps { icon: AgentControlIcon; diff --git a/packages/app/src/composer/agent-controls/index.tsx b/packages/app/src/composer/agent-controls/index.tsx index dc9b0bee0..6e727fee0 100644 --- a/packages/app/src/composer/agent-controls/index.tsx +++ b/packages/app/src/composer/agent-controls/index.tsx @@ -22,7 +22,9 @@ import { } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useShallow } from "zustand/shallow"; -import { Brain, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native"; +import { Settings2 } from "lucide-react-native"; +import { getAgentFeatureIcon, ThinkingIcon } from "@/agent-controls/icons"; +import { formatThinkingOptionLabel } from "@/agent-controls/labels"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { CombinedModelSelector } from "@/components/combined-model-selector"; import { @@ -58,16 +60,13 @@ import { getFeatureHighlightColor, getFeatureTooltip, getAgentControlHintKey, - formatThinkingOptionLabel, resolveAgentModelSelection, } from "@/composer/agent-controls/utils"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useToast } from "@/contexts/toast-context"; import { toErrorMessage } from "@/utils/error-messages"; import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; -import { useCommandCenterActions } from "@/command-center/provider"; -import { buildModelChoiceContributions } from "@/command-center/model-contributions"; -import { getCommandCenterProviderIcon } from "@/command-center/provider-icon"; +import { useAgentControlCommandCenterActions } from "@/command-center/agent-control-registration"; import { isNative } from "@/constants/platform"; import { resolveComposerControlDensity, @@ -88,6 +87,8 @@ interface AgentControlOption { type AgentControlSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`; +const EMPTY_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = []; + interface ControlledAgentControlsProps { provider: string; providerOptions?: AgentControlOption[]; @@ -164,14 +165,17 @@ function findOptionLabel( return selected?.label ?? fallback; } -const FEATURE_ICONS: Record = { - "list-todo": ListTodo, - "shield-check": ShieldCheck, - zap: Zap, -}; +function toCommandCenterModes(modeControl: AgentModeControlValue | null) { + if (!modeControl) return undefined; + return { + options: modeControl.modeOptions, + selectedId: modeControl.selectedModeId, + select: modeControl.onSelectMode, + }; +} -function getFeatureIcon(icon?: string) { - return (icon && FEATURE_ICONS[icon]) || Settings2; +function getModeProviderDefinitions(modeControl: AgentModeControlValue | null) { + return modeControl?.providerDefinitions ?? EMPTY_AGENT_PROVIDER_DEFINITIONS; } function getFeatureIconColor( @@ -923,7 +927,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { @@ -1246,7 +1250,7 @@ function DesktopFeatureItem({ } if (feature.type === "select") { - const FeatureIcon = getFeatureIcon(feature.icon); + const FeatureIcon = getAgentFeatureIcon(feature.icon); const selectedOption = feature.options.find((o) => o.id === feature.value); return ( <> @@ -1334,7 +1338,7 @@ function SheetFeatureItem({ ); if (feature.type === "toggle") { - const FeatureIcon = getFeatureIcon(feature.icon); + const FeatureIcon = getAgentFeatureIcon(feature.icon); return ( o.id === feature.value); return ( <> @@ -1402,7 +1406,7 @@ function ThinkingComboboxOption({ onPress: () => void; iconColor: string; }) { - const leadingSlot = useMemo(() => , [iconColor]); + const leadingSlot = useMemo(() => , [iconColor]); return ( selectAgentControlsSlice(state, serverId, agentId)), @@ -1429,6 +1432,8 @@ export const AgentControls = memo(function AgentControls({ const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); const toast = useToast(); const modeControl = useLiveAgentModeControl(serverId, agentId); + const commandCenterModes = toCommandCenterModes(modeControl); + const modeProviderDefinitions = getModeProviderDefinitions(modeControl); const { entries: snapshotEntries, @@ -1504,9 +1509,7 @@ export const AgentControls = memo(function AgentControls({ mergeProviderPreferences({ preferences: current, provider: agentProvider, - updates: { - model: modelId, - }, + updates: { model: modelId }, }), ); } catch (error) { @@ -1516,26 +1519,10 @@ export const AgentControls = memo(function AgentControls({ }, [agentId, agentProvider, client, toast, updatePreferences], ); - - const commandCenterModelActions = useMemo( - () => - buildModelChoiceContributions({ - serverId, - providers: agentModelSelectorProviders, - selectedProvider: agentProvider ?? null, - selectedModelId: activeModelId, - groupLabel: t("shell.commandCenter.modelGroupLabel"), - searchKeywords: t("shell.commandCenter.modelSearchKeywords"), - getIcon: getCommandCenterProviderIcon, - select: (_provider, modelId) => handleSelectModel(modelId), - }), - [activeModelId, agentModelSelectorProviders, agentProvider, handleSelectModel, serverId, t], + const handleSelectCommandCenterModel = useCallback( + (_provider: AgentProvider, modelId: string) => handleSelectModel(modelId), + [handleSelectModel], ); - useCommandCenterActions({ - sourceId: `agent:${serverId}:${agentId}`, - enabled: isPaneFocused && Boolean(client), - actions: commandCenterModelActions, - }); const handleToggleFavoriteModel = useCallback( (provider: string, modelId: string) => { @@ -1606,6 +1593,33 @@ export const AgentControls = memo(function AgentControls({ [agentId, agentProvider, client, toast, updatePreferences], ); + useAgentControlCommandCenterActions({ + sourceId: `agent:${serverId}:${agentId}`, + enabled: isPaneFocused && Boolean(client), + controls: { + serverId, + ownerKey: agentId, + provider: agentProvider, + providerDefinitions: modeProviderDefinitions, + models: { + providers: agentModelSelectorProviders, + selectedProvider: agentProvider, + selectedModelId: activeModelId, + select: handleSelectCommandCenterModel, + }, + thinking: { + options: modelSelection.thinkingOptions, + selectedId: modelSelection.selectedThinkingId, + select: handleSelectThinkingOption, + }, + modes: commandCenterModes, + features: { + list: agent?.features, + set: handleSetFeature, + }, + }, + }); + const handleModelSelectorOpen = useCallback(() => { refetchSnapshotIfStale(agentProvider); }, [agentProvider, refetchSnapshotIfStale]); diff --git a/packages/app/src/composer/agent-controls/mode-control.tsx b/packages/app/src/composer/agent-controls/mode-control.tsx index c4c049c77..4534dd150 100644 --- a/packages/app/src/composer/agent-controls/mode-control.tsx +++ b/packages/app/src/composer/agent-controls/mode-control.tsx @@ -1,38 +1,15 @@ -import { - useCallback, - useMemo, - useRef, - useState, - type ComponentType, - type ReactElement, -} from "react"; +import { useCallback, useMemo, useRef, useState, type ReactElement } from "react"; import { useTranslation } from "react-i18next"; import { Text, View } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useShallow } from "zustand/shallow"; import { useStoreWithEqualityFn } from "zustand/traditional"; -import { - Bot, - Shield, - ShieldAlert, - ShieldCheck, - ShieldEllipsis, - ShieldOff, - ShieldPlus, - ShieldQuestionMark, -} from "lucide-react-native"; import { type SheetHeader } from "@/components/adaptive-modal-sheet"; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; -import { useSessionStore } from "@/stores/session-store"; -import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; -import { mergeProviderPreferences, useFormPreferences } from "@/hooks/use-form-preferences"; -import { resolveProviderDefinition } from "@/utils/provider-definitions"; -import { useToast } from "@/contexts/toast-context"; -import { toErrorMessage } from "@/utils/error-messages"; -import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; -import { formatAgentModeLabel, getAgentControlHintKey } from "@/composer/agent-controls/utils"; +import { formatAgentModeLabel } from "@/agent-controls/labels"; +import { getAgentControlHintKey } from "@/composer/agent-controls/utils"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; @@ -40,25 +17,16 @@ import { resolveNextAgentModeId } from "@/composer/agent-controls/mode"; import { useComposerKeyboardScope } from "@/composer/keyboard-scope"; import { useComposerControlLayout } from "@/composer/agent-controls/layout-context"; import { AgentControlTrigger } from "@/composer/agent-controls/control"; +import { useSessionStore } from "@/stores/session-store"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import { mergeProviderPreferences, useFormPreferences } from "@/hooks/use-form-preferences"; +import { resolveProviderDefinition } from "@/utils/provider-definitions"; +import { useToast } from "@/contexts/toast-context"; +import { toErrorMessage } from "@/utils/error-messages"; +import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; import type { AgentMode } from "@getpaseo/protocol/agent-types"; -import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; - -interface ModeIconProps { - size?: number; - color?: string; -} - -const MODE_ICONS: Record> = { - Bot, - Shield, - ShieldCheck, - ShieldAlert, - ShieldEllipsis, - ShieldOff, - ShieldPlus, - ShieldQuestionMark, -}; - +import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; +import { getAgentModeIcon, getAgentModeOptionIcon } from "@/agent-controls/icons"; interface ModeComboboxOptionProps { option: ComboboxOption; selected: boolean; @@ -78,8 +46,7 @@ function ModeComboboxOption({ providerDefinitions, iconColor, }: ModeComboboxOptionProps) { - const visuals = getModeVisuals(provider, option.id, providerDefinitions); - const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : undefined; + const IconComponent = getAgentModeOptionIcon(provider, option.id, providerDefinitions); const leadingSlot = useMemo( () => (IconComponent ? : null), [IconComponent, iconColor], @@ -134,10 +101,7 @@ export function AgentModeControl({ return modeOptions.find((m) => m.id === selectedModeId) ?? modeOptions[0]; }, [modeOptions, selectedModeId]); - const visuals = selectedMode - ? getModeVisuals(provider, selectedMode.id, providerDefinitions) - : undefined; - const Icon = visuals?.icon ? (MODE_ICONS[visuals.icon] ?? Bot) : Bot; + const Icon = getAgentModeIcon(provider, selectedMode?.id ?? "", providerDefinitions); const iconColor = theme.colors.foregroundMuted; const selectedModeLabel = selectedMode ? formatAgentModeLabel(selectedMode) : ""; @@ -316,9 +280,7 @@ export function useLiveAgentModeControl( mergeProviderPreferences({ preferences: current, provider: slice.provider, - updates: { - mode: modeId || undefined, - }, + updates: { mode: modeId || undefined }, }), ).catch((error) => { console.warn("[AgentModeControl] persist mode preference failed", error); diff --git a/packages/app/src/composer/agent-controls/utils.test.ts b/packages/app/src/composer/agent-controls/utils.test.ts index 1ab8e0b22..cd6934d12 100644 --- a/packages/app/src/composer/agent-controls/utils.test.ts +++ b/packages/app/src/composer/agent-controls/utils.test.ts @@ -1,10 +1,8 @@ import { describe, expect, it } from "vitest"; import { - formatAgentModeLabel, getFeatureHighlightColor, getFeatureTooltip, getAgentControlHintKey, - formatThinkingOptionLabel, normalizeModelId, resolveAgentModelSelection, } from "./utils"; @@ -54,36 +52,6 @@ describe("normalizeModelId", () => { }); }); -describe("formatAgentModeLabel", () => { - it("sentence-cases provider mode labels", () => { - expect(formatAgentModeLabel({ id: "plan", label: "Plan" })).toBe("Plan"); - expect(formatAgentModeLabel({ id: "full-access", label: "Full Access" })).toBe("Full access"); - expect(formatAgentModeLabel({ id: "auto-review", label: "Auto-review" })).toBe("Auto-review"); - expect(formatAgentModeLabel({ id: "read_only", label: "read_only" })).toBe("Read only"); - expect(formatAgentModeLabel({ id: "acceptEdits", label: "acceptEdits" })).toBe("Accept edits"); - }); - - it("splits compact mode ids when no provider label is available", () => { - expect(formatAgentModeLabel({ id: "auto-review" })).toBe("Auto review"); - }); -}); - -describe("formatThinkingOptionLabel", () => { - it("formats compact thinking option labels for display", () => { - expect(formatThinkingOptionLabel({ id: "none", label: "none" })).toBe("None"); - expect(formatThinkingOptionLabel({ id: "low", label: "low" })).toBe("Low"); - expect(formatThinkingOptionLabel({ id: "medium", label: "medium" })).toBe("Medium"); - expect(formatThinkingOptionLabel({ id: "high", label: "high" })).toBe("High"); - expect(formatThinkingOptionLabel({ id: "xhigh", label: "xhigh" })).toBe("Extra high"); - }); - - it("sentence-cases split provider labels", () => { - expect(formatThinkingOptionLabel({ id: "extra_high", label: "extra_high" })).toBe("Extra high"); - expect(formatThinkingOptionLabel({ id: "think-hard", label: "think-hard" })).toBe("Think hard"); - expect(formatThinkingOptionLabel({ id: "xhigh", label: "XHigh" })).toBe("Extra high"); - }); -}); - describe("resolveAgentModelSelection", () => { it("prefers runtime model over configured model", () => { const selection = resolveAgentModelSelection({ diff --git a/packages/app/src/composer/agent-controls/utils.ts b/packages/app/src/composer/agent-controls/utils.ts index e2229a0e9..7313e160b 100644 --- a/packages/app/src/composer/agent-controls/utils.ts +++ b/packages/app/src/composer/agent-controls/utils.ts @@ -1,5 +1,7 @@ import type { AgentFeature, AgentModelDefinition } from "@getpaseo/protocol/agent-types"; import { i18n } from "@/i18n/i18next"; +import { formatThinkingOptionLabel } from "@/agent-controls/labels"; +import { FAST_MODE_FEATURE_ID, PLAN_MODE_FEATURE_ID } from "@/agent-controls/policy"; export type ExplainedAgentControl = "mode" | "model" | "thinking"; export type FeatureHighlightColor = "blue" | "default" | "green" | "yellow"; @@ -35,61 +37,17 @@ export function getFeatureTooltip(feature: Pick - buildModelChoiceContributions({ - serverId, - providers: composerState.modelSelectorProviders, - selectedProvider: composerState.selectedProvider, - selectedModelId: composerState.effectiveModelId || null, - groupLabel: t("shell.commandCenter.modelGroupLabel"), - searchKeywords: t("shell.commandCenter.modelSearchKeywords"), - getIcon: getCommandCenterProviderIcon, - select: composerState.setProviderAndModelFromUser, - }), - [ - composerState.effectiveModelId, - composerState.modelSelectorProviders, - composerState.selectedProvider, - composerState.setProviderAndModelFromUser, - serverId, - t, - ], - ); + const draftProvider = composerState.selectedProvider; + const draftProviderDefinitions = composerState.providerDefinitions; + const draftThinkingOptions = composerState.availableThinkingOptions; + const draftSelectedThinkingId = composerState.selectedThinkingOptionId; + const draftSetThinkingOption = composerState.setThinkingOptionFromUser; + const draftModeOptions = composerState.modeOptions; + const draftSelectedMode = composerState.selectedMode; + const draftSetMode = composerState.setModeFromUser; + const draftFeatures = composerState.agentControls.features; + const draftOnSetFeature = composerState.agentControls.onSetFeature; + const clearDraftInput = draftInput.clear; const setDraftText = draftInput.setText; const setDraftAttachments = draftInput.setAttachments; @@ -541,12 +529,36 @@ export function WorkspaceDraftAgentTab({ onCreated(result); }, }); - useCommandCenterActions({ + useAgentControlCommandCenterActions({ sourceId: `draft:${serverId}:${tabId}`, enabled: isPaneFocused && !isSubmitting, - actions: draftModelActions, + controls: { + serverId, + ownerKey: tabId, + provider: draftProvider, + providerDefinitions: draftProviderDefinitions, + models: { + providers: composerState.modelSelectorProviders, + selectedProvider: draftProvider, + selectedModelId: composerState.effectiveModelId, + select: composerState.setProviderAndModelFromUser, + }, + thinking: { + options: draftThinkingOptions, + selectedId: draftSelectedThinkingId, + select: draftSetThinkingOption, + }, + modes: { + options: draftModeOptions, + selectedId: draftSelectedMode, + select: draftSetMode, + }, + features: { + list: draftFeatures, + set: draftOnSetFeature, + }, + }, }); - const isReadyForPendingAutoSubmit = Boolean( pendingAutoSubmit && draftInput.isHydrated && diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts index 124c6643e..fcc16aa67 100644 --- a/packages/app/src/i18n/resources.test.ts +++ b/packages/app/src/i18n/resources.test.ts @@ -188,6 +188,12 @@ describe("translation resources", () => { expect(en.shell.commandCenter.modelSearchKeywords).toBe( "switch model change model set model select model", ); + expect(en.shell.commandCenter.thinkingGroupLabel).toBe("Thinking"); + expect(en.shell.commandCenter.modeGroupLabel).toBe("Mode"); + expect(en.shell.commandCenter.planModeGroupLabel).toBe("Plan mode"); + expect(en.shell.commandCenter.fastModeGroupLabel).toBe("Fast"); + expect(en.shell.commandCenter.settingOn).toBe("On"); + expect(en.shell.commandCenter.settingOff).toBe("Off"); }); it("includes composer and agent workflow keys for the Batch 2 migration", () => { diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index c24cb3ddb..4a9ffe67f 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -64,6 +64,16 @@ export const ar: TranslationResources = { home: "بيت", modelGroupLabel: "النموذج", modelSearchKeywords: "تبديل النموذج تغيير النموذج تعيين النموذج اختيار النموذج", + thinkingGroupLabel: "التفكير", + thinkingSearchKeywords: "الاستدلال الجهد التفكير", + modeGroupLabel: "الوضع", + modeSearchKeywords: "الوصول الإذن الموافقة الوضع", + planModeGroupLabel: "وضع التخطيط", + planModeSearchKeywords: "التخطيط خطة وضع التخطيط", + fastModeGroupLabel: "سريع", + fastModeSearchKeywords: "سريع سرعة زمن انتقال منخفض", + settingOn: "تشغيل", + settingOff: "إيقاف", }, }, composer: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index dd0191c7a..a5aef1b40 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -62,6 +62,16 @@ export const en = { home: "Home", modelGroupLabel: "Model", modelSearchKeywords: "switch model change model set model select model", + thinkingGroupLabel: "Thinking", + thinkingSearchKeywords: "reasoning effort thinking think", + modeGroupLabel: "Mode", + modeSearchKeywords: "access permission approval mode", + planModeGroupLabel: "Plan mode", + planModeSearchKeywords: "plan planning plan mode", + fastModeGroupLabel: "Fast", + fastModeSearchKeywords: "fast speed low latency", + settingOn: "On", + settingOff: "Off", }, }, composer: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index ebc95b0b7..eee598789 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -64,6 +64,16 @@ export const es: TranslationResources = { home: "Hogar", modelGroupLabel: "Modelo", modelSearchKeywords: "cambiar modelo modificar modelo establecer modelo seleccionar modelo", + thinkingGroupLabel: "Razonamiento", + thinkingSearchKeywords: "razonamiento esfuerzo pensamiento", + modeGroupLabel: "Modo", + modeSearchKeywords: "acceso permiso aprobación modo", + planModeGroupLabel: "Modo plan", + planModeSearchKeywords: "plan planificación modo plan", + fastModeGroupLabel: "Rápido", + fastModeSearchKeywords: "rápido velocidad baja latencia", + settingOn: "Activado", + settingOff: "Desactivado", }, }, composer: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index ab59cee82..a015a9156 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -66,6 +66,16 @@ export const fr: TranslationResources = { modelGroupLabel: "Modèle", modelSearchKeywords: "changer de modèle modifier le modèle définir le modèle sélectionner le modèle", + thinkingGroupLabel: "Raisonnement", + thinkingSearchKeywords: "raisonnement effort réflexion", + modeGroupLabel: "Mode", + modeSearchKeywords: "accès permission approbation mode", + planModeGroupLabel: "Mode plan", + planModeSearchKeywords: "plan planification mode plan", + fastModeGroupLabel: "Rapide", + fastModeSearchKeywords: "rapide vitesse faible latence", + settingOn: "Activé", + settingOff: "Désactivé", }, }, composer: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 0cd866913..54b19efc3 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -64,6 +64,16 @@ export const ja: TranslationResources = { home: "ホーム", modelGroupLabel: "モデル", modelSearchKeywords: "モデルを切り替え モデルを変更 モデルを設定 モデルを選択", + thinkingGroupLabel: "思考", + thinkingSearchKeywords: "推論 思考レベル 考える", + modeGroupLabel: "モード", + modeSearchKeywords: "アクセス 権限 承認 モード", + planModeGroupLabel: "プランモード", + planModeSearchKeywords: "プラン 計画 プランモード", + fastModeGroupLabel: "高速", + fastModeSearchKeywords: "高速 速度 低遅延", + settingOn: "オン", + settingOff: "オフ", }, }, composer: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index a826e149c..b79f7b977 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -64,6 +64,16 @@ export const ptBR: TranslationResources = { home: "Início", modelGroupLabel: "Modelo", modelSearchKeywords: "trocar modelo mudar modelo definir modelo selecionar modelo", + thinkingGroupLabel: "Raciocínio", + thinkingSearchKeywords: "raciocínio esforço pensamento", + modeGroupLabel: "Modo", + modeSearchKeywords: "acesso permissão aprovação modo", + planModeGroupLabel: "Modo plano", + planModeSearchKeywords: "plano planejamento modo plano", + fastModeGroupLabel: "Rápido", + fastModeSearchKeywords: "rápido velocidade baixa latência", + settingOn: "Ligado", + settingOff: "Desligado", }, }, composer: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 2e2fbc30a..5557989c0 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -64,6 +64,16 @@ export const ru: TranslationResources = { home: "Дом", modelGroupLabel: "Модель", modelSearchKeywords: "сменить модель изменить модель выбрать модель установить модель", + thinkingGroupLabel: "Мышление", + thinkingSearchKeywords: "рассуждение усилие мышление", + modeGroupLabel: "Режим", + modeSearchKeywords: "доступ разрешение одобрение режим", + planModeGroupLabel: "Режим плана", + planModeSearchKeywords: "план планирование режим плана", + fastModeGroupLabel: "Быстрый", + fastModeSearchKeywords: "быстрый скорость низкая задержка", + settingOn: "Вкл", + settingOff: "Выкл", }, }, composer: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 0fb6af49c..d40c499c6 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -64,6 +64,16 @@ export const zhCN: TranslationResources = { home: "首页", modelGroupLabel: "模型", modelSearchKeywords: "切换模型 更改模型 设置模型 选择模型", + thinkingGroupLabel: "思考", + thinkingSearchKeywords: "推理 努力 思考", + modeGroupLabel: "模式", + modeSearchKeywords: "访问 权限 批准 模式", + planModeGroupLabel: "计划模式", + planModeSearchKeywords: "计划 规划 计划模式", + fastModeGroupLabel: "快速", + fastModeSearchKeywords: "快速 速度 低延迟", + settingOn: "开", + settingOff: "关", }, }, composer: { diff --git a/packages/app/src/schedules/schedule-form-model.ts b/packages/app/src/schedules/schedule-form-model.ts index d81c5c92a..483545391 100644 --- a/packages/app/src/schedules/schedule-form-model.ts +++ b/packages/app/src/schedules/schedule-form-model.ts @@ -6,7 +6,7 @@ import type { } from "@getpaseo/protocol/agent-types"; import type { ScheduleCadence, ScheduleSummary } from "@getpaseo/protocol/schedule/types"; import type { FormPreferences } from "@/create-agent-preferences/preferences"; -import { formatThinkingOptionLabel } from "@/composer/agent-controls/utils"; +import { formatThinkingOptionLabel } from "@/agent-controls/labels"; import { buildSelectableProviderSelectorProviders, type ProviderSelectorProvider,