authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 20:17:44-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 17:17:44-07:00
log31891a1a0c208a6b244b64937741066c61fefd9c
tree9fc700b9dd7dd1836357390b621e4c38e901e8d3
parent45d9aa90baab8f2d6b13c7ae3cf2f97128edaf7b
signature Signed by PGP key B5690EEEBB952194

feat(web): fold plan mode and token-by-token output into Legacy features (#5664)

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

6 files changed, 120 insertions(+), 60 deletions(-)

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+2-2
......@@ -2199,7 +2199,7 @@ describe("ProviderRuntimeIngestion", () => {
21992199 });
22002200
22012201 it("starts a new streaming assistant message segment after approval", async () => {
2202 const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } });
2202 const harness = await createHarness({ serverSettings: { enableLegacyTokenStreaming: true } });
22032203 const startedAt = "2026-03-28T07:00:00.000Z";
22042204 const pausedAt = "2026-03-28T07:00:01.000Z";
22052205 const resumedAt = "2026-03-28T07:00:02.000Z";
......@@ -2306,7 +2306,7 @@ describe("ProviderRuntimeIngestion", () => {
23062306 });
23072307
23082308 it("streams assistant deltas when thread.turn.start requests streaming mode", async () => {
2309 const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } });
2309 const harness = await createHarness({ serverSettings: { enableLegacyTokenStreaming: true } });
23102310 const now = "2026-01-01T00:00:00.000Z";
23112311
23122312 await Effect.runPromise(
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+2-2
......@@ -1655,7 +1655,7 @@ const make = Effect.gen(function* () {
16551655
16561656 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
16571657 serverSettingsService.getSettings,
1658 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),
1658 (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"),
16591659 );
16601660 if (assistantDeliveryMode === "buffered") {
16611661 const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);
......@@ -1691,7 +1691,7 @@ const make = Effect.gen(function* () {
16911691 const detailedThread = yield* getLoadedThreadDetail();
16921692 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
16931693 serverSettingsService.getSettings,
1694 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),
1694 (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"),
16951695 );
16961696 const flushedMessageIds =
16971697 assistantDeliveryMode === "buffered"
apps/web/src/components/settings/BetaSettingsPanel.tsx-12
......@@ -60,7 +60,6 @@ export function BetaSettingsPanel() {
6060 const sidebarAutoSettleAfterDays = useClientSettings(
6161 (settings) => settings.sidebarAutoSettleAfterDays,
6262 );
63 const planModeEnabled = useClientSettings((settings) => settings.planModeEnabled);
6463 const updateSettings = useUpdateClientSettings();
6564
6665 return (
......@@ -115,17 +114,6 @@ export function BetaSettingsPanel() {
115114 ) : null}
116115 </>
117116 ) : 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 />
129117 </SettingsSection>
130118 </SettingsPageContainer>
131119 );
apps/web/src/components/settings/SettingsPanels.tsx+99-32
......@@ -1,4 +1,4 @@
1import { ArchiveIcon, ArchiveX, LoaderIcon, SettingsIcon } from "lucide-react";
1import { ArchiveIcon, ArchiveX, ChevronRightIcon, LoaderIcon, SettingsIcon } from "lucide-react";
22import { Link } from "@tanstack/react-router";
33import type { CSSProperties, ReactNode } from "react";
44import { useCallback, useEffect, useMemo, useRef, useState } from "react";
......@@ -79,6 +79,7 @@ import { useProjects } from "../../state/entities";
7979import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";
8080import { formatRelativeTimeLabel } from "../../timestampFormat";
8181import { Button } from "../ui/button";
82import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
8283import {
8384 Dialog,
8485 DialogDescription,
......@@ -476,8 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) {
476477 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
477478 ? ["Diff whitespace changes"]
478479 : []),
479 ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming
480 ? ["Assistant output"]
480 ...(settings.enableLegacyTokenStreaming !==
481 DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming
482 ? ["Stream token by token"]
481483 : []),
482484 ...(settings.enableProviderUpdateChecks !==
483485 DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks
......@@ -521,7 +523,7 @@ export function useSettingsRestore(onRestored?: () => void) {
521523 settings.fontSizePrompt,
522524 settings.fontSizeTerminal,
523525 settings.glassOpacity,
524 settings.enableAssistantStreaming,
526 settings.enableLegacyTokenStreaming,
525527 settings.enableProviderUpdateChecks,
526528 settings.sidebarProjectGroupingMode,
527529 settings.sidebarThreadPreviewCount,
......@@ -601,7 +603,7 @@ export function useSettingsRestore(onRestored?: () => void) {
601603 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
602604 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
603605 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
604 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
606 enableLegacyTokenStreaming: DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming,
605607 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
606608 backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
607609 backgroundActivityProfile: DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile,
......@@ -1504,6 +1506,96 @@ function FontFamilySettingsRow({
15041506 );
15051507}
15061508
1509// Both legacy rows sit behind the fold, so a settings-search jump has to
1510// expand the section before its target can mount and scroll.
1511const LEGACY_FEATURE_TARGET_IDS: ReadonlySet<string> = new Set([
1512 "legacy-plan-mode",
1513 "legacy-token-streaming",
1514]);
1515
1516/**
1517 * Retired features kept only for users who still depend on them. Collapsed by
1518 * default so they stay out of the everyday settings path; a settings-search
1519 * jump to one of the rows unfolds the section.
1520 */
1521function LegacyFeaturesSection() {
1522 const settings = usePrimarySettings();
1523 const updateSettings = useUpdatePrimarySettings();
1524 const [open, setOpen] = useState(false);
1525 const searchTargetId = useSettingsSearchTargetId();
1526 // Unfold once per search jump; tracking the handled id lets the user fold
1527 // the section back up without the still-set target immediately reopening it.
1528 const lastExpandedTargetRef = useRef<string | null>(null);
1529 useEffect(() => {
1530 if (searchTargetId === null) {
1531 // A handled jump clears the target; forgetting it here lets a later
1532 // jump to the same row expand the section again.
1533 lastExpandedTargetRef.current = null;
1534 return;
1535 }
1536 if (!LEGACY_FEATURE_TARGET_IDS.has(searchTargetId)) return;
1537 if (lastExpandedTargetRef.current === searchTargetId) return;
1538 lastExpandedTargetRef.current = searchTargetId;
1539 setOpen(true);
1540 }, [searchTargetId]);
1541
1542 return (
1543 <section className="space-y-3">
1544 <Collapsible open={open} onOpenChange={setOpen}>
1545 <CollapsibleTrigger className="group flex min-h-8 w-full items-center gap-2 px-3 sm:px-4">
1546 <h2 className="text-lg font-semibold tracking-[-0.025em] text-muted-foreground transition-colors group-hover:text-foreground">
1547 Legacy features
1548 </h2>
1549 <ChevronRightIcon className="size-4 text-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-90" />
1550 </CollapsibleTrigger>
1551 <CollapsiblePanel>
1552 <div className="relative space-y-1 overflow-visible pt-3 text-foreground">
1553 <SettingsRow
1554 {...searchableSetting("legacy-plan-mode")}
1555 description="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."
1556 control={
1557 <Switch
1558 checked={settings.planModeEnabled}
1559 onCheckedChange={(checked) =>
1560 updateSettings({ planModeEnabled: Boolean(checked) })
1561 }
1562 aria-label="Plan mode (legacy)"
1563 />
1564 }
1565 />
1566 <SettingsRow
1567 {...searchableSetting("legacy-token-streaming")}
1568 description="Paints assistant output token by token instead of in complete chunks. Not recommended: it is significantly slower, and long responses become harder to follow. Kept only for compatibility with the old behavior."
1569 control={
1570 <Switch
1571 checked={settings.enableLegacyTokenStreaming}
1572 onCheckedChange={(checked) => {
1573 if (!checked) {
1574 updateSettings({ enableLegacyTokenStreaming: false });
1575 return;
1576 }
1577 void (async () => {
1578 const api = readLocalApi();
1579 const confirmed = await (api ?? ensureLocalApi()).dialogs.confirm(
1580 [
1581 "Turn on token-by-token output?",
1582 "It is significantly slower than the default buffered output and hurts the reading experience. This switch exists only for backwards compatibility.",
1583 ].join("\n"),
1584 );
1585 if (confirmed) updateSettings({ enableLegacyTokenStreaming: true });
1586 })();
1587 }}
1588 aria-label="Stream token by token (legacy)"
1589 />
1590 }
1591 />
1592 </div>
1593 </CollapsiblePanel>
1594 </Collapsible>
1595 </section>
1596 );
1597}
1598
15071599export function GeneralSettingsPanel() {
15081600 const settings = usePrimarySettings();
15091601 const updateSettings = useUpdatePrimarySettings();
......@@ -1664,33 +1756,6 @@ export function GeneralSettingsPanel() {
16641756 }
16651757 />
16661758
1667 <SettingsRow
1668 {...searchableSetting("assistant-output")}
1669 description="Show token-by-token output while a response is in progress."
1670 resetAction={
1671 settings.enableAssistantStreaming !==
1672 DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming ? (
1673 <SettingResetButton
1674 label="assistant output"
1675 onClick={() =>
1676 updateSettings({
1677 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
1678 })
1679 }
1680 />
1681 ) : null
1682 }
1683 control={
1684 <Switch
1685 checked={settings.enableAssistantStreaming}
1686 onCheckedChange={(checked) =>
1687 updateSettings({ enableAssistantStreaming: Boolean(checked) })
1688 }
1689 aria-label="Stream assistant messages"
1690 />
1691 }
1692 />
1693
16941759 <SettingsRow
16951760 {...searchableSetting("provider-update-checks")}
16961761 description="Check installed provider CLIs for newer available versions."
......@@ -2050,6 +2115,8 @@ export function GeneralSettingsPanel() {
20502115 }
20512116 />
20522117 </SettingsSection>
2118
2119 <LegacyFeaturesSection />
20532120 </SettingsPageContainer>
20542121 );
20552122}
apps/web/src/components/settings/settingsSearch.ts+10-10
......@@ -110,11 +110,6 @@ export const SETTINGS_SEARCH_ITEMS = [
110110 title: "Hide whitespace changes",
111111 to: "/settings/general",
112112 },
113 {
114 id: "assistant-output",
115 title: "Assistant output",
116 to: "/settings/general",
117 },
118113 {
119114 id: "provider-update-checks",
120115 title: "Provider update checks",
......@@ -156,6 +151,16 @@ export const SETTINGS_SEARCH_ITEMS = [
156151 title: "Diagnostics",
157152 to: "/settings/general",
158153 },
154 {
155 id: "legacy-plan-mode",
156 title: "Plan mode (legacy)",
157 to: "/settings/general",
158 },
159 {
160 id: "legacy-token-streaming",
161 title: "Stream token by token (legacy)",
162 to: "/settings/general",
163 },
159164 {
160165 id: "keybindings",
161166 title: "Keybindings",
......@@ -187,11 +192,6 @@ export const SETTINGS_SEARCH_ITEMS = [
187192 to: "/settings/beta",
188193 targetId: "sidebar-v2",
189194 },
190 {
191 id: "restore-plan-mode",
192 title: "Restore plan mode (legacy)",
193 to: "/settings/beta",
194 },
195195 {
196196 id: "archive",
197197 title: "Archived threads",
packages/contracts/src/settings.ts+7-2
......@@ -538,7 +538,12 @@ export const BackgroundActivitySettings = Schema.Struct({
538538export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type;
539539
540540export const ServerSettings = Schema.Struct({
541 enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
541 // Legacy token-by-token assistant output. Deliberately a fresh key (was
542 // `enableAssistantStreaming`): decoding drops the old key, so everyone,
543 // including prior opt-ins, resets to the buffered default.
544 enableLegacyTokenStreaming: Schema.Boolean.pipe(
545 Schema.withDecodingDefault(Effect.succeed(false)),
546 ),
542547 enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
543548 backgroundActivity: BackgroundActivitySettings,
544549 // Legacy flat fields retained for old settings files and old clients. New
......@@ -701,7 +706,7 @@ const OpenCodeSettingsPatch = Schema.Struct({
701706
702707export const ServerSettingsPatch = Schema.Struct({
703708 // Server settings
704 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
709 enableLegacyTokenStreaming: Schema.optionalKey(Schema.Boolean),
705710 enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
706711 backgroundActivity: Schema.optionalKey(
707712 Schema.Struct({