From 698a55ff37c95770fb23c63b042852b15981f8cb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 16:36:28 -0700 Subject: [PATCH 1/2] feat(web): fold plan mode and token-by-token output into Legacy features Co-Authored-By: Claude Fable 5 --- .../Layers/ProviderRuntimeIngestion.test.ts | 4 +- .../Layers/ProviderRuntimeIngestion.ts | 4 +- .../components/settings/BetaSettingsPanel.tsx | 12 -- .../components/settings/SettingsPanels.tsx | 125 +++++++++++++----- .../src/components/settings/settingsSearch.ts | 20 +-- packages/contracts/src/settings.ts | 9 +- 6 files changed, 114 insertions(+), 60 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts index dfc47320768..b4468bd4c6d 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts @@ -2199,7 +2199,7 @@ describe("ProviderRuntimeIngestion", () => { }); it("starts a new streaming assistant message segment after approval", async () => { - const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } }); + const harness = await createHarness({ serverSettings: { enableLegacyTokenStreaming: true } }); const startedAt = "2026-03-28T07:00:00.000Z"; const pausedAt = "2026-03-28T07:00:01.000Z"; const resumedAt = "2026-03-28T07:00:02.000Z"; @@ -2306,7 +2306,7 @@ describe("ProviderRuntimeIngestion", () => { }); it("streams assistant deltas when thread.turn.start requests streaming mode", async () => { - const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } }); + const harness = await createHarness({ serverSettings: { enableLegacyTokenStreaming: true } }); const now = "2026-01-01T00:00:00.000Z"; await Effect.runPromise( diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index a86adea5232..40307cd9f25 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -1655,7 +1655,7 @@ const make = Effect.gen(function* () { const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map( serverSettingsService.getSettings, - (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"), + (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"), ); if (assistantDeliveryMode === "buffered") { const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta); @@ -1691,7 +1691,7 @@ const make = Effect.gen(function* () { const detailedThread = yield* getLoadedThreadDetail(); const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map( serverSettingsService.getSettings, - (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"), + (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"), ); const flushedMessageIds = assistantDeliveryMode === "buffered" diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index 4b96fb15398..740d3048f0e 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -60,7 +60,6 @@ export function BetaSettingsPanel() { const sidebarAutoSettleAfterDays = useClientSettings( (settings) => settings.sidebarAutoSettleAfterDays, ); - const planModeEnabled = useClientSettings((settings) => settings.planModeEnabled); const updateSettings = useUpdateClientSettings(); return ( @@ -115,17 +114,6 @@ export function BetaSettingsPanel() { ) : null} ) : null} - updateSettings({ planModeEnabled: Boolean(checked) })} - aria-label="Restore plan mode (legacy)" - /> - } - /> ); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 5b9347f0307..30618a36504 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1,4 +1,4 @@ -import { ArchiveIcon, ArchiveX, LoaderIcon, SettingsIcon } from "lucide-react"; +import { ArchiveIcon, ArchiveX, ChevronRightIcon, LoaderIcon, SettingsIcon } from "lucide-react"; import { Link } from "@tanstack/react-router"; import type { CSSProperties, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -79,6 +79,7 @@ import { useProjects } from "../../state/entities"; import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState"; import { formatRelativeTimeLabel } from "../../timestampFormat"; import { Button } from "../ui/button"; +import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; import { Dialog, DialogDescription, @@ -476,8 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace ? ["Diff whitespace changes"] : []), - ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming - ? ["Assistant output"] + ...(settings.enableLegacyTokenStreaming !== + DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming + ? ["Stream token by token"] : []), ...(settings.enableProviderUpdateChecks !== DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks @@ -521,7 +523,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.fontSizePrompt, settings.fontSizeTerminal, settings.glassOpacity, - settings.enableAssistantStreaming, + settings.enableLegacyTokenStreaming, settings.enableProviderUpdateChecks, settings.sidebarProjectGroupingMode, settings.sidebarThreadPreviewCount, @@ -601,7 +603,7 @@ export function useSettingsRestore(onRestored?: () => void) { glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity, sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, - enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, + enableLegacyTokenStreaming: DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming, enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, backgroundActivityProfile: DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile, @@ -1504,6 +1506,90 @@ function FontFamilySettingsRow({ ); } +// Both legacy rows sit behind the fold, so a settings-search jump has to +// expand the section before its target can mount and scroll. +const LEGACY_FEATURE_TARGET_IDS: ReadonlySet = new Set([ + "legacy-plan-mode", + "legacy-token-streaming", +]); + +/** + * Retired features kept only for users who still depend on them. Collapsed by + * default so they stay out of the everyday settings path; a settings-search + * jump to one of the rows unfolds the section. + */ +function LegacyFeaturesSection() { + const settings = usePrimarySettings(); + const updateSettings = useUpdatePrimarySettings(); + const [open, setOpen] = useState(false); + const searchTargetId = useSettingsSearchTargetId(); + // Unfold once per search jump; tracking the handled id lets the user fold + // the section back up without the still-set target immediately reopening it. + const lastExpandedTargetRef = useRef(null); + useEffect(() => { + if (searchTargetId === null || !LEGACY_FEATURE_TARGET_IDS.has(searchTargetId)) return; + if (lastExpandedTargetRef.current === searchTargetId) return; + lastExpandedTargetRef.current = searchTargetId; + setOpen(true); + }, [searchTargetId]); + + return ( +
+ + +

+ Legacy features +

+ +
+ +
+ + updateSettings({ planModeEnabled: Boolean(checked) }) + } + aria-label="Plan mode (legacy)" + /> + } + /> + { + if (!checked) { + updateSettings({ enableLegacyTokenStreaming: false }); + return; + } + void (async () => { + const api = readLocalApi(); + const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm( + [ + "Turn on token-by-token output?", + "It is significantly slower than the default buffered output and hurts the reading experience. This switch exists only for backwards compatibility.", + ].join("\n"), + ); + if (confirmed) updateSettings({ enableLegacyTokenStreaming: true }); + })(); + }} + aria-label="Stream token by token (legacy)" + /> + } + /> +
+
+
+
+ ); +} + export function GeneralSettingsPanel() { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); @@ -1664,33 +1750,6 @@ export function GeneralSettingsPanel() { } /> - - updateSettings({ - enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming, - }) - } - /> - ) : null - } - control={ - - updateSettings({ enableAssistantStreaming: Boolean(checked) }) - } - aria-label="Stream assistant messages" - /> - } - /> - + + ); } diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 2dcdd66f136..4e8502442f7 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -110,11 +110,6 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Hide whitespace changes", to: "/settings/general", }, - { - id: "assistant-output", - title: "Assistant output", - to: "/settings/general", - }, { id: "provider-update-checks", title: "Provider update checks", @@ -156,6 +151,16 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Diagnostics", to: "/settings/general", }, + { + id: "legacy-plan-mode", + title: "Plan mode (legacy)", + to: "/settings/general", + }, + { + id: "legacy-token-streaming", + title: "Stream token by token (legacy)", + to: "/settings/general", + }, { id: "keybindings", title: "Keybindings", @@ -187,11 +192,6 @@ 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 4b477227f26..0ef1a6a8b75 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -538,7 +538,12 @@ export const BackgroundActivitySettings = Schema.Struct({ export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; export const ServerSettings = Schema.Struct({ - enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + // Legacy token-by-token assistant output. Deliberately a fresh key (was + // `enableAssistantStreaming`): decoding drops the old key, so everyone, + // including prior opt-ins, resets to the buffered default. + enableLegacyTokenStreaming: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + ), enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), backgroundActivity: BackgroundActivitySettings, // Legacy flat fields retained for old settings files and old clients. New @@ -701,7 +706,7 @@ const OpenCodeSettingsPatch = Schema.Struct({ export const ServerSettingsPatch = Schema.Struct({ // Server settings - enableAssistantStreaming: Schema.optionalKey(Schema.Boolean), + enableLegacyTokenStreaming: Schema.optionalKey(Schema.Boolean), enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean), backgroundActivity: Schema.optionalKey( Schema.Struct({ From d95757bf4f4da02362d7e42590ccf65a8b36f9de Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 7 Aug 2026 16:39:54 -0700 Subject: [PATCH 2/2] fix(web): re-expand Legacy features on repeat search jumps Co-Authored-By: Claude Fable 5 --- apps/web/src/components/settings/SettingsPanels.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 30618a36504..407f0e77be4 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1527,7 +1527,13 @@ function LegacyFeaturesSection() { // the section back up without the still-set target immediately reopening it. const lastExpandedTargetRef = useRef(null); useEffect(() => { - if (searchTargetId === null || !LEGACY_FEATURE_TARGET_IDS.has(searchTargetId)) return; + if (searchTargetId === null) { + // A handled jump clears the target; forgetting it here lets a later + // jump to the same row expand the section again. + lastExpandedTargetRef.current = null; + return; + } + if (!LEGACY_FEATURE_TARGET_IDS.has(searchTargetId)) return; if (lastExpandedTargetRef.current === searchTargetId) return; lastExpandedTargetRef.current = searchTargetId; setOpen(true);