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:
Michał Kędrzyński
2026-07-31 17:37:04 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Mohamed Boudra
parent 1ddca07d57
commit 35f985baff
34 changed files with 1526 additions and 387 deletions
@@ -1,16 +1,10 @@
import { forwardRef, useCallback, type ComponentType } from "react";
import { forwardRef, useCallback } from "react";
import { Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph";
export interface AgentControlIconProps {
size?: number;
color?: string;
}
export type AgentControlIcon = ComponentType<AgentControlIconProps>;
import type { AgentControlIcon } from "@/agent-controls/icons";
interface AgentControlTriggerProps {
icon: AgentControlIcon;
@@ -22,7 +22,9 @@ import {
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { Brain, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
import { Settings2 } from "lucide-react-native";
import { getAgentFeatureIcon, ThinkingIcon } from "@/agent-controls/icons";
import { formatThinkingOptionLabel } from "@/agent-controls/labels";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import {
@@ -58,16 +60,13 @@ import {
getFeatureHighlightColor,
getFeatureTooltip,
getAgentControlHintKey,
formatThinkingOptionLabel,
resolveAgentModelSelection,
} from "@/composer/agent-controls/utils";
import { useIsCompactFormFactor } from "@/constants/layout";
import { useToast } from "@/contexts/toast-context";
import { toErrorMessage } from "@/utils/error-messages";
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
import { useCommandCenterActions } from "@/command-center/provider";
import { buildModelChoiceContributions } from "@/command-center/model-contributions";
import { getCommandCenterProviderIcon } from "@/command-center/provider-icon";
import { useAgentControlCommandCenterActions } from "@/command-center/agent-control-registration";
import { isNative } from "@/constants/platform";
import {
resolveComposerControlDensity,
@@ -88,6 +87,8 @@ interface AgentControlOption {
type AgentControlSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
const EMPTY_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [];
interface ControlledAgentControlsProps {
provider: string;
providerOptions?: AgentControlOption[];
@@ -164,14 +165,17 @@ function findOptionLabel(
return selected?.label ?? fallback;
}
const FEATURE_ICONS: Record<string, typeof Zap> = {
"list-todo": ListTodo,
"shield-check": ShieldCheck,
zap: Zap,
};
function toCommandCenterModes(modeControl: AgentModeControlValue | null) {
if (!modeControl) return undefined;
return {
options: modeControl.modeOptions,
selectedId: modeControl.selectedModeId,
select: modeControl.onSelectMode,
};
}
function getFeatureIcon(icon?: string) {
return (icon && FEATURE_ICONS[icon]) || Settings2;
function getModeProviderDefinitions(modeControl: AgentModeControlValue | null) {
return modeControl?.providerDefinitions ?? EMPTY_AGENT_PROVIDER_DEFINITIONS;
}
function getFeatureIconColor(
@@ -923,7 +927,7 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) {
<TooltipTrigger asChild triggerRefProp="ref">
<AgentControlTrigger
ref={thinkingAnchorRef}
icon={Brain}
icon={ThinkingIcon}
surface="toolbar"
label={t("agentControls.thinking.title")}
value={displayThinking}
@@ -1103,7 +1107,7 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) {
<>
<AgentControlTrigger
ref={thinkingAnchorRef}
icon={Brain}
icon={ThinkingIcon}
surface="sheet"
label={t("agentControls.thinking.title")}
value={displayThinking}
@@ -1217,7 +1221,7 @@ function DesktopFeatureItem({
);
if (feature.type === "toggle") {
const FeatureIcon = getFeatureIcon(feature.icon);
const FeatureIcon = getAgentFeatureIcon(feature.icon);
return (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild triggerRefProp="ref">
@@ -1246,7 +1250,7 @@ function DesktopFeatureItem({
}
if (feature.type === "select") {
const FeatureIcon = getFeatureIcon(feature.icon);
const FeatureIcon = getAgentFeatureIcon(feature.icon);
const selectedOption = feature.options.find((o) => o.id === feature.value);
return (
<>
@@ -1334,7 +1338,7 @@ function SheetFeatureItem({
);
if (feature.type === "toggle") {
const FeatureIcon = getFeatureIcon(feature.icon);
const FeatureIcon = getAgentFeatureIcon(feature.icon);
return (
<AgentControlTrigger
icon={FeatureIcon}
@@ -1356,7 +1360,7 @@ function SheetFeatureItem({
}
if (feature.type === "select") {
const FeatureIcon = getFeatureIcon(feature.icon);
const FeatureIcon = getAgentFeatureIcon(feature.icon);
const selectedOption = feature.options.find((o) => o.id === feature.value);
return (
<>
@@ -1402,7 +1406,7 @@ function ThinkingComboboxOption({
onPress: () => void;
iconColor: string;
}) {
const leadingSlot = useMemo(() => <Brain size={16} color={iconColor} />, [iconColor]);
const leadingSlot = useMemo(() => <ThinkingIcon size={16} color={iconColor} />, [iconColor]);
return (
<ComboboxItem
label={option.label}
@@ -1421,7 +1425,6 @@ export const AgentControls = memo(function AgentControls({
onDropdownClose,
isCompactLayout,
}: AgentControlsProps) {
const { t } = useTranslation();
const { preferences, updatePreferences } = useFormPreferences();
const agent = useSessionStore(
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 toast = useToast();
const modeControl = useLiveAgentModeControl(serverId, agentId);
const commandCenterModes = toCommandCenterModes(modeControl);
const modeProviderDefinitions = getModeProviderDefinitions(modeControl);
const {
entries: snapshotEntries,
@@ -1504,9 +1509,7 @@ export const AgentControls = memo(function AgentControls({
mergeProviderPreferences({
preferences: current,
provider: agentProvider,
updates: {
model: modelId,
},
updates: { model: modelId },
}),
);
} catch (error) {
@@ -1516,26 +1519,10 @@ export const AgentControls = memo(function AgentControls({
},
[agentId, agentProvider, client, toast, updatePreferences],
);
const commandCenterModelActions = useMemo(
() =>
buildModelChoiceContributions({
serverId,
providers: agentModelSelectorProviders,
selectedProvider: agentProvider ?? null,
selectedModelId: activeModelId,
groupLabel: t("shell.commandCenter.modelGroupLabel"),
searchKeywords: t("shell.commandCenter.modelSearchKeywords"),
getIcon: getCommandCenterProviderIcon,
select: (_provider, modelId) => handleSelectModel(modelId),
}),
[activeModelId, agentModelSelectorProviders, agentProvider, handleSelectModel, serverId, t],
const handleSelectCommandCenterModel = useCallback(
(_provider: AgentProvider, modelId: string) => handleSelectModel(modelId),
[handleSelectModel],
);
useCommandCenterActions({
sourceId: `agent:${serverId}:${agentId}`,
enabled: isPaneFocused && Boolean(client),
actions: commandCenterModelActions,
});
const handleToggleFavoriteModel = useCallback(
(provider: string, modelId: string) => {
@@ -1606,6 +1593,33 @@ export const AgentControls = memo(function AgentControls({
[agentId, agentProvider, client, toast, updatePreferences],
);
useAgentControlCommandCenterActions({
sourceId: `agent:${serverId}:${agentId}`,
enabled: isPaneFocused && Boolean(client),
controls: {
serverId,
ownerKey: agentId,
provider: agentProvider,
providerDefinitions: modeProviderDefinitions,
models: {
providers: agentModelSelectorProviders,
selectedProvider: agentProvider,
selectedModelId: activeModelId,
select: handleSelectCommandCenterModel,
},
thinking: {
options: modelSelection.thinkingOptions,
selectedId: modelSelection.selectedThinkingId,
select: handleSelectThinkingOption,
},
modes: commandCenterModes,
features: {
list: agent?.features,
set: handleSetFeature,
},
},
});
const handleModelSelectorOpen = useCallback(() => {
refetchSnapshotIfStale(agentProvider);
}, [agentProvider, refetchSnapshotIfStale]);
@@ -1,38 +1,15 @@
import {
useCallback,
useMemo,
useRef,
useState,
type ComponentType,
type ReactElement,
} from "react";
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
import {
Bot,
Shield,
ShieldAlert,
ShieldCheck,
ShieldEllipsis,
ShieldOff,
ShieldPlus,
ShieldQuestionMark,
} from "lucide-react-native";
import { type SheetHeader } from "@/components/adaptive-modal-sheet";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Shortcut } from "@/components/ui/shortcut";
import { useSessionStore } from "@/stores/session-store";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { mergeProviderPreferences, useFormPreferences } from "@/hooks/use-form-preferences";
import { resolveProviderDefinition } from "@/utils/provider-definitions";
import { useToast } from "@/contexts/toast-context";
import { toErrorMessage } from "@/utils/error-messages";
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
import { formatAgentModeLabel, getAgentControlHintKey } from "@/composer/agent-controls/utils";
import { formatAgentModeLabel } from "@/agent-controls/labels";
import { getAgentControlHintKey } from "@/composer/agent-controls/utils";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
@@ -40,25 +17,16 @@ import { resolveNextAgentModeId } from "@/composer/agent-controls/mode";
import { useComposerKeyboardScope } from "@/composer/keyboard-scope";
import { useComposerControlLayout } from "@/composer/agent-controls/layout-context";
import { AgentControlTrigger } from "@/composer/agent-controls/control";
import { useSessionStore } from "@/stores/session-store";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { mergeProviderPreferences, useFormPreferences } from "@/hooks/use-form-preferences";
import { resolveProviderDefinition } from "@/utils/provider-definitions";
import { useToast } from "@/contexts/toast-context";
import { toErrorMessage } from "@/utils/error-messages";
import { showProviderNoticeToast } from "@/utils/provider-notice-toast";
import type { AgentMode } from "@getpaseo/protocol/agent-types";
import { getModeVisuals, type AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
interface ModeIconProps {
size?: number;
color?: string;
}
const MODE_ICONS: Record<string, ComponentType<ModeIconProps>> = {
Bot,
Shield,
ShieldCheck,
ShieldAlert,
ShieldEllipsis,
ShieldOff,
ShieldPlus,
ShieldQuestionMark,
};
import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import { getAgentModeIcon, getAgentModeOptionIcon } from "@/agent-controls/icons";
interface ModeComboboxOptionProps {
option: ComboboxOption;
selected: boolean;
@@ -78,8 +46,7 @@ function ModeComboboxOption({
providerDefinitions,
iconColor,
}: ModeComboboxOptionProps) {
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : undefined;
const IconComponent = getAgentModeOptionIcon(provider, option.id, providerDefinitions);
const leadingSlot = useMemo(
() => (IconComponent ? <IconComponent size={16} color={iconColor} /> : null),
[IconComponent, iconColor],
@@ -134,10 +101,7 @@ export function AgentModeControl({
return modeOptions.find((m) => m.id === selectedModeId) ?? modeOptions[0];
}, [modeOptions, selectedModeId]);
const visuals = selectedMode
? getModeVisuals(provider, selectedMode.id, providerDefinitions)
: undefined;
const Icon = visuals?.icon ? (MODE_ICONS[visuals.icon] ?? Bot) : Bot;
const Icon = getAgentModeIcon(provider, selectedMode?.id ?? "", providerDefinitions);
const iconColor = theme.colors.foregroundMuted;
const selectedModeLabel = selectedMode ? formatAgentModeLabel(selectedMode) : "";
@@ -316,9 +280,7 @@ export function useLiveAgentModeControl(
mergeProviderPreferences({
preferences: current,
provider: slice.provider,
updates: {
mode: modeId || undefined,
},
updates: { mode: modeId || undefined },
}),
).catch((error) => {
console.warn("[AgentModeControl] persist mode preference failed", error);
@@ -1,10 +1,8 @@
import { describe, expect, it } from "vitest";
import {
formatAgentModeLabel,
getFeatureHighlightColor,
getFeatureTooltip,
getAgentControlHintKey,
formatThinkingOptionLabel,
normalizeModelId,
resolveAgentModelSelection,
} from "./utils";
@@ -54,36 +52,6 @@ describe("normalizeModelId", () => {
});
});
describe("formatAgentModeLabel", () => {
it("sentence-cases provider mode labels", () => {
expect(formatAgentModeLabel({ id: "plan", label: "Plan" })).toBe("Plan");
expect(formatAgentModeLabel({ id: "full-access", label: "Full Access" })).toBe("Full access");
expect(formatAgentModeLabel({ id: "auto-review", label: "Auto-review" })).toBe("Auto-review");
expect(formatAgentModeLabel({ id: "read_only", label: "read_only" })).toBe("Read only");
expect(formatAgentModeLabel({ id: "acceptEdits", label: "acceptEdits" })).toBe("Accept edits");
});
it("splits compact mode ids when no provider label is available", () => {
expect(formatAgentModeLabel({ id: "auto-review" })).toBe("Auto review");
});
});
describe("formatThinkingOptionLabel", () => {
it("formats compact thinking option labels for display", () => {
expect(formatThinkingOptionLabel({ id: "none", label: "none" })).toBe("None");
expect(formatThinkingOptionLabel({ id: "low", label: "low" })).toBe("Low");
expect(formatThinkingOptionLabel({ id: "medium", label: "medium" })).toBe("Medium");
expect(formatThinkingOptionLabel({ id: "high", label: "high" })).toBe("High");
expect(formatThinkingOptionLabel({ id: "xhigh", label: "xhigh" })).toBe("Extra high");
});
it("sentence-cases split provider labels", () => {
expect(formatThinkingOptionLabel({ id: "extra_high", label: "extra_high" })).toBe("Extra high");
expect(formatThinkingOptionLabel({ id: "think-hard", label: "think-hard" })).toBe("Think hard");
expect(formatThinkingOptionLabel({ id: "xhigh", label: "XHigh" })).toBe("Extra high");
});
});
describe("resolveAgentModelSelection", () => {
it("prefers runtime model over configured model", () => {
const selection = resolveAgentModelSelection({
@@ -1,5 +1,7 @@
import type { AgentFeature, AgentModelDefinition } from "@getpaseo/protocol/agent-types";
import { i18n } from "@/i18n/i18next";
import { formatThinkingOptionLabel } from "@/agent-controls/labels";
import { FAST_MODE_FEATURE_ID, PLAN_MODE_FEATURE_ID } from "@/agent-controls/policy";
export type ExplainedAgentControl = "mode" | "model" | "thinking";
export type FeatureHighlightColor = "blue" | "default" | "green" | "yellow";
@@ -35,61 +37,17 @@ export function getFeatureTooltip(feature: Pick<AgentFeature, "label" | "tooltip
export function getFeatureHighlightColor(featureId: string): FeatureHighlightColor {
switch (featureId) {
case "fast_mode":
case FAST_MODE_FEATURE_ID:
return "yellow";
case "auto_accept":
return "green";
case "plan_mode":
case PLAN_MODE_FEATURE_ID:
return "blue";
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(
models: AgentModelDefinition[] | null,
modelId: string | null,
@@ -23,9 +23,7 @@ import { useCreateFlowStore } from "@/stores/create-flow-store";
import type { Agent } from "@/stores/session-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
import { useCommandCenterActions } from "@/command-center/provider";
import { buildModelChoiceContributions } from "@/command-center/model-contributions";
import { getCommandCenterProviderIcon } from "@/command-center/provider-icon";
import { useAgentControlCommandCenterActions } from "@/command-center/agent-control-registration";
import { encodeImages } from "@/utils/encode-images";
import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
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");
}
const draftModelActions = useMemo(
() =>
buildModelChoiceContributions({
serverId,
providers: composerState.modelSelectorProviders,
selectedProvider: composerState.selectedProvider,
selectedModelId: composerState.effectiveModelId || null,
groupLabel: t("shell.commandCenter.modelGroupLabel"),
searchKeywords: t("shell.commandCenter.modelSearchKeywords"),
getIcon: getCommandCenterProviderIcon,
select: composerState.setProviderAndModelFromUser,
}),
[
composerState.effectiveModelId,
composerState.modelSelectorProviders,
composerState.selectedProvider,
composerState.setProviderAndModelFromUser,
serverId,
t,
],
);
const draftProvider = composerState.selectedProvider;
const draftProviderDefinitions = composerState.providerDefinitions;
const draftThinkingOptions = composerState.availableThinkingOptions;
const draftSelectedThinkingId = composerState.selectedThinkingOptionId;
const draftSetThinkingOption = composerState.setThinkingOptionFromUser;
const draftModeOptions = composerState.modeOptions;
const draftSelectedMode = composerState.selectedMode;
const draftSetMode = composerState.setModeFromUser;
const draftFeatures = composerState.agentControls.features;
const draftOnSetFeature = composerState.agentControls.onSetFeature;
const clearDraftInput = draftInput.clear;
const setDraftText = draftInput.setText;
const setDraftAttachments = draftInput.setAttachments;
@@ -541,12 +529,36 @@ export function WorkspaceDraftAgentTab({
onCreated(result);
},
});
useCommandCenterActions({
useAgentControlCommandCenterActions({
sourceId: `draft:${serverId}:${tabId}`,
enabled: isPaneFocused && !isSubmitting,
actions: draftModelActions,
controls: {
serverId,
ownerKey: tabId,
provider: draftProvider,
providerDefinitions: draftProviderDefinitions,
models: {
providers: composerState.modelSelectorProviders,
selectedProvider: draftProvider,
selectedModelId: composerState.effectiveModelId,
select: composerState.setProviderAndModelFromUser,
},
thinking: {
options: draftThinkingOptions,
selectedId: draftSelectedThinkingId,
select: draftSetThinkingOption,
},
modes: {
options: draftModeOptions,
selectedId: draftSelectedMode,
select: draftSetMode,
},
features: {
list: draftFeatures,
set: draftOnSetFeature,
},
},
});
const isReadyForPendingAutoSubmit = Boolean(
pendingAutoSubmit &&
draftInput.isHydrated &&