authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 03:42:50-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 00:42:50-07:00
log48aa875c0e6f8f2ee83d6972dd4357a4a083fe30
tree9c6dfec0d7b2d0c41e52e7c40393acfac30d4e9c
parenta8cd2ad2ebb32ad789e8e0ecd2fc713c2edc38f4
signature Signed by PGP key B5690EEEBB952194

feat(web): remove Build/Plan toggle from the composer (#5551)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

6 files changed, 66 insertions(+), 22 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1
...@@ -29,6 +29,7 @@ const clientSettings: ClientSettings = {...@@ -29,6 +29,7 @@ const clientSettings: ClientSettings = {
29 fontSizeTerminal: 12,29 fontSizeTerminal: 12,
30 fontSmoothing: true,30 fontSmoothing: true,
31 glassOpacity: 80,31 glassOpacity: 80,
32 planModeEnabled: false,
32 providerModelPreferences: {},33 providerModelPreferences: {},
33 sidebarAutoSettleAfterDays: 3,34 sidebarAutoSettleAfterDays: 3,
34 sidebarProjectGroupingMode: "repository_path",35 sidebarProjectGroupingMode: "repository_path",
apps/web/src/components/ChatView.tsx+10-2
...@@ -1481,8 +1481,13 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1481,8 +1481,13 @@ function ChatViewContent(props: ChatViewProps) {
1481 ? (localServerError ?? activeServerThread?.session?.lastError ?? null)1481 ? (localServerError ?? activeServerThread?.session?.lastError ?? null)
1482 : localDraftError;1482 : localDraftError;
1483 const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;1483 const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;
1484 const interactionMode =1484 // Plan mode is legacy (Settings → Beta). With the flag off the effective
1485 composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;1485 // mode is forced to "default" — even for threads with a stored plan mode —
1486 // so nobody is trapped in plan mode while its toggle is hidden. The next
1487 // send persists "default" back to the thread.
1488 const interactionMode = settings.planModeEnabled
1489 ? (composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE)
1490 : DEFAULT_INTERACTION_MODE;
1486 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;1491 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
1487 const canCheckoutPullRequestIntoThread = isLocalDraftThread;1492 const canCheckoutPullRequestIntoThread = isLocalDraftThread;
1488 const activeThreadId = activeThread?.id ?? null;1493 const activeThreadId = activeThread?.id ?? null;
...@@ -4811,7 +4816,10 @@ function ChatViewContent(props: ChatViewProps) {...@@ -4811,7 +4816,10 @@ function ChatViewContent(props: ChatViewProps) {
4811 });4816 });
4812 return;4817 return;
4813 }4818 }
4819 // Legacy plan mode: /plan and /default only act when the beta flag is on;
4820 // otherwise they send as plain text like any other message.
4814 const standaloneSlashCommand =4821 const standaloneSlashCommand =
4822 settings.planModeEnabled &&
4815 composerImages.length === 0 &&4823 composerImages.length === 0 &&
4816 sendableComposerTerminalContexts.length === 0 &&4824 sendableComposerTerminalContexts.length === 0 &&
4817 composerElementContexts.length === 0 &&4825 composerElementContexts.length === 0 &&
apps/web/src/components/chat/ChatComposer.tsx+33-20
...@@ -875,14 +875,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -875,14 +875,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
875875
876 const selectedPromptEffort = composerProviderState.promptEffort;876 const selectedPromptEffort = composerProviderState.promptEffort;
877 const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch;877 const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch;
878 // Plan mode is a legacy feature behind Settings → Beta. With the flag off,
879 // ChatView forces the effective mode to "default", so hiding the toggle
880 // can't trap anyone in plan mode.
881 const planModeUiEnabled = settings.planModeEnabled;
878 const composerProviderControls = useMemo(882 const composerProviderControls = useMemo(
879 () => ({883 () => ({
880 showInteractionModeToggle: getProviderInteractionModeToggle(884 showInteractionModeToggle:
881 providerStatuses,885 planModeUiEnabled && getProviderInteractionModeToggle(providerStatuses, selectedProvider),
882 selectedProvider,
883 ),
884 }),886 }),
885 [providerStatuses, selectedProvider],887 [planModeUiEnabled, providerStatuses, selectedProvider],
886 );888 );
887 const selectedModelSelection = useMemo<ModelSelection>(889 const selectedModelSelection = useMemo<ModelSelection>(
888 () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch),890 () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch),
...@@ -1043,20 +1045,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1043,20 +1045,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1043 label: "/model",1045 label: "/model",
1044 description: "Switch response model for this thread",1046 description: "Switch response model for this thread",
1045 },1047 },
1046 {1048 ...(planModeUiEnabled
1047 id: "slash:plan",1049 ? ([
1048 type: "slash-command",1050 {
1049 command: "plan",1051 id: "slash:plan",
1050 label: "/plan",1052 type: "slash-command",
1051 description: "Switch this thread into plan mode",1053 command: "plan",
1052 },1054 label: "/plan",
1053 {1055 description: "Switch this thread into plan mode",
1054 id: "slash:default",1056 },
1055 type: "slash-command",1057 {
1056 command: "default",1058 id: "slash:default",
1057 label: "/default",1059 type: "slash-command",
1058 description: "Switch this thread back to normal build mode",1060 command: "default",
1059 },1061 label: "/default",
1062 description: "Switch this thread back to normal build mode",
1063 },
1064 ] as const)
1065 : []),
1060 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;1066 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;
1061 const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map(1067 const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map(
1062 (command) => ({1068 (command) => ({
...@@ -1091,7 +1097,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1091,7 +1097,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1091 );1097 );
1092 }1098 }
1093 return [];1099 return [];
1094 }, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries.entries]);1100 }, [
1101 composerTrigger,
1102 planModeUiEnabled,
1103 selectedProvider,
1104 selectedProviderStatus,
1105 workspaceEntries.entries,
1106 ]);
10951107
1096 const composerMenuOpen = Boolean(composerTrigger);1108 const composerMenuOpen = Boolean(composerTrigger);
1097 const composerMenuSearchKey = composerTrigger1109 const composerMenuSearchKey = composerTrigger
...@@ -1857,6 +1869,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1857,6 +1869,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1857 event: KeyboardEvent,1869 event: KeyboardEvent,
1858 ) => {1870 ) => {
1859 if (key === "Tab" && event.shiftKey) {1871 if (key === "Tab" && event.shiftKey) {
1872 if (!planModeUiEnabled) return false;
1860 toggleInteractionMode();1873 toggleInteractionMode();
1861 return true;1874 return true;
1862 }1875 }
apps/web/src/components/settings/BetaSettingsPanel.tsx+12
...@@ -60,6 +60,7 @@ export function BetaSettingsPanel() {...@@ -60,6 +60,7 @@ export function BetaSettingsPanel() {
60 const sidebarAutoSettleAfterDays = useClientSettings(60 const sidebarAutoSettleAfterDays = useClientSettings(
61 (settings) => settings.sidebarAutoSettleAfterDays,61 (settings) => settings.sidebarAutoSettleAfterDays,
62 );62 );
63 const planModeEnabled = useClientSettings((settings) => settings.planModeEnabled);
63 const updateSettings = useUpdateClientSettings();64 const updateSettings = useUpdateClientSettings();
6465
65 return (66 return (
...@@ -114,6 +115,17 @@ export function BetaSettingsPanel() {...@@ -114,6 +115,17 @@ export function BetaSettingsPanel() {
114 ) : null}115 ) : null}
115 </>116 </>
116 ) : null}117 ) : null}
118 <SettingsRow
119 {...searchableSetting("restore-plan-mode")}
120 description="Legacy feature. Brings back the Build/Plan toggle in the composer along with the /plan and /default commands and the Shift+Tab shortcut. While off, every thread runs in build mode."
121 control={
122 <Switch
123 checked={planModeEnabled}
124 onCheckedChange={(checked) => updateSettings({ planModeEnabled: Boolean(checked) })}
125 aria-label="Restore plan mode (legacy)"
126 />
127 }
128 />
117 </SettingsSection>129 </SettingsSection>
118 </SettingsPageContainer>130 </SettingsPageContainer>
119 );131 );
apps/web/src/components/settings/settingsSearch.ts+5
...@@ -177,6 +177,11 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -177,6 +177,11 @@ export const SETTINGS_SEARCH_ITEMS = [
177 to: "/settings/beta",177 to: "/settings/beta",
178 targetId: "sidebar-v2",178 targetId: "sidebar-v2",
179 },179 },
180 {
181 id: "restore-plan-mode",
182 title: "Restore plan mode (legacy)",
183 to: "/settings/beta",
184 },
180 {185 {
181 id: "archive",186 id: "archive",
182 title: "Archived threads",187 title: "Archived threads",
packages/contracts/src/settings.ts+5
...@@ -167,6 +167,10 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -167,6 +167,10 @@ export const ClientSettingsSchema = Schema.Struct({
167 modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),167 modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
168 }),168 }),
169 ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),169 ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
170 // Legacy plan mode. The composer's Build/Plan toggle was removed from the
171 // default UI; this beta flag restores it (plus the /plan and /default slash
172 // commands) for users who still rely on the old workflow.
173 planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
170 sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe(174 sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe(
171 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)),175 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)),
172 ),176 ),
...@@ -781,6 +785,7 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -781,6 +785,7 @@ export const ClientSettingsPatch = Schema.Struct({
781 }),785 }),
782 ),786 ),
783 ),787 ),
788 planModeEnabled: Schema.optionalKey(Schema.Boolean),
784 sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),789 sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),
785 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),790 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),
786 sidebarProjectGroupingOverrides: Schema.optionalKey(791 sidebarProjectGroupingOverrides: Schema.optionalKey(