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 = {
2929 fontSizeTerminal: 12,
3030 fontSmoothing: true,
3131 glassOpacity: 80,
32 planModeEnabled: false,
3233 providerModelPreferences: {},
3334 sidebarAutoSettleAfterDays: 3,
3435 sidebarProjectGroupingMode: "repository_path",
apps/web/src/components/ChatView.tsx+10-2
......@@ -1481,8 +1481,13 @@ function ChatViewContent(props: ChatViewProps) {
14811481 ? (localServerError ?? activeServerThread?.session?.lastError ?? null)
14821482 : localDraftError;
14831483 const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE;
1484 const interactionMode =
1485 composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
1484 // Plan mode is legacy (Settings → Beta). With the flag off the effective
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;
14861491 const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
14871492 const canCheckoutPullRequestIntoThread = isLocalDraftThread;
14881493 const activeThreadId = activeThread?.id ?? null;
......@@ -4811,7 +4816,10 @@ function ChatViewContent(props: ChatViewProps) {
48114816 });
48124817 return;
48134818 }
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.
48144821 const standaloneSlashCommand =
4822 settings.planModeEnabled &&
48154823 composerImages.length === 0 &&
48164824 sendableComposerTerminalContexts.length === 0 &&
48174825 composerElementContexts.length === 0 &&
apps/web/src/components/chat/ChatComposer.tsx+33-20
......@@ -875,14 +875,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
875875
876876 const selectedPromptEffort = composerProviderState.promptEffort;
877877 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;
878882 const composerProviderControls = useMemo(
879883 () => ({
880 showInteractionModeToggle: getProviderInteractionModeToggle(
881 providerStatuses,
882 selectedProvider,
883 ),
884 showInteractionModeToggle:
885 planModeUiEnabled && getProviderInteractionModeToggle(providerStatuses, selectedProvider),
884886 }),
885 [providerStatuses, selectedProvider],
887 [planModeUiEnabled, providerStatuses, selectedProvider],
886888 );
887889 const selectedModelSelection = useMemo<ModelSelection>(
888890 () => createModelSelection(selectedInstanceId, selectedModel, selectedModelOptionsForDispatch),
......@@ -1043,20 +1045,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
10431045 label: "/model",
10441046 description: "Switch response model for this thread",
10451047 },
1046 {
1047 id: "slash:plan",
1048 type: "slash-command",
1049 command: "plan",
1050 label: "/plan",
1051 description: "Switch this thread into plan mode",
1052 },
1053 {
1054 id: "slash:default",
1055 type: "slash-command",
1056 command: "default",
1057 label: "/default",
1058 description: "Switch this thread back to normal build mode",
1059 },
1048 ...(planModeUiEnabled
1049 ? ([
1050 {
1051 id: "slash:plan",
1052 type: "slash-command",
1053 command: "plan",
1054 label: "/plan",
1055 description: "Switch this thread into plan mode",
1056 },
1057 {
1058 id: "slash:default",
1059 type: "slash-command",
1060 command: "default",
1061 label: "/default",
1062 description: "Switch this thread back to normal build mode",
1063 },
1064 ] as const)
1065 : []),
10601066 ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>;
10611067 const providerSlashCommandItems = (selectedProviderStatus?.slashCommands ?? []).map(
10621068 (command) => ({
......@@ -1091,7 +1097,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
10911097 );
10921098 }
10931099 return [];
1094 }, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries.entries]);
1100 }, [
1101 composerTrigger,
1102 planModeUiEnabled,
1103 selectedProvider,
1104 selectedProviderStatus,
1105 workspaceEntries.entries,
1106 ]);
10951107
10961108 const composerMenuOpen = Boolean(composerTrigger);
10971109 const composerMenuSearchKey = composerTrigger
......@@ -1857,6 +1869,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
18571869 event: KeyboardEvent,
18581870 ) => {
18591871 if (key === "Tab" && event.shiftKey) {
1872 if (!planModeUiEnabled) return false;
18601873 toggleInteractionMode();
18611874 return true;
18621875 }
apps/web/src/components/settings/BetaSettingsPanel.tsx+12
......@@ -60,6 +60,7 @@ export function BetaSettingsPanel() {
6060 const sidebarAutoSettleAfterDays = useClientSettings(
6161 (settings) => settings.sidebarAutoSettleAfterDays,
6262 );
63 const planModeEnabled = useClientSettings((settings) => settings.planModeEnabled);
6364 const updateSettings = useUpdateClientSettings();
6465
6566 return (
......@@ -114,6 +115,17 @@ export function BetaSettingsPanel() {
114115 ) : null}
115116 </>
116117 ) : 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 />
117129 </SettingsSection>
118130 </SettingsPageContainer>
119131 );
apps/web/src/components/settings/settingsSearch.ts+5
......@@ -177,6 +177,11 @@ export const SETTINGS_SEARCH_ITEMS = [
177177 to: "/settings/beta",
178178 targetId: "sidebar-v2",
179179 },
180 {
181 id: "restore-plan-mode",
182 title: "Restore plan mode (legacy)",
183 to: "/settings/beta",
184 },
180185 {
181186 id: "archive",
182187 title: "Archived threads",
packages/contracts/src/settings.ts+5
......@@ -167,6 +167,10 @@ export const ClientSettingsSchema = Schema.Struct({
167167 modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
168168 }),
169169 ).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))),
170174 sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe(
171175 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)),
172176 ),
......@@ -781,6 +785,7 @@ export const ClientSettingsPatch = Schema.Struct({
781785 }),
782786 ),
783787 ),
788 planModeEnabled: Schema.optionalKey(Schema.Boolean),
784789 sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)),
785790 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),
786791 sidebarProjectGroupingOverrides: Schema.optionalKey(