diff --git a/changelog/unreleased.md b/changelog/unreleased.md index 99dd81ab..dee69e24 100644 --- a/changelog/unreleased.md +++ b/changelog/unreleased.md @@ -1,7 +1,81 @@ --- -title: +title: Turn stats in work status --- ## App +### New + +- **Turn stats:** The work status panel now shows response speed, model and tool time, tokens, and reported cost after a turn finishes. It's on by default; hide it in the panel settings (thanks to @alvins82). +- **Projects:** Move project actions, worktree setup commands, and draft starters into the repository for teammates to use. Repository commands ask for trust before running, and ask again when they change. +- Plans: Move plans into the repository, or point the Plans tab at an existing folder of Markdown files in your project. +- Sessions: Search projects by name or path in the new-session project picker on web and desktop (thanks to @maximtop). +- Usage: Charm Hyper now shows your remaining Hypercredits and their dollar value (thanks to @airtaxi). +- Settings: "Always show scrollbars" keeps scrollbars visible on this device when you move the pointer away. + +### Improvements + +- Chat: `/btw` now opens a separate composer with its own draft, model, and effort. Select message text and choose "By the way…" to ask about it, or use `/btw ` to send immediately (thanks to @ChangeHow). +- Settings: Theme, fonts, and chat layout can differ between web, desktop, mobile, and VS Code. Panel sizes and other device choices stay on the device. +- Terminal: Text renders consistently across tabs, borders and block graphics join cleanly, and touch users get a copy button beside the tabs. +- Chat: Ctrl+N/P navigation works across model lists, menus, and autocomplete. Reopening the model picker brings the selected model into view (thanks to @ChangeHow). +- Settings/Chat: Send-shortcut choices and large-text paste behavior have clearer descriptions (thanks to @ChangeHow). +- Chat: More compact text and spacing, stronger headings and contrast, consistent Activity rows, and a divider before the final answer make replies easier to read. +- Chat: Selected text uses the same visible highlight in messages, file previews, and comments across themes. + +### Fixes + +- Chat: Forking a user message restores its text and attachments in the new composer's draft and preserves the source draft (thanks to @karimodm). +- Chat: Interrupted tools stop showing an endless running timer after a reload (thanks to @alvins82). +- Chat: Attached images no longer appear twice just after sending. +- Chat: Opening panels or resizing the window keeps you at the end when following the latest reply. Sending no longer leaves a large blank area below the message. +- Chat: Streaming Thinking stays inside its scroll box. Scrolling or dragging upward pauses its automatic scrolling so you can read earlier reasoning (thanks to @alvins82). +- Chat: Enter adds a newline in the expanded composer; Ctrl/Cmd+Enter sends. Keyboard selection of a project or worktree returns focus to the input (thanks to @ChangeHow). +- Chat: Narrow Markdown tables fit their columns, removing the empty bordered space on the right (thanks to @ChangeHow). +- Sessions: Opening or restoring a session whose worktree was deleted leaves moving it to another directory up to you. +- Mobile: The uncommitted-changes tooltip no longer flashes over the startup screen while the last session loads. +- Terminal: Switching projects or tabs keeps each terminal's output separate. Reopening or resizing the panel no longer leaves stray prompt fragments. +- Terminal: Exiting Node-based commands on macOS and Linux no longer prints an empty IPC-channel warning. +- Updates: Updating a desktop host from the browser uses its native updater, confirms the installed version, and reports restart failures with a retry option (thanks to @ChangeHow). +- Git: Switching to a token-based identity no longer fails with a credential-helper permission error (thanks to @ICEY16360). +- Usage: OpenRouter shows per-key spending and limits, or monthly spending for unlimited keys, fixing misleading zero balances (thanks to @leducmaxime). +- Usage: Ollama Cloud's dollar-based plans show monthly spending and extra credits, fixing missing usage and rejected credentials (thanks to @kydorn). +- Usage: NeuralWatt allowance rows show usage percentages and respond to the used/remaining toggle (thanks to @kydorn). +- Usage: Slow connections to providers such as z.ai no longer fail because the connection attempt ends too early (thanks to @ouyangjian28). +- Desktop/Linux: "Open in" no longer lists unrelated editors or launches the wrong app when an installed app has a non-Latin name (thanks to @ouyangjian28). +- Scrollbars: Hovering over a scrollable area reveals its scrollbar, including in Settings and dialogs, without shifting the content (thanks to @sergiofspedro). + +### Misc + +- Server: `OPENCHAMBER_DATA_DIR` also covers project settings, themes, speech models, and new managed chats. Existing managed chats stay in their current location. + ## VS Code + +### New + +- Projects: Store worktree setup commands and draft starters in the repository from Project settings. Repository commands require trust before running and after changes. +- Usage: Charm Hyper shows your remaining Hypercredits and their dollar value (thanks to @airtaxi). +- Settings: "Always show scrollbars" keeps scrollbars visible when the pointer leaves a scrollable area. + +### Improvements + +- **Chat:** `/btw` now has a separate composer with its own draft, model, and effort. The "By the way…" text-selection action prefills a question with the selected passage (thanks to @ChangeHow). +- Settings: VS Code keeps its own appearance and chat layout preferences, separate from web, desktop, and mobile. +- Chat: Ctrl+N/P navigation works across model lists, menus, and autocomplete. The model picker reopens with your selected model in view (thanks to @ChangeHow). +- Settings/Chat: Send-shortcut and large-text paste options have clearer descriptions (thanks to @ChangeHow). +- Chat: More compact Markdown, stronger headings and contrast, and a divider before the final answer make replies easier to scan. +- Chat: Text selection and comment highlights use a consistent, readable accent tint across themes. + +### Fixes + +- Chat: Forking a user message fills the destination composer with its prompt and attachments while keeping the original session's draft intact (thanks to @karimodm). +- Chat: Tools interrupted before a reload no longer keep a running timer indefinitely (thanks to @alvins82). +- Chat: Images attached to a sent message appear only once. +- Chat: Resizing the chat keeps the latest reply in view when following the end. Sending no longer creates a large blank space below the message. +- Chat: Long Thinking output stays in a capped scroll box while streaming; scrolling upward pauses its automatic scrolling (thanks to @alvins82). +- Chat: Narrow tables keep their border and toolbar close to the columns (thanks to @ChangeHow). +- Usage: OpenRouter reports key spending and limits accurately, including monthly spending for unlimited keys (thanks to @leducmaxime). +- Usage: Ollama Cloud dollar-based plans show monthly spending and extra credits; credential checks reject unreadable usage pages (thanks to @kydorn). +- Usage: NeuralWatt shows allowance percentages correctly in both used and remaining modes (thanks to @kydorn). +- Usage: Provider requests have enough time to connect on slower networks, fixing premature "fetch failed" errors (thanks to @ouyangjian28). +- Scrollbars: Hover reveals scrollbars in chat, Settings, and shared dialogs without moving the content sideways (thanks to @sergiofspedro). diff --git a/packages/ui/src/components/chat/work-status/DOCUMENTATION.md b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md index bae6b2f2..7bfb4f81 100644 --- a/packages/ui/src/components/chat/work-status/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md @@ -17,7 +17,7 @@ conditionally; passing "am I first?" down would mean each one tracking what the sections above it decided to render. Sections render nothing when they have no rows, so the panel collapses upward -instead of reserving empty space. Opt-in Turn stats keeps its header for a +instead of reserving empty space. Turn stats keeps its header for a selected session even without metrics, so a saved collapsed state can reopen. ## What it is not @@ -104,7 +104,7 @@ which requests only providers enabled for this panel. | Subagent blockers | directory `permission` / `question` maps | one subscription covers every child | | Usage | `components/usage/usageGroups.ts` over `useQuotaStore` | grouping shared with the mobile popover; presentation is not | | Linked threads | `lib/linkedIssues.ts` over session metadata | written by the flows that attach an issue or PR | -| Turn stats | `telemetry.ts` over `useSessionMessageRecords` | opt-in; computed only while expanded and authoritatively idle | +| Turn stats | `telemetry.ts` over `useSessionMessageRecords` | computed only while expanded and authoritatively idle | | Goal | `useSessionGoal` | respects the Settings toggle | | MCP | `useMcpStore` | connect/disconnect reuses the dropdown's actions | | Pinned messages | `getContextObligatoryMessages` + `state.part` | see below | @@ -230,7 +230,7 @@ the row reflects the reset tree rather than a mid-creation snapshot. Ordering is by durability, not category: 1. **Session** (goal, context, cost), **Project** (attention, branch, - changes, PR, checks), **Usage**, and **Turn stats** (opt-in session telemetry: + changes, PR, checks), **Usage**, and **Turn stats** (session telemetry: throughput, duration, TTFT, cache hit rate) — true for as long as the session is open. Usage sits here rather than lower down because a spent quota stops the work outright; @@ -241,12 +241,12 @@ Ordering is by durability, not category: A persisted preference (`workStatusPanelEnabled`) drives a header toggle, and a dialog behind the equalizer icon switches individual sections off. Hidden -sections are stored rather than visible ones. Telemetry is the opt-in exception: -UI-store v20 and legacy server-list hydration add it to the hidden set. A -`workStatusHiddenSectionsExplicit` marker records that a list was chosen in a -client with telemetry support. The marker and list travel together through -autosave, sanitization, and server settings, so an explicit empty list enables -everything while an old empty list does not enable telemetry. Complete settings +sections are stored rather than visible ones. Every section, including Turn +stats, is enabled by default. UI-store v21 migration and server-list hydration +remove the old automatic telemetry hiding unless `workStatusHiddenSectionsExplicit` +records a user-chosen list. Explicit hiding and other hidden sections survive. +The marker and list travel together through autosave, sanitization, and server +settings; an empty list enables everything. Complete settings snapshots own this preference; unrelated partial save echoes leave it unchanged. `workStatusPanelVisible` is separate and transient: the switch can be on while diff --git a/packages/ui/src/components/chat/work-status/sections.test.ts b/packages/ui/src/components/chat/work-status/sections.test.ts index 3273257f..72e7450b 100644 --- a/packages/ui/src/components/chat/work-status/sections.test.ts +++ b/packages/ui/src/components/chat/work-status/sections.test.ts @@ -112,8 +112,18 @@ describe('sanitizeWorkStatusHiddenSections', () => { }); test('treats a non-array payload as default hidden preference', () => { - expect(sanitizeWorkStatusHiddenSections(undefined)).toEqual(['telemetry']); - expect(sanitizeWorkStatusHiddenSections('usage')).toEqual(['telemetry']); - expect(sanitizeWorkStatusHiddenSections({ usage: true })).toEqual(['telemetry']); + expect(sanitizeWorkStatusHiddenSections(undefined)).toEqual([]); + expect(sanitizeWorkStatusHiddenSections('usage')).toEqual([]); + expect(sanitizeWorkStatusHiddenSections({ usage: true })).toEqual([]); + }); + + test('removes only the old implicit telemetry default', () => { + expect(sanitizeWorkStatusHiddenSections(['mcp', 'telemetry'], false)).toEqual(['mcp']); + expect(sanitizeWorkStatusHiddenSections([], false)).toEqual([]); + }); + + test('preserves explicit hiding, including hiding every section', () => { + expect(sanitizeWorkStatusHiddenSections(['mcp', 'telemetry'], true)).toEqual(['mcp', 'telemetry']); + expect(sanitizeWorkStatusHiddenSections([...WORK_STATUS_SECTION_IDS], true)).toEqual([...WORK_STATUS_SECTION_IDS]); }); }); diff --git a/packages/ui/src/components/chat/work-status/sections.ts b/packages/ui/src/components/chat/work-status/sections.ts index 41601d5f..99d1a124 100644 --- a/packages/ui/src/components/chat/work-status/sections.ts +++ b/packages/ui/src/components/chat/work-status/sections.ts @@ -42,8 +42,7 @@ const isWorkStatusSectionId = (value: unknown): value is WorkStatusSectionId => typeof value === 'string' && KNOWN_IDS.has(value); /** - * Hidden sections are stored, not visible ones. Telemetry is opt-in; legacy - * lists must be normalized before use so adding it does not enable it. + * Hidden sections are stored, not visible ones. Every section is on by default. */ export const isWorkStatusSectionVisible = ( hidden: readonly string[] | null | undefined, @@ -76,16 +75,13 @@ export const getWorkStatusPanelPresentation = ({ showEmptyState: contentMounted && allSectionsHidden, }); -const WORK_STATUS_DEFAULT_HIDDEN_SECTIONS = [ - 'telemetry', -] as const satisfies readonly WorkStatusSectionId[]; - export const sanitizeWorkStatusHiddenSections = (value: unknown, explicit = true): WorkStatusSectionId[] => { - if (!Array.isArray(value)) return [...WORK_STATUS_DEFAULT_HIDDEN_SECTIONS]; + if (!Array.isArray(value)) return []; const seen = new Set(); for (const entry of value) { if (isWorkStatusSectionId(entry)) seen.add(entry); } - if (!explicit) seen.add('telemetry'); + // Older clients hid telemetry automatically until the user chose a list. + if (!explicit) seen.delete('telemetry'); return [...seen]; }; diff --git a/packages/ui/src/lib/persistence.test.ts b/packages/ui/src/lib/persistence.test.ts index a49ae839..37130989 100644 --- a/packages/ui/src/lib/persistence.test.ts +++ b/packages/ui/src/lib/persistence.test.ts @@ -863,22 +863,22 @@ describe('updateDesktopSettings', () => { expect(saveCalls.some((changes) => changes.toolJsonViewMode === 'formatted')).toBe(true); }); - test('legacy server lists keep telemetry hidden, while explicit opt-ins survive hydration', async () => { + test('legacy server lists show telemetry, while explicit hiding survives hydration', async () => { getWindow(); for (const explicit of [undefined, false, true]) { invalidateSettingsCache(); registerSettingsApi(async (changes) => changes, async () => ({ - settings: { workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: explicit, + settings: { workStatusHiddenSections: ['mcp', 'telemetry'], workStatusHiddenSectionsExplicit: explicit, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true }, source: 'web', })); await syncDesktopSettings(); - expect(useUIStore.getState().workStatusHiddenSections).toEqual(explicit ? ['mcp'] : ['mcp', 'telemetry']); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(explicit ? ['mcp', 'telemetry'] : ['mcp']); expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(explicit === true); } }); - test('autosaves telemetry opt-in and its list together, then restores them through settings load', async () => { + test('autosaves telemetry hiding and its list together, then restores them through settings load', async () => { getWindow(); invalidateSettingsCache(); let server: SettingsPayload = { workStatusHiddenSections: [], draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true }; @@ -886,23 +886,20 @@ describe('updateDesktopSettings', () => { registerSettingsApi(async (changes) => { saves.push(changes); server = { ...server, ...changes }; return changes; }, async () => ({ settings: server, source: 'web' })); await syncDesktopSettings(); - expect(useUIStore.getState().workStatusHiddenSections).toEqual(['telemetry']); + expect(useUIStore.getState().workStatusHiddenSections).toEqual([]); startAppearanceAutoSave(); - useUIStore.getState().setWorkStatusSectionVisible('telemetry', true); + useUIStore.getState().setWorkStatusSectionVisible('telemetry', false); await delay(600); - // The list itself already matches the server ([]), so only the explicit - // marker needs to travel; the server merges per key, so the end state is - // the same as sending both. expect(saves.some((changes) => changes.workStatusHiddenSectionsExplicit === true)).toBe(true); - expect(server.workStatusHiddenSections).toEqual([]); + expect(server.workStatusHiddenSections).toEqual(['telemetry']); expect(server.workStatusHiddenSectionsExplicit).toBe(true); invalidateSettingsCache(); await syncDesktopSettings(); - expect(useUIStore.getState().workStatusHiddenSections).toEqual([]); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['telemetry']); expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(true); - // An unrelated partial save response must not turn an opt-in back off. + // An unrelated partial save response must not re-enable a hidden section. await updateDesktopSettings({ workStatusPanelEnabled: useUIStore.getState().workStatusPanelEnabled }); - expect(useUIStore.getState().workStatusHiddenSections).toEqual([]); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['telemetry']); }); test('applies persisted autoSaveEnabled from server settings', async () => { @@ -1116,7 +1113,7 @@ describe('updateDesktopSettings', () => { favoriteModels: [{ providerID: 'anthropic', modelID: 'claude-sonnet-4' }], // A legacy list the client normalises on read: the normalised copy is // still not this window's change and must not be written back. - workStatusHiddenSections: ['mcp'], + workStatusHiddenSections: ['mcp', 'telemetry'], draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true, }, @@ -1137,7 +1134,7 @@ describe('updateDesktopSettings', () => { expect(useUIStore.getState().showReasoningTraces).toBe(false); expect(useUIStore.getState().terminalShell).toBe('fish'); expect(useUIStore.getState().favoriteModels).toHaveLength(1); - expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp']); expect(saveCalls).toEqual([]); } finally { stopModelPrefs(); diff --git a/packages/ui/src/lib/settings/registry.test.ts b/packages/ui/src/lib/settings/registry.test.ts index 2c91f379..dcdebd87 100644 --- a/packages/ui/src/lib/settings/registry.test.ts +++ b/packages/ui/src/lib/settings/registry.test.ts @@ -87,11 +87,11 @@ describe('settings registry', () => { }); test('applies the hidden-sections list together with its explicit marker', () => { - applySettingsToStores({ workStatusHiddenSections: ['mcp'] }); - expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); - expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(false); - applySettingsToStores({ workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true }); + applySettingsToStores({ workStatusHiddenSections: ['mcp', 'telemetry'] }); expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp']); + expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(false); + applySettingsToStores({ workStatusHiddenSections: ['mcp', 'telemetry'], workStatusHiddenSectionsExplicit: true }); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(true); }); diff --git a/packages/ui/src/lib/settings/registry.ts b/packages/ui/src/lib/settings/registry.ts index 80badb7d..162b8868 100644 --- a/packages/ui/src/lib/settings/registry.ts +++ b/packages/ui/src/lib/settings/registry.ts @@ -296,8 +296,8 @@ export const SETTINGS_REGISTRY = { parse: parseWorkStatusHiddenSections, ui: { read: () => useUIStore.getState().workStatusHiddenSections, - // The explicit marker decides whether a legacy list implicitly hides - // telemetry; both land in one store update so subscribers never see the + // The explicit marker distinguishes chosen lists from the old telemetry + // default; both land in one store update so subscribers never see the // list without its marker. write: (value, snapshot) => { const explicit = snapshot.workStatusHiddenSectionsExplicit === true; diff --git a/packages/ui/src/stores/useUIStore.telemetry.test.ts b/packages/ui/src/stores/useUIStore.telemetry.test.ts index 8e8f75d3..fd580f46 100644 --- a/packages/ui/src/stores/useUIStore.telemetry.test.ts +++ b/packages/ui/src/stores/useUIStore.telemetry.test.ts @@ -9,32 +9,54 @@ afterEach(() => { }); describe('telemetry settings migration', () => { - for (const version of [18, 19]) { + test('shows telemetry by default', () => { + expect(useUIStore.getInitialState().workStatusHiddenSections).toEqual([]); + }); + + for (const version of [18, 19, 20]) { test(`migrates real v${version} hydration without losing existing hidden sections`, async () => { useUIStore.persist.setOptions({ storage: { - getItem: () => ({ version, state: { ...useUIStore.getInitialState(), workStatusHiddenSections: ['mcp'] } }), + getItem: () => ({ version, state: { ...useUIStore.getInitialState(), workStatusHiddenSections: ['mcp', 'telemetry'] } }), setItem: () => undefined, removeItem: () => undefined, } }); await useUIStore.persist.rehydrate(); - expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp']); expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(false); - expect(useUIStore.persist.getOptions().version).toBe(20); + expect(useUIStore.persist.getOptions().version).toBe(21); }); } - test('explicit opt-in round-trips through the actual persisted projection and hydration', async () => { - let saved: Parameters['setItem']>[1] = { state: useUIStore.getInitialState(), version: 20 }; + test('preserves an explicitly hidden section from v20', async () => { + useUIStore.persist.setOptions({ storage: { + getItem: () => ({ version: 20, state: { ...useUIStore.getInitialState(), workStatusHiddenSections: ['mcp', 'telemetry'], workStatusHiddenSectionsExplicit: true } }), + setItem: () => undefined, + removeItem: () => undefined, + } }); + await useUIStore.persist.rehydrate(); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); + expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(true); + }); + + test('explicit hiding round-trips through the actual persisted projection and hydration', async () => { + let saved: Parameters['setItem']>[1] = { state: useUIStore.getInitialState(), version: originalOptions.version }; useUIStore.persist.setOptions({ storage: { getItem: () => saved, setItem: (_name, value) => { saved = value; }, removeItem: () => undefined, } }); - useUIStore.setState({ workStatusHiddenSections: ['telemetry', 'mcp'], workStatusHiddenSectionsExplicit: false }); - useUIStore.getState().setWorkStatusSectionVisible('telemetry', true); + useUIStore.setState({ workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: false }); + useUIStore.getState().setWorkStatusSectionVisible('telemetry', false); useUIStore.persist.setOptions({ storage: { getItem: () => saved, setItem: () => undefined, removeItem: () => undefined } }); - useUIStore.setState({ workStatusHiddenSections: ['telemetry'], workStatusHiddenSectionsExplicit: false }); + useUIStore.setState({ workStatusHiddenSections: [], workStatusHiddenSectionsExplicit: false }); await useUIStore.persist.rehydrate(); + expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp', 'telemetry']); + expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(true); + }); + + test('can show telemetry again after hiding it', () => { + useUIStore.setState({ workStatusHiddenSections: ['mcp', 'telemetry'], workStatusHiddenSectionsExplicit: true }); + useUIStore.getState().setWorkStatusSectionVisible('telemetry', true); expect(useUIStore.getState().workStatusHiddenSections).toEqual(['mcp']); expect(useUIStore.getState().workStatusHiddenSectionsExplicit).toBe(true); }); diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 8a515c1e..35e4c141 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -1190,7 +1190,7 @@ export const useUIStore = create()( workStatusPanelVisible: false, workStatusPanelFits: false, workStatusOverlayOpen: false, - workStatusHiddenSections: ['telemetry'], + workStatusHiddenSections: [], workStatusHiddenSectionsExplicit: false, isSessionSwitcherOpen: false, isSessionDropdownOpen: false, @@ -2720,22 +2720,18 @@ export const useUIStore = create()( { name: 'ui-store', storage: createDeferredSafeJSONStorage(), - version: 20, + version: 21, migrate: (persistedState, version) => { if (!persistedState || typeof persistedState !== 'object') { return persistedState; } const state = persistedState as Record; - // v19 -> v20: lists written before telemetry existed are not opt-ins. - if (version < 20 && state.workStatusHiddenSectionsExplicit !== true) { - if (Array.isArray(state.workStatusHiddenSections)) { - if (!state.workStatusHiddenSections.includes('telemetry')) { - state.workStatusHiddenSections.push('telemetry'); - } - } else { - state.workStatusHiddenSections = ['telemetry']; - } + // v20 -> v21: enable telemetry by default; preserve explicit choices. + if (version < 21 && state.workStatusHiddenSectionsExplicit !== true) { + state.workStatusHiddenSections = Array.isArray(state.workStatusHiddenSections) + ? state.workStatusHiddenSections.filter((id) => id !== 'telemetry') + : []; state.workStatusHiddenSectionsExplicit = false; }