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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user