Files
paseo/packages/app/src/command-center/agent-control-contributions.ts
T
35f985baff 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>
2026-07-31 17:37:04 +02:00

320 lines
9.7 KiB
TypeScript

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);
}