diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 26c3127de5f84635d16793106c57c84b081840b4..c1cb8588b5ea45ddde0de063a885792c4df6e3cb 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -29,6 +29,7 @@ const clientSettings: ClientSettings = { fontSizeTerminal: 12, fontSmoothing: true, glassOpacity: 80, + planModeEnabled: false, providerModelPreferences: {}, sidebarAutoSettleAfterDays: 3, sidebarProjectGroupingMode: "repository_path", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index a4c9bcc7c5402dc716b55a63e23a5d676f6e1718..f0e25e644751214617277b380f4d32f971e49ef7 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1481,8 +1481,13 @@ function ChatViewContent(props: ChatViewProps) { ? (localServerError ?? activeServerThread?.session?.lastError ?? null) : localDraftError; const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; - const interactionMode = - composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; + // Plan mode is legacy (Settings → Beta). With the flag off the effective + // mode is forced to "default" — even for threads with a stored plan mode — + // so nobody is trapped in plan mode while its toggle is hidden. The next + // send persists "default" back to the thread. + const interactionMode = settings.planModeEnabled + ? (composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE) + : DEFAULT_INTERACTION_MODE; const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; const canCheckoutPullRequestIntoThread = isLocalDraftThread; const activeThreadId = activeThread?.id ?? null; @@ -4811,7 +4816,10 @@ function ChatViewContent(props: ChatViewProps) { }); return; } + // Legacy plan mode: /plan and /default only act when the beta flag is on; + // otherwise they send as plain text like any other message. const standaloneSlashCommand = + settings.planModeEnabled && composerImages.length === 0 && sendableComposerTerminalContexts.length === 0 && composerElementContexts.length === 0 && diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 6f3a6ec22cde684f505ddd2f54a097549509527d..795cb910227ae3725c35c44d626b15570221ea38 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -875,14 +875,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const selectedPromptEffort = composerProviderState.promptEffort; const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch; + // Plan mode is a legacy feature behind Settings → Beta. With the flag off, + // ChatView forces the effective mode to "default", so hiding the toggle + // can't trap anyone in plan mode. + const planModeUiEnabled = settings.planModeEnabled; const composerProviderControls = useMemo( () => ({ - showInteractionModeToggle: getProviderInteractionModeToggle( - providerStatuses, - selectedProvider, - ), + showInteractionModeToggle: + planModeUiEnabled && getProviderInteractionModeToggle(providerStatuses, selectedProvider), }), - [providerStatuses, selectedProvider], + [planModeUiEnabled, providerStatuses, selectedProvider], ); const selectedModelSelection = useMemo( () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch), @@ -1043,20 +1045,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) label: "/model", description: "Switch response model for this thread", }, - { - id: "slash:plan", - type: "slash-command", - command: "plan", - label: "/plan", - description: "Switch this thread into plan mode", - }, - { - id: "slash:default", - type: "slash-command", - command: "default", - label: "/default", - description: "Switch this thread back to normal build mode", - }, + ...(planModeUiEnabled + ? ([ + { + id: "slash:plan", + type: "slash-command", + command: "plan", + label: "/plan", + description: "Switch this thread into plan mode", + }, + { + id: "slash:default", + type: "slash-command", + command: "default", + label: "/default", + description: "Switch this thread back to normal build mode", + }, + ] as const) + : []), ] satisfies ReadonlyArray>; const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map( (command) => ({ @@ -1091,7 +1097,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ); } return []; - }, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries.entries]); + }, [ + composerTrigger, + planModeUiEnabled, + selectedProvider, + selectedProviderStatus, + workspaceEntries.entries, + ]); const composerMenuOpen = Boolean(composerTrigger); const composerMenuSearchKey = composerTrigger @@ -1857,6 +1869,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) event: KeyboardEvent, ) => { if (key === "Tab" && event.shiftKey) { + if (!planModeUiEnabled) return false; toggleInteractionMode(); return true; } diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index 740d3048f0e38a203aaedba68f3d22a9855f4f24..4b96fb15398d2d79d9f5ba666ef5d8be9753b4de 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -60,6 +60,7 @@ export function BetaSettingsPanel() { const sidebarAutoSettleAfterDays = useClientSettings( (settings) => settings.sidebarAutoSettleAfterDays, ); + const planModeEnabled = useClientSettings((settings) => settings.planModeEnabled); const updateSettings = useUpdateClientSettings(); return ( @@ -114,6 +115,17 @@ export function BetaSettingsPanel() { ) : null} ) : null} + updateSettings({ planModeEnabled: Boolean(checked) })} + aria-label="Restore plan mode (legacy)" + /> + } + /> ); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 69d16147ac7efe1aa1a5ad94db8d2d41e0df9329..3b3a8c220ac47077348d554baf6c59be9898be2f 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -177,6 +177,11 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/beta", targetId: "sidebar-v2", }, + { + id: "restore-plan-mode", + title: "Restore plan mode (legacy)", + to: "/settings/beta", + }, { id: "archive", title: "Archived threads", diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7679ab6e4929ecadf28ca69d988c57fa6d476e72..4b477227f26e263e01f1027a602fd05119f6c0ac 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -167,6 +167,10 @@ export const ClientSettingsSchema = Schema.Struct({ modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))), }), ).pipe(Schema.withDecodingDefault(Effect.succeed({}))), + // Legacy plan mode. The composer's Build/Plan toggle was removed from the + // default UI; this beta flag restores it (plus the /plan and /default slash + // commands) for users who still rely on the old workflow. + planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)), ), @@ -781,6 +785,7 @@ export const ClientSettingsPatch = Schema.Struct({ }), ), ), + planModeEnabled: Schema.optionalKey(Schema.Boolean), sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey(