feat: enable turn stats by default and prepare release notes

Turn stats was hidden unless users opted in. Show it by default, migrate implicit hidden lists, and preserve explicit section choices through reloads and settings sync.

Prepare App and VS Code release notes with Turn stats as the headline and BTW composer changes under improvements.

Testing: 115 focused tests passed; UI type-check passed; UI lint has one existing warning. Changelog validation and diff checks passed. Oxlint findings are limited to existing code in sections.ts; interactive app validation was not run.
This commit is contained in:
Bohdan Triapitsyn
2026-09-08 01:38:28 +03:00
parent 5df08ade90
commit 9e2163d839
9 changed files with 157 additions and 62 deletions
@@ -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
@@ -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]);
});
});
@@ -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<WorkStatusSectionId>();
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];
};
+12 -15
View File
@@ -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();
@@ -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);
});
+2 -2
View File
@@ -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;
@@ -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<NonNullable<typeof originalOptions.storage>['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<NonNullable<typeof originalOptions.storage>['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);
});
+7 -11
View File
@@ -1190,7 +1190,7 @@ export const useUIStore = create<UIStore>()(
workStatusPanelVisible: false,
workStatusPanelFits: false,
workStatusOverlayOpen: false,
workStatusHiddenSections: ['telemetry'],
workStatusHiddenSections: [],
workStatusHiddenSectionsExplicit: false,
isSessionSwitcherOpen: false,
isSessionDropdownOpen: false,
@@ -2720,22 +2720,18 @@ export const useUIStore = create<UIStore>()(
{
name: 'ui-store',
storage: createDeferredSafeJSONStorage(),
version: 20,
version: 21,
migrate: (persistedState, version) => {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
}
const state = persistedState as Record<string, unknown>;
// 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;
}