authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 03:55:23-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 03:55:23-04:00
loga19f01fc19d209d9962c4bb66372ed9e03e320a9
tree64546d40239c1a6b180310bc02d7e8145be07581
parent6ff537f03c4df500da7283c0566fe6f8feca709b
signature Signed by PGP key B5690EEEBB952194

feat(web): make context window indicator opt-in (#9190)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

6 files changed, 45 insertions(+), 1 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1
...@@ -23,6 +23,7 @@ const clientSettings: ClientSettings = {...@@ -23,6 +23,7 @@ const clientSettings: ClientSettings = {
23 confirmThreadArchive: true,23 confirmThreadArchive: true,
24 confirmThreadDelete: false,24 confirmThreadDelete: false,
25 confirmThreadUnpin: false,25 confirmThreadUnpin: false,
26 contextWindowMeterEnabled: false,
26 dismissedProviderUpdateNotificationKeys: [],27 dismissedProviderUpdateNotificationKeys: [],
27 diffIgnoreWhitespace: true,28 diffIgnoreWhitespace: true,
28 environmentIdentificationMode: "artwork",29 environmentIdentificationMode: "artwork",
apps/web/src/components/chat/ChatComposer.tsx+3-1
...@@ -4339,7 +4339,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -4339,7 +4339,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
4339 ) : null}4339 ) : null}
4340 <ComposerFooterPrimaryActions4340 <ComposerFooterPrimaryActions
4341 compact={isComposerPrimaryActionsCompact}4341 compact={isComposerPrimaryActionsCompact}
4342 activeContextWindow={activeContextWindow}4342 activeContextWindow={
4343 settings.contextWindowMeterEnabled ? activeContextWindow : null
4344 }
4343 activeThreadModelDisplayName={activeThreadModelDisplayName}4345 activeThreadModelDisplayName={activeThreadModelDisplayName}
4344 pendingAction={pendingPrimaryAction}4346 pendingAction={pendingPrimaryAction}
4345 isRunning={phase === "running"}4347 isRunning={phase === "running"}
apps/web/src/components/settings/SettingsPanels.tsx+19
...@@ -522,6 +522,9 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -522,6 +522,9 @@ export function useSettingsRestore(onRestored?: () => void) {
522 ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu522 ...(settings.showSkillsInSlashMenu !== DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu
523 ? ["Show skills in slash menu"]523 ? ["Show skills in slash menu"]
524 : []),524 : []),
525 ...(settings.contextWindowMeterEnabled !== DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled
526 ? ["Context window indicator"]
527 : []),
525 ...(settings.enableLegacyTokenStreaming !==528 ...(settings.enableLegacyTokenStreaming !==
526 DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming529 DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming
527 ? ["Stream token by token"]530 ? ["Stream token by token"]
...@@ -576,6 +579,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -576,6 +579,7 @@ export function useSettingsRestore(onRestored?: () => void) {
576 settings.newWorktreesStartFromOrigin,579 settings.newWorktreesStartFromOrigin,
577 settings.diffIgnoreWhitespace,580 settings.diffIgnoreWhitespace,
578 settings.environmentIdentificationMode,581 settings.environmentIdentificationMode,
582 settings.contextWindowMeterEnabled,
579 settings.fontFamilyCode,583 settings.fontFamilyCode,
580 settings.fontFamilyComposer,584 settings.fontFamilyComposer,
581 settings.fontFamilySans,585 settings.fontFamilySans,
...@@ -668,6 +672,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -668,6 +672,7 @@ export function useSettingsRestore(onRestored?: () => void) {
668 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,672 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
669 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,673 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
670 showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,674 showSkillsInSlashMenu: DEFAULT_UNIFIED_SETTINGS.showSkillsInSlashMenu,
675 contextWindowMeterEnabled: DEFAULT_UNIFIED_SETTINGS.contextWindowMeterEnabled,
671 environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,676 environmentIdentificationMode: DEFAULT_UNIFIED_SETTINGS.environmentIdentificationMode,
672 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,677 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
673 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,678 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
...@@ -1750,6 +1755,7 @@ function AutoSettleDaysInput({...@@ -1750,6 +1755,7 @@ function AutoSettleDaysInput({
1750// expand the section before its target can mount and scroll.1755// expand the section before its target can mount and scroll.
1751const LEGACY_FEATURE_TARGET_IDS: ReadonlySet<string> = new Set([1756const LEGACY_FEATURE_TARGET_IDS: ReadonlySet<string> = new Set([
1752 "legacy-plan-mode",1757 "legacy-plan-mode",
1758 "legacy-context-window-indicator",
1753 "legacy-token-streaming",1759 "legacy-token-streaming",
1754 "legacy-sidebar",1760 "legacy-sidebar",
1755]);1761]);
...@@ -1826,6 +1832,19 @@ function LegacyFeaturesSection() {...@@ -1826,6 +1832,19 @@ function LegacyFeaturesSection() {
1826 />1832 />
1827 }1833 }
1828 />1834 />
1835 <SettingsRow
1836 {...searchableSetting("legacy-context-window-indicator")}
1837 description="Shows context window usage as a circular indicator in the composer."
1838 control={
1839 <Switch
1840 checked={settings.contextWindowMeterEnabled}
1841 onCheckedChange={(checked) =>
1842 updateSettings({ contextWindowMeterEnabled: Boolean(checked) })
1843 }
1844 aria-label="Context window indicator (legacy)"
1845 />
1846 }
1847 />
1829 <SettingsRow1848 <SettingsRow
1830 serverScoped1849 serverScoped
1831 {...searchableSetting("legacy-token-streaming")}1850 {...searchableSetting("legacy-token-streaming")}
apps/web/src/components/settings/settingsSearch.ts+6
...@@ -261,6 +261,12 @@ export const SETTINGS_SEARCH_ITEMS = [...@@ -261,6 +261,12 @@ export const SETTINGS_SEARCH_ITEMS = [
261 to: "/settings/general",261 to: "/settings/general",
262 searchTerms: ["build plan composer old"],262 searchTerms: ["build plan composer old"],
263 },263 },
264 {
265 id: "legacy-context-window-indicator",
266 title: "Context window indicator (legacy)",
267 to: "/settings/general",
268 searchTerms: ["composer meter usage tokens circle old"],
269 },
264 {270 {
265 id: "legacy-token-streaming",271 id: "legacy-token-streaming",
266 title: "Stream token by token (legacy)",272 title: "Stream token by token (legacy)",
packages/contracts/src/settings.test.ts+12
...@@ -191,6 +191,18 @@ describe("ClientSettings sidebar", () => {...@@ -191,6 +191,18 @@ describe("ClientSettings sidebar", () => {
191 });191 });
192});192});
193193
194describe("ClientSettings context window meter", () => {
195 it("defaults off and preserves an explicit legacy opt-in", () => {
196 expect(decodeClientSettings({}).contextWindowMeterEnabled).toBe(false);
197 expect(
198 decodeClientSettings({ contextWindowMeterEnabled: true }).contextWindowMeterEnabled,
199 ).toBe(true);
200 expect(
201 decodeClientSettingsPatch({ contextWindowMeterEnabled: true }).contextWindowMeterEnabled,
202 ).toBe(true);
203 });
204});
205
194describe("ServerSettings thread settlement", () => {206describe("ServerSettings thread settlement", () => {
195 it("defaults merge settlement on and inactivity settlement to three days", () => {207 it("defaults merge settlement on and inactivity settlement to three days", () => {
196 const settings = decodeServerSettings({});208 const settings = decodeServerSettings({});
packages/contracts/src/settings.ts+4
...@@ -265,6 +265,9 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -265,6 +265,9 @@ export const ClientSettingsSchema = Schema.Struct({
265 // default UI; this beta flag restores it (plus the /plan and /default slash265 // default UI; this beta flag restores it (plus the /plan and /default slash
266 // commands) for users who still rely on the old workflow.266 // commands) for users who still rely on the old workflow.
267 planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),267 planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
268 // Legacy context window meter. The composer hides it by default; users who
269 // still want the old usage indicator can restore it from Settings.
270 contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
268 showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),271 showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
269 // Legacy sidebar (the original per-project tree). Deliberately a fresh key272 // Legacy sidebar (the original per-project tree). Deliberately a fresh key
270 // (was `sidebarV2Enabled` + `sidebarV2ConfiguredByUser`): decoding drops the273 // (was `sidebarV2Enabled` + `sidebarV2ConfiguredByUser`): decoding drops the
...@@ -990,6 +993,7 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -990,6 +993,7 @@ export const ClientSettingsPatch = Schema.Struct({
990 ),993 ),
991 ),994 ),
992 planModeEnabled: Schema.optionalKey(Schema.Boolean),995 planModeEnabled: Schema.optionalKey(Schema.Boolean),
996 contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean),
993 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),997 showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean),
994 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),998 legacySidebarEnabled: Schema.optionalKey(Schema.Boolean),
995 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),999 sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode),