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", () => {...@@ -2199,7 +2199,7 @@ describe("ProviderRuntimeIngestion", () => {
2199 });2199 });
22002200
2201 it("starts a new streaming assistant message segment after approval", async () => {2201 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 } });
2203 const startedAt = "2026-03-28T07:00:00.000Z";2203 const startedAt = "2026-03-28T07:00:00.000Z";
2204 const pausedAt = "2026-03-28T07:00:01.000Z";2204 const pausedAt = "2026-03-28T07:00:01.000Z";
2205 const resumedAt = "2026-03-28T07:00:02.000Z";2205 const resumedAt = "2026-03-28T07:00:02.000Z";
...@@ -2306,7 +2306,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2306,7 +2306,7 @@ describe("ProviderRuntimeIngestion", () => {
2306 });2306 });
23072307
2308 it("streams assistant deltas when thread.turn.start requests streaming mode", async () => {2308 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 } });
2310 const now = "2026-01-01T00:00:00.000Z";2310 const now = "2026-01-01T00:00:00.000Z";
23112311
2312 await Effect.runPromise(2312 await Effect.runPromise(
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+2-2
...@@ -1655,7 +1655,7 @@ const make = Effect.gen(function* () {...@@ -1655,7 +1655,7 @@ const make = Effect.gen(function* () {
16551655
1656 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(1656 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
1657 serverSettingsService.getSettings,1657 serverSettingsService.getSettings,
1658 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),1658 (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"),
1659 );1659 );
1660 if (assistantDeliveryMode === "buffered") {1660 if (assistantDeliveryMode === "buffered") {
1661 const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);1661 const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);
...@@ -1691,7 +1691,7 @@ const make = Effect.gen(function* () {...@@ -1691,7 +1691,7 @@ const make = Effect.gen(function* () {
1691 const detailedThread = yield* getLoadedThreadDetail();1691 const detailedThread = yield* getLoadedThreadDetail();
1692 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(1692 const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(
1693 serverSettingsService.getSettings,1693 serverSettingsService.getSettings,
1694 (settings) => (settings.enableAssistantStreaming ? "streaming" : "buffered"),1694 (settings) => (settings.enableLegacyTokenStreaming ? "streaming" : "buffered"),
1695 );1695 );
1696 const flushedMessageIds =1696 const flushedMessageIds =
1697 assistantDeliveryMode === "buffered"1697 assistantDeliveryMode === "buffered"
apps/web/src/components/settings/BetaSettingsPanel.tsx-12
...@@ -60,7 +60,6 @@ export function BetaSettingsPanel() {...@@ -60,7 +60,6 @@ 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);
64 const updateSettings = useUpdateClientSettings();63 const updateSettings = useUpdateClientSettings();
6564
66 return (65 return (
...@@ -115,17 +114,6 @@ export function BetaSettingsPanel() {...@@ -115,17 +114,6 @@ export function BetaSettingsPanel() {
115 ) : null}114 ) : null}
116 </>115 </>
117 ) : null}116 ) : 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 />
129 </SettingsSection>117 </SettingsSection>
130 </SettingsPageContainer>118 </SettingsPageContainer>
131 );119 );
apps/web/src/components/settings/SettingsPanels.tsx+99-32
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { ArchiveIcon, ArchiveX, LoaderIcon, SettingsIcon } from "lucide-react";1import { ArchiveIcon, ArchiveX, ChevronRightIcon, LoaderIcon, SettingsIcon } from "lucide-react";
2import { Link } from "@tanstack/react-router";2import { Link } from "@tanstack/react-router";
3import type { CSSProperties, ReactNode } from "react";3import type { CSSProperties, ReactNode } from "react";
4import { useCallback, useEffect, useMemo, useRef, useState } from "react";4import { useCallback, useEffect, useMemo, useRef, useState } from "react";
...@@ -79,6 +79,7 @@ import { useProjects } from "../../state/entities";...@@ -79,6 +79,7 @@ import { useProjects } from "../../state/entities";
79import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";79import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";
80import { formatRelativeTimeLabel } from "../../timestampFormat";80import { formatRelativeTimeLabel } from "../../timestampFormat";
81import { Button } from "../ui/button";81import { Button } from "../ui/button";
82import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
82import {83import {
83 Dialog,84 Dialog,
84 DialogDescription,85 DialogDescription,
...@@ -476,8 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -476,8 +477,9 @@ export function useSettingsRestore(onRestored?: () => void) {
476 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace477 ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace
477 ? ["Diff whitespace changes"]478 ? ["Diff whitespace changes"]
478 : []),479 : []),
479 ...(settings.enableAssistantStreaming !== DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming480 ...(settings.enableLegacyTokenStreaming !==
480 ? ["Assistant output"]481 DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming
482 ? ["Stream token by token"]
481 : []),483 : []),
482 ...(settings.enableProviderUpdateChecks !==484 ...(settings.enableProviderUpdateChecks !==
483 DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks485 DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks
...@@ -521,7 +523,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -521,7 +523,7 @@ export function useSettingsRestore(onRestored?: () => void) {
521 settings.fontSizePrompt,523 settings.fontSizePrompt,
522 settings.fontSizeTerminal,524 settings.fontSizeTerminal,
523 settings.glassOpacity,525 settings.glassOpacity,
524 settings.enableAssistantStreaming,526 settings.enableLegacyTokenStreaming,
525 settings.enableProviderUpdateChecks,527 settings.enableProviderUpdateChecks,
526 settings.sidebarProjectGroupingMode,528 settings.sidebarProjectGroupingMode,
527 settings.sidebarThreadPreviewCount,529 settings.sidebarThreadPreviewCount,
...@@ -601,7 +603,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -601,7 +603,7 @@ export function useSettingsRestore(onRestored?: () => void) {
601 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,603 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
602 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,604 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
603 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,605 sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode,
604 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,606 enableLegacyTokenStreaming: DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming,
605 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,607 enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks,
606 backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity,608 backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
607 backgroundActivityProfile: DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile,609 backgroundActivityProfile: DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile,
...@@ -1504,6 +1506,96 @@ function FontFamilySettingsRow({...@@ -1504,6 +1506,96 @@ function FontFamilySettingsRow({
1504 );1506 );
1505}1507}
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
1507export function GeneralSettingsPanel() {1599export function GeneralSettingsPanel() {
1508 const settings = usePrimarySettings();1600 const settings = usePrimarySettings();
1509 const updateSettings = useUpdatePrimarySettings();1601 const updateSettings = useUpdatePrimarySettings();
...@@ -1664,33 +1756,6 @@ export function GeneralSettingsPanel() {...@@ -1664,33 +1756,6 @@ export function GeneralSettingsPanel() {
1664 }1756 }
1665 />1757 />
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
1694 <SettingsRow1759 <SettingsRow
1695 {...searchableSetting("provider-update-checks")}1760 {...searchableSetting("provider-update-checks")}
1696 description="Check installed provider CLIs for newer available versions."1761 description="Check installed provider CLIs for newer available versions."
...@@ -2050,6 +2115,8 @@ export function GeneralSettingsPanel() {...@@ -2050,6 +2115,8 @@ export function GeneralSettingsPanel() {
2050 }2115 }
2051 />2116 />
2052 </SettingsSection>2117 </SettingsSection>
2118
2119 <LegacyFeaturesSection />
2053 </SettingsPageContainer>2120 </SettingsPageContainer>
2054 );2121 );
2055}2122}
apps/web/src/components/settings/settingsSearch.ts+10-10
...@@ -110,11 +110,6 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -110,11 +110,6 @@ export const SETTINGS_SEARCH_ITEMS = [
110 title: "Hide whitespace changes",110 title: "Hide whitespace changes",
111 to: "/settings/general",111 to: "/settings/general",
112 },112 },
113 {
114 id: "assistant-output",
115 title: "Assistant output",
116 to: "/settings/general",
117 },
118 {113 {
119 id: "provider-update-checks",114 id: "provider-update-checks",
120 title: "Provider update checks",115 title: "Provider update checks",
...@@ -156,6 +151,16 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -156,6 +151,16 @@ export const SETTINGS_SEARCH_ITEMS = [
156 title: "Diagnostics",151 title: "Diagnostics",
157 to: "/settings/general",152 to: "/settings/general",
158 },153 },
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 },
159 {164 {
160 id: "keybindings",165 id: "keybindings",
161 title: "Keybindings",166 title: "Keybindings",
...@@ -187,11 +192,6 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -187,11 +192,6 @@ export const SETTINGS_SEARCH_ITEMS = [
187 to: "/settings/beta",192 to: "/settings/beta",
188 targetId: "sidebar-v2",193 targetId: "sidebar-v2",
189 },194 },
190 {
191 id: "restore-plan-mode",
192 title: "Restore plan mode (legacy)",
193 to: "/settings/beta",
194 },
195 {195 {
196 id: "archive",196 id: "archive",
197 title: "Archived threads",197 title: "Archived threads",
packages/contracts/src/settings.ts+7-2
...@@ -538,7 +538,12 @@ export const BackgroundActivitySettings = Schema.Struct({...@@ -538,7 +538,12 @@ export const BackgroundActivitySettings = Schema.Struct({
538export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type;538export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type;
539539
540export const ServerSettings = Schema.Struct({540export 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 ),
542 enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),547 enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
543 backgroundActivity: BackgroundActivitySettings,548 backgroundActivity: BackgroundActivitySettings,
544 // Legacy flat fields retained for old settings files and old clients. New549 // Legacy flat fields retained for old settings files and old clients. New
...@@ -701,7 +706,7 @@ const OpenCodeSettingsPatch = Schema.Struct({...@@ -701,7 +706,7 @@ const OpenCodeSettingsPatch = Schema.Struct({
701706
702export const ServerSettingsPatch = Schema.Struct({707export const ServerSettingsPatch = Schema.Struct({
703 // Server settings708 // Server settings
704 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),709 enableLegacyTokenStreaming: Schema.optionalKey(Schema.Boolean),
705 enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),710 enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean),
706 backgroundActivity: Schema.optionalKey(711 backgroundActivity: Schema.optionalKey(
707 Schema.Struct({712 Schema.Struct({