Switch reasoning, mode, plan, and fast from the Command Center (#2274)
* 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 "<Setting> › <Value>" 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 <noreply@anthropic.com> * Unify Command Center agent controls * refactor: clarify agent control ownership * refactor(app): tighten command center agent controls --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Mohamed Boudra
parent
1ddca07d57
commit
35f985baff
@@ -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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
for (const choice of choices) {
|
||||||
|
await openCommandCenter(page);
|
||||||
|
await chooseCommandCenterAgentControl({ page, ...choice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectFocusedAgentControls(
|
||||||
|
page: Page,
|
||||||
|
choices: readonly CommandCenterAgentControlChoice[],
|
||||||
|
): Promise<void> {
|
||||||
|
await openCommandCenter(page);
|
||||||
|
for (const choice of choices) {
|
||||||
|
await expectCommandCenterAgentControlSelected({ page, ...choice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitDraftAgent(page: Page, prompt: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
await expect(page.getByRole("textbox", { name: "Message agent..." }).first()).toBeEditable({
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectWorkspaceAgentConfiguration(
|
||||||
|
workspace: Pick<SeededWorkspace, "client" | "workspaceId">,
|
||||||
|
expected: { id?: string; provider: string; model: string | null; modeId: string | null },
|
||||||
|
): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -7,3 +7,8 @@ export async function openCommandCenter(page: Page): Promise<Locator> {
|
|||||||
await expect(panel).toBeVisible({ timeout: 30_000 });
|
await expect(panel).toBeVisible({ timeout: 30_000 });
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function closeCommandCenter(page: Page): Promise<void> {
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.getByTestId("command-center-panel")).not.toBeVisible();
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export interface SeedDaemonClient {
|
|||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
updateAgent(agentId: string, updates: { name?: string }): Promise<void>;
|
updateAgent(agentId: string, updates: { name?: string }): Promise<void>;
|
||||||
|
setAgentMode(agentId: string, modeId: string): Promise<unknown>;
|
||||||
waitForAgentUpsert(
|
waitForAgentUpsert(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
predicate: (snapshot: { status: string }) => boolean,
|
predicate: (snapshot: { status: string }) => boolean,
|
||||||
|
|||||||
@@ -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<AgentControlIconProps>;
|
||||||
|
|
||||||
|
export const ThinkingIcon = Brain;
|
||||||
|
export const PlanModeIcon = ListTodo;
|
||||||
|
|
||||||
|
const MODE_ICONS: Record<string, AgentControlIcon> = {
|
||||||
|
Bot,
|
||||||
|
Shield,
|
||||||
|
ShieldAlert,
|
||||||
|
ShieldCheck,
|
||||||
|
ShieldEllipsis,
|
||||||
|
ShieldOff,
|
||||||
|
ShieldPlus,
|
||||||
|
ShieldQuestionMark,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FEATURE_ICONS: Record<string, AgentControlIcon> = {
|
||||||
|
"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;
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<AgentMode, "id" | "colorTier">): 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;
|
||||||
|
}
|
||||||
@@ -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<AgentControlContributionSource["models"]>;
|
||||||
|
thinking?: Partial<AgentControlContributionSource["thinking"]>;
|
||||||
|
modes?: Partial<NonNullable<AgentControlContributionSource["modes"]>>;
|
||||||
|
features?: Partial<AgentControlContributionSource["features"]>;
|
||||||
|
} = {},
|
||||||
|
): 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<string, number>();
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
};
|
||||||
|
thinking: {
|
||||||
|
options: readonly AgentSelectOption[];
|
||||||
|
selectedId: string | null;
|
||||||
|
select(optionId: string): void | Promise<void>;
|
||||||
|
};
|
||||||
|
modes?: {
|
||||||
|
options: readonly AgentMode[];
|
||||||
|
selectedId: string | null;
|
||||||
|
defaultModeId: string | null;
|
||||||
|
select(modeId: string): void | Promise<void>;
|
||||||
|
};
|
||||||
|
features: {
|
||||||
|
list: readonly AgentFeature[];
|
||||||
|
set(featureId: string, value: unknown): void | Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
turnOff(): void | Promise<void>;
|
||||||
|
}): 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);
|
||||||
|
}
|
||||||
@@ -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<void>;
|
||||||
|
};
|
||||||
|
thinking: {
|
||||||
|
options: readonly AgentSelectOption[] | null | undefined;
|
||||||
|
selectedId: string | null | undefined;
|
||||||
|
select(optionId: string): void | Promise<void>;
|
||||||
|
};
|
||||||
|
modes?: {
|
||||||
|
options: readonly AgentMode[];
|
||||||
|
selectedId: string | null | undefined;
|
||||||
|
select(modeId: string): void | Promise<void>;
|
||||||
|
};
|
||||||
|
features: {
|
||||||
|
list: readonly AgentFeature[] | undefined;
|
||||||
|
set?: (featureId: string, value: unknown) => void | Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>) | undefined,
|
||||||
|
defaultModeId: string | null,
|
||||||
|
): Pick<AgentControlContributionSource, "modes"> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -401,6 +401,15 @@ interface ResultRowProps {
|
|||||||
|
|
||||||
const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRowProps) {
|
const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRowProps) {
|
||||||
const press = useCallback(() => onSelect(result), [onSelect, result]);
|
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(
|
const style = useCallback(
|
||||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||||
styles.row,
|
styles.row,
|
||||||
@@ -415,7 +424,15 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo
|
|||||||
[active, result],
|
[active, result],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Pressable style={style} onPress={press}>
|
<Pressable
|
||||||
|
style={style}
|
||||||
|
onPress={press}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
accessibilityState={accessibilityState}
|
||||||
|
aria-pressed={isWeb ? choice?.selected : undefined}
|
||||||
|
testID={choice?.testId}
|
||||||
|
>
|
||||||
<ResultContent result={result} />
|
<ResultContent result={result} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
@@ -497,7 +514,7 @@ function ResultContent({ result }: { result: CommandCenterResult }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<View style={styles.rowContent} testID={presentation.testId}>
|
<View style={styles.rowContent}>
|
||||||
<View style={styles.rowMain}>
|
<View style={styles.rowMain}>
|
||||||
{Icon ? (
|
{Icon ? (
|
||||||
<View style={styles.iconSlot}>
|
<View style={styles.iconSlot}>
|
||||||
|
|||||||
@@ -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<AgentControlIcon, CommandCenterIcon>();
|
||||||
|
|
||||||
|
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 <ThemedIcon size={size} />;
|
||||||
|
}
|
||||||
|
commandCenterIcons.set(Component, CommandCenterIconAdapter);
|
||||||
|
return CommandCenterIconAdapter;
|
||||||
|
}
|
||||||
@@ -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([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<AgentProvider, CommandCenterIcon>();
|
|
||||||
|
|
||||||
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 <ProviderIcon size={size} />;
|
|
||||||
}
|
|
||||||
commandCenterProviderIcons.set(provider, CommandCenterProviderIcon);
|
|
||||||
return CommandCenterProviderIcon;
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
type SelectFieldOption,
|
type SelectFieldOption,
|
||||||
type SelectFieldRenderOptionInput,
|
type SelectFieldRenderOptionInput,
|
||||||
} from "@/components/ui/select-field";
|
} from "@/components/ui/select-field";
|
||||||
import { formatThinkingOptionLabel } from "@/composer/agent-controls/utils";
|
import { formatThinkingOptionLabel } from "@/agent-controls/labels";
|
||||||
import {
|
import {
|
||||||
mergeProviderPreferences,
|
mergeProviderPreferences,
|
||||||
useFormPreferences,
|
useFormPreferences,
|
||||||
|
|||||||
@@ -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 { Text, View, type PressableStateCallbackType } from "react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
|
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
|
||||||
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
|
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
|
||||||
|
import type { AgentControlIcon } from "@/agent-controls/icons";
|
||||||
export interface AgentControlIconProps {
|
|
||||||
size?: number;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AgentControlIcon = ComponentType<AgentControlIconProps>;
|
|
||||||
|
|
||||||
interface AgentControlTriggerProps {
|
interface AgentControlTriggerProps {
|
||||||
icon: AgentControlIcon;
|
icon: AgentControlIcon;
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useShallow } from "zustand/shallow";
|
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 { ComboboxTrigger } from "@/components/ui/combobox-trigger";
|
||||||
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
||||||
import {
|
import {
|
||||||
@@ -58,16 +60,13 @@ import {
|
|||||||
getFeatureHighlightColor,
|
getFeatureHighlightColor,
|
||||||
getFeatureTooltip,
|
getFeatureTooltip,
|
||||||
getAgentControlHintKey,
|
getAgentControlHintKey,
|
||||||
formatThinkingOptionLabel,
|
|
||||||
resolveAgentModelSelection,
|
resolveAgentModelSelection,
|
||||||
} from "@/composer/agent-controls/utils";
|
} from "@/composer/agent-controls/utils";
|
||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
|
||||||
import { useCommandCenterActions } from "@/command-center/provider";
|
import { useAgentControlCommandCenterActions } from "@/command-center/agent-control-registration";
|
||||||
import { buildModelChoiceContributions } from "@/command-center/model-contributions";
|
|
||||||
import { getCommandCenterProviderIcon } from "@/command-center/provider-icon";
|
|
||||||
import { isNative } from "@/constants/platform";
|
import { isNative } from "@/constants/platform";
|
||||||
import {
|
import {
|
||||||
resolveComposerControlDensity,
|
resolveComposerControlDensity,
|
||||||
@@ -88,6 +87,8 @@ interface AgentControlOption {
|
|||||||
|
|
||||||
type AgentControlSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
|
type AgentControlSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
|
||||||
|
|
||||||
|
const EMPTY_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [];
|
||||||
|
|
||||||
interface ControlledAgentControlsProps {
|
interface ControlledAgentControlsProps {
|
||||||
provider: string;
|
provider: string;
|
||||||
providerOptions?: AgentControlOption[];
|
providerOptions?: AgentControlOption[];
|
||||||
@@ -164,14 +165,17 @@ function findOptionLabel(
|
|||||||
return selected?.label ?? fallback;
|
return selected?.label ?? fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FEATURE_ICONS: Record<string, typeof Zap> = {
|
function toCommandCenterModes(modeControl: AgentModeControlValue | null) {
|
||||||
"list-todo": ListTodo,
|
if (!modeControl) return undefined;
|
||||||
"shield-check": ShieldCheck,
|
return {
|
||||||
zap: Zap,
|
options: modeControl.modeOptions,
|
||||||
};
|
selectedId: modeControl.selectedModeId,
|
||||||
|
select: modeControl.onSelectMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getFeatureIcon(icon?: string) {
|
function getModeProviderDefinitions(modeControl: AgentModeControlValue | null) {
|
||||||
return (icon && FEATURE_ICONS[icon]) || Settings2;
|
return modeControl?.providerDefinitions ?? EMPTY_AGENT_PROVIDER_DEFINITIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFeatureIconColor(
|
function getFeatureIconColor(
|
||||||
@@ -923,7 +927,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
|
|||||||
<TooltipTrigger asChild triggerRefProp="ref">
|
<TooltipTrigger asChild triggerRefProp="ref">
|
||||||
<AgentControlTrigger
|
<AgentControlTrigger
|
||||||
ref={thinkingAnchorRef}
|
ref={thinkingAnchorRef}
|
||||||
icon={Brain}
|
icon={ThinkingIcon}
|
||||||
surface="toolbar"
|
surface="toolbar"
|
||||||
label={t("agentControls.thinking.title")}
|
label={t("agentControls.thinking.title")}
|
||||||
value={displayThinking}
|
value={displayThinking}
|
||||||
@@ -1103,7 +1107,7 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
|
|||||||
<>
|
<>
|
||||||
<AgentControlTrigger
|
<AgentControlTrigger
|
||||||
ref={thinkingAnchorRef}
|
ref={thinkingAnchorRef}
|
||||||
icon={Brain}
|
icon={ThinkingIcon}
|
||||||
surface="sheet"
|
surface="sheet"
|
||||||
label={t("agentControls.thinking.title")}
|
label={t("agentControls.thinking.title")}
|
||||||
value={displayThinking}
|
value={displayThinking}
|
||||||
@@ -1217,7 +1221,7 @@ function DesktopFeatureItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (feature.type === "toggle") {
|
if (feature.type === "toggle") {
|
||||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
||||||
return (
|
return (
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger asChild triggerRefProp="ref">
|
<TooltipTrigger asChild triggerRefProp="ref">
|
||||||
@@ -1246,7 +1250,7 @@ function DesktopFeatureItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (feature.type === "select") {
|
if (feature.type === "select") {
|
||||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
||||||
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1334,7 +1338,7 @@ function SheetFeatureItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (feature.type === "toggle") {
|
if (feature.type === "toggle") {
|
||||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
||||||
return (
|
return (
|
||||||
<AgentControlTrigger
|
<AgentControlTrigger
|
||||||
icon={FeatureIcon}
|
icon={FeatureIcon}
|
||||||
@@ -1356,7 +1360,7 @@ function SheetFeatureItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (feature.type === "select") {
|
if (feature.type === "select") {
|
||||||
const FeatureIcon = getFeatureIcon(feature.icon);
|
const FeatureIcon = getAgentFeatureIcon(feature.icon);
|
||||||
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
const selectedOption = feature.options.find((o) => o.id === feature.value);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1402,7 +1406,7 @@ function ThinkingComboboxOption({
|
|||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
iconColor: string;
|
iconColor: string;
|
||||||
}) {
|
}) {
|
||||||
const leadingSlot = useMemo(() => <Brain size={16} color={iconColor} />, [iconColor]);
|
const leadingSlot = useMemo(() => <ThinkingIcon size={16} color={iconColor} />, [iconColor]);
|
||||||
return (
|
return (
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
label={option.label}
|
label={option.label}
|
||||||
@@ -1421,7 +1425,6 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
onDropdownClose,
|
onDropdownClose,
|
||||||
isCompactLayout,
|
isCompactLayout,
|
||||||
}: AgentControlsProps) {
|
}: AgentControlsProps) {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { preferences, updatePreferences } = useFormPreferences();
|
const { preferences, updatePreferences } = useFormPreferences();
|
||||||
const agent = useSessionStore(
|
const agent = useSessionStore(
|
||||||
useShallow((state) => selectAgentControlsSlice(state, serverId, agentId)),
|
useShallow((state) => selectAgentControlsSlice(state, serverId, agentId)),
|
||||||
@@ -1429,6 +1432,8 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const modeControl = useLiveAgentModeControl(serverId, agentId);
|
const modeControl = useLiveAgentModeControl(serverId, agentId);
|
||||||
|
const commandCenterModes = toCommandCenterModes(modeControl);
|
||||||
|
const modeProviderDefinitions = getModeProviderDefinitions(modeControl);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
entries: snapshotEntries,
|
entries: snapshotEntries,
|
||||||
@@ -1504,9 +1509,7 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
mergeProviderPreferences({
|
mergeProviderPreferences({
|
||||||
preferences: current,
|
preferences: current,
|
||||||
provider: agentProvider,
|
provider: agentProvider,
|
||||||
updates: {
|
updates: { model: modelId },
|
||||||
model: modelId,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1516,26 +1519,10 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
},
|
},
|
||||||
[agentId, agentProvider, client, toast, updatePreferences],
|
[agentId, agentProvider, client, toast, updatePreferences],
|
||||||
);
|
);
|
||||||
|
const handleSelectCommandCenterModel = useCallback(
|
||||||
const commandCenterModelActions = useMemo(
|
(_provider: AgentProvider, modelId: string) => handleSelectModel(modelId),
|
||||||
() =>
|
[handleSelectModel],
|
||||||
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],
|
|
||||||
);
|
);
|
||||||
useCommandCenterActions({
|
|
||||||
sourceId: `agent:${serverId}:${agentId}`,
|
|
||||||
enabled: isPaneFocused && Boolean(client),
|
|
||||||
actions: commandCenterModelActions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToggleFavoriteModel = useCallback(
|
const handleToggleFavoriteModel = useCallback(
|
||||||
(provider: string, modelId: string) => {
|
(provider: string, modelId: string) => {
|
||||||
@@ -1606,6 +1593,33 @@ export const AgentControls = memo(function AgentControls({
|
|||||||
[agentId, agentProvider, client, toast, updatePreferences],
|
[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(() => {
|
const handleModelSelectorOpen = useCallback(() => {
|
||||||
refetchSnapshotIfStale(agentProvider);
|
refetchSnapshotIfStale(agentProvider);
|
||||||
}, [agentProvider, refetchSnapshotIfStale]);
|
}, [agentProvider, refetchSnapshotIfStale]);
|
||||||
|
|||||||
@@ -1,38 +1,15 @@
|
|||||||
import {
|
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
type ComponentType,
|
|
||||||
type ReactElement,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useShallow } from "zustand/shallow";
|
import { useShallow } from "zustand/shallow";
|
||||||
import { useStoreWithEqualityFn } from "zustand/traditional";
|
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 { type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { formatAgentModeLabel } from "@/agent-controls/labels";
|
||||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
import { getAgentControlHintKey } from "@/composer/agent-controls/utils";
|
||||||
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 { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
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 { useComposerKeyboardScope } from "@/composer/keyboard-scope";
|
||||||
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
|
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
|
||||||
import { AgentControlTrigger } from "@/composer/agent-controls/control";
|
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 type { AgentMode } from "@getpaseo/protocol/agent-types";
|
||||||
import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
|
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
|
||||||
|
import { getAgentModeIcon, getAgentModeOptionIcon } from "@/agent-controls/icons";
|
||||||
interface ModeIconProps {
|
|
||||||
size?: number;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODE_ICONS: Record<string, ComponentType<ModeIconProps>> = {
|
|
||||||
Bot,
|
|
||||||
Shield,
|
|
||||||
ShieldCheck,
|
|
||||||
ShieldAlert,
|
|
||||||
ShieldEllipsis,
|
|
||||||
ShieldOff,
|
|
||||||
ShieldPlus,
|
|
||||||
ShieldQuestionMark,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ModeComboboxOptionProps {
|
interface ModeComboboxOptionProps {
|
||||||
option: ComboboxOption;
|
option: ComboboxOption;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
@@ -78,8 +46,7 @@ function ModeComboboxOption({
|
|||||||
providerDefinitions,
|
providerDefinitions,
|
||||||
iconColor,
|
iconColor,
|
||||||
}: ModeComboboxOptionProps) {
|
}: ModeComboboxOptionProps) {
|
||||||
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
|
const IconComponent = getAgentModeOptionIcon(provider, option.id, providerDefinitions);
|
||||||
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : undefined;
|
|
||||||
const leadingSlot = useMemo(
|
const leadingSlot = useMemo(
|
||||||
() => (IconComponent ? <IconComponent size={16} color={iconColor} /> : null),
|
() => (IconComponent ? <IconComponent size={16} color={iconColor} /> : null),
|
||||||
[IconComponent, iconColor],
|
[IconComponent, iconColor],
|
||||||
@@ -134,10 +101,7 @@ export function AgentModeControl({
|
|||||||
return modeOptions.find((m) => m.id === selectedModeId) ?? modeOptions[0];
|
return modeOptions.find((m) => m.id === selectedModeId) ?? modeOptions[0];
|
||||||
}, [modeOptions, selectedModeId]);
|
}, [modeOptions, selectedModeId]);
|
||||||
|
|
||||||
const visuals = selectedMode
|
const Icon = getAgentModeIcon(provider, selectedMode?.id ?? "", providerDefinitions);
|
||||||
? getModeVisuals(provider, selectedMode.id, providerDefinitions)
|
|
||||||
: undefined;
|
|
||||||
const Icon = visuals?.icon ? (MODE_ICONS[visuals.icon] ?? Bot) : Bot;
|
|
||||||
const iconColor = theme.colors.foregroundMuted;
|
const iconColor = theme.colors.foregroundMuted;
|
||||||
const selectedModeLabel = selectedMode ? formatAgentModeLabel(selectedMode) : "";
|
const selectedModeLabel = selectedMode ? formatAgentModeLabel(selectedMode) : "";
|
||||||
|
|
||||||
@@ -316,9 +280,7 @@ export function useLiveAgentModeControl(
|
|||||||
mergeProviderPreferences({
|
mergeProviderPreferences({
|
||||||
preferences: current,
|
preferences: current,
|
||||||
provider: slice.provider,
|
provider: slice.provider,
|
||||||
updates: {
|
updates: { mode: modeId || undefined },
|
||||||
mode: modeId || undefined,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
console.warn("[AgentModeControl] persist mode preference failed", error);
|
console.warn("[AgentModeControl] persist mode preference failed", error);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
formatAgentModeLabel,
|
|
||||||
getFeatureHighlightColor,
|
getFeatureHighlightColor,
|
||||||
getFeatureTooltip,
|
getFeatureTooltip,
|
||||||
getAgentControlHintKey,
|
getAgentControlHintKey,
|
||||||
formatThinkingOptionLabel,
|
|
||||||
normalizeModelId,
|
normalizeModelId,
|
||||||
resolveAgentModelSelection,
|
resolveAgentModelSelection,
|
||||||
} from "./utils";
|
} 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", () => {
|
describe("resolveAgentModelSelection", () => {
|
||||||
it("prefers runtime model over configured model", () => {
|
it("prefers runtime model over configured model", () => {
|
||||||
const selection = resolveAgentModelSelection({
|
const selection = resolveAgentModelSelection({
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AgentFeature, AgentModelDefinition } from "@getpaseo/protocol/agent-types";
|
import type { AgentFeature, AgentModelDefinition } from "@getpaseo/protocol/agent-types";
|
||||||
import { i18n } from "@/i18n/i18next";
|
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 ExplainedAgentControl = "mode" | "model" | "thinking";
|
||||||
export type FeatureHighlightColor = "blue" | "default" | "green" | "yellow";
|
export type FeatureHighlightColor = "blue" | "default" | "green" | "yellow";
|
||||||
@@ -35,61 +37,17 @@ export function getFeatureTooltip(feature: Pick<AgentFeature, "label" | "tooltip
|
|||||||
|
|
||||||
export function getFeatureHighlightColor(featureId: string): FeatureHighlightColor {
|
export function getFeatureHighlightColor(featureId: string): FeatureHighlightColor {
|
||||||
switch (featureId) {
|
switch (featureId) {
|
||||||
case "fast_mode":
|
case FAST_MODE_FEATURE_ID:
|
||||||
return "yellow";
|
return "yellow";
|
||||||
case "auto_accept":
|
case "auto_accept":
|
||||||
return "green";
|
return "green";
|
||||||
case "plan_mode":
|
case PLAN_MODE_FEATURE_ID:
|
||||||
return "blue";
|
return "blue";
|
||||||
default:
|
default:
|
||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findModelById(
|
function findModelById(
|
||||||
models: AgentModelDefinition[] | null,
|
models: AgentModelDefinition[] | null,
|
||||||
modelId: string | null,
|
modelId: string | null,
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ import { useCreateFlowStore } from "@/stores/create-flow-store";
|
|||||||
import type { Agent } from "@/stores/session-store";
|
import type { Agent } from "@/stores/session-store";
|
||||||
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
import { useWorkspaceFields } from "@/stores/session-store-hooks";
|
||||||
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
|
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
|
||||||
import { useCommandCenterActions } from "@/command-center/provider";
|
import { useAgentControlCommandCenterActions } from "@/command-center/agent-control-registration";
|
||||||
import { buildModelChoiceContributions } from "@/command-center/model-contributions";
|
|
||||||
import { getCommandCenterProviderIcon } from "@/command-center/provider-icon";
|
|
||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
|
||||||
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
|
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
|
||||||
@@ -378,27 +376,17 @@ export function WorkspaceDraftAgentTab({
|
|||||||
throw new Error("Workspace draft composer state is required");
|
throw new Error("Workspace draft composer state is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftModelActions = useMemo(
|
const draftProvider = composerState.selectedProvider;
|
||||||
() =>
|
const draftProviderDefinitions = composerState.providerDefinitions;
|
||||||
buildModelChoiceContributions({
|
const draftThinkingOptions = composerState.availableThinkingOptions;
|
||||||
serverId,
|
const draftSelectedThinkingId = composerState.selectedThinkingOptionId;
|
||||||
providers: composerState.modelSelectorProviders,
|
const draftSetThinkingOption = composerState.setThinkingOptionFromUser;
|
||||||
selectedProvider: composerState.selectedProvider,
|
const draftModeOptions = composerState.modeOptions;
|
||||||
selectedModelId: composerState.effectiveModelId || null,
|
const draftSelectedMode = composerState.selectedMode;
|
||||||
groupLabel: t("shell.commandCenter.modelGroupLabel"),
|
const draftSetMode = composerState.setModeFromUser;
|
||||||
searchKeywords: t("shell.commandCenter.modelSearchKeywords"),
|
const draftFeatures = composerState.agentControls.features;
|
||||||
getIcon: getCommandCenterProviderIcon,
|
const draftOnSetFeature = composerState.agentControls.onSetFeature;
|
||||||
select: composerState.setProviderAndModelFromUser,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
composerState.effectiveModelId,
|
|
||||||
composerState.modelSelectorProviders,
|
|
||||||
composerState.selectedProvider,
|
|
||||||
composerState.setProviderAndModelFromUser,
|
|
||||||
serverId,
|
|
||||||
t,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const clearDraftInput = draftInput.clear;
|
const clearDraftInput = draftInput.clear;
|
||||||
const setDraftText = draftInput.setText;
|
const setDraftText = draftInput.setText;
|
||||||
const setDraftAttachments = draftInput.setAttachments;
|
const setDraftAttachments = draftInput.setAttachments;
|
||||||
@@ -541,12 +529,36 @@ export function WorkspaceDraftAgentTab({
|
|||||||
onCreated(result);
|
onCreated(result);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
useCommandCenterActions({
|
useAgentControlCommandCenterActions({
|
||||||
sourceId: `draft:${serverId}:${tabId}`,
|
sourceId: `draft:${serverId}:${tabId}`,
|
||||||
enabled: isPaneFocused && !isSubmitting,
|
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(
|
const isReadyForPendingAutoSubmit = Boolean(
|
||||||
pendingAutoSubmit &&
|
pendingAutoSubmit &&
|
||||||
draftInput.isHydrated &&
|
draftInput.isHydrated &&
|
||||||
|
|||||||
@@ -188,6 +188,12 @@ describe("translation resources", () => {
|
|||||||
expect(en.shell.commandCenter.modelSearchKeywords).toBe(
|
expect(en.shell.commandCenter.modelSearchKeywords).toBe(
|
||||||
"switch model change model set model select model",
|
"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", () => {
|
it("includes composer and agent workflow keys for the Batch 2 migration", () => {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const ar: TranslationResources = {
|
|||||||
home: "بيت",
|
home: "بيت",
|
||||||
modelGroupLabel: "النموذج",
|
modelGroupLabel: "النموذج",
|
||||||
modelSearchKeywords: "تبديل النموذج تغيير النموذج تعيين النموذج اختيار النموذج",
|
modelSearchKeywords: "تبديل النموذج تغيير النموذج تعيين النموذج اختيار النموذج",
|
||||||
|
thinkingGroupLabel: "التفكير",
|
||||||
|
thinkingSearchKeywords: "الاستدلال الجهد التفكير",
|
||||||
|
modeGroupLabel: "الوضع",
|
||||||
|
modeSearchKeywords: "الوصول الإذن الموافقة الوضع",
|
||||||
|
planModeGroupLabel: "وضع التخطيط",
|
||||||
|
planModeSearchKeywords: "التخطيط خطة وضع التخطيط",
|
||||||
|
fastModeGroupLabel: "سريع",
|
||||||
|
fastModeSearchKeywords: "سريع سرعة زمن انتقال منخفض",
|
||||||
|
settingOn: "تشغيل",
|
||||||
|
settingOff: "إيقاف",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
composer: {
|
composer: {
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ export const en = {
|
|||||||
home: "Home",
|
home: "Home",
|
||||||
modelGroupLabel: "Model",
|
modelGroupLabel: "Model",
|
||||||
modelSearchKeywords: "switch model change model set model select 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: {
|
composer: {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const es: TranslationResources = {
|
|||||||
home: "Hogar",
|
home: "Hogar",
|
||||||
modelGroupLabel: "Modelo",
|
modelGroupLabel: "Modelo",
|
||||||
modelSearchKeywords: "cambiar modelo modificar modelo establecer modelo seleccionar 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: {
|
composer: {
|
||||||
|
|||||||
@@ -66,6 +66,16 @@ export const fr: TranslationResources = {
|
|||||||
modelGroupLabel: "Modèle",
|
modelGroupLabel: "Modèle",
|
||||||
modelSearchKeywords:
|
modelSearchKeywords:
|
||||||
"changer de modèle modifier le modèle définir le modèle sélectionner le modèle",
|
"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: {
|
composer: {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const ja: TranslationResources = {
|
|||||||
home: "ホーム",
|
home: "ホーム",
|
||||||
modelGroupLabel: "モデル",
|
modelGroupLabel: "モデル",
|
||||||
modelSearchKeywords: "モデルを切り替え モデルを変更 モデルを設定 モデルを選択",
|
modelSearchKeywords: "モデルを切り替え モデルを変更 モデルを設定 モデルを選択",
|
||||||
|
thinkingGroupLabel: "思考",
|
||||||
|
thinkingSearchKeywords: "推論 思考レベル 考える",
|
||||||
|
modeGroupLabel: "モード",
|
||||||
|
modeSearchKeywords: "アクセス 権限 承認 モード",
|
||||||
|
planModeGroupLabel: "プランモード",
|
||||||
|
planModeSearchKeywords: "プラン 計画 プランモード",
|
||||||
|
fastModeGroupLabel: "高速",
|
||||||
|
fastModeSearchKeywords: "高速 速度 低遅延",
|
||||||
|
settingOn: "オン",
|
||||||
|
settingOff: "オフ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
composer: {
|
composer: {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const ptBR: TranslationResources = {
|
|||||||
home: "Início",
|
home: "Início",
|
||||||
modelGroupLabel: "Modelo",
|
modelGroupLabel: "Modelo",
|
||||||
modelSearchKeywords: "trocar modelo mudar modelo definir modelo selecionar 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: {
|
composer: {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const ru: TranslationResources = {
|
|||||||
home: "Дом",
|
home: "Дом",
|
||||||
modelGroupLabel: "Модель",
|
modelGroupLabel: "Модель",
|
||||||
modelSearchKeywords: "сменить модель изменить модель выбрать модель установить модель",
|
modelSearchKeywords: "сменить модель изменить модель выбрать модель установить модель",
|
||||||
|
thinkingGroupLabel: "Мышление",
|
||||||
|
thinkingSearchKeywords: "рассуждение усилие мышление",
|
||||||
|
modeGroupLabel: "Режим",
|
||||||
|
modeSearchKeywords: "доступ разрешение одобрение режим",
|
||||||
|
planModeGroupLabel: "Режим плана",
|
||||||
|
planModeSearchKeywords: "план планирование режим плана",
|
||||||
|
fastModeGroupLabel: "Быстрый",
|
||||||
|
fastModeSearchKeywords: "быстрый скорость низкая задержка",
|
||||||
|
settingOn: "Вкл",
|
||||||
|
settingOff: "Выкл",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
composer: {
|
composer: {
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const zhCN: TranslationResources = {
|
|||||||
home: "首页",
|
home: "首页",
|
||||||
modelGroupLabel: "模型",
|
modelGroupLabel: "模型",
|
||||||
modelSearchKeywords: "切换模型 更改模型 设置模型 选择模型",
|
modelSearchKeywords: "切换模型 更改模型 设置模型 选择模型",
|
||||||
|
thinkingGroupLabel: "思考",
|
||||||
|
thinkingSearchKeywords: "推理 努力 思考",
|
||||||
|
modeGroupLabel: "模式",
|
||||||
|
modeSearchKeywords: "访问 权限 批准 模式",
|
||||||
|
planModeGroupLabel: "计划模式",
|
||||||
|
planModeSearchKeywords: "计划 规划 计划模式",
|
||||||
|
fastModeGroupLabel: "快速",
|
||||||
|
fastModeSearchKeywords: "快速 速度 低延迟",
|
||||||
|
settingOn: "开",
|
||||||
|
settingOff: "关",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
composer: {
|
composer: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
} from "@getpaseo/protocol/agent-types";
|
} from "@getpaseo/protocol/agent-types";
|
||||||
import type { ScheduleCadence, ScheduleSummary } from "@getpaseo/protocol/schedule/types";
|
import type { ScheduleCadence, ScheduleSummary } from "@getpaseo/protocol/schedule/types";
|
||||||
import type { FormPreferences } from "@/create-agent-preferences/preferences";
|
import type { FormPreferences } from "@/create-agent-preferences/preferences";
|
||||||
import { formatThinkingOptionLabel } from "@/composer/agent-controls/utils";
|
import { formatThinkingOptionLabel } from "@/agent-controls/labels";
|
||||||
import {
|
import {
|
||||||
buildSelectableProviderSelectorProviders,
|
buildSelectableProviderSelectorProviders,
|
||||||
type ProviderSelectorProvider,
|
type ProviderSelectorProvider,
|
||||||
|
|||||||
Reference in New Issue
Block a user