From 69a776e34dcae60516c5f6a34733bf0c0aaff354 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 6 Aug 2026 15:04:15 -0700 Subject: [PATCH 1/2] feat(web): remove Build/Plan toggle; legacy plan mode behind beta flag Co-Authored-By: Claude Fable 5 --- .../settings/DesktopClientSettings.test.ts | 1 + apps/web/src/components/ChatView.tsx | 3 ++ apps/web/src/components/chat/ChatComposer.tsx | 52 ++++++++++++------- .../components/settings/BetaSettingsPanel.tsx | 12 +++++ .../src/components/settings/settingsSearch.ts | 5 ++ packages/contracts/src/settings.ts | 5 ++ 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 53ef74f2191..b965752ad11 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -30,6 +30,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 7b59530c955..5829f7e4d27 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -4774,7 +4774,10 @@ function ChatViewContent(props: ChatViewProps) { }); return; } + // Legacy plan mode: only intercept /plan and /default when the beta flag + // is on, or the thread is already in plan mode (so /default can exit it). const standaloneSlashCommand = + (settings.planModeEnabled || interactionMode === "plan") && 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 f6d34315dac..43e352358f2 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -924,14 +924,15 @@ 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. Threads already in + // plan mode keep the toggle visible regardless, so users can still exit. + const planModeUiEnabled = settings.planModeEnabled || interactionMode === "plan"; const composerProviderControls = useMemo( () => ({ - showInteractionModeToggle: getProviderInteractionModeToggle( - providerStatuses, - selectedProvider, - ), + showInteractionModeToggle: + planModeUiEnabled && getProviderInteractionModeToggle(providerStatuses, selectedProvider), }), - [providerStatuses, selectedProvider], + [planModeUiEnabled, providerStatuses, selectedProvider], ); const selectedModelSelection = useMemo( () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch), @@ -1092,20 +1093,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) => ({ @@ -1140,7 +1145,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 @@ -1907,6 +1918,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 740d3048f0e..f508a9dbccf 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 1ba231a5835..32c487acdc4 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -182,6 +182,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 cbb547b95fb..a15b3c3f67d 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -168,6 +168,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)), ), @@ -783,6 +787,7 @@ export const ClientSettingsPatch = Schema.Struct({ }), ), ), + planModeEnabled: Schema.optionalKey(Schema.Boolean), sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey( From d533626d784075c92bb4aca2692d726cff28eef0 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 6 Aug 2026 19:39:51 -0700 Subject: [PATCH 2/2] fix(web): force build mode while plan-mode beta flag is off Co-Authored-By: Claude Fable 5 --- apps/web/src/components/ChatView.tsx | 15 ++++++++++----- apps/web/src/components/chat/ChatComposer.tsx | 7 ++++--- .../src/components/settings/BetaSettingsPanel.tsx | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 5829f7e4d27..d4d1dd0f847 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1467,8 +1467,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; @@ -4774,10 +4779,10 @@ function ChatViewContent(props: ChatViewProps) { }); return; } - // Legacy plan mode: only intercept /plan and /default when the beta flag - // is on, or the thread is already in plan mode (so /default can exit it). + // 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 || interactionMode === "plan") && + 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 43e352358f2..3d1705f254f 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -924,9 +924,10 @@ 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. Threads already in - // plan mode keep the toggle visible regardless, so users can still exit. - const planModeUiEnabled = settings.planModeEnabled || interactionMode === "plan"; + // 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: diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index f508a9dbccf..4b96fb15398 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -117,7 +117,7 @@ export function BetaSettingsPanel() { ) : null}