From 171efa33ec9f5c83053f21c1b8f08de5ac297119 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:22:11 +1200 Subject: [PATCH 01/30] feat(desktop): refine usage history ranges and real data Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../src/main/maker-ipc/usageHandlers.ts | 16 +- .../main/usage/__tests__/usageHistory.test.ts | 30 +++ apps/desktop/src/main/usage/usageHistory.ts | 61 +++-- apps/desktop/src/preload/preload.ts | 6 +- .../homeUsageDashboardSource.test.ts | 6 +- .../components/new-chat/UsageHeatmap.tsx | 250 ++++++++++++------ .../__tests__/usageHeatmapMetric.test.tsx | 70 ++++- .../settings/usage/UsageBreakdownTables.tsx | 40 ++- .../settings/usage/UsageHistorySection.tsx | 162 +++++++++--- .../settings/usage/UsageStatRow.tsx | 29 +- .../settings/usage/UsageTaskTable.tsx | 37 ++- .../settings/usage/UsageTokenBars.tsx | 16 +- .../usage/__tests__/usageHistoryStats.test.ts | 89 +++++-- .../settings/usage/usageHistoryStats.ts | 118 ++++++++- .../src/renderer/hooks/useUsageHistory.ts | 84 +++++- .../src/renderer/i18n/locales/en/common.json | 17 +- .../src/renderer/i18n/locales/ja/common.json | 17 +- .../src/renderer/i18n/locales/ko/common.json | 17 +- .../renderer/i18n/locales/zh-CN/common.json | 17 +- .../renderer/i18n/locales/zh-TW/common.json | 17 +- apps/desktop/src/renderer/vite-env.d.ts | 3 +- 21 files changed, 871 insertions(+), 231 deletions(-) diff --git a/apps/desktop/src/main/maker-ipc/usageHandlers.ts b/apps/desktop/src/main/maker-ipc/usageHandlers.ts index 98c631e819c..645a62a35d5 100644 --- a/apps/desktop/src/main/maker-ipc/usageHandlers.ts +++ b/apps/desktop/src/main/maker-ipc/usageHandlers.ts @@ -159,11 +159,23 @@ export function registerMakerUsageHandlers( // 用量历史聚合 (首页仪表盘) — 查询型 handler, DB 出错回退空 payload 让 // renderer 正常渲染空态 (与同文件其它 usage 读取的 fallback-data 口径一致)。 registry.handle(MAKER_INVOKE.USAGE_HISTORY, async (_e, opts: unknown) => { - const raw = (opts ?? {}) as { days?: unknown; forceRefresh?: unknown }; - const days = typeof raw.days === 'number' && Number.isFinite(raw.days) ? raw.days : undefined; + const raw = (opts ?? {}) as { days?: unknown; modelDays?: unknown; forceRefresh?: unknown }; + const days = + raw.days === 'all' + ? ('all' as const) + : typeof raw.days === 'number' && Number.isFinite(raw.days) + ? raw.days + : undefined; + const modelDays = + raw.modelDays === 'all' + ? ('all' as const) + : typeof raw.modelDays === 'number' && Number.isFinite(raw.modelDays) + ? raw.modelDays + : undefined; const forceRefresh = raw.forceRefresh === true; const readOpts = { ...(days === undefined ? {} : { days }), + ...(modelDays === undefined ? {} : { modelDays }), ...(forceRefresh ? { forceRefresh: true } : {}), }; try { diff --git a/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts b/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts index 84e6525c7b5..679da20c851 100644 --- a/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts +++ b/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts @@ -320,6 +320,36 @@ describe('getSubscriptionValuePriceFor', () => { }); describe('readUsageHistoryWith', () => { + it('supports an all-time request without changing the default 30-day model window', async () => { + const oldRow = modelRow('2026-04-01', 'codex', 'gpt-5.5', actual(0), { + inputTokens: 123, + }); + const recentRow = modelRow(TODAY, 'codex', 'gpt-5.5', actual(0), { + inputTokens: 456, + }); + let requestedSince = ''; + const deps = makeDeps({ + getModelUsageSince: async (sinceDayKey) => { + requestedSince = sinceDayKey; + return [oldRow, recentRow]; + }, + }); + + const defaultResult = await readUsageHistoryWith(deps); + expect(defaultResult.modelDaily.map((row) => row.day)).toEqual([TODAY]); + + const allResult = await readUsageHistoryWith(deps, { days: 'all', modelDays: 'all' }); + expect(requestedSince).toBe('0000-01-01'); + expect(allResult.modelDaily.map((row) => row.day)).toEqual(['2026-04-01', TODAY]); + expect(allResult.totals.last30DaysTokens).toBe(579); + expect(allResult.modelDaily[0]).toMatchObject({ + inputTokens: 123, + outputTokens: 0, + cacheReadTokens: 0, + cacheCreateTokens: 0, + }); + }); + it('aggregates actual money and subscription value without double counting', async () => { const estimateAmount = regionalUsdAmount(2); const result = await readUsageHistoryWith(makeDeps({ diff --git a/apps/desktop/src/main/usage/usageHistory.ts b/apps/desktop/src/main/usage/usageHistory.ts index 459c27ff080..2ec9132923d 100644 --- a/apps/desktop/src/main/usage/usageHistory.ts +++ b/apps/desktop/src/main/usage/usageHistory.ts @@ -101,6 +101,10 @@ export interface UsageHistoryModelDay { /** 订阅 token 价值估算金额。 */ subscriptionEstimateMoney: RegionalMoney; tokens: number; + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheCreateTokens: number; } export interface UsageHistoryPayload { @@ -119,9 +123,9 @@ export interface UsageHistoryPayload { estimatesPending: boolean; /** day >= today-windowDays 的日总额 (稀疏, 无消费日无行; renderer 补格)。 */ days: UsageHistoryDay[]; - /** 近 30 天每日 × 模型明细 (堆叠柱状图分段用; 表上线前的历史日无行)。 */ + /** 按 modelDays 窗口返回的每日 × 模型明细 (堆叠柱状图分段用)。 */ modelDaily: UsageHistoryModelDay[]; - /** 近 30 天按 (agentKind, model) 聚合, 按可比金额降序。 */ + /** 按 modelDays 窗口聚合的 (agentKind, model), 按可比金额降序。 */ models: UsageHistoryModel[]; streak: { current: number; longest: number }; totals: { @@ -265,10 +269,19 @@ function diskCachePath(): string { return path.join(app.getPath('userData'), 'cache', DISK_CACHE_FILE); } -function optsKey(opts?: { days?: number }): string { - const days = Math.min(366, Math.max(1, Math.floor(opts?.days ?? 140))); +function normalizeWindowDays(value: number | 'all' | undefined, fallback: number): number | 'all' { + if (value === 'all') return 'all'; + return Math.min(366, Math.max(1, Math.floor(value ?? fallback))); +} + +function optsKey(opts?: UsageHistoryReadOptions): string { + const days = normalizeWindowDays(opts?.days, 140); + const modelDays = normalizeWindowDays(opts?.modelDays, MODEL_WINDOW_DAYS); const userId = getCurrentDbClientUserId() ?? 'anonymous'; - return `user=${encodeURIComponent(userId)}|days=${days}`; + // Keep the pre-window-split key for the default request so existing disk + // snapshots remain readable. Non-default model windows get their own key. + const modelSuffix = modelDays === MODEL_WINDOW_DAYS ? '' : `|modelDays=${modelDays}`; + return `user=${encodeURIComponent(userId)}|days=${days}${modelSuffix}`; } function isFiniteNumber(value: unknown): value is number { @@ -459,7 +472,9 @@ function isLatestRefreshGeneration(expectedOptsKey: string, generation: number): } export interface UsageHistoryReadOptions { - days?: number; + days?: number | 'all'; + /** Model/table aggregation window. `all` is used by Settings → Usage History. */ + modelDays?: number | 'all'; /** * true = 事件触发的刷新, 需要绕过 10s 内存快返, 立即重新聚合 DB。 * mount / 展开仍使用 stale-while-refresh 快路径保证首帧速度。 @@ -515,7 +530,8 @@ export async function readUsageHistoryWith( deps: UsageHistoryDeps, opts?: UsageHistoryReadOptions, ): Promise { - const windowDays = Math.min(366, Math.max(1, Math.floor(opts?.days ?? 140))); + const windowDays = normalizeWindowDays(opts?.days, 140); + const modelWindowDays = normalizeWindowDays(opts?.modelDays, MODEL_WINDOW_DAYS); const todayKey = deps.todayKey(); // 历史聚合只有一个金额口径:兼容当前账本币种的金额保留,无法确定换算语义的 @@ -573,28 +589,40 @@ export async function readUsageHistoryWith( allDays.map((row) => [row.day, row.money.amount]), ); - const heatmapCutoff = shiftDayKey(todayKey, -(windowDays - 1)); - const modelCutoff = shiftDayKey(todayKey, -(MODEL_WINDOW_DAYS - 1)); + const heatmapCutoff = windowDays === 'all' ? null : shiftDayKey(todayKey, -(windowDays - 1)); + const modelCutoff = modelWindowDays === 'all' + ? null + : shiftDayKey(todayKey, -(modelWindowDays - 1)); // 一次查询同时服务两个窗口: 热力图 tooltip 的每日 token (heatmap 窗口) 与 // 模型拆分聚合 (30 天窗口)。取更早的 cutoff (ISO day key 字符串可直接比较)。 - const usageRowsSince = heatmapCutoff < modelCutoff ? heatmapCutoff : modelCutoff; + const usageRowsSince = heatmapCutoff === null && modelCutoff === null + ? '0000-01-01' + : heatmapCutoff === null + ? modelCutoff! + : modelCutoff === null + ? heatmapCutoff + : heatmapCutoff < modelCutoff + ? heatmapCutoff + : modelCutoff; const allModelRows = (await deps.getModelUsageSince(usageRowsSince)).map((row) => ({ ...row, money: keepCompatibleMoney(row.money), })); - const modelRows = allModelRows.filter((r) => r.day >= modelCutoff); + const modelRows = modelCutoff === null + ? allModelRows + : allModelRows.filter((r) => r.day >= modelCutoff); // 每日 token 合计 → days 的 tooltip 数据。codex-only 日 daily_spend 无行 ($ 只有 // Claude 记), 也要并进 days, 否则热力图那天 hover 不到 token。 const tokensByDay = new Map(); for (const row of allModelRows) { - if (row.day < heatmapCutoff) continue; + if (heatmapCutoff !== null && row.day < heatmapCutoff) continue; const rowTokens = row.inputTokens + row.outputTokens + row.cacheReadTokens + row.cacheCreateTokens; tokensByDay.set(row.day, (tokensByDay.get(row.day) ?? 0) + rowTokens); } const daysMap = new Map(); for (const r of allDays) { - if (r.day >= heatmapCutoff && r.money.amount > 0) { + if ((heatmapCutoff === null || r.day >= heatmapCutoff) && r.money.amount > 0) { daysMap.set(r.day, { day: r.day, money: r.money, tokens: 0 }); } } @@ -721,6 +749,10 @@ export async function readUsageHistoryWith( apiMoney, subscriptionEstimateMoney, tokens: row.inputTokens + row.outputTokens + row.cacheReadTokens + row.cacheCreateTokens, + inputTokens: row.inputTokens, + outputTokens: row.outputTokens, + cacheReadTokens: row.cacheReadTokens, + cacheCreateTokens: row.cacheCreateTokens, }; }); // 可比金额 (实报或估算) 降序; 无金额的 token-only 行排最后 (按 token 量降序) @@ -750,10 +782,9 @@ export async function readUsageHistoryWith( heuristicThreshold(ACTIVE_DAY_MIN_USD, row.money.currency), ) .map((row) => row.day); - const last30Cutoff = shiftDayKey(todayKey, -(MODEL_WINDOW_DAYS - 1)); const last30ActualByDay = new Map(); for (const row of allDays) { - if (row.day >= last30Cutoff) { + if (modelCutoff === null || row.day >= modelCutoff) { last30ActualByDay.set(row.day, row.money); } } diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 432c5e98ce0..de44f55363c 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -6870,7 +6870,11 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('maker:usage:reference-model-pricing'), onReferenceModelPricingChanged: fanOutMakerUsageReferenceModelPricing, /** 用量历史聚合 (首页仪表盘: 热力图 + streak + 按模型拆分, main 侧算好)。 */ - getHistory: (opts?: { days?: number; forceRefresh?: boolean }): Promise => + getHistory: (opts?: { + days?: number | 'all'; + modelDays?: number | 'all'; + forceRefresh?: boolean; + }): Promise => ipcRenderer.invoke('maker:usage:history', opts), /** Claude USD 推送 (per-turn, agentKind=claude-code 时订阅它)。 */ onTodaySpendChanged: fanOutMakerUsageTodaySpend, diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 7923be9ef59..9b0c365d54e 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -116,9 +116,9 @@ describe('HomeUsageDashboard source contract', () => { expect(hookSource).toContain( 'const [historyState, setHistoryState] = useState<{ scopeKey: string; value: UsageHistoryPayload | null }>', ); - expect(hookSource).toContain('setHistoryState({ scopeKey, value });'); + expect(hookSource).toContain('setHistoryState({ scopeKey: scopedKey, value });'); expect(hookSource).toContain( - 'history: historyState.scopeKey === scopeKey ? historyState.value : scope.cache,', + 'history: historyState.scopeKey === scopedKey ? historyState.value : scope.cache,', ); }); @@ -158,7 +158,7 @@ describe('HomeUsageDashboard source contract', () => { expect(hookSource).toContain('if (opts?.resetPricingRetry) scope.pricingRetryDone = false;'); expect(hookSource).toContain('scope.pricingRetryDone = false;'); expect(hookSource).toContain( - 'void load(scopeKey, { forceRefresh: true, resetPricingRetry: true });', + 'void load(scopedKey, { forceRefresh: true, resetPricingRetry: true });', ); }); diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index ced17bc05b5..80d11fc0cbb 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -7,22 +7,20 @@ * 强度口径由 `metric` 决定: 'money' (默认, 首页仪表盘) 或 'tokens' (设置 → 用量历史, * 那个页面不出现任何金额, 见 issue #2785)。两种口径共用同一套分位分桶与色阶。 * - * 视觉: 7 行 (周日起) × ~20 列周网格, 单色阶 — 非零值按 4 分位分桶, + * 视觉: 7 行 (周日起) × 至少 20 列周网格, 单色阶 — 非零值按 4 分位分桶, * 用 color-mix 在 --accent-emphasis 上做透明度阶梯 (黑白反色设计, 不引入彩色)。 - * 140 个格子用原生 title 做 tooltip (Radix per-cell 实例太重)。 + * 格子用原生 title 做 tooltip (Radix per-cell 实例太重)。 */ -import React, { useMemo } from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { formatCompactTokens, formatMoney } from '@/lib/usageFormat'; -import { - DEFAULT_USAGE_CURRENCY, - type RegionalMoney, -} from '../../../shared/regionalMoney'; +import { DEFAULT_USAGE_CURRENCY, type RegionalMoney } from '../../../shared/regionalMoney'; const CELL_PX = 12; const GAP_PX = 3; +const MIN_HEATMAP_WEEKS = 20; const EMPTY_MONEY_CURRENCY = DEFAULT_USAGE_CURRENCY; /** 非零值分桶的 color-mix 浓度阶梯 (level 1..4)。 */ const LEVEL_MIX = [0.22, 0.42, 0.68, 1]; @@ -49,6 +47,54 @@ function toDayKey(date: Date): string { return `${date.getFullYear()}-${mm}-${dd}`; } +function startOfWeek(date: Date): Date { + const result = new Date(date); + result.setHours(12, 0, 0, 0); + result.setDate(result.getDate() - result.getDay()); + return result; +} + +function calendarDayDistance(from: Date, to: Date): number { + const fromUtc = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate()); + const toUtc = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate()); + return Math.round((toUtc - fromUtc) / 86_400_000); +} + +function fitWeeksForWidth(width: number): number { + if (!(width > 0)) return 0; + return Math.max(1, Math.floor((width + GAP_PX) / (CELL_PX + GAP_PX))); +} + +/** 至少保留 20 周;有更早数据且容器放得下时,尽量展示更多历史。 */ +export function resolveHeatmapWeeks({ + days, + todayKey, + availableWidth, + minimumWeeks = MIN_HEATMAP_WEEKS, +}: { + days: Array<{ day: string }>; + todayKey: string; + availableWidth: number; + minimumWeeks?: number; +}): number { + const minWeeks = Math.max(MIN_HEATMAP_WEEKS, Math.ceil(minimumWeeks)); + const today = parseDayKey(todayKey); + if (!todayKey || Number.isNaN(today.getTime())) return minWeeks; + + const earliestDay = days + .map((day) => day.day) + .filter((day) => day && day <= todayKey) + .sort()[0]; + const dataWeeks = earliestDay + ? Math.floor( + calendarDayDistance(startOfWeek(parseDayKey(earliestDay)), startOfWeek(today)) / 7, + ) + 1 + : 0; + const fitWeeks = fitWeeksForWidth(availableWidth); + const widthLimit = fitWeeks > 0 ? fitWeeks : minWeeks; + return Math.max(minWeeks, Math.min(Math.max(minWeeks, dataWeeks), widthLimit)); +} + /** 非零花费的 4 分位阈值 → level 1..4。 */ function levelFor(cost: number, thresholds: [number, number, number]): number { if (cost <= 0) return 0; @@ -63,33 +109,75 @@ export function UsageHeatmap({ todayKey, windowDays, metric = 'money', + selectedDay, + onDayClick, + onVisibleWeeksChange, }: { days: Array<{ day: string; money: RegionalMoney; tokens?: number }>; todayKey: string; windowDays: number; /** 格子深浅按哪一维分桶。'tokens' 下 tooltip 也只显示 token, 不出现金额。 */ metric?: 'money' | 'tokens'; + selectedDay?: string | null; + onDayClick?: (day: string) => void; + onVisibleWeeksChange?: (weeks: number) => void; }): React.JSX.Element { const { t, i18n } = useTranslation(); + const plotRef = useRef(null); + const [availableWidth, setAvailableWidth] = useState(0); + + useLayoutEffect(() => { + const element = plotRef.current; + if (!element) return undefined; + + const updateWidth = (width: number): void => { + const nextWidth = Math.max(0, Math.round(width)); + setAvailableWidth((previous) => (previous === nextWidth ? previous : nextWidth)); + }; + updateWidth(element.clientWidth); + if (typeof ResizeObserver === 'undefined') return undefined; + + const observer = new ResizeObserver((entries) => { + updateWidth(entries[0]?.contentRect.width ?? element.clientWidth); + }); + observer.observe(element); + return () => observer.disconnect(); + }, []); + + const minimumWeeks = Math.max(MIN_HEATMAP_WEEKS, Math.ceil(windowDays / 7)); + const visibleWeeks = resolveHeatmapWeeks({ + days, + todayKey, + availableWidth, + minimumWeeks, + }); + + useLayoutEffect(() => { + onVisibleWeeksChange?.(visibleWeeks); + }, [onVisibleWeeksChange, visibleWeeks]); const { columns, monthLabels } = useMemo(() => { const spendByDay = new Map(days.map((d) => [d.day, d.money])); const tokensByDay = new Map(days.map((d) => [d.day, d.tokens ?? 0])); const intensityOf = (row: { money: RegionalMoney; tokens?: number }): number => metric === 'tokens' ? (row.tokens ?? 0) : row.money.amount; - const nonZero = days.map(intensityOf).filter((v) => v > 0).sort((a, b) => a - b); - const q = (p: number): number => (nonZero.length ? nonZero[Math.min(nonZero.length - 1, Math.floor(p * nonZero.length))] : 0); + const nonZero = days + .map(intensityOf) + .filter((v) => v > 0) + .sort((a, b) => a - b); + const q = (p: number): number => + nonZero.length ? nonZero[Math.min(nonZero.length - 1, Math.floor(p * nonZero.length))] : 0; const thresholds: [number, number, number] = [q(0.25), q(0.5), q(0.75)]; const today = parseDayKey(todayKey); - const start = new Date(today); - start.setDate(start.getDate() - (windowDays - 1)); - start.setDate(start.getDate() - start.getDay()); // 对齐到周日, 行号 = weekday + const start = startOfWeek(today); + start.setDate(start.getDate() - (visibleWeeks - 1) * 7); const cells: HeatCell[] = []; const cursor = new Date(start); - while (cursor <= today) { + for (let index = 0; index < visibleWeeks * 7; index += 1) { const key = toDayKey(cursor); + const placeholder = cursor > today; const money = spendByDay.get(key) ?? { amount: 0, currency: days[0]?.money.currency ?? EMPTY_MONEY_CURRENCY, @@ -101,25 +189,10 @@ export function UsageHeatmap({ money, tokens: tokensByDay.get(key) ?? 0, level: levelFor(intensityOf({ money, tokens: tokensByDay.get(key) ?? 0 }), thresholds), - placeholder: false, + placeholder, }); cursor.setDate(cursor.getDate() + 1); } - // 末列补满 7 行 (未来日占位, 保持网格矩形) - while (cells.length % 7 !== 0) { - cells.push({ - day: '', - money: { - amount: 0, - currency: days[0]?.money.currency ?? EMPTY_MONEY_CURRENCY, - approximate: false, - kind: 'actual-cost', - }, - tokens: 0, - level: 0, - placeholder: true, - }); - } const cols: HeatCell[][] = []; for (let i = 0; i < cells.length; i += 7) cols.push(cells.slice(i, i + 7)); @@ -135,63 +208,76 @@ export function UsageHeatmap({ }); return { columns: cols, monthLabels: labels }; - }, [days, todayKey, windowDays, i18n.language, metric]); + }, [days, todayKey, visibleWeeks, i18n.language, metric]); const colPitch = CELL_PX + GAP_PX; return ( -
- {/* 月份标签行 */} -
- {monthLabels.map((m) => ( - - {m.text} - - ))} -
- {/* 网格 */} -
- {columns.map((col, ci) => ( -
- {col.map((cell, ri) => - cell.placeholder ? ( -
- ) : ( -
0 - ? t('usageDashboard.tokensOnly', { - tokens: formatCompactTokens(cell.tokens), - }) - : t('usageHistory.heatmap.emptyCell') - }` - : `${cell.day} · ${formatMoney(cell.money)}${ - cell.tokens > 0 - ? ` · ${t('usageDashboard.tokensOnly', { tokens: formatCompactTokens(cell.tokens) })}` - : '' - }` - } - className="rounded-[3px]" - style={{ - width: CELL_PX, - height: CELL_PX, - backgroundColor: - cell.level === 0 - ? 'var(--surface-chip)' - : `color-mix(in srgb, var(--accent-emphasis) ${LEVEL_MIX[cell.level - 1] * 100}%, var(--surface-chip))`, - }} - /> - ), - )} -
- ))} +
+
+ {/* 月份标签行。nowrap 防止最右侧月份被挤成上下两行。 */} +
+ {monthLabels.map((m) => ( + + {m.text} + + ))} +
+ {/* 网格 */} +
+ {columns.map((col, ci) => ( +
+ {col.map((cell, ri) => + cell.placeholder ? ( +
+ ) : ( + + ), + )} +
+ ))} +
); diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index 5101e71e397..fc54bd1c600 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -1,8 +1,8 @@ // @vitest-environment jsdom -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { UsageHeatmap } from '../UsageHeatmap'; +import { resolveHeatmapWeeks, UsageHeatmap } from '../UsageHeatmap'; vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -39,10 +39,50 @@ function cellTitles(container: HTMLElement): string[] { } describe('UsageHeatmap metric', () => { - it('分桶取值随 metric 切换 (金额口径与 token 口径给出不同深浅)', () => { - const asMoney = render( - , + it('至少显示 20 周,且宽容器会展示数据覆盖的更多周数', () => { + expect( + resolveHeatmapWeeks({ + days: [{ day: '2026-08-22' }], + todayKey: '2026-08-22', + availableWidth: 100, + }), + ).toBe(20); + + expect( + resolveHeatmapWeeks({ + days: [{ day: '2026-01-01' }], + todayKey: '2026-08-22', + availableWidth: 500, + }), + ).toBe(33); + + expect( + resolveHeatmapWeeks({ + days: [{ day: '2025-01-01' }], + todayKey: '2026-08-22', + availableWidth: 500, + }), + ).toBe(33); + + expect( + resolveHeatmapWeeks({ + days: [{ day: '2025-01-01' }], + todayKey: '2026-08-22', + availableWidth: 900, + }), + ).toBe(60); + }); + + it('月份标签保持单行,避免最右侧月份换行', () => { + const { container } = render(); + const monthLabel = [...container.querySelectorAll('span')].find((node) => + node.className.includes('whitespace-nowrap'), ); + expect(monthLabel).toBeTruthy(); + }); + + it('分桶取值随 metric 切换 (金额口径与 token 口径给出不同深浅)', () => { + const asMoney = render(); const moneyStyles = cellStyles(asMoney.container); asMoney.unmount(); @@ -65,9 +105,23 @@ describe('UsageHeatmap metric', () => { }); it('默认仍是金额口径 (首页仪表盘行为不变)', () => { - const { container } = render( - , - ); + const { container } = render(); expect(cellTitles(container).some((title) => title.includes('$'))).toBe(true); }); + + it('日期格可点击并上报所选日期', () => { + const onDayClick = vi.fn(); + const { getByRole } = render( + , + ); + + fireEvent.click(getByRole('button', { name: '2026-08-21' })); + expect(onDayClick).toHaveBeenCalledWith('2026-08-21'); + }); }); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx index f56dc8b4cca..61cccc4343a 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx @@ -15,11 +15,7 @@ import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { formatCompactTokens, formatModelShort } from '@/lib/usageFormat'; import { usageRankColor } from '@/components/new-chat/usagePalette'; -import { - type AgentTokenRow, - type ModelTokenRow, - type UsageAgentKind, -} from './usageHistoryStats'; +import { type AgentTokenRow, type ModelTokenRow, type UsageAgentKind } from './usageHistoryStats'; import { formatUsagePercent } from './formatUsagePercent'; const UNKNOWN_VALUE = '—'; @@ -53,9 +49,7 @@ function Swatch({ rank }: { rank: number }): React.JSX.Element { } function HitRateCell({ value }: { value: number | null }): React.JSX.Element { - return ( - {value === null ? UNKNOWN_VALUE : formatUsagePercent(value)} - ); + return {value === null ? UNKNOWN_VALUE : formatUsagePercent(value)}; } function ShareCell({ share, rank }: { share: number; rank: number }): React.JSX.Element { @@ -70,7 +64,15 @@ function ShareCell({ share, rank }: { share: number; rank: number }): React.JSX. ); } -export function UsageAgentTable({ rows }: { rows: AgentTokenRow[] }): React.JSX.Element { +export function UsageAgentTable({ + rows, + rangeLabel, + todayLabel, +}: { + rows: AgentTokenRow[]; + rangeLabel: string; + todayLabel?: string; +}): React.JSX.Element { const { t } = useTranslation(); const total = rows.reduce((sum, row) => sum + row.tokens, 0); @@ -98,9 +100,13 @@ export function UsageAgentTable({ rows }: { rows: AgentTokenRow[] }): React.JSX. {t('usageHistory.byAgent.col.agent')} - {t('usageHistory.byAgent.col.total')} + + {t('usageHistory.byAgent.col.totalInRange', { range: rangeLabel })} + {t('usageHistory.byAgent.col.share')} - {t('usageHistory.byAgent.col.today')} + + {todayLabel ?? t('usageHistory.byAgent.col.today')} + {t('usageHistory.byAgent.col.hitRate')} @@ -136,7 +142,13 @@ export function UsageAgentTable({ rows }: { rows: AgentTokenRow[] }): React.JSX. ); } -export function UsageModelTable({ rows }: { rows: ModelTokenRow[] }): React.JSX.Element { +export function UsageModelTable({ + rows, + rangeLabel, +}: { + rows: ModelTokenRow[]; + rangeLabel: string; +}): React.JSX.Element { const { t } = useTranslation(); return ( @@ -146,7 +158,9 @@ export function UsageModelTable({ rows }: { rows: ModelTokenRow[] }): React.JSX. {t('usageHistory.byModel.col.model')} - {t('usageHistory.byModel.col.total')} + + {t('usageHistory.byModel.col.totalInRange', { range: rangeLabel })} + {t('usageHistory.byModel.col.share')} {t('usageHistory.byModel.col.input')} {t('usageHistory.byModel.col.output')} diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index e03acfd5d19..00c1b615ccc 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -9,16 +9,18 @@ * 不读任何账号快照 —— local / cloud personal / cloud org 三种身份看到的是同一个页面。 * * 两个窗口不同, 标题里分别写明: - * - 热力图与连续活跃天数走 days[] (140 天 = 20 周) - * - 按模型 / 按 agent / 每日柱图走 models[] 与 modelDaily (30 天) + * - 热力图走完整 days[],按卡片实际宽度显示至少 20 周;连续活跃天数也走同一份历史 + * - 按模型 / 按 agent 走按筛选范围重聚合的 models[] 与 modelDaily + * - 每日柱图固定展示近 30 天, 与筛选范围无关 * * 首页的 HomeUsageDashboard 是金额口径的姊妹实现, 本页不复用它的外壳组件 * (见 UsageTokenBars 的注释), 但共享同一条聚合链路与配色。 */ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { RefreshCw } from 'lucide-react'; +import * as Select from '@radix-ui/react-select'; +import { Check, ChevronDown, RefreshCw } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { useAuth } from '@/contexts/AuthContext'; @@ -33,7 +35,11 @@ import { buildAgentRows, buildModelRows, buildSummary, + chartUsageHistoryPayload, + filterUsageHistoryPayload, isUsageHistoryEmpty, + usageRangeDay, + type UsageHistoryRange, } from './usageHistoryStats'; /** 与 useUsageHistory 的拉取窗口一致 (20 周)。 */ @@ -55,9 +61,7 @@ function Card({
{title} - {subtitle ? ( - {subtitle} - ) : null} + {subtitle ? {subtitle} : null} {refreshing ? ( @@ -71,26 +75,64 @@ function Card({ } export function UsageHistorySection(): React.JSX.Element { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { user } = useAuth(); - const { history, refreshing } = useUsageHistory({ userId: user?.id }); + const { history, refreshing } = useUsageHistory({ + userId: user?.id, + days: 'all', + modelDays: 'all', + }); + const [range, setRange] = useState('30d'); + const [heatmapWeeks, setHeatmapWeeks] = useState(20); - const summary = useMemo(() => buildSummary(history), [history]); - const modelRows = useMemo(() => buildModelRows(history), [history]); - const agentRows = useMemo(() => buildAgentRows(history), [history]); + const selectedDay = usageRangeDay(range); + const rangeLabel = useMemo(() => { + if (selectedDay) { + const [year, month, day] = selectedDay.split('-').map(Number); + return new Intl.DateTimeFormat(i18n.language, { + year: 'numeric', + month: 'short', + day: 'numeric', + }).format(new Date(year, (month ?? 1) - 1, day ?? 1)); + } + return t(`usageHistory.range.${range}`); + }, [i18n.language, range, selectedDay, t]); + const todayLabel = selectedDay + ? t('usageHistory.stats.todayTokensInRange', { range: rangeLabel }) + : t('usageHistory.stats.todayTokens'); + + const filteredHistory = useMemo( + () => filterUsageHistoryPayload(history, range), + [history, range], + ); + const chartHistory = useMemo(() => chartUsageHistoryPayload(history), [history]); + const summary = useMemo(() => buildSummary(filteredHistory), [filteredHistory]); + const modelRows = useMemo(() => buildModelRows(filteredHistory), [filteredHistory]); + const agentRows = useMemo(() => buildAgentRows(filteredHistory), [filteredHistory]); // 配色顺序必须来自 modelRows 而不是 history.models: 后者由 main 按**可比金额**降序排 // (usageHistory.ts 的 comparable), 本页只讲 token —— 直接用它会让同一个模型在柱图与 // 模型表里配到不同颜色, 还会让"金额高但 token 很少"的模型挤掉真正的 token 前 N 名。 const colorOrder = useMemo( - () => modelRows.slice(0, USAGE_TOP_MODELS).map((m) => usageModelKey(m.agentKind, m.model)), - [modelRows], + () => + buildModelRows(chartHistory) + .slice(0, USAGE_TOP_MODELS) + .map((m) => usageModelKey(m.agentKind, m.model)), + [chartHistory], ); // 任务行与用量聚合是两条数据源: 聚合里有 token, 本地任务却可能一条都没有 // (用户删光了会话, 或列表还没加载完)。空时整张卡片不渲染, 不留空壳。 - const taskRows = useTopTokenSessions(); + const taskRows = useTopTokenSessions(range, history?.todayKey); const empty = isUsageHistoryEmpty(history); + const handleRangeChange = (value: string): void => { + setRange(value as UsageHistoryRange); + }; + + const handleDayClick = (day: string): void => { + setRange(`day:${day}` as UsageHistoryRange); + }; + return (

@@ -100,6 +142,58 @@ export function UsageHistorySection(): React.JSX.Element { {t('usageHistory.description')}

+
+ + {t('usageHistory.range.label')} + + + + + + + + + + + + {(['all', '30d', '7d', 'today'] as const).map((option) => ( + + {t(`usageHistory.range.${option}`)} + + + + + ))} + {selectedDay ? ( + + {rangeLabel} + + + + + ) : null} + + + + +
+ {empty ? (
{t('usageHistory.empty')} @@ -107,50 +201,46 @@ export function UsageHistorySection(): React.JSX.Element { ) : ( <> - + - + {agentRows.length > 0 && ( - +
- +
)} {modelRows.length > 0 && ( - +
- +
)} @@ -158,7 +248,7 @@ export function UsageHistorySection(): React.JSX.Element { {taskRows.length > 0 && (
diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx index 2538ddfd032..36dc612dafa 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx @@ -38,16 +38,22 @@ function StatCell({ return tip ? {cell} : cell; } -export function UsageStatRow({ summary }: { summary: UsageSummary }): React.JSX.Element { +export function UsageStatRow({ + summary, + rangeLabel, + todayLabel, +}: { + summary: UsageSummary; + rangeLabel: string; + todayLabel?: string; +}): React.JSX.Element { const { t } = useTranslation(); return (
0 ? formatCompactTokens(summary.todayTokens) : UNKNOWN_VALUE - } - label={t('usageHistory.stats.todayTokens')} + value={summary.todayTokens > 0 ? formatCompactTokens(summary.todayTokens) : UNKNOWN_VALUE} + label={todayLabel ?? t('usageHistory.stats.todayTokens')} /> - +
); } diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index ae9fd4d3feb..f136851d264 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -37,11 +37,10 @@ import { useProviders } from '@/hooks/useProviders'; import { usageRankColor } from '@/components/new-chat/usagePalette'; import { providerDisplayNameById } from '@/lib/providerDisplayName'; import { formatUsagePercent } from './formatUsagePercent'; +import { isUsageDayInRange, type UsageHistoryRange } from './usageHistoryStats'; /** 展示条数 —— 与"最耗"的语义匹配, 不做成完整列表 (那是任务侧栏的事)。 */ const TOP_TASKS = 8; -/** 与本页其它区块一致的窗口。 */ -const WINDOW_DAYS = 30; const UNKNOWN_VALUE = '—'; /** @@ -72,32 +71,50 @@ function lastActiveIso(session: { updatedAt: string; userSendAt: string | null } : session.updatedAt; } -function activeWithinWindow(iso: string | null | undefined, cutoffMs: number): boolean { +function localDayKeyFromIso(iso: string): string | null { + const date = new Date(iso); + if (!Number.isFinite(date.getTime())) return null; + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${date.getFullYear()}-${month}-${day}`; +} + +function activeWithinRange( + iso: string | null | undefined, + range: UsageHistoryRange, + todayKey: string, +): boolean { if (!iso) return false; - const ts = Date.parse(iso); - return Number.isFinite(ts) && ts >= cutoffMs; + const day = localDayKeyFromIso(iso); + return day !== null && isUsageDayInRange(day, todayKey, range); } /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 * 组件内部返回 null 会留下一张只有标题、正文全空的卡片 (会话被删光 / 列表首次加载中)。 */ -export function useTopTokenSessions(): Session[] { +export function useTopTokenSessions( + range: UsageHistoryRange = '30d', + todayKeyOverride?: string, +): Session[] { // 归档的任务同样消耗过 token, 统计口径不该因为用户归档而变。 const { sessions } = useCCSessions({ includeArchived: 'all' }); return useMemo(() => { - const cutoff = Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000; + const now = new Date(); + const todayKey = + todayKeyOverride ?? + `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; return sessions .filter( (session) => session.totalTokenUsage > 0 && - (activeWithinWindow(session.updatedAt, cutoff) || - activeWithinWindow(session.userSendAt, cutoff)), + (activeWithinRange(session.updatedAt, range, todayKey) || + activeWithinRange(session.userSendAt, range, todayKey)), ) .sort((a, b) => b.totalTokenUsage - a.totalTokenUsage) .slice(0, TOP_TASKS); - }, [sessions]); + }, [range, sessions, todayKeyOverride]); } export function UsageTaskTable({ rows }: { rows: Session[] }): React.JSX.Element { diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx index 9fb3d0d6815..aaa5a280261 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx @@ -60,11 +60,15 @@ export function UsageTokenBars({ modelDaily, colorOrder, todayKey, + selectedDay, + onDayClick, }: { modelDaily: UsageHistoryModelDay[]; /** 前 N 名模型 key (payload.models 排序), 决定分段与图例配色。 */ colorOrder: string[]; todayKey: string; + selectedDay?: string | null; + onDayClick?: (day: string) => void; }): React.JSX.Element { const { t } = useTranslation(); @@ -151,14 +155,20 @@ export function UsageTokenBars({ ), ]; return ( -
onDayClick?.(b.day)} + disabled={!onDayClick} // 列容器只负责高度与圆角裁切; 分段自上而下 = rank 降序 ("其它"在顶, 大头在底) - className="flex min-w-0 flex-1 flex-col justify-end overflow-hidden rounded-[2px]" + className="flex min-w-0 flex-1 cursor-pointer flex-col justify-end overflow-hidden rounded-[2px] border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" style={{ height, backgroundColor: b.segments.length === 0 ? 'var(--surface-chip)' : undefined, + boxShadow: selectedDay === b.day ? '0 0 0 2px var(--focus-ring-soft)' : undefined, }} > {[...b.segments].reverse().map((s) => ( @@ -170,7 +180,7 @@ export function UsageTokenBars({ }} /> ))} -
+ ); })}
diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index bb136dd7d5e..4253227bc4c 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -6,7 +6,9 @@ import { buildModelRows, buildSummary, cacheHitRate, + chartUsageHistoryPayload, computeTokenStreak, + filterUsageHistoryPayload, isUsageHistoryEmpty, toUsageDays, } from '../usageHistoryStats'; @@ -61,9 +63,7 @@ describe('cacheHitRate', () => { }); it('分母为 0 时返回 null 而不是 0 —— 没有上下文可复用与命中率为零是两回事', () => { - expect( - cacheHitRate({ inputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0 }), - ).toBeNull(); + expect(cacheHitRate({ inputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0 })).toBeNull(); }); }); @@ -71,10 +71,7 @@ describe('computeTokenStreak', () => { const days = (...keys: string[]) => keys.map((day) => ({ day, tokens: 1000 })); it('今日有记录时从今日起算', () => { - const streak = computeTokenStreak( - days('2026-08-20', '2026-08-21', '2026-08-22'), - '2026-08-22', - ); + const streak = computeTokenStreak(days('2026-08-20', '2026-08-21', '2026-08-22'), '2026-08-22'); expect(streak.current).toBe(3); }); @@ -93,10 +90,7 @@ describe('computeTokenStreak', () => { }); it('跨月连续不断档', () => { - const streak = computeTokenStreak( - days('2026-07-30', '2026-07-31', '2026-08-01'), - '2026-08-01', - ); + const streak = computeTokenStreak(days('2026-07-30', '2026-07-31', '2026-08-01'), '2026-08-01'); expect(streak).toEqual({ current: 3, longest: 3 }); }); @@ -119,9 +113,7 @@ describe('toUsageDays', () => { }); it('旧快照缺 tokens 字段时按 0 兜底, 不会 NaN', () => { - const rows = toUsageDays( - payload({ days: [{ day: '2026-08-22', money: zeroMoney }] }), - ); + const rows = toUsageDays(payload({ days: [{ day: '2026-08-22', money: zeroMoney }] })); expect(rows).toEqual([]); }); }); @@ -267,8 +259,71 @@ describe('buildSummary / isUsageHistoryEmpty', () => { }); it('有 token 记录时不算空', () => { - expect( - isUsageHistoryEmpty(payload({ models: [model({ inputTokens: 10 })] })), - ).toBe(false); + expect(isUsageHistoryEmpty(payload({ models: [model({ inputTokens: 10 })] }))).toBe(false); + }); +}); + +describe('filterUsageHistoryPayload', () => { + const daily = (over: Partial) => ({ + day: '2026-08-22', + agentKind: 'codex' as const, + model: 'gpt-5.5', + money: zeroMoney, + apiMoney: zeroMoney, + subscriptionEstimateMoney: zeroMoney, + tokens: 0, + ...over, + }); + + const source = payload({ + days: [ + { day: '2026-08-10', money: zeroMoney, tokens: 300 }, + { day: '2026-08-20', money: zeroMoney, tokens: 60 }, + { day: '2026-08-22', money: zeroMoney, tokens: 120 }, + ], + modelDaily: [ + daily({ + day: '2026-08-10', + agentKind: 'claude-code', + model: 'opus', + tokens: 300, + inputTokens: 300, + }), + daily({ day: '2026-08-20', tokens: 60, inputTokens: 50, outputTokens: 10 }), + daily({ day: '2026-08-22', tokens: 120, inputTokens: 100, outputTokens: 20 }), + ], + }); + + it('按选择的窗口过滤并重新聚合模型与统计值', () => { + const result = filterUsageHistoryPayload(source, '7d'); + expect(result?.days.map((row) => row.day)).toEqual(['2026-08-20', '2026-08-22']); + expect(result?.models).toHaveLength(1); + expect(result?.models[0]).toMatchObject({ + agentKind: 'codex', + model: 'gpt-5.5', + inputTokens: 150, + outputTokens: 30, + }); + expect(result?.totals).toMatchObject({ todayTokens: 120, last30DaysTokens: 180 }); + }); + + it('选中热力图中的历史日时只保留该日并把它作为统计锚点', () => { + const result = filterUsageHistoryPayload(source, 'day:2026-08-20'); + expect(result?.todayKey).toBe('2026-08-20'); + expect(result?.days.map((row) => row.day)).toEqual(['2026-08-20']); + expect(result?.modelDaily.map((row) => row.day)).toEqual(['2026-08-20']); + expect(result?.totals.todayTokens).toBe(60); + expect(result?.totals.last30DaysTokens).toBe(60); + expect(result?.streak).toEqual({ current: 1, longest: 1 }); + }); + + it('图表保留固定窗口,不随表格筛选范围变化', () => { + const result = chartUsageHistoryPayload(source); + expect(result?.modelDaily.map((row) => row.day)).toEqual([ + '2026-08-10', + '2026-08-20', + '2026-08-22', + ]); + expect(result?.days.map((row) => row.day)).toEqual(source.days.map((row) => row.day)); }); }); diff --git a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts index 5e4534b7d5e..b7aaa4463e3 100644 --- a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts +++ b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts @@ -10,10 +10,7 @@ * - `models[]` / `modelDaily` 是 30 天 → 按模型、按 agent、每日柱图 */ -import type { - UsageHistoryModel, - UsageHistoryPayload, -} from '@/hooks/useUsageHistory'; +import type { UsageHistoryModel, UsageHistoryPayload } from '@/hooks/useUsageHistory'; export type UsageAgentKind = UsageHistoryModel['agentKind']; @@ -22,6 +19,8 @@ export interface UsageDay { tokens: number; } +export type UsageHistoryRange = 'all' | '30d' | '7d' | 'today' | `day:${string}`; + export interface ModelTokenRow { key: string; agentKind: UsageAgentKind; @@ -78,6 +77,117 @@ function shiftDayKey(dayKey: string, deltaDays: number): string { return `${date.getFullYear()}-${mm}-${dd}`; } +export function usageRangeDay(range: UsageHistoryRange): string | null { + return range.startsWith('day:') ? range.slice(4) : null; +} + +export function isUsageDayInRange( + day: string, + todayKey: string, + range: UsageHistoryRange, +): boolean { + const selectedDay = usageRangeDay(range); + if (selectedDay) return day === selectedDay; + if (range === 'all') return true; + if (range === 'today') return day === todayKey; + const days = range === '7d' ? 7 : 30; + return day >= shiftDayKey(todayKey, -(days - 1)) && day <= todayKey; +} + +/** + * Derive the table/statistics payload for a selected range. Charts intentionally + * receive their own fixed-window slices so changing this range never changes + * the heatmap or daily-token chart. + */ +export function filterUsageHistoryPayload( + payload: UsageHistoryPayload | null, + range: UsageHistoryRange, +): UsageHistoryPayload | null { + if (!payload) return null; + + const selectedDay = usageRangeDay(range); + const rangeTodayKey = selectedDay ?? payload.todayKey; + const modelByKey = new Map(); + for (const row of payload.modelDaily) { + if (!isUsageDayInRange(row.day, payload.todayKey, range) || row.tokens <= 0) continue; + const key = `${row.agentKind}\u0000${row.model}`; + const existing = modelByKey.get(key); + const componentTotal = + (row.inputTokens ?? 0) + + (row.outputTokens ?? 0) + + (row.cacheReadTokens ?? 0) + + (row.cacheCreateTokens ?? 0); + // Old renderer snapshots only carried `tokens`; preserve their totals until + // the next main-process refresh supplies the component fields. + const inputTokens = row.inputTokens ?? (componentTotal === 0 ? row.tokens : 0); + const outputTokens = row.outputTokens ?? 0; + const cacheReadTokens = row.cacheReadTokens ?? 0; + const cacheCreateTokens = row.cacheCreateTokens ?? 0; + if (existing) { + existing.inputTokens += inputTokens; + existing.outputTokens += outputTokens; + existing.cacheReadTokens += cacheReadTokens; + existing.cacheCreateTokens += cacheCreateTokens; + continue; + } + modelByKey.set(key, { + agentKind: row.agentKind, + model: row.model, + money: row.money, + estimatedMoney: + row.subscriptionEstimateMoney.amount > 0 ? row.subscriptionEstimateMoney : null, + inputTokens, + outputTokens, + cacheReadTokens, + cacheCreateTokens, + }); + } + + const modelDaily = payload.modelDaily.filter((row) => + isUsageDayInRange(row.day, payload.todayKey, range), + ); + const days = payload.days.filter((row) => isUsageDayInRange(row.day, payload.todayKey, range)); + const models = [...modelByKey.values()]; + const lastRangeTokens = models.reduce((sum, model) => sum + modelTokenTotal(model), 0); + const todayTokens = modelDaily + .filter((row) => row.day === rangeTodayKey) + .reduce((sum, row) => sum + row.tokens, 0); + + return { + ...payload, + todayKey: rangeTodayKey, + days, + modelDaily, + models, + totals: { + ...payload.totals, + todayTokens, + last30DaysTokens: lastRangeTokens, + }, + streak: computeTokenStreak( + days + .map((row) => ({ day: row.day, tokens: row.tokens ?? 0 })) + .filter((row) => row.tokens > 0), + rangeTodayKey, + ), + }; +} + +export function chartUsageHistoryPayload( + payload: UsageHistoryPayload | null, +): UsageHistoryPayload | null { + if (!payload) return null; + const charts = filterUsageHistoryPayload(payload, '30d'); + return { + ...(charts ?? payload), + // The heatmap decides its own visible window from the measured card width. + // Keep the full `days` history here so a wide settings card can expose more + // than the old fixed 20-week slice; the daily bar chart still uses modelDaily + // from the fixed 30-day chart payload above. + days: payload.days, + }; +} + /** payload.days → 只保留 token 维度 (热力图与 streak 的共同输入)。 */ export function toUsageDays(payload: UsageHistoryPayload | null): UsageDay[] { if (!payload) return []; diff --git a/apps/desktop/src/renderer/hooks/useUsageHistory.ts b/apps/desktop/src/renderer/hooks/useUsageHistory.ts index de6bfbcefcd..103fbc218a6 100644 --- a/apps/desktop/src/renderer/hooks/useUsageHistory.ts +++ b/apps/desktop/src/renderer/hooks/useUsageHistory.ts @@ -50,6 +50,10 @@ export interface UsageHistoryModelDay { apiMoney: RegionalMoney; subscriptionEstimateMoney: RegionalMoney; tokens: number; + inputTokens?: number; + outputTokens?: number; + cacheReadTokens?: number; + cacheCreateTokens?: number; } export interface UsageHistoryPayload { @@ -59,7 +63,7 @@ export interface UsageHistoryPayload { estimatesPending?: boolean; /** tokens: 当日合计 (旧快照可能缺该字段, 消费端用 ?? 0 兜底)。 */ days: Array<{ day: string; money: RegionalMoney; tokens?: number }>; - /** 近 30 天每日 × 模型明细 (旧快照缺该字段时补 [])。 */ + /** 按请求的 modelDays 窗口返回每日 × 模型明细 (旧快照缺该字段时补 [])。 */ modelDaily: UsageHistoryModelDay[]; models: UsageHistoryModel[]; streak: { current: number; longest: number }; @@ -76,6 +80,7 @@ export interface UsageHistoryPayload { /** 热力图窗口: 20 周 = 140 天。 */ const HISTORY_WINDOW_DAYS = 140; +const MODEL_WINDOW_DAYS = 30; const REFRESH_DEBOUNCE_MS = 2000; /** 价格表冷启动未就绪时的补拉延迟 (对齐 main 侧 5s fetch 超时 + 余量)。 */ const PRICING_RETRY_DELAY_MS = 6000; @@ -109,6 +114,10 @@ interface StoredUsageHistoryModelDay { subscriptionEstimateMoney?: unknown; subscriptionEstimateUsd?: unknown; tokens?: unknown; + inputTokens?: unknown; + outputTokens?: unknown; + cacheReadTokens?: unknown; + cacheCreateTokens?: unknown; } interface StoredUsageHistoryModel { @@ -209,6 +218,18 @@ function readSnapshot(scopeKey: string): UsageHistoryPayload | null { normalizeRegionalMoney(row.subscriptionEstimateMoney) ?? legacyEstimate(row.subscriptionEstimateUsd), tokens: finiteNumber(row.tokens), + ...(typeof row.inputTokens === 'number' && Number.isFinite(row.inputTokens) + ? { inputTokens: row.inputTokens } + : {}), + ...(typeof row.outputTokens === 'number' && Number.isFinite(row.outputTokens) + ? { outputTokens: row.outputTokens } + : {}), + ...(typeof row.cacheReadTokens === 'number' && Number.isFinite(row.cacheReadTokens) + ? { cacheReadTokens: row.cacheReadTokens } + : {}), + ...(typeof row.cacheCreateTokens === 'number' && Number.isFinite(row.cacheCreateTokens) + ? { cacheCreateTokens: row.cacheCreateTokens } + : {}), })); const models = (parsed.models as StoredUsageHistoryModel[]) .filter( @@ -303,6 +324,7 @@ interface UsageHistoryScopeState { listeners: Set<(p: UsageHistoryPayload | null) => void>; statusListeners: Set<(refreshing: boolean) => void>; refreshing: boolean; + request: { days: number | 'all'; modelDays: number | 'all' }; } const scopes = new Map(); @@ -311,7 +333,13 @@ function normalizeScopeKey(userId?: string | null): string { return userId || DEFAULT_SCOPE_KEY; } -function getScope(scopeKey: string): UsageHistoryScopeState { +function getScope( + scopeKey: string, + request: { days: number | 'all'; modelDays: number | 'all' } = { + days: HISTORY_WINDOW_DAYS, + modelDays: MODEL_WINDOW_DAYS, + }, +): UsageHistoryScopeState { let state = scopes.get(scopeKey); if (!state) { const snapshot = readSnapshot(scopeKey); @@ -326,6 +354,7 @@ function getScope(scopeKey: string): UsageHistoryScopeState { listeners: new Set<(p: UsageHistoryPayload | null) => void>(), statusListeners: new Set<(refreshing: boolean) => void>(), refreshing: false, + request, }; scopes.set(scopeKey, state); } @@ -365,7 +394,11 @@ async function load(scopeKey: string, opts?: { forceRefresh?: boolean; resetPric const seq = ++scope.loadSeq; setRefreshing(scope, true); scope.inflight = window.electronAPI.maker.usage - .getHistory({ days: HISTORY_WINDOW_DAYS, ...(opts?.forceRefresh ? { forceRefresh: true } : {}) }) + .getHistory({ + days: scope.request.days, + modelDays: scope.request.modelDays, + ...(opts?.forceRefresh ? { forceRefresh: true } : {}), + }) .then((res) => { if (seq === scope.loadSeq) scope.inflight = null; if (!res || typeof res !== 'object') return null; @@ -422,23 +455,50 @@ async function load(scopeKey: string, opts?: { forceRefresh?: boolean; resetPric return scope.inflight; } -export function useUsageHistory(opts?: { paused?: boolean; userId?: string | null }): { +export type UsageHistoryWindow = number | 'all'; + +function normalizeWindow(value: UsageHistoryWindow | undefined, fallback: number): number | 'all' { + if (value === 'all') return 'all'; + return Math.min(366, Math.max(1, Math.floor(value ?? fallback))); +} + +function scopeKeyForRequest( + scopeKey: string, + request: { days: number | 'all'; modelDays: number | 'all' }, +): string { + if (request.days === HISTORY_WINDOW_DAYS && request.modelDays === MODEL_WINDOW_DAYS) { + return scopeKey; + } + return `${scopeKey}|days=${request.days}|modelDays=${request.modelDays}`; +} + +export function useUsageHistory(opts?: { + paused?: boolean; + userId?: string | null; + days?: UsageHistoryWindow; + modelDays?: UsageHistoryWindow; +}): { history: UsageHistoryPayload | null; refreshing: boolean; } { const paused = opts?.paused ?? false; const scopeKey = normalizeScopeKey(opts?.userId); - const scope = getScope(scopeKey); + const request = { + days: normalizeWindow(opts?.days, HISTORY_WINDOW_DAYS), + modelDays: normalizeWindow(opts?.modelDays, MODEL_WINDOW_DAYS), + }; + const scopedKey = scopeKeyForRequest(scopeKey, request); + const scope = getScope(scopedKey, request); const [historyState, setHistoryState] = useState<{ scopeKey: string; value: UsageHistoryPayload | null }>(() => ({ - scopeKey, + scopeKey: scopedKey, value: scope.cache, })); const [isRefreshing, setIsRefreshing] = useState(scope.refreshing); useEffect(() => { - const activeScope = getScope(scopeKey); + const activeScope = getScope(scopedKey, request); const setScopedHistory = (value: UsageHistoryPayload | null) => { - setHistoryState({ scopeKey, value }); + setHistoryState({ scopeKey: scopedKey, value }); }; activeScope.listeners.add(setScopedHistory); activeScope.statusListeners.add(setIsRefreshing); @@ -449,7 +509,7 @@ export function useUsageHistory(opts?: { paused?: boolean; userId?: string | nul setIsRefreshing(activeScope.refreshing); // mount 与 paused→false (展开瞬间) 都拉一次 — 折叠摘要 / 图表数据都保持新鲜。 // 收起时跳过后续 push 订阅, 避免每个 turn 触发后台重拉。 - void load(scopeKey); + void load(scopedKey); // paused (卡片收起): 不订阅消费推送, 收起用户的 per-turn 重拉成本归零 if (paused) { @@ -466,7 +526,7 @@ export function useUsageHistory(opts?: { paused?: boolean; userId?: string | nul if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; - void load(scopeKey, { forceRefresh: true, resetPricingRetry: true }); + void load(scopedKey, { forceRefresh: true, resetPricingRetry: true }); }, REFRESH_DEBOUNCE_MS); }; const offSpend = window.electronAPI.maker.usage.onTodaySpendChanged(scheduleRefresh); @@ -482,10 +542,10 @@ export function useUsageHistory(opts?: { paused?: boolean; userId?: string | nul offTokens(); offPricing(); }; - }, [paused, scopeKey]); + }, [paused, scopedKey]); return { - history: historyState.scopeKey === scopeKey ? historyState.value : scope.cache, + history: historyState.scopeKey === scopedKey ? historyState.value : scope.cache, refreshing: isRefreshing, }; } diff --git a/apps/desktop/src/renderer/i18n/locales/en/common.json b/apps/desktop/src/renderer/i18n/locales/en/common.json index 035db0d019a..4a481463904 100644 --- a/apps/desktop/src/renderer/i18n/locales/en/common.json +++ b/apps/desktop/src/renderer/i18n/locales/en/common.json @@ -5795,20 +5795,31 @@ "usageHistory": { "description": "Token usage produced inside Cindy on this device. Records start accumulating when the feature ships; earlier history is not backfilled.", "empty": "No usage recorded yet. This fills in once you start a chat.", + "range": { + "label": "Time range", + "ariaLabel": "Filter usage time range", + "all": "All time", + "30d": "Last 30 days", + "7d": "Last 7 days", + "today": "Today" + }, "cacheHitTooltip": "Cache read / (input + cache read + cache write) — the same formula as the message card.", "summary": { "title": "Overview" }, "stats": { "todayTokens": "Tokens today", + "todayTokensInRange": "Tokens ({{range}})", "totalTokens": "Tokens (30d)", + "totalTokensInRange": "Tokens ({{range}})", "streak": "Active streak", "cacheHitRate": "Cache hit rate (30d)", + "cacheHitRateInRange": "Cache hit rate ({{range}})", "models": "Models used" }, "heatmap": { "title": "Activity heatmap", - "subtitle": "Last 20 weeks", + "subtitle": "Last {{count}} weeks", "emptyCell": "No usage recorded" }, "daily": { @@ -5821,6 +5832,7 @@ "col": { "agent": "Agent / harness", "total": "Tokens (30d)", + "totalInRange": "Tokens ({{range}})", "share": "Share", "today": "Today", "hitRate": "Cache hit rate", @@ -5833,6 +5845,7 @@ "col": { "model": "Model", "total": "Total tokens", + "totalInRange": "Tokens ({{range}})", "share": "Share", "input": "Input", "output": "Output", @@ -5843,7 +5856,7 @@ }, "tasks": { "title": "Top sessions by tokens", - "subtitle": "Sessions active in the last 30 days, ranked by lifetime tokens", + "subtitle": "Sessions active in {{range}}, ranked by lifetime tokens", "col": { "task": "Session", "model": "Model", diff --git a/apps/desktop/src/renderer/i18n/locales/ja/common.json b/apps/desktop/src/renderer/i18n/locales/ja/common.json index 12ff28c9a7b..0862c199d32 100644 --- a/apps/desktop/src/renderer/i18n/locales/ja/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ja/common.json @@ -5793,20 +5793,31 @@ "usageHistory": { "description": "このデバイスの Cindy 内で発生したトークン使用量です。記録は機能の提供開始時から蓄積され、それ以前の履歴は補完されません。", "empty": "まだ使用記録がありません。会話を実行すると蓄積が始まります。", + "range": { + "label": "期間", + "ariaLabel": "使用量の期間を絞り込む", + "all": "全期間", + "30d": "直近 30 日", + "7d": "直近 7 日", + "today": "今日" + }, "cacheHitTooltip": "キャッシュ読取 ÷(入力 + キャッシュ読取 + キャッシュ書込)。メッセージカードと同じ基準です。", "summary": { "title": "概要" }, "stats": { "todayTokens": "今日のトークン", + "todayTokensInRange": "{{range}}のトークン", "totalTokens": "直近 30 日のトークン", + "totalTokensInRange": "{{range}}のトークン", "streak": "連続稼働日数", "cacheHitRate": "キャッシュヒット率(30 日)", + "cacheHitRateInRange": "キャッシュヒット率({{range}})", "models": "使用モデル数" }, "heatmap": { "title": "アクティビティヒートマップ", - "subtitle": "直近 20 週", + "subtitle": "直近 {{count}} 週", "emptyCell": "この日は使用記録なし" }, "daily": { @@ -5819,6 +5830,7 @@ "col": { "agent": "Agent / harness", "total": "直近 30 日のトークン", + "totalInRange": "{{range}}のトークン", "share": "割合", "today": "今日", "hitRate": "キャッシュヒット率", @@ -5831,6 +5843,7 @@ "col": { "model": "モデル", "total": "合計トークン", + "totalInRange": "{{range}}のトークン", "share": "割合", "input": "入力", "output": "出力", @@ -5841,7 +5854,7 @@ }, "tasks": { "title": "トークン消費の多いセッション", - "subtitle": "直近 30 日に稼働したセッションを累計トークン順に表示", + "subtitle": "{{range}}に稼働したセッションを累計トークン順に表示", "col": { "task": "セッション", "model": "モデル", diff --git a/apps/desktop/src/renderer/i18n/locales/ko/common.json b/apps/desktop/src/renderer/i18n/locales/ko/common.json index e60d8a5a08c..d97421dc0ff 100644 --- a/apps/desktop/src/renderer/i18n/locales/ko/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ko/common.json @@ -5793,20 +5793,31 @@ "usageHistory": { "description": "이 기기의 Cindy 내부에서 발생한 토큰 사용량입니다. 기능 출시 시점부터 기록이 쌓이며 이전 기록은 채워지지 않습니다.", "empty": "아직 사용 기록이 없습니다. 대화를 실행하면 쌓이기 시작합니다.", + "range": { + "label": "기간", + "ariaLabel": "사용량 기간 필터", + "all": "전체 기간", + "30d": "최근 30일", + "7d": "최근 7일", + "today": "오늘" + }, "cacheHitTooltip": "캐시 읽기 ÷ (입력 + 캐시 읽기 + 캐시 쓰기) — 메시지 카드와 동일한 기준입니다.", "summary": { "title": "개요" }, "stats": { "todayTokens": "오늘 토큰", + "todayTokensInRange": "{{range}} 토큰", "totalTokens": "최근 30일 토큰", + "totalTokensInRange": "{{range}} 토큰", "streak": "연속 활동일", "cacheHitRate": "캐시 적중률(30일)", + "cacheHitRateInRange": "캐시 적중률({{range}})", "models": "사용한 모델" }, "heatmap": { "title": "활동 히트맵", - "subtitle": "최근 20주", + "subtitle": "최근 {{count}}주", "emptyCell": "해당 날짜에 사용 기록 없음" }, "daily": { @@ -5819,6 +5830,7 @@ "col": { "agent": "Agent / harness", "total": "최근 30일 토큰", + "totalInRange": "{{range}} 토큰", "share": "비중", "today": "오늘", "hitRate": "캐시 적중률", @@ -5831,6 +5843,7 @@ "col": { "model": "모델", "total": "총 토큰", + "totalInRange": "{{range}} 토큰", "share": "비중", "input": "입력", "output": "출력", @@ -5841,7 +5854,7 @@ }, "tasks": { "title": "토큰 사용이 많은 세션", - "subtitle": "최근 30일 동안 활동한 세션을 누적 토큰 순으로 정렬", + "subtitle": "{{range}} 동안 활동한 세션을 누적 토큰 순으로 정렬", "col": { "task": "세션", "model": "모델", diff --git a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json index 4d3adf4ae93..37403839007 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json @@ -5793,20 +5793,31 @@ "usageHistory": { "description": "这台设备上 Cindy 内部产生的 token 用量。记录从该功能上线起积累,不回填更早的历史。", "empty": "还没有用量记录。跑过对话之后,这里会开始积累。", + "range": { + "label": "时间范围", + "ariaLabel": "筛选用量时间范围", + "all": "所有时间", + "30d": "最近 30 天", + "7d": "最近 7 天", + "today": "今天" + }, "cacheHitTooltip": "缓存读取 ÷(输入 + 缓存读取 + 缓存写入),与消息卡片同口径。", "summary": { "title": "概览" }, "stats": { "todayTokens": "今日 token", + "todayTokensInRange": "{{range}} token", "totalTokens": "近 30 天 token", + "totalTokensInRange": "{{range}} token", "streak": "连续活跃天数", "cacheHitRate": "缓存命中率(近 30 天)", + "cacheHitRateInRange": "缓存命中率({{range}})", "models": "用到的模型" }, "heatmap": { "title": "活跃热力图", - "subtitle": "近 20 周", + "subtitle": "近 {{count}} 周", "emptyCell": "当天没有用量记录" }, "daily": { @@ -5819,6 +5830,7 @@ "col": { "agent": "Agent / harness", "total": "近 30 天 token", + "totalInRange": "{{range}} token", "share": "占比", "today": "今日", "hitRate": "缓存命中率", @@ -5831,6 +5843,7 @@ "col": { "model": "模型", "total": "总 token", + "totalInRange": "{{range}} token", "share": "占比", "input": "输入", "output": "输出", @@ -5841,7 +5854,7 @@ }, "tasks": { "title": "最耗 token 的任务", - "subtitle": "近 30 天内活跃过的任务,按累计 token 排序", + "subtitle": "{{range}}内活跃过的任务,按累计 token 排序", "col": { "task": "任务", "model": "模型", diff --git a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json index 91f511e8428..2a943820d9e 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json @@ -5793,20 +5793,31 @@ "usageHistory": { "description": "這台裝置上 Cindy 內部產生的 token 用量。紀錄從該功能上線起累積,不回填更早的歷史。", "empty": "還沒有用量紀錄。跑過對話之後,這裡會開始累積。", + "range": { + "label": "時間範圍", + "ariaLabel": "篩選用量時間範圍", + "all": "所有時間", + "30d": "最近 30 天", + "7d": "最近 7 天", + "today": "今天" + }, "cacheHitTooltip": "快取讀取 ÷(輸入 + 快取讀取 + 快取寫入),與訊息卡片同口徑。", "summary": { "title": "概覽" }, "stats": { "todayTokens": "今日 token", + "todayTokensInRange": "{{range}} token", "totalTokens": "近 30 天 token", + "totalTokensInRange": "{{range}} token", "streak": "連續活躍天數", "cacheHitRate": "快取命中率(近 30 天)", + "cacheHitRateInRange": "快取命中率({{range}})", "models": "用到的模型" }, "heatmap": { "title": "活躍熱力圖", - "subtitle": "近 20 週", + "subtitle": "近 {{count}} 週", "emptyCell": "當天沒有用量紀錄" }, "daily": { @@ -5819,6 +5830,7 @@ "col": { "agent": "Agent / harness", "total": "近 30 天 token", + "totalInRange": "{{range}} token", "share": "佔比", "today": "今日", "hitRate": "快取命中率", @@ -5831,6 +5843,7 @@ "col": { "model": "模型", "total": "總 token", + "totalInRange": "{{range}} token", "share": "佔比", "input": "輸入", "output": "輸出", @@ -5841,7 +5854,7 @@ }, "tasks": { "title": "最耗 token 的任務", - "subtitle": "近 30 天內活躍過的任務,按累計 token 排序", + "subtitle": "{{range}}內活躍過的任務,按累計 token 排序", "col": { "task": "任務", "model": "模型", diff --git a/apps/desktop/src/renderer/vite-env.d.ts b/apps/desktop/src/renderer/vite-env.d.ts index 00741fbe94c..c034c614b8a 100644 --- a/apps/desktop/src/renderer/vite-env.d.ts +++ b/apps/desktop/src/renderer/vite-env.d.ts @@ -6134,7 +6134,8 @@ interface ElectronAPI { ) => () => void; /** 用量历史聚合 (首页仪表盘)。wire 形态与 main/usage/usageHistory.ts 的 UsageHistoryPayload 同形。 */ getHistory: (opts?: { - days?: number; + days?: number | 'all'; + modelDays?: number | 'all'; forceRefresh?: boolean; }) => Promise; onTodaySpendChanged: ( From c2698e3d63a5de148fd40e904972ee73a27f5c4d Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:15:52 +1200 Subject: [PATCH 02/30] chore(design): refresh usage history surface inventory Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- docs/design-rules/design-inventory.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design-rules/design-inventory.md b/docs/design-rules/design-inventory.md index 90f7220a93c..0ae92454d0b 100644 --- a/docs/design-rules/design-inventory.md +++ b/docs/design-rules/design-inventory.md @@ -20,9 +20,9 @@ | `desktop.auth.login` | desktop | 登录页 | hash `/login`(GuestRoute → LoginPage) | LoginBrandStage, LoginCaptchaOverlay, LoginControls, LoginPage | apps/desktop/src/renderer/components/login/LoginBrandStage.tsx, apps/desktop/src/renderer/components/login/LoginCaptchaOverlay.tsx, apps/desktop/src/renderer/components/login/LoginControls.tsx, apps/desktop/src/renderer/components/login/LoginPage.tsx, apps/desktop/src/renderer/components/login/loginDesignTokens.ts | 59 | 0 | 21 | | `desktop.chat.files` | desktop | 工作目录文件浏览 | hash `/cc-agent/files/:sessionId`(WorkdirBrowseRoute) | FileBodyView, FileTreeView, WorkdirBrowseRoute | apps/desktop/src/renderer/features/cc-agent/workdir-browse/DocSearchBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/DrawioPreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileBodyView.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileFilterInput.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileTabsBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileTreeView.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FilterResultList.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/ImagePreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/OpenInSystemActions.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/PdfPreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/SessionTabsBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/UnrenderablePlaceholder.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/WorkdirBrowseRoute.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/WorkdirBrowseSidebar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useChatRailCollapsed.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useChatRailResize.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useConfirmSwitchAwayIfDirty.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useDelayedFlag.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useFileContent.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useFileTree.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useOpenTabs.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useProjectFileList.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useRevealFileInTree.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/activeFileBodyHandle.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/docModeSwitchProjects.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/drawioExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/expandedStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileContentCache.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileIcon.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileMeta.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileScrollStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileSelectionParams.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/filterFiles.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/imageExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/markdownAutosave.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/openTabsStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/pdfExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/selectedFileStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchInput.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchPanel.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchResults.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/hooks/useProjectSearch.ts | 22 | 7 | 64 | | `desktop.chat.new-draft` | desktop | 新建任务草稿页 | hash `/cc-agent/new`(NewMakerDraftRoute) | NewMakerDraftRoute | apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx | 15 | 0 | 2 | -| `desktop.chat.orca-workflow` | desktop | Orca 协同工作台 | hash `/cc-agent/orca/:sessionId`(OrcaWorkflowRoute) | CCAgentSessionView, OrcaSplitView, OrcaWorkerPanel, OrcaWorkflowRoute | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaSplitView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkerPanel.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkflowRoute.tsx | 266 | 56 | 411 | +| `desktop.chat.orca-workflow` | desktop | Orca 协同工作台 | hash `/cc-agent/orca/:sessionId`(OrcaWorkflowRoute) | CCAgentSessionView, OrcaSplitView, OrcaWorkerPanel, OrcaWorkflowRoute | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaSplitView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkerPanel.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkflowRoute.tsx | 266 | 56 | 412 | | `desktop.chat.scheduled` | desktop | 调度任务列表 | hash `/cc-agent/scheduled`(SchedulerPage) | SchedulerPage | apps/desktop/src/renderer/features/scheduler/SchedulerPage.tsx, apps/desktop/src/renderer/features/scheduler/components/AgentMark.tsx, apps/desktop/src/renderer/features/scheduler/components/BoundSessionCard.tsx, apps/desktop/src/renderer/features/scheduler/components/ProjectAutomationNotifyBridge.tsx, apps/desktop/src/renderer/features/scheduler/components/RunHistoryCard.tsx, apps/desktop/src/renderer/features/scheduler/components/RunHistoryPane.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleChips.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleFormDialog.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleStatusChip.tsx, apps/desktop/src/renderer/features/scheduler/components/ScriptCapabilityMultiSelect.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListCell.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListFilterPopover.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListPane.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateCard.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateGallery.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateParamForm.tsx, apps/desktop/src/renderer/features/scheduler/hooks/useDeleteScheduleWithSessions.tsx, apps/desktop/src/renderer/features/scheduler/hooks/useRunNowBusyGuard.ts, apps/desktop/src/renderer/features/scheduler/hooks/useRuns.ts, apps/desktop/src/renderer/features/scheduler/hooks/useScheduleForm.ts, apps/desktop/src/renderer/features/scheduler/hooks/useScheduleUnreadRunCounts.ts, apps/desktop/src/renderer/features/scheduler/hooks/useSchedules.ts, apps/desktop/src/renderer/features/scheduler/hooks/useSessionReferences.ts, apps/desktop/src/renderer/features/scheduler/hooks/useTemplates.ts, apps/desktop/src/renderer/features/scheduler/index.ts, apps/desktop/src/renderer/features/scheduler/lib/cronCodexPreset.ts, apps/desktop/src/renderer/features/scheduler/lib/cronToHuman.ts, apps/desktop/src/renderer/features/scheduler/lib/formatters.ts, apps/desktop/src/renderer/features/scheduler/lib/pluginScheduleCreateIntent.ts, apps/desktop/src/renderer/features/scheduler/lib/projectAutomationConfig.ts, apps/desktop/src/renderer/features/scheduler/lib/runHistoryGrouping.ts, apps/desktop/src/renderer/features/scheduler/lib/runUnread.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleFormLogic.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleRunReadSync.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleSessionBinding.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleSidebarIndexRuns.ts, apps/desktop/src/renderer/features/scheduler/lib/schedulesStore.ts, apps/desktop/src/renderer/features/scheduler/lib/statusFilter.ts, apps/desktop/src/renderer/features/scheduler/lib/statusFilterStorage.ts, apps/desktop/src/renderer/features/scheduler/lib/templateLocalization.ts, apps/desktop/src/renderer/features/scheduler/lib/usageLimitScheduleCreateIntent.ts | 44 | 0 | 118 | -| `desktop.chat.session` | desktop | 会话工作台 | hash `/cc-agent/:sessionId`;副窗经 `/cc-agent/boot` 解析后落到同一会话视图 | AssistantMessage, CCAgentFeatureLayout, CCAgentSessionView, ChatInput, PermissionPrompt | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx | 266 | 56 | 410 | +| `desktop.chat.session` | desktop | 会话工作台 | hash `/cc-agent/:sessionId`;副窗经 `/cc-agent/boot` 解析后落到同一会话视图 | AssistantMessage, CCAgentFeatureLayout, CCAgentSessionView, ChatInput, PermissionPrompt | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx | 266 | 56 | 411 | | `desktop.dev.maker-experimental` | desktop | Maker 实验诊断页 | hash `/maker-experimental`(MakerExperimentalView;EXPERIMENTAL_FEATURES 现为空,仍是生产路由) | MakerExperimentalView | apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx | 1 | 39 | 11 | | `desktop.issues.guide` | desktop | Issue 引导页 | hash `/issues`(IssueTrackerFeatureLayout,已迁 GitHub 后的引导页) | IssueTrackerFeatureLayout | apps/desktop/src/renderer/features/issue-tracker/IssueTrackerFeatureLayout.tsx, apps/desktop/src/renderer/features/issue-tracker/MyIssueList.tsx, apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts, apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts, apps/desktop/src/renderer/features/issue-tracker/lib/startIssueChat.ts | 0 | 0 | 8 | | `desktop.native.app-menu` | desktop | 应用菜单 | Menu.setApplicationMenu(用户可见出口,不展开业务逻辑) | applicationMenuLabels | apps/desktop/src/main/applicationMenuLabels.ts | 0 | 0 | 0 | @@ -37,7 +37,7 @@ | `desktop.overlay.toast` | desktop | Toast | App 常驻 ToastContainer(用户可见出口,不展开业务逻辑) | Toast, ToastContainer | apps/desktop/src/renderer/components/ui/toast/Toast.tsx, apps/desktop/src/renderer/components/ui/toast/ToastContainer.tsx, apps/desktop/src/renderer/components/ui/toast/index.ts | 4 | 4 | 2 | | `desktop.plugins.app-main` | desktop | 插件主视图 | hash `/apps/:ghostId`(GhostMainViewFeatureLayout) | GhostMainViewFeatureLayout, GhostMainViewHost | apps/desktop/src/renderer/features/plugin/GhostMainViewFeatureLayout.tsx, apps/desktop/src/renderer/features/plugin/GhostMainViewHost.tsx | 0 | 0 | 0 | | `desktop.plugins.installed` | desktop | 已装插件 | hash `/plugins`(GhostPluginPage) | AddMarketplaceDialog, GhostPluginDetailView, GhostPluginPage, MarketPluginDetailView, MyPublishesSection, PluginManagementLayout, PluginScopePicker, UpdateAllDialog | apps/desktop/src/renderer/features/plugin/AddMarketplaceDialog.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginPage.tsx, apps/desktop/src/renderer/features/plugin/MarketPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/MyPublishesSection.tsx, apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/plugin/PluginScopePicker.tsx, apps/desktop/src/renderer/features/plugin/UpdateAllDialog.tsx, apps/desktop/src/renderer/features/plugin/plugin-motion.css | 49 | 0 | 81 | -| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 722 | +| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 726 | | `desktop.shell.main-layout` | desktop | 主窗口壳(标题栏 / 左右栏 / 内容区) | main BrowserWindow → renderer/index.tsx → main-entry.tsx → App → MainLayout (`#/` 受保护壳) | ChromeActions, FeishuConflictDialogHost, GhostMediaLightboxHost, MainLayout, RightSidebar, RightSidebarShell, SessionShareImportWizard, Sidebar, UpdateNoticeDialog, WindowControls | apps/desktop/src/renderer/cindy-brain/GhostMediaLightboxHost.tsx, apps/desktop/src/renderer/components/UpdateNoticeDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialogHost.tsx, apps/desktop/src/renderer/components/layout/BrowserWebviewPool.tsx, apps/desktop/src/renderer/components/layout/ChromeActions.tsx, apps/desktop/src/renderer/components/layout/ContentHeader.tsx, apps/desktop/src/renderer/components/layout/CredentialStoreBanner.tsx, apps/desktop/src/renderer/components/layout/FadeSwitcher.tsx, apps/desktop/src/renderer/components/layout/GhostPanelWindowLayout.tsx, apps/desktop/src/renderer/components/layout/GlobalDropImportListener.tsx, apps/desktop/src/renderer/components/layout/MainLayout.tsx, apps/desktop/src/renderer/components/layout/RightSidebar.tsx, apps/desktop/src/renderer/components/layout/RightSidebarDetach.tsx, apps/desktop/src/renderer/components/layout/RightSidebarMaximize.tsx, apps/desktop/src/renderer/components/layout/RightSidebarToggle.tsx, apps/desktop/src/renderer/components/layout/SidebarWindowLayout.tsx, apps/desktop/src/renderer/components/layout/chromeActionsGeometry.ts, apps/desktop/src/renderer/components/layout/chromeActionsLayout.ts, apps/desktop/src/renderer/components/layout/railChromeActions.ts, apps/desktop/src/renderer/components/layout/windowDrag.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/sidebar/AccountSwitcherDialog.tsx, apps/desktop/src/renderer/components/sidebar/AttentionDot.tsx, apps/desktop/src/renderer/components/sidebar/GhostMainViewNavEntries.tsx, apps/desktop/src/renderer/components/sidebar/MobileDownloadDialog.tsx, apps/desktop/src/renderer/components/sidebar/Sidebar.tsx, apps/desktop/src/renderer/components/sidebar/SidebarIconButton.tsx, apps/desktop/src/renderer/components/sidebar/SidebarTopNav.tsx, apps/desktop/src/renderer/components/sidebar/SortableList.tsx, apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx, apps/desktop/src/renderer/components/sidebar/UserInfoSection.tsx, apps/desktop/src/renderer/components/sidebar/VendorIcon.tsx, apps/desktop/src/renderer/components/sidebar/VendorReadinessBadge.tsx, apps/desktop/src/renderer/components/sidebar/WorktreeBadge.tsx, apps/desktop/src/renderer/components/title-bar/ChromeIconButton.tsx, apps/desktop/src/renderer/components/title-bar/MenuButton.tsx, apps/desktop/src/renderer/components/title-bar/WindowControls.tsx, apps/desktop/src/renderer/features/right-sidebar/AddTabDropdown.tsx, apps/desktop/src/renderer/features/right-sidebar/EmptyState.tsx, apps/desktop/src/renderer/features/right-sidebar/RightSidebarShell.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBar.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBodyErrorBoundary.tsx, apps/desktop/src/renderer/features/right-sidebar/hooks/useBrowserWebview.ts, apps/desktop/src/renderer/features/right-sidebar/hooks/useNativePopupSurface.ts, apps/desktop/src/renderer/features/right-sidebar/iosSimulatorPluginAvailability.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserPartition.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserWebviewPool.ts, apps/desktop/src/renderer/features/right-sidebar/lib/detachedSidebarRouting.ts, apps/desktop/src/renderer/features/right-sidebar/lib/executeSidebarCommand.ts, apps/desktop/src/renderer/features/right-sidebar/lib/iosSimulatorFocusBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/lastTurnChangedFiles.ts, apps/desktop/src/renderer/features/right-sidebar/lib/nativePopupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openBackgroundTasksTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarFileBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openSubagentsTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTerminalShortcut.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTurnReview.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupRouter.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/rsbBrowserBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarCommands.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarHostSession.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/BackgroundTasksBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowAgentStrip.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowProgressTree.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/chatTaskFocusIntent.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/listSessionTasks.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/workflowProgressModel.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.css, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/dropExternalFile.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/fileTreeImagePreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/useSessionScopedTreeWidth.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/index.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorInstanceGrid.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/iosSimulatorH264Decoder.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersAttentionIcon.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/actions.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/closeDecision.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/ResourceUsageBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/resource-usage.css, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/subscription.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/ImageDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/MarkdownDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/PlainUnifiedDiff.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/diffRows.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/highlight.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/inlineDiff.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/useDiffHighlights.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/ReviewTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/diffExpansionPreference.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/fileTree.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/gitApplyCommand.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/markdownPreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useLastTurnFilter.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewGitState.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewSource.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/ConversationStream.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/DetailView.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/RunList.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentToolCard.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentsBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentChangeFence.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentConversation.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentFormat.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/TerminalTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/lib/xtermPool.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserCommentPopover.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/browserCommentEditorDraft.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/lib/parseOmnibox.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useBrowserComment.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useLocalHtmlAutoReload.ts, apps/desktop/src/renderer/features/right-sidebar/registry.ts, apps/desktop/src/renderer/features/right-sidebar/store.ts, apps/desktop/src/renderer/features/right-sidebar/types.ts, apps/desktop/src/renderer/layout/LayoutRoot.tsx, apps/desktop/src/renderer/layout/PanelDragController.tsx, apps/desktop/src/renderer/layout/PanelDragPrototype.tsx, apps/desktop/src/renderer/layout/collapsePrefs.ts, apps/desktop/src/renderer/layout/layoutDevTools.ts, apps/desktop/src/renderer/layout/panePlacement.tsx, apps/desktop/src/renderer/layout/paneWidths.tsx, apps/desktop/src/renderer/layout/panelMaximize.tsx, apps/desktop/src/renderer/styles/globals.css | 145 | 72 | 288 | | `desktop.skillhub.local` | desktop | SkillHub 本地技能 | hash `/skillhub/local` 及详情 `/skillhub/local/:kind/global/:name`、`/skillhub/local/:kind/project/:projectHash/:name` | InstallTargetPicker, PluginManagementLayout, SkillhubDetailView, SkillhubFeatureLayout, SkillhubHomeView, SkillhubMarketPreviewPanel | apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubDetailView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubFeatureLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubHomeView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx | 41 | 0 | 71 | | `desktop.skillhub.market` | desktop | SkillHub 市场 | hash `/skillhub/market`(SkillhubMarketListView) | InstallTargetPicker, MarketCard, MarketInfoEditDialog, SkillhubMarketListView, SkillhubMarketPreviewPanel, VisibilityEditorDialog | apps/desktop/src/renderer/features/skillhub/SkillhubMarketListView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketCard.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketInfoEditDialog.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketPreviewTree.tsx, apps/desktop/src/renderer/features/skillhub/components/PlatformTagSelector.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillCategoryFilterBar.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillIcon.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillTagList.tsx, apps/desktop/src/renderer/features/skillhub/components/TeamScopePicker.tsx, apps/desktop/src/renderer/features/skillhub/components/VisibilityEditorDialog.tsx | 46 | 0 | 59 | From 14372014a7c7759c1fa7c80a3fb03f1dad976b6d Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:39:04 +1200 Subject: [PATCH 03/30] fix(desktop): refine usage heatmap layout and intensity Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../new-chat/HomeUsageDashboard.tsx | 12 +++++---- .../components/new-chat/UsageHeatmap.tsx | 12 ++++++--- .../__tests__/usageHeatmapMetric.test.tsx | 25 +++++++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx b/apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx index 43c1545b3d0..db5a224625a 100644 --- a/apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx +++ b/apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx @@ -373,11 +373,13 @@ export function HomeUsageDashboard(): React.JSX.Element { {/* 展开区: 热力图 + 右栏 (按模型拆分; 拆分数据未积累出来前退化为每日总额柱状图) */} {!collapsed && (
- +
+ +
{/* 右栏: 每日堆叠柱状图 (高=日总额, 分段=模型构成)。不放图例 — 每日模型 明细全在柱子 hover tooltip 里 (省空间, 用户确认的取舍)。 diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index 80d11fc0cbb..753c5868310 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -161,7 +161,15 @@ export function UsageHeatmap({ const tokensByDay = new Map(days.map((d) => [d.day, d.tokens ?? 0])); const intensityOf = (row: { money: RegionalMoney; tokens?: number }): number => metric === 'tokens' ? (row.tokens ?? 0) : row.money.amount; + const today = parseDayKey(todayKey); + const start = startOfWeek(today); + start.setDate(start.getDate() - (visibleWeeks - 1) * 7); + const startKey = toDayKey(start); + + // 分桶只反映当前屏幕可见的日期。全量历史中较早的极端值不应改变 + // 用户正在看的这些格子的相对深浅。 const nonZero = days + .filter((day) => day.day >= startKey && day.day <= todayKey) .map(intensityOf) .filter((v) => v > 0) .sort((a, b) => a - b); @@ -169,10 +177,6 @@ export function UsageHeatmap({ nonZero.length ? nonZero[Math.min(nonZero.length - 1, Math.floor(p * nonZero.length))] : 0; const thresholds: [number, number, number] = [q(0.25), q(0.5), q(0.75)]; - const today = parseDayKey(todayKey); - const start = startOfWeek(today); - start.setDate(start.getDate() - (visibleWeeks - 1) * 7); - const cells: HeatCell[] = []; const cursor = new Date(start); for (let index = 0; index < visibleWeeks * 7; index += 1) { diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index fc54bd1c600..909ac409b30 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -95,6 +95,31 @@ describe('UsageHeatmap metric', () => { expect(moneyStyles).not.toEqual(tokenStyles); }); + it('只用当前可见日期计算分桶阈值', () => { + const visibleDays = [ + { day: '2026-08-20', money: money(10), tokens: 10 }, + { day: '2026-08-21', money: money(20), tokens: 20 }, + { day: '2026-08-22', money: money(30), tokens: 30 }, + ]; + const withOlderOutlier = [ + { day: '2025-01-01', money: money(1_000_000), tokens: 1_000_000 }, + ...visibleDays, + ]; + + const baseline = render( + , + ); + const baselineCell = baseline.getByRole('button', { name: '2026-08-21' }).firstElementChild; + const baselineColor = (baselineCell as HTMLElement).style.backgroundColor; + baseline.unmount(); + + const withOutlier = render( + , + ); + const outlierCell = withOutlier.getByRole('button', { name: '2026-08-21' }).firstElementChild; + expect((outlierCell as HTMLElement).style.backgroundColor).toBe(baselineColor); + }); + it('token 口径的 tooltip 不出现金额', () => { const { container } = render( , From 8ec1f101b6025196b08822be94d678674d2e8b4a Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:11:15 +1200 Subject: [PATCH 04/30] fix(desktop): query complete usage history sessions Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../sessionsListIncludePinned.test.ts | 15 + apps/desktop/src/main/localDb/ipc/sessions.ts | 521 ++++++++++-------- .../settings/usage/UsageHistorySection.tsx | 2 +- .../settings/usage/UsageTaskTable.tsx | 41 +- .../src/renderer/lib/sessionService.ts | 2 + 5 files changed, 333 insertions(+), 248 deletions(-) diff --git a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts index b7c49463db9..324fd18c6d3 100644 --- a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts +++ b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts @@ -195,6 +195,21 @@ describe('local-db:sessions:list includePinned', () => { expect(h.queryResults).toHaveLength(1); }); + it('returns the full sessions set for the usage-history query without message projections', async () => { + const handler = sessionsListHandler(); + h.queryResults.push([ + { session: sessionRow('recent', { totalTokenUsage: 20 }) }, + { session: sessionRow('old', { totalTokenUsage: 200 }) }, + ]); + + const result = await handler({}, 20, 'all', { usageHistory: true }); + + expect(result.map((s) => s.id)).toEqual(['recent', 'old']); + expect(h.fakeDb.select).toHaveBeenCalledTimes(1); + expect(h.listQuery).not.toHaveBeenCalled(); + expect(h.queryResults).toHaveLength(0); + }); + it('also includes pinned rows for the all-status bucket used by mobile detail filters', async () => { const handler = sessionsListHandler(); h.queryResults.push( diff --git a/apps/desktop/src/main/localDb/ipc/sessions.ts b/apps/desktop/src/main/localDb/ipc/sessions.ts index 9b1245ed7c2..a66a9182c4b 100644 --- a/apps/desktop/src/main/localDb/ipc/sessions.ts +++ b/apps/desktop/src/main/localDb/ipc/sessions.ts @@ -1079,6 +1079,7 @@ export function registerSessionIpc( // sidebar-card-mode: 首次 list(db 必然 ready)触发一次置顶摘要回填—— // 老置顶会话没有 turn-done 触发点。模块内部 once 守卫 + 串行 + swallow。 void import('../../sessionTaskSummary.js').then((m) => m.backfillPinnedSessionSummaries()); + const usageHistory = shouldUseUsageHistoryQuery(options); const cap = clampLimit(limit, 20); const includePinned = shouldIncludePinnedSessions(options); const fresh = shouldBypassSessionListSingleFlight(options); @@ -1118,10 +1119,28 @@ export function registerSessionIpc( }), ); }; + const loadUsageHistoryRows = async () => { + // 用量历史的“最耗任务”必须覆盖整个会话表,再由 renderer 按所选日历范围 + // 精确筛选;不能复用侧栏按 updatedAt 截断的 1000 行列表。这里刻意不算 + // messageCount / preview,避免为统计页引入整库 messages 扫描。 + const sourceFilter = inArray(sessions.source, DESKTOP_VISIBLE_SESSION_SOURCES); + const statusWhere = () => + statusFilter ? eq(sessions.status, statusFilter) : ne(sessions.status, 'deleted'); + const rows = await selectSessionUsageRows(db, and(sourceFilter, statusWhere())); + return rows.map((session) => + sessionToCamel({ + ...session, + messageCount: 0, + latestMessageExtract: null, + latestMessageRole: null, + }), + ); + }; // key 用同一快照上的 userId + clientEpoch + 归一化参数。 // forceRefresh / status 重拉带 fresh,不并入写前那次查询。 - const result = - userId && !fresh + const result = usageHistory + ? await loadUsageHistoryRows() + : userId && !fresh ? await runSessionListSingleFlight( buildSessionListFlightKey({ userId, @@ -1139,7 +1158,8 @@ export function registerSessionIpc( const fields = JSON.stringify({ event: 'localDb.sessions.list.done', filter, - cap, + cap: usageHistory ? 'all' : cap, + usageHistory, includePinned, rows: result.length, queryElapsedMs: elapsedMs, @@ -1503,247 +1523,247 @@ export function registerSessionIpc( // 工作目录切换必须和发送/懒启动共用同一把路由锁。否则发送可能在 // 读取旧目录后、写入新目录前重建 runtime,随后仍在旧目录执行。 const update = async () => { - if (p.workspaceKind !== undefined) { - const value = p.workspaceKind; - if (value !== 'project' && value !== 'dialogue') { - throwIpcError('INVALID_PARAMS', `invalid workspaceKind: ${String(value)}`); + if (p.workspaceKind !== undefined) { + const value = p.workspaceKind; + if (value !== 'project' && value !== 'dialogue') { + throwIpcError('INVALID_PARAMS', `invalid workspaceKind: ${String(value)}`); + } } - } - const ALLOWED_UPDATE_ORCA_ROLES = new Set(['lead', 'worker']); - if ( - p.orcaRole !== undefined && - p.orcaRole !== null && - !ALLOWED_UPDATE_ORCA_ROLES.has(p.orcaRole as string) - ) { - throwIpcError('INVALID_PARAMS', `invalid orcaRole: ${String(p.orcaRole)}`); - } - if (typeof p.workingDir === 'string') { - p.workingDir = normalizeWorkingDirForStorage(p.workingDir) ?? null; - } - const REVIEW_IMMUTABLE_FIELDS = new Set([ - 'workingDir', - 'workspaceKind', - 'model', - 'providerId', - 'effort', - 'permissionMode', - 'fastMode', - 'planModeEnabled', - 'orcaRole', - 'extraDirs', - 'writableDirs', - ]); - if (Object.keys(p).some((key) => REVIEW_IMMUTABLE_FIELDS.has(key))) { - const [target] = await db - .select({ source: sessions.source }) - .from(sessions) - .where(eq(sessions.id, sid)) - .limit(1); - if (target?.source === 'review') { - throwIpcError( - 'UNSUPPORTED_CAPABILITY', - 'Review task settings are fixed to the source task', - ); + const ALLOWED_UPDATE_ORCA_ROLES = new Set(['lead', 'worker']); + if ( + p.orcaRole !== undefined && + p.orcaRole !== null && + !ALLOWED_UPDATE_ORCA_ROLES.has(p.orcaRole as string) + ) { + throwIpcError('INVALID_PARAMS', `invalid orcaRole: ${String(p.orcaRole)}`); } - } - // 会话移动转录迁移:patch 带 workingDir 时先留存旧值,update 后对比实际变化。 - // CLI 转录按 cwd 转码目录存放,workingDir 变了必须跟着搬,否则 resume 报 - // "No conversation found with session ID"(见 claude-transcript-relocation.ts)。 - const beforeMove = - p.workingDir !== undefined - ? ( - await db - .select({ - workingDir: sessions.workingDir, - agentKind: sessions.agentKind, - remoteHostId: sessions.remoteHostId, - }) - .from(sessions) - .where(eq(sessions.id, sid)) - )[0] - : undefined; - const movingLocalNonClaudeSession = - beforeMove && - beforeMove.agentKind !== 'cc' && - !beforeMove.remoteHostId && - beforeMove.workingDir && - typeof p.workingDir === 'string' && - p.workingDir && - normalizeWorkingDirForStorage(beforeMove.workingDir) !== p.workingDir; - // Pi/Codex keep a live Maker handle whose cwd is fixed at bootstrap. Close it - // before persisting the new directory so the next send lazily recreates the - // runtime with the moved session's cwd instead of continuing in the old one. - if (movingLocalNonClaudeSession) { - if (!opts.closeIdleSessionForMove) { - throwIpcError('INTERNAL', '会话移动 runtime 操作未配置'); + if (typeof p.workingDir === 'string') { + p.workingDir = normalizeWorkingDirForStorage(p.workingDir) ?? null; } - const idle = await opts.closeIdleSessionForMove(sid); - if (idle === false) { - throwIpcError('PRECONDITION_FAILED', '运行中的任务不能移动'); + const REVIEW_IMMUTABLE_FIELDS = new Set([ + 'workingDir', + 'workspaceKind', + 'model', + 'providerId', + 'effort', + 'permissionMode', + 'fastMode', + 'planModeEnabled', + 'orcaRole', + 'extraDirs', + 'writableDirs', + ]); + if (Object.keys(p).some((key) => REVIEW_IMMUTABLE_FIELDS.has(key))) { + const [target] = await db + .select({ source: sessions.source }) + .from(sessions) + .where(eq(sessions.id, sid)) + .limit(1); + if (target?.source === 'review') { + throwIpcError( + 'UNSUPPORTED_CAPABILITY', + 'Review task settings are fixed to the source task', + ); + } } - } - // 只有纯设置字段(model/effort 等)才跳过 bump;凡带 activity 字段 - // (clearedAt / sdkSessionId / status / token 用量等)仍需更新 updatedAt, - // 否则本地 /clear 后重启侧栏时间回退旧值。 - const SETTINGS_ONLY_FIELDS = new Set([ - 'model', - 'effort', - 'permissionMode', - 'fastMode', - 'planModeEnabled', - 'providerId', - 'orcaRole', - 'extraDirs', - 'writableDirs', - 'pinnedAt', - 'workingDir', - 'workspaceKind', - 'title', - ]); - const isSettingsOnly = Object.keys(p).every((k) => SETTINGS_ONLY_FIELDS.has(k)); - const setObj = sessionPatchToRow(p as Parameters[0], { - bumpUpdatedAt: !isSettingsOnly, - }); - if (p.clearedAt !== undefined) { - setObj.summary = null; - setObj.listPreview = null; - setObj.listPreviewRole = null; - } - // 用户手动改名(重命名框 / 侧边栏)走这条:告诉自动起名收手。同值改名不会让 - // 条件写落空,不显式说一声的话智能标题会把他刚保存的名字盖掉(review P1)。 - // **必须先于 UPDATE**:写库是一次 worker RPC 往返,改名提交与这里拿到回执之间 - // 有真实时间差,在那期间智能标题仍能满足 `WHERE title = 期望值` 把名字盖掉。 - // 先记号后写库,代价只是写库失败时该会话本进程内不再自动起名 —— 用户毕竟确实 - // 按下过保存,这个方向的偏差是安全的。 - if (typeof p.title === 'string') noteUserTitleWritten(sid); - await withStatusWriteLock( - sid, - p.status, - async () => { - await writeSessionPatch(db, sid, setObj, p.status); - cleanupSessionRuntimeForTerminalStatus(sid, p.status); - }, - p.workingDir !== undefined, - ); - // session-git-pr-context:/clear 经此处写 clearedAt——边界之前的消息对用户 - // 不可见,PR 引用同步重算(fire-and-forget,内部按 clearedAt/rewindAt 过滤)。 - if (p.clearedAt !== undefined) { - noteSessionClearBoundary(sid, p.clearedAt as string | null); - // sidebar-card-mode(codex review):summary 是基于 clear 前内容生成的,clear 后 - // 已过时;置顶卡片优先用 summary 而非 preview,不清就会继续显示旧任务摘要。 - // 与 clearedAt 同一句 UPDATE 置空,避免崩溃后非 NULL 缓存绕过 clear 边界。 - if (isOwnerScopeCurrent(ownerScope)) { - broadcastSessionPatched(sid, { summary: null, preview: null }, ownerScope); + // 会话移动转录迁移:patch 带 workingDir 时先留存旧值,update 后对比实际变化。 + // CLI 转录按 cwd 转码目录存放,workingDir 变了必须跟着搬,否则 resume 报 + // "No conversation found with session ID"(见 claude-transcript-relocation.ts)。 + const beforeMove = + p.workingDir !== undefined + ? ( + await db + .select({ + workingDir: sessions.workingDir, + agentKind: sessions.agentKind, + remoteHostId: sessions.remoteHostId, + }) + .from(sessions) + .where(eq(sessions.id, sid)) + )[0] + : undefined; + const movingLocalNonClaudeSession = + beforeMove && + beforeMove.agentKind !== 'cc' && + !beforeMove.remoteHostId && + beforeMove.workingDir && + typeof p.workingDir === 'string' && + p.workingDir && + normalizeWorkingDirForStorage(beforeMove.workingDir) !== p.workingDir; + // Pi/Codex keep a live Maker handle whose cwd is fixed at bootstrap. Close it + // before persisting the new directory so the next send lazily recreates the + // runtime with the moved session's cwd instead of continuing in the old one. + if (movingLocalNonClaudeSession) { + if (!opts.closeIdleSessionForMove) { + throwIpcError('INTERNAL', '会话移动 runtime 操作未配置'); + } + const idle = await opts.closeIdleSessionForMove(sid); + if (idle === false) { + throwIpcError('PRECONDITION_FAILED', '运行中的任务不能移动'); + } } - void recomputePrRefsForSession(sid).catch(() => undefined); - } - // workingDir 实际变化的本机 cc 会话:迁移 CLI 转录后再查询返回行/广播,保证 - // renderer 拿到更新结果时转录已就位(用户可立即续聊),且迁移中持久化的最新 - // sdkSessionId 能进返回行与广播 patch——否则 renderer 留着旧 resume id,下一次 - // lazy-create 仍会 resume 到 pre-fork 会话。内部 best-effort 不抛错。 - // 动态 import 避免 localDb → maker-host 的静态模块环(同下方 sessionTaskSummary)。 - if ( - beforeMove && - beforeMove.agentKind === 'cc' && - !beforeMove.remoteHostId && - beforeMove.workingDir && - typeof p.workingDir === 'string' && - p.workingDir && - normalizeWorkingDirForStorage(beforeMove.workingDir) !== p.workingDir - ) { - const m = await import('../../maker-host/claude-transcript-relocation.js'); - const reloc = await m.relocateClaudeTranscriptsForSessionMove( + // 只有纯设置字段(model/effort 等)才跳过 bump;凡带 activity 字段 + // (clearedAt / sdkSessionId / status / token 用量等)仍需更新 updatedAt, + // 否则本地 /clear 后重启侧栏时间回退旧值。 + const SETTINGS_ONLY_FIELDS = new Set([ + 'model', + 'effort', + 'permissionMode', + 'fastMode', + 'planModeEnabled', + 'providerId', + 'orcaRole', + 'extraDirs', + 'writableDirs', + 'pinnedAt', + 'workingDir', + 'workspaceKind', + 'title', + ]); + const isSettingsOnly = Object.keys(p).every((k) => SETTINGS_ONLY_FIELDS.has(k)); + const setObj = sessionPatchToRow(p as Parameters[0], { + bumpUpdatedAt: !isSettingsOnly, + }); + if (p.clearedAt !== undefined) { + setObj.summary = null; + setObj.listPreview = null; + setObj.listPreviewRole = null; + } + // 用户手动改名(重命名框 / 侧边栏)走这条:告诉自动起名收手。同值改名不会让 + // 条件写落空,不显式说一声的话智能标题会把他刚保存的名字盖掉(review P1)。 + // **必须先于 UPDATE**:写库是一次 worker RPC 往返,改名提交与这里拿到回执之间 + // 有真实时间差,在那期间智能标题仍能满足 `WHERE title = 期望值` 把名字盖掉。 + // 先记号后写库,代价只是写库失败时该会话本进程内不再自动起名 —— 用户毕竟确实 + // 按下过保存,这个方向的偏差是安全的。 + if (typeof p.title === 'string') noteUserTitleWritten(sid); + await withStatusWriteLock( sid, - beforeMove.workingDir, - p.workingDir, + p.status, + async () => { + await writeSessionPatch(db, sid, setObj, p.status); + cleanupSessionRuntimeForTerminalStatus(sid, p.status); + }, + p.workingDir !== undefined, ); - if (reloc.persistedSdkSessionId) { - (p as Record).sdkSessionId = reloc.persistedSdkSessionId; + // session-git-pr-context:/clear 经此处写 clearedAt——边界之前的消息对用户 + // 不可见,PR 引用同步重算(fire-and-forget,内部按 clearedAt/rewindAt 过滤)。 + if (p.clearedAt !== undefined) { + noteSessionClearBoundary(sid, p.clearedAt as string | null); + // sidebar-card-mode(codex review):summary 是基于 clear 前内容生成的,clear 后 + // 已过时;置顶卡片优先用 summary 而非 preview,不清就会继续显示旧任务摘要。 + // 与 clearedAt 同一句 UPDATE 置空,避免崩溃后非 NULL 缓存绕过 clear 边界。 + if (isOwnerScopeCurrent(ownerScope)) { + broadcastSessionPatched(sid, { summary: null, preview: null }, ownerScope); + } + void recomputePrRefsForSession(sid).catch(() => undefined); } - } - const row = await selectSessionWithCount(db, sid); - if (!row) throwIpcError('NOT_FOUND', 'Session 不存在'); - // 取消置顶后摘要不再有展示面,立刻清掉,避免列表/再次置顶前继续吃旧句。 - if (p.pinnedAt !== undefined && row.pinnedAt == null) { - await db.update(sessions).set({ summary: null }).where(eq(sessions.id, sid)); - row.summary = null; - } - const updated = sessionToCamel(row); - const projectTargetChanged = p.workspaceKind !== undefined || p.workingDir !== undefined; - const settingsChanged = Object.keys(p).some((key) => REMOTE_PERSIST_FIELDS.has(key)); - const titleChanged = p.title !== undefined; - // 归档/删除这类纯 status 变化也要广播:本机多窗口收敛靠 sessions:patched, - // 否则「在新窗口打开」的副窗口无从得知会话已被移除,仍停留在旧视图(#3175)。 - const statusChanged = p.status !== undefined; - if ( - projectTargetChanged && - row.workspaceKind === 'project' && - row.workingDir && - !row.remoteHostId - ) { - await upsertRecentWorkdir(row.workingDir, Date.now()); - } - // status 广播必须用**广播时刻的持久化真值**,不能带请求值 p.status,也不能用 - // 上方读行的快照:写入(withStatusWriteLock)与广播不在同一串行区间,且读行 - // 之后、广播之前还有 await(摘要清理 / recent-workdir / 转录迁移),两个窗口 - // 对同一任务并发操作时,本请求可能在此期间被另一窗口推进到更晚的终态(如 - // 归档写入后被删除)。用过期值广播会把镜像回滚成旧 UI 状态(已删除任务在 - // 副窗/控制端复活),且若本广播是最后一条,镜像不会自愈。 - // - // 因此含 status 的 patch 在广播前(所有 await 之后)**重读一次**:重读与广播 - // 之间无 await,同进程单事件循环下不可能再插入并发写;即便并发删除的广播 - // 晚于本广播到达,镜像最终也收敛到 deleted。 - let broadcastStatus = updated.status; - if (p.status !== undefined) { - const [currentRow] = await db - .select({ status: sessions.status }) - .from(sessions) - .where(eq(sessions.id, sid)) - .limit(1); - if (currentRow) broadcastStatus = currentRow.status; - } - const broadcastPatch = - p.pinnedAt === undefined && p.status === undefined - ? p - : { - ...p, - ...(p.pinnedAt !== undefined ? { pinnedAt: updated.pinnedAt } : {}), - ...(p.status !== undefined ? { status: broadcastStatus } : {}), - ...(p.pinnedAt !== undefined && updated.pinnedAt === null ? { summary: null } : {}), - ...(p.pinnedAt !== undefined && updated.pinnedAt !== null - ? { status: broadcastStatus } - : {}), - }; - if ( - projectTargetChanged || - settingsChanged || - titleChanged || - statusChanged || - p.pinnedAt !== undefined - ) { - if (isOwnerScopeCurrent(ownerScope)) { - broadcastSessionPatched(sid, broadcastPatch, ownerScope); + // workingDir 实际变化的本机 cc 会话:迁移 CLI 转录后再查询返回行/广播,保证 + // renderer 拿到更新结果时转录已就位(用户可立即续聊),且迁移中持久化的最新 + // sdkSessionId 能进返回行与广播 patch——否则 renderer 留着旧 resume id,下一次 + // lazy-create 仍会 resume 到 pre-fork 会话。内部 best-effort 不抛错。 + // 动态 import 避免 localDb → maker-host 的静态模块环(同下方 sessionTaskSummary)。 + if ( + beforeMove && + beforeMove.agentKind === 'cc' && + !beforeMove.remoteHostId && + beforeMove.workingDir && + typeof p.workingDir === 'string' && + p.workingDir && + normalizeWorkingDirForStorage(beforeMove.workingDir) !== p.workingDir + ) { + const m = await import('../../maker-host/claude-transcript-relocation.js'); + const reloc = await m.relocateClaudeTranscriptsForSessionMove( + sid, + beforeMove.workingDir, + p.workingDir, + ); + if (reloc.persistedSdkSessionId) { + (p as Record).sdkSessionId = reloc.persistedSdkSessionId; + } } - } - // sidebar-card-mode: 会话被置顶那一刻补生成任务摘要(turn-done 路径只覆盖 - // "置顶后又跑过 turn"的会话)。动态 import 避免 localDb → maker-host 的静态 - // 模块环;fire-and-forget,模块内部自带置顶/节流守卫。 - if (p.pinnedAt !== undefined && updated.pinnedAt !== null) { - void import('../../sessionTaskSummary.js').then((m) => - m.maybeGenerateSessionTaskSummary(sid, { force: true }), - ); - } - notifyAgentIslandSessionPatch(updated.id, { - status: updated.status, - title: updated.title, - workingDir: updated.workingDir, - workspaceKind: updated.workspaceKind, - }); - scheduleWorktreeRecycleForStatusChange(sid, p.status, { ownerScope, mediaDb: db }); - notifyGhostSessionStatusChange(sid, p.status, updated.workingDir); - cleanupSessionTerminalArtifacts(sid, p.status); - compactTerminalSessionToolResults(dbClient, sid, p.status); - return updated; + const row = await selectSessionWithCount(db, sid); + if (!row) throwIpcError('NOT_FOUND', 'Session 不存在'); + // 取消置顶后摘要不再有展示面,立刻清掉,避免列表/再次置顶前继续吃旧句。 + if (p.pinnedAt !== undefined && row.pinnedAt == null) { + await db.update(sessions).set({ summary: null }).where(eq(sessions.id, sid)); + row.summary = null; + } + const updated = sessionToCamel(row); + const projectTargetChanged = p.workspaceKind !== undefined || p.workingDir !== undefined; + const settingsChanged = Object.keys(p).some((key) => REMOTE_PERSIST_FIELDS.has(key)); + const titleChanged = p.title !== undefined; + // 归档/删除这类纯 status 变化也要广播:本机多窗口收敛靠 sessions:patched, + // 否则「在新窗口打开」的副窗口无从得知会话已被移除,仍停留在旧视图(#3175)。 + const statusChanged = p.status !== undefined; + if ( + projectTargetChanged && + row.workspaceKind === 'project' && + row.workingDir && + !row.remoteHostId + ) { + await upsertRecentWorkdir(row.workingDir, Date.now()); + } + // status 广播必须用**广播时刻的持久化真值**,不能带请求值 p.status,也不能用 + // 上方读行的快照:写入(withStatusWriteLock)与广播不在同一串行区间,且读行 + // 之后、广播之前还有 await(摘要清理 / recent-workdir / 转录迁移),两个窗口 + // 对同一任务并发操作时,本请求可能在此期间被另一窗口推进到更晚的终态(如 + // 归档写入后被删除)。用过期值广播会把镜像回滚成旧 UI 状态(已删除任务在 + // 副窗/控制端复活),且若本广播是最后一条,镜像不会自愈。 + // + // 因此含 status 的 patch 在广播前(所有 await 之后)**重读一次**:重读与广播 + // 之间无 await,同进程单事件循环下不可能再插入并发写;即便并发删除的广播 + // 晚于本广播到达,镜像最终也收敛到 deleted。 + let broadcastStatus = updated.status; + if (p.status !== undefined) { + const [currentRow] = await db + .select({ status: sessions.status }) + .from(sessions) + .where(eq(sessions.id, sid)) + .limit(1); + if (currentRow) broadcastStatus = currentRow.status; + } + const broadcastPatch = + p.pinnedAt === undefined && p.status === undefined + ? p + : { + ...p, + ...(p.pinnedAt !== undefined ? { pinnedAt: updated.pinnedAt } : {}), + ...(p.status !== undefined ? { status: broadcastStatus } : {}), + ...(p.pinnedAt !== undefined && updated.pinnedAt === null ? { summary: null } : {}), + ...(p.pinnedAt !== undefined && updated.pinnedAt !== null + ? { status: broadcastStatus } + : {}), + }; + if ( + projectTargetChanged || + settingsChanged || + titleChanged || + statusChanged || + p.pinnedAt !== undefined + ) { + if (isOwnerScopeCurrent(ownerScope)) { + broadcastSessionPatched(sid, broadcastPatch, ownerScope); + } + } + // sidebar-card-mode: 会话被置顶那一刻补生成任务摘要(turn-done 路径只覆盖 + // "置顶后又跑过 turn"的会话)。动态 import 避免 localDb → maker-host 的静态 + // 模块环;fire-and-forget,模块内部自带置顶/节流守卫。 + if (p.pinnedAt !== undefined && updated.pinnedAt !== null) { + void import('../../sessionTaskSummary.js').then((m) => + m.maybeGenerateSessionTaskSummary(sid, { force: true }), + ); + } + notifyAgentIslandSessionPatch(updated.id, { + status: updated.status, + title: updated.title, + workingDir: updated.workingDir, + workspaceKind: updated.workspaceKind, + }); + scheduleWorktreeRecycleForStatusChange(sid, p.status, { ownerScope, mediaDb: db }); + notifyGhostSessionStatusChange(sid, p.status, updated.workingDir); + cleanupSessionTerminalArtifacts(sid, p.status); + compactTerminalSessionToolResults(dbClient, sid, p.status); + return updated; }; return p.workingDir === undefined ? update() : withSessionRouteLock(sid, update); }); @@ -2216,11 +2236,11 @@ export async function resumeDeletedPiSubagentCleanup(): Promise { .where(and(eq(sessions.status, 'deleted'), eq(sessions.agentKind, 'pi'))); const diskDeleted = idsFromDisk.length === 0 - ? [] - : await db - .select({ id: sessions.id }) - .from(sessions) - .where(and(inArray(sessions.id, idsFromDisk), eq(sessions.status, 'deleted'))); + ? [] + : await db + .select({ id: sessions.id }) + .from(sessions) + .where(and(inArray(sessions.id, idsFromDisk), eq(sessions.status, 'deleted'))); const ids = new Set(); for (const row of deletedPi) ids.add(row.id); @@ -2317,6 +2337,19 @@ interface SessionListRow { latestMessageRole: string | null; } +/** 用量历史专用行查询:全量读取 sessions,但跳过 sidebar 的消息预览子查询。 */ +function selectSessionUsageRows( + db: DbClient['drizzle'], + where: SQL | undefined, +): Promise> { + return db + .select({ session: sessions }) + .from(sessions) + .where(where) + .orderBy(desc(sessions.updatedAt)) + .then((rows) => rows.map((row) => row.session)); +} + /** * sessions:list 的行查询——**两段式**:CTE 先按排序取够 `cap` 个 id,主查询只对这批行算 * messageCount 与 preview。 @@ -2412,6 +2445,14 @@ function shouldBypassSessionListSingleFlight(options: unknown): boolean { ); } +function shouldUseUsageHistoryQuery(options: unknown): boolean { + return !!( + options && + typeof options === 'object' && + (options as { usageHistory?: unknown }).usageHistory === true + ); +} + function mergeSessionListRows( recentRows: readonly T[], pinnedRows: readonly T[], diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index 00c1b615ccc..66c35706ac6 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -122,7 +122,7 @@ export function UsageHistorySection(): React.JSX.Element { // 任务行与用量聚合是两条数据源: 聚合里有 token, 本地任务却可能一条都没有 // (用户删光了会话, 或列表还没加载完)。空时整张卡片不渲染, 不留空壳。 - const taskRows = useTopTokenSessions(range, history?.todayKey); + const taskRows = useTopTokenSessions(range, history?.todayKey, user?.id); const empty = isUsageHistoryEmpty(history); const handleRangeChange = (value: string): void => { diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index f136851d264..53e1a9e7b64 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -1,7 +1,7 @@ /** * UsageTaskTable — 「最耗 token 的任务」。 * - * 数据来自会话列表 (useCCSessions → sessionsStore), 不新增 IPC: + * 数据来自本地会话表的用量历史查询 (并与 useCCSessions 的实时快照合并): * - tokens: Session.totalTokenUsage * - 上下文: contextTokens / contextWindow * - 供应商: Session.providerId, 经 providerDisplayNameById 映射成展示名 @@ -11,7 +11,7 @@ * * 两处口径必须在 UI 上讲清楚, 否则会被误读: * 1. totalTokenUsage 是该任务的**生命周期累计**, 不是窗口内增量。本表筛的是 - * "近 30 天内活跃过的任务", 所以一个三个月前开始、昨天还在跑的任务会带着 + * "所选时间范围内活跃过的任务", 所以一个三个月前开始、昨天还在跑的任务会带着 * 它的全部累计出现 —— 表头 tooltip 写明这一点。 * 2. providerId 是会话**当前值**而非每轮事实, 且可为 null (跟随默认路由)。 * null 留空 (照 SessionInfoMeta 既有的"无数据不显示"), 不臆造默认供应商; @@ -20,17 +20,18 @@ * * 远程会话 (device-link) 的 token 字段可能缺失或为 0 —— 同样按"无数据不显示"过滤掉。 * - * 已知取舍: 候选集来自 sessionsStore 的列表桶 (DEFAULT_LIMIT = 1000, 按 updatedAt 降序), - * 因此"近 30 天内更新过的会话超过 1000 条"时, 被截断的高用量会话进不了 Top N。 - * 修它需要一次按 total_token_usage 排序的专用查询 —— 那是新增 IPC, 不进本版。 + * 用量历史查询跳过侧栏按 updatedAt 截断的 1000 行列表,返回完整 sessions 候选集; + * 因此 all / 较早日期范围仍能从全量历史中筛出 Top N。查询只读取 sessions 表, + * 不计算消息预览,避免把统计页变成整库 messages 扫描。 */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { formatCompactTokens, formatModelShort } from '@/lib/usageFormat'; import { useCCSessions } from '@/hooks/useCCSessions'; +import * as sessionService from '@/lib/sessionService'; import type { Session } from '@/lib/ccAgent.types'; import { formatSidebarTime } from '@/features/cc-agent/lib/formatSidebarTime'; import { useProviders } from '@/hooks/useProviders'; @@ -96,9 +97,35 @@ function activeWithinRange( export function useTopTokenSessions( range: UsageHistoryRange = '30d', todayKeyOverride?: string, + accountScopeKey?: string, ): Session[] { // 归档的任务同样消耗过 token, 统计口径不该因为用户归档而变。 - const { sessions } = useCCSessions({ includeArchived: 'all' }); + const { sessions: recentSessions } = useCCSessions({ includeArchived: 'all' }); + const [usageSessions, setUsageSessions] = useState(null); + + useEffect(() => { + let cancelled = false; + setUsageSessions(null); + void sessionService + .list(20, 'all', { usageHistory: true }) + .then((rows) => { + if (!cancelled) setUsageSessions(rows); + }) + .catch(() => { + if (!cancelled) setUsageSessions([]); + }); + return () => { + cancelled = true; + }; + }, [accountScopeKey]); + + const sessions = useMemo(() => { + if (!usageSessions) return recentSessions; + const byId = new Map(usageSessions.map((session) => [session.id, session])); + // 当前列表快照包含最近的实时 token/status patch,优先覆盖全量查询的旧行。 + for (const session of recentSessions) byId.set(session.id, session); + return [...byId.values()]; + }, [recentSessions, usageSessions]); return useMemo(() => { const now = new Date(); diff --git a/apps/desktop/src/renderer/lib/sessionService.ts b/apps/desktop/src/renderer/lib/sessionService.ts index 0352cd24c50..70746471451 100644 --- a/apps/desktop/src/renderer/lib/sessionService.ts +++ b/apps/desktop/src/renderer/lib/sessionService.ts @@ -52,6 +52,8 @@ export type SessionListOptions = { includePinned?: boolean; /** forceRefresh / status 重拉:绕开 main 侧 in-flight 合并。 */ fresh?: boolean; + /** 用量历史页读取完整会话候选集,跳过侧栏的 1000 行上限与消息预览。 */ + usageHistory?: boolean; }; export async function list( From e3448037dd30fe3f35df8860eead64648e58845a Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:54:22 +1200 Subject: [PATCH 05/30] fix(usage): preserve unbounded history windows Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../main/usage/__tests__/usageHistory.test.ts | 46 +++++++++++++++++++ apps/desktop/src/main/usage/usageHistory.ts | 12 ++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts b/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts index 679da20c851..11ae95429cf 100644 --- a/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts +++ b/apps/desktop/src/main/usage/__tests__/usageHistory.test.ts @@ -350,6 +350,52 @@ describe('readUsageHistoryWith', () => { }); }); + it('keeps all-time heatmap token rows when the model window is finite', async () => { + const oldRow = modelRow('2026-04-01', 'codex', 'gpt-5.5', actual(0), { + inputTokens: 123, + }); + const recentRow = modelRow(TODAY, 'codex', 'gpt-5.5', actual(0), { + inputTokens: 456, + }); + let requestedSince = ''; + const result = await readUsageHistoryWith( + makeDeps({ + getModelUsageSince: async (sinceDayKey) => { + requestedSince = sinceDayKey; + return [oldRow, recentRow]; + }, + }), + { days: 'all', modelDays: 30 }, + ); + + expect(requestedSince).toBe('0000-01-01'); + expect(result.days.map((row) => row.day)).toEqual(['2026-04-01', TODAY]); + expect(result.modelDaily.map((row) => row.day)).toEqual([TODAY]); + }); + + it('keeps all-time model rows when the heatmap window is finite', async () => { + const oldRow = modelRow('2026-04-01', 'codex', 'gpt-5.5', actual(0), { + inputTokens: 123, + }); + const recentRow = modelRow(TODAY, 'codex', 'gpt-5.5', actual(0), { + inputTokens: 456, + }); + let requestedSince = ''; + const result = await readUsageHistoryWith( + makeDeps({ + getModelUsageSince: async (sinceDayKey) => { + requestedSince = sinceDayKey; + return [oldRow, recentRow]; + }, + }), + { days: 30, modelDays: 'all' }, + ); + + expect(requestedSince).toBe('0000-01-01'); + expect(result.days.map((row) => row.day)).toEqual([TODAY]); + expect(result.modelDaily.map((row) => row.day)).toEqual(['2026-04-01', TODAY]); + }); + it('aggregates actual money and subscription value without double counting', async () => { const estimateAmount = regionalUsdAmount(2); const result = await readUsageHistoryWith(makeDeps({ diff --git a/apps/desktop/src/main/usage/usageHistory.ts b/apps/desktop/src/main/usage/usageHistory.ts index 2ec9132923d..d46a5710157 100644 --- a/apps/desktop/src/main/usage/usageHistory.ts +++ b/apps/desktop/src/main/usage/usageHistory.ts @@ -595,15 +595,11 @@ export async function readUsageHistoryWith( : shiftDayKey(todayKey, -(modelWindowDays - 1)); // 一次查询同时服务两个窗口: 热力图 tooltip 的每日 token (heatmap 窗口) 与 // 模型拆分聚合 (30 天窗口)。取更早的 cutoff (ISO day key 字符串可直接比较)。 - const usageRowsSince = heatmapCutoff === null && modelCutoff === null + const usageRowsSince = heatmapCutoff === null || modelCutoff === null ? '0000-01-01' - : heatmapCutoff === null - ? modelCutoff! - : modelCutoff === null - ? heatmapCutoff - : heatmapCutoff < modelCutoff - ? heatmapCutoff - : modelCutoff; + : heatmapCutoff < modelCutoff + ? heatmapCutoff + : modelCutoff; const allModelRows = (await deps.getModelUsageSince(usageRowsSince)).map((row) => ({ ...row, money: keepCompatibleMoney(row.money), From e18b21d096aff29558268f1cc9869b29e75ebb92 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:11:13 +1200 Subject: [PATCH 06/30] fix(desktop): use pill shape for usage chart controls Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx | 2 +- .../src/renderer/components/settings/usage/UsageTokenBars.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index 753c5868310..8d3d71c2fa4 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -245,7 +245,7 @@ export function UsageHeatmap({ aria-label={cell.day} aria-pressed={selectedDay === cell.day} onClick={() => onDayClick?.(cell.day)} - className="cursor-pointer rounded-[3px] border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" + className="cursor-pointer rounded-full border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" disabled={!onDayClick} >
onDayClick?.(b.day)} disabled={!onDayClick} // 列容器只负责高度与圆角裁切; 分段自上而下 = rank 降序 ("其它"在顶, 大头在底) - className="flex min-w-0 flex-1 cursor-pointer flex-col justify-end overflow-hidden rounded-[2px] border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" + className="flex min-w-0 flex-1 cursor-pointer flex-col justify-end overflow-hidden rounded-full border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" style={{ height, backgroundColor: b.segments.length === 0 ? 'var(--surface-chip)' : undefined, From 154f9dbff207cf91da70aad74fa041aadfa08947 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:25:49 +1200 Subject: [PATCH 07/30] fix(usage): guard history reads and exact-day tasks Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../__tests__/sessionsListIncludePinned.test.ts | 17 +++++++++++++++++ apps/desktop/src/main/localDb/ipc/sessions.ts | 9 +++++++-- .../settings/usage/UsageTaskTable.tsx | 8 +++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts index 324fd18c6d3..a7d995c5c5f 100644 --- a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts +++ b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts @@ -52,6 +52,7 @@ const h = vi.hoisted(() => { return { ipcHandle: vi.fn(), + assertTrustedAppRendererEvent: vi.fn(), logDebug: vi.fn(), logInfo: vi.fn(), queryResults, @@ -89,6 +90,9 @@ vi.mock('../agent-island/service.js', () => ({ vi.mock('../imageCacheStore', () => ({ removeSession: vi.fn() })); vi.mock('../messagePersistBroadcaster', () => ({ noteSessionClearBoundary: vi.fn() })); vi.mock('../sessionTaskSummary.js', () => ({ backfillPinnedSessionSummaries: vi.fn() })); +vi.mock('../security/trustedAppRenderer.js', () => ({ + assertTrustedAppRendererEvent: h.assertTrustedAppRendererEvent, +})); import { registerSessionIpc } from '../localDb/ipc/sessions.js'; @@ -208,6 +212,19 @@ describe('local-db:sessions:list includePinned', () => { expect(h.fakeDb.select).toHaveBeenCalledTimes(1); expect(h.listQuery).not.toHaveBeenCalled(); expect(h.queryResults).toHaveLength(0); + expect(h.assertTrustedAppRendererEvent).toHaveBeenCalledWith({}); + }); + + it('rejects an untrusted renderer before running the unbounded usage-history query', async () => { + const handler = sessionsListHandler(); + h.assertTrustedAppRendererEvent.mockImplementationOnce(() => { + throw new Error('[PERMISSION_DENIED]'); + }); + + await expect(handler({}, 20, 'all', { usageHistory: true })).rejects.toThrow( + '[PERMISSION_DENIED]', + ); + expect(h.fakeDb.select).not.toHaveBeenCalled(); }); it('also includes pinned rows for the all-status bucket used by mobile detail filters', async () => { diff --git a/apps/desktop/src/main/localDb/ipc/sessions.ts b/apps/desktop/src/main/localDb/ipc/sessions.ts index a66a9182c4b..c8a4b113573 100644 --- a/apps/desktop/src/main/localDb/ipc/sessions.ts +++ b/apps/desktop/src/main/localDb/ipc/sessions.ts @@ -1070,8 +1070,14 @@ export function registerSessionIpc( ); ipcMain.handle( 'local-db:sessions:list', - async (_e, limit: unknown, status: unknown, options: unknown) => { + async (event, limit: unknown, status: unknown, options: unknown) => { const startedAt = performance.now(); + const usageHistory = shouldUseUsageHistoryQuery(options); + // The usage-history branch is an unbounded privileged read. Keep the + // legacy capped list available to device-link's synthetic event, but do + // not let an untrusted renderer turn the new branch into a full-table + // session disclosure. + if (usageHistory) assertTrustedAppRendererEvent(event); const snapshot = readCurrentDbClientSnapshot(); const db = snapshot?.client.drizzle ?? getDbClient().drizzle; const userId = snapshot?.userId ?? readCurrentDbClientUserId(); @@ -1079,7 +1085,6 @@ export function registerSessionIpc( // sidebar-card-mode: 首次 list(db 必然 ready)触发一次置顶摘要回填—— // 老置顶会话没有 turn-done 触发点。模块内部 once 守卫 + 串行 + swallow。 void import('../../sessionTaskSummary.js').then((m) => m.backfillPinnedSessionSummaries()); - const usageHistory = shouldUseUsageHistoryQuery(options); const cap = clampLimit(limit, 20); const includePinned = shouldIncludePinnedSessions(options); const fresh = shouldBypassSessionListSingleFlight(options); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 53e1a9e7b64..41ee92f68be 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -38,7 +38,7 @@ import { useProviders } from '@/hooks/useProviders'; import { usageRankColor } from '@/components/new-chat/usagePalette'; import { providerDisplayNameById } from '@/lib/providerDisplayName'; import { formatUsagePercent } from './formatUsagePercent'; -import { isUsageDayInRange, type UsageHistoryRange } from './usageHistoryStats'; +import { isUsageDayInRange, usageRangeDay, type UsageHistoryRange } from './usageHistoryStats'; /** 展示条数 —— 与"最耗"的语义匹配, 不做成完整列表 (那是任务侧栏的事)。 */ const TOP_TASKS = 8; @@ -128,6 +128,12 @@ export function useTopTokenSessions( }, [recentSessions, usageSessions]); return useMemo(() => { + // Sessions retain only their latest activity timestamp, so they cannot + // prove historical membership for an exact calendar day. Hide this + // aggregate rather than presenting a task as active on a day it may not + // have used. The day-level charts and agent/model tables use dated usage + // records and remain exact. + if (usageRangeDay(range)) return []; const now = new Date(); const todayKey = todayKeyOverride ?? From 68be22426743e823fb4c666b4d08bf22318129f4 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:24:08 +1200 Subject: [PATCH 08/30] fix(usage): address history review feedback Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../__tests__/authStatusUsageHandlers.test.ts | 47 +++++++++++++++---- .../src/main/maker-ipc/usageHandlers.ts | 9 +++- .../settings/usage/UsageHistorySection.tsx | 9 +++- .../settings/usage/UsageStatRow.tsx | 13 +++-- .../settings/usage/UsageTaskTable.tsx | 13 ++++- .../src/renderer/i18n/locales/en/common.json | 2 +- .../src/renderer/i18n/locales/ja/common.json | 2 +- .../src/renderer/i18n/locales/ko/common.json | 2 +- .../renderer/i18n/locales/zh-CN/common.json | 2 +- .../renderer/i18n/locales/zh-TW/common.json | 2 +- 10 files changed, 77 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/main/maker-ipc/__tests__/authStatusUsageHandlers.test.ts b/apps/desktop/src/main/maker-ipc/__tests__/authStatusUsageHandlers.test.ts index be8fb61d5ef..ca7a6e6a657 100644 --- a/apps/desktop/src/main/maker-ipc/__tests__/authStatusUsageHandlers.test.ts +++ b/apps/desktop/src/main/maker-ipc/__tests__/authStatusUsageHandlers.test.ts @@ -1198,6 +1198,7 @@ describe('maker usage IPC handlers', () => { triggerClaudeAccountUsageRefresh: vi.fn(), readModelPricing: vi.fn(), readReferenceModelPricing: vi.fn(() => ({})), + assertTrustedSender: vi.fn(), readUsageHistory: vi.fn().mockResolvedValue(emptyHistory), emptyUsageHistory: vi.fn(() => emptyHistory), ...over, @@ -1219,14 +1220,19 @@ describe('maker usage IPC handlers', () => { it('rejects SuperGrok usage reads from untrusted senders', async () => { const harness = new IpcHarness(); - const readXaiSubscriptionUsageSnapshot = vi.fn().mockResolvedValue({ planLabel: 'SuperGrok Heavy' }); + const readXaiSubscriptionUsageSnapshot = vi + .fn() + .mockResolvedValue({ planLabel: 'SuperGrok Heavy' }); const assertTrustedSender = vi.fn(() => { throw new Error('[PERMISSION_DENIED] untrusted'); }); - registerMakerUsageHandlers(harness, makeUsageDeps({ - readXaiSubscriptionUsageSnapshot, - assertTrustedSender, - })); + registerMakerUsageHandlers( + harness, + makeUsageDeps({ + readXaiSubscriptionUsageSnapshot, + assertTrustedSender, + }), + ); await expect(harness.invoke(MAKER_INVOKE.USAGE_XAI_SUBSCRIPTION)).rejects.toThrow( /PERMISSION_DENIED/, @@ -1239,10 +1245,13 @@ describe('maker usage IPC handlers', () => { const snapshot = { planLabel: 'SuperGrok Heavy', creditUsagePercent: 2 }; const readXaiSubscriptionUsageSnapshot = vi.fn().mockResolvedValue(snapshot); const assertTrustedSender = vi.fn(); - registerMakerUsageHandlers(harness, makeUsageDeps({ - readXaiSubscriptionUsageSnapshot, - assertTrustedSender, - })); + registerMakerUsageHandlers( + harness, + makeUsageDeps({ + readXaiSubscriptionUsageSnapshot, + assertTrustedSender, + }), + ); await expect(harness.invoke(MAKER_INVOKE.USAGE_XAI_SUBSCRIPTION)).resolves.toEqual(snapshot); expect(assertTrustedSender).toHaveBeenCalledOnce(); @@ -1486,6 +1495,26 @@ describe('maker usage IPC handlers', () => { expect(readUsageHistory).toHaveBeenLastCalledWith(undefined); }); + it('rejects usage-history reads from an untrusted sender before parsing or querying', async () => { + const harness = new IpcHarness(); + const readUsageHistory = vi.fn().mockResolvedValue(emptyHistory); + const assertTrustedSender = vi.fn(() => { + throw new Error('[PERMISSION_DENIED] untrusted'); + }); + + registerMakerUsageHandlers(harness, makeUsageDeps({ readUsageHistory, assertTrustedSender })); + + await expect( + harness.invoke(MAKER_INVOKE.USAGE_HISTORY, { + days: 'all', + modelDays: 'all', + forceRefresh: true, + }), + ).rejects.toThrow(/PERMISSION_DENIED/); + expect(assertTrustedSender).toHaveBeenCalledOnce(); + expect(readUsageHistory).not.toHaveBeenCalled(); + }); + it('falls back to empty history payload when the read throws', async () => { const harness = new IpcHarness(); const readUsageHistory = vi.fn().mockRejectedValue(new Error('db gone')); diff --git a/apps/desktop/src/main/maker-ipc/usageHandlers.ts b/apps/desktop/src/main/maker-ipc/usageHandlers.ts index 645a62a35d5..d8688726a84 100644 --- a/apps/desktop/src/main/maker-ipc/usageHandlers.ts +++ b/apps/desktop/src/main/maker-ipc/usageHandlers.ts @@ -73,7 +73,11 @@ export interface MakerUsageHandlerDeps { consumeCodexRateLimitReset(idempotencyKey: string): Promise; readClaudeSubscriptionUsageSnapshot(): Promise; readXaiSubscriptionUsageSnapshot(): Promise; - assertTrustedSender?(event: unknown): void; + /** + * 用量历史会读取完整的 sessions 表,必须在任何参数解析或 DB 查询前确认 + * 请求来自受信任的主页面 renderer。 + */ + assertTrustedSender(event: unknown): void; readClaudeAccountUsageSnapshot(): ClaudeAccountUsageSnapshot | null; triggerClaudeAccountUsageRefresh(force: boolean): Promise; readModelPricing(): Promise; @@ -158,7 +162,8 @@ export function registerMakerUsageHandlers( // 用量历史聚合 (首页仪表盘) — 查询型 handler, DB 出错回退空 payload 让 // renderer 正常渲染空态 (与同文件其它 usage 读取的 fallback-data 口径一致)。 - registry.handle(MAKER_INVOKE.USAGE_HISTORY, async (_e, opts: unknown) => { + registry.handle(MAKER_INVOKE.USAGE_HISTORY, async (event, opts: unknown) => { + deps.assertTrustedSender(event); const raw = (opts ?? {}) as { days?: unknown; modelDays?: unknown; forceRefresh?: unknown }; const days = raw.days === 'all' diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index 66c35706ac6..11dd0861ae2 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -201,7 +201,12 @@ export function UsageHistorySection(): React.JSX.Element { ) : ( <> - +
- +
)} diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx index 36dc612dafa..b555bbb59a1 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx @@ -42,19 +42,24 @@ export function UsageStatRow({ summary, rangeLabel, todayLabel, + hideToday = false, }: { summary: UsageSummary; rangeLabel: string; todayLabel?: string; + /** Exact-day drilldown already has the same value in the range card. */ + hideToday?: boolean; }): React.JSX.Element { const { t } = useTranslation(); return (
- 0 ? formatCompactTokens(summary.todayTokens) : UNKNOWN_VALUE} - label={todayLabel ?? t('usageHistory.stats.todayTokens')} - /> + {!hideToday ? ( + 0 ? formatCompactTokens(summary.todayTokens) : UNKNOWN_VALUE} + label={todayLabel ?? t('usageHistory.stats.todayTokens')} + /> + ) : null} 0 diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 41ee92f68be..2a6d03a80fc 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -150,7 +150,13 @@ export function useTopTokenSessions( }, [range, sessions, todayKeyOverride]); } -export function UsageTaskTable({ rows }: { rows: Session[] }): React.JSX.Element { +export function UsageTaskTable({ + rows, + rangeLabel, +}: { + rows: Session[]; + rangeLabel: string; +}): React.JSX.Element { const { t } = useTranslation(); const { providers } = useProviders(); @@ -165,7 +171,10 @@ export function UsageTaskTable({ rows }: { rows: Session[] }): React.JSX.Element {t('usageHistory.tasks.col.provider')} - + {t('usageHistory.tasks.col.tokens')} {t('usageHistory.tasks.col.context')} diff --git a/apps/desktop/src/renderer/i18n/locales/en/common.json b/apps/desktop/src/renderer/i18n/locales/en/common.json index 4a481463904..742e8c74d10 100644 --- a/apps/desktop/src/renderer/i18n/locales/en/common.json +++ b/apps/desktop/src/renderer/i18n/locales/en/common.json @@ -5866,7 +5866,7 @@ "lastActive": "Last active" }, "providerTooltip": "The provider currently selected for the session; blank when none is pinned.", - "tokensTooltip": "The session's lifetime total, not the tokens spent within the last 30 days." + "tokensTooltip": "The session's lifetime total; {{range}} only filters active sessions and is not an incremental total." } }, "usageDashboard": { diff --git a/apps/desktop/src/renderer/i18n/locales/ja/common.json b/apps/desktop/src/renderer/i18n/locales/ja/common.json index 0862c199d32..fdb51486269 100644 --- a/apps/desktop/src/renderer/i18n/locales/ja/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ja/common.json @@ -5864,7 +5864,7 @@ "lastActive": "最終アクティブ" }, "providerTooltip": "セッションで現在選択されているプロバイダー。未指定の場合は空欄です。", - "tokensTooltip": "セッションの累計値であり、直近 30 日の増分ではありません。" + "tokensTooltip": "セッションの累計値です。{{range}}はアクティブなセッションの絞り込みにのみ使われ、期間内の増分ではありません。" } }, "usageDashboard": { diff --git a/apps/desktop/src/renderer/i18n/locales/ko/common.json b/apps/desktop/src/renderer/i18n/locales/ko/common.json index d97421dc0ff..153d17e8b2b 100644 --- a/apps/desktop/src/renderer/i18n/locales/ko/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ko/common.json @@ -5864,7 +5864,7 @@ "lastActive": "마지막 활동" }, "providerTooltip": "세션에 현재 선택된 제공자입니다. 지정하지 않은 경우 비워 둡니다.", - "tokensTooltip": "해당 세션의 누적 값이며 최근 30일 증가분이 아닙니다." + "tokensTooltip": "해당 세션의 누적 값입니다. {{range}}은(는) 활성 세션을 필터링하는 데만 사용되며 해당 기간의 증가분이 아닙니다." } }, "usageDashboard": { diff --git a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json index 37403839007..69450e3aa2e 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json @@ -5864,7 +5864,7 @@ "lastActive": "最后活跃" }, "providerTooltip": "取该任务当前选定的供应商;未指定时留空。", - "tokensTooltip": "该任务的生命周期累计,不是近 30 天内的增量。" + "tokensTooltip": "该任务的生命周期累计;{{range}}仅用于筛选活跃任务,不表示该范围内的增量。" } }, "usageDashboard": { diff --git a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json index 2a943820d9e..93f1ae81523 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json @@ -5864,7 +5864,7 @@ "lastActive": "最後活躍" }, "providerTooltip": "取該任務目前選定的供應商;未指定時留空。", - "tokensTooltip": "該任務的生命週期累計,不是近 30 天內的增量。" + "tokensTooltip": "該任務的生命週期累計;{{range}}僅用於篩選活躍任務,不代表該範圍內的增量。" } }, "usageDashboard": { From cfe93b6522658c33681115a72a18a287f407f8b9 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:52:27 +1200 Subject: [PATCH 09/30] fix(usage): preserve activity and heatmap windows Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../components/new-chat/UsageHeatmap.tsx | 21 ++++++++++++++++++- .../__tests__/usageHeatmapMetric.test.tsx | 15 ++++++++++++- .../settings/usage/UsageTaskTable.tsx | 11 ++++++++-- .../usage/__tests__/usageHistoryStats.test.ts | 15 +++++++++++++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index 8d3d71c2fa4..530c2e75ffc 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -65,19 +65,37 @@ function fitWeeksForWidth(width: number): number { return Math.max(1, Math.floor((width + GAP_PX) / (CELL_PX + GAP_PX))); } +/** + * 周网格按周日对齐时,当前周的未来占位也会占用格子。返回覆盖完整 + * `windowDays` 历史的最小周数,避免周日等锚点把窗口前端截掉。 + */ +export function heatmapWeeksForWindow(todayKey: string, windowDays: number): number { + const today = parseDayKey(todayKey); + if (!todayKey || Number.isNaN(today.getTime()) || !(windowDays > 0)) return MIN_HEATMAP_WEEKS; + const windowStart = new Date(today); + windowStart.setDate(windowStart.getDate() - Math.ceil(windowDays) + 1); + return Math.floor(calendarDayDistance(startOfWeek(windowStart), startOfWeek(today)) / 7) + 1; +} + /** 至少保留 20 周;有更早数据且容器放得下时,尽量展示更多历史。 */ export function resolveHeatmapWeeks({ days, todayKey, availableWidth, minimumWeeks = MIN_HEATMAP_WEEKS, + windowDays, }: { days: Array<{ day: string }>; todayKey: string; availableWidth: number; minimumWeeks?: number; + windowDays?: number; }): number { - const minWeeks = Math.max(MIN_HEATMAP_WEEKS, Math.ceil(minimumWeeks)); + const minWeeks = Math.max( + MIN_HEATMAP_WEEKS, + Math.ceil(minimumWeeks), + windowDays === undefined ? 0 : heatmapWeeksForWindow(todayKey, windowDays), + ); const today = parseDayKey(todayKey); if (!todayKey || Number.isNaN(today.getTime())) return minWeeks; @@ -150,6 +168,7 @@ export function UsageHeatmap({ todayKey, availableWidth, minimumWeeks, + windowDays, }); useLayoutEffect(() => { diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index 909ac409b30..4aa947f8679 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { resolveHeatmapWeeks, UsageHeatmap } from '../UsageHeatmap'; +import { heatmapWeeksForWindow, resolveHeatmapWeeks, UsageHeatmap } from '../UsageHeatmap'; vi.mock('react-i18next', () => ({ useTranslation: () => ({ @@ -73,6 +73,19 @@ describe('UsageHeatmap metric', () => { ).toBe(60); }); + it('周日锚点也覆盖完整的 windowDays 历史,不把未来占位挤掉最早日期', () => { + expect(heatmapWeeksForWindow('2026-08-22', 140)).toBe(20); + expect(heatmapWeeksForWindow('2026-08-23', 140)).toBe(21); + expect( + resolveHeatmapWeeks({ + days: [{ day: '2026-08-23' }], + todayKey: '2026-08-23', + availableWidth: 100, + windowDays: 140, + }), + ).toBe(21); + }); + it('月份标签保持单行,避免最右侧月份换行', () => { const { container } = render(); const monthLabel = [...container.querySelectorAll('span')].find((node) => diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 2a6d03a80fc..52b477984dc 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -90,6 +90,14 @@ function activeWithinRange( return day !== null && isUsageDayInRange(day, todayKey, range); } +/** + * 用量范围只应按用户真正发送消息的时间筛选。updatedAt 还会被重命名、归档、 + * 置顶等元数据更新推进;只有存量行没有 userSendAt 时才回退到 updatedAt。 + */ +export function usageActivityIso(session: Pick): string { + return session.userSendAt ?? session.updatedAt; +} + /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 * 组件内部返回 null 会留下一张只有标题、正文全空的卡片 (会话被删光 / 列表首次加载中)。 @@ -142,8 +150,7 @@ export function useTopTokenSessions( .filter( (session) => session.totalTokenUsage > 0 && - (activeWithinRange(session.updatedAt, range, todayKey) || - activeWithinRange(session.userSendAt, range, todayKey)), + activeWithinRange(usageActivityIso(session), range, todayKey), ) .sort((a, b) => b.totalTokenUsage - a.totalTokenUsage) .slice(0, TOP_TASKS); diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index 4253227bc4c..62da42c5385 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -12,6 +12,7 @@ import { isUsageHistoryEmpty, toUsageDays, } from '../usageHistoryStats'; +import { usageActivityIso } from '../UsageTaskTable'; const zeroMoney = { amount: 0, @@ -327,3 +328,17 @@ describe('filterUsageHistoryPayload', () => { expect(result?.days.map((row) => row.day)).toEqual(source.days.map((row) => row.day)); }); }); + +describe('usageActivityIso', () => { + it('忽略元数据更新时间,优先使用用户发送时间;存量行才回退 updatedAt', () => { + expect( + usageActivityIso({ + userSendAt: '2026-08-20T10:00:00.000Z', + updatedAt: '2026-08-22T10:00:00.000Z', + }), + ).toBe('2026-08-20T10:00:00.000Z'); + expect(usageActivityIso({ userSendAt: null, updatedAt: '2026-08-22T10:00:00.000Z' })).toBe( + '2026-08-22T10:00:00.000Z', + ); + }); +}); From 334f11670b5e67b458db2b4ffcbe3b1ced853d5f Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:17:46 +1200 Subject: [PATCH 10/30] fix(usage): hide duplicate today metric Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../renderer/components/settings/usage/UsageHistorySection.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index 11dd0861ae2..e6983536ecc 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -100,6 +100,7 @@ export function UsageHistorySection(): React.JSX.Element { const todayLabel = selectedDay ? t('usageHistory.stats.todayTokensInRange', { range: rangeLabel }) : t('usageHistory.stats.todayTokens'); + const hideToday = selectedDay !== null || range === 'today'; const filteredHistory = useMemo( () => filterUsageHistoryPayload(history, range), @@ -205,7 +206,7 @@ export function UsageHistorySection(): React.JSX.Element { summary={summary} rangeLabel={rangeLabel} todayLabel={todayLabel} - hideToday={selectedDay !== null} + hideToday={hideToday} /> From 38f17af8bbc9d66ee4fd3705f133959a89ed79be Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:20:43 +1200 Subject: [PATCH 11/30] fix(usage): hide today task table Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../components/settings/usage/UsageTaskTable.tsx | 10 +++++++++- .../settings/usage/__tests__/usageHistoryStats.test.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 52b477984dc..80867c104e0 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -98,6 +98,14 @@ export function usageActivityIso(session: Pick = {}): UsageHistoryPayload { }; } +describe('shouldHideUsageTaskTable', () => { + it('单日范围(包括 today)隐藏无法精确归因的任务表', () => { + expect(shouldHideUsageTaskTable('today')).toBe(true); + expect(shouldHideUsageTaskTable('day:2026-08-20')).toBe(true); + expect(shouldHideUsageTaskTable('7d')).toBe(false); + }); +}); + describe('cacheHitRate', () => { it('输出 token 不进分母 (与逐轮卡片同一公式)', () => { expect( From f1877e75fc59f7b329e8f62ac815ed290ac9c401 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:33:57 +1200 Subject: [PATCH 12/30] fix(usage): close exact-range and loading gaps Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../settings/usage/UsageBreakdownTables.tsx | 16 +++++++----- .../settings/usage/UsageHistorySection.tsx | 10 +++++-- .../settings/usage/UsageTaskTable.tsx | 26 +++++++++++++------ .../usage/__tests__/usageHistoryStats.test.ts | 9 +++++++ .../settings/usage/usageHistoryStats.ts | 5 ++++ 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx index 61cccc4343a..5af7596e767 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx @@ -68,10 +68,12 @@ export function UsageAgentTable({ rows, rangeLabel, todayLabel, + hideToday = false, }: { rows: AgentTokenRow[]; rangeLabel: string; todayLabel?: string; + hideToday?: boolean; }): React.JSX.Element { const { t } = useTranslation(); const total = rows.reduce((sum, row) => sum + row.tokens, 0); @@ -104,9 +106,9 @@ export function UsageAgentTable({ {t('usageHistory.byAgent.col.totalInRange', { range: rangeLabel })} {t('usageHistory.byAgent.col.share')} - - {todayLabel ?? t('usageHistory.byAgent.col.today')} - + {!hideToday ? ( + {todayLabel ?? t('usageHistory.byAgent.col.today')} + ) : null} {t('usageHistory.byAgent.col.hitRate')} @@ -128,9 +130,11 @@ export function UsageAgentTable({ {formatCompactTokens(row.tokens)} - - {row.todayTokens > 0 ? formatCompactTokens(row.todayTokens) : UNKNOWN_VALUE} - + {!hideToday ? ( + + {row.todayTokens > 0 ? formatCompactTokens(row.todayTokens) : UNKNOWN_VALUE} + + ) : null} {row.modelCount} diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index e6983536ecc..b20fb871df0 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -37,6 +37,7 @@ import { buildSummary, chartUsageHistoryPayload, filterUsageHistoryPayload, + isUsageHistorySingleDay, isUsageHistoryEmpty, usageRangeDay, type UsageHistoryRange, @@ -100,7 +101,7 @@ export function UsageHistorySection(): React.JSX.Element { const todayLabel = selectedDay ? t('usageHistory.stats.todayTokensInRange', { range: rangeLabel }) : t('usageHistory.stats.todayTokens'); - const hideToday = selectedDay !== null || range === 'today'; + const hideToday = isUsageHistorySingleDay(range); const filteredHistory = useMemo( () => filterUsageHistoryPayload(history, range), @@ -238,7 +239,12 @@ export function UsageHistorySection(): React.JSX.Element { {agentRows.length > 0 && (
- +
)} diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 80867c104e0..172e64df2d5 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -38,7 +38,11 @@ import { useProviders } from '@/hooks/useProviders'; import { usageRankColor } from '@/components/new-chat/usagePalette'; import { providerDisplayNameById } from '@/lib/providerDisplayName'; import { formatUsagePercent } from './formatUsagePercent'; -import { isUsageDayInRange, usageRangeDay, type UsageHistoryRange } from './usageHistoryStats'; +import { + isUsageDayInRange, + isUsageHistorySingleDay, + type UsageHistoryRange, +} from './usageHistoryStats'; /** 展示条数 —— 与"最耗"的语义匹配, 不做成完整列表 (那是任务侧栏的事)。 */ const TOP_TASKS = 8; @@ -103,9 +107,12 @@ export function usageActivityIso(session: Pick(null); + const [usageSessions, setUsageSessions] = useState({ status: 'loading' }); useEffect(() => { let cancelled = false; - setUsageSessions(null); + setUsageSessions({ status: 'loading' }); void sessionService .list(20, 'all', { usageHistory: true }) .then((rows) => { - if (!cancelled) setUsageSessions(rows); + if (!cancelled) setUsageSessions({ status: 'ready', sessions: rows }); }) .catch(() => { - if (!cancelled) setUsageSessions([]); + if (!cancelled) setUsageSessions({ status: 'error' }); }); return () => { cancelled = true; @@ -136,8 +143,11 @@ export function useTopTokenSessions( }, [accountScopeKey]); const sessions = useMemo(() => { - if (!usageSessions) return recentSessions; - const byId = new Map(usageSessions.map((session) => [session.id, session])); + // The sidebar snapshot is capped at 1000 rows and cannot stand in for the + // full usage-history query. Hide the aggregate until that query succeeds; + // showing the truncated or failed fallback would mislabel the ranking. + if (usageSessions.status !== 'ready') return []; + const byId = new Map(usageSessions.sessions.map((session) => [session.id, session])); // 当前列表快照包含最近的实时 token/status patch,优先覆盖全量查询的旧行。 for (const session of recentSessions) byId.set(session.id, session); return [...byId.values()]; diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index a6f5abfda3f..63409844e72 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -9,6 +9,7 @@ import { chartUsageHistoryPayload, computeTokenStreak, filterUsageHistoryPayload, + isUsageHistorySingleDay, isUsageHistoryEmpty, toUsageDays, } from '../usageHistoryStats'; @@ -64,6 +65,14 @@ describe('shouldHideUsageTaskTable', () => { }); }); +describe('isUsageHistorySingleDay', () => { + it('识别 today 与日期钻取范围', () => { + expect(isUsageHistorySingleDay('today')).toBe(true); + expect(isUsageHistorySingleDay('day:2026-08-20')).toBe(true); + expect(isUsageHistorySingleDay('7d')).toBe(false); + }); +}); + describe('cacheHitRate', () => { it('输出 token 不进分母 (与逐轮卡片同一公式)', () => { expect( diff --git a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts index b7aaa4463e3..e2fd1617b2f 100644 --- a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts +++ b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts @@ -81,6 +81,11 @@ export function usageRangeDay(range: UsageHistoryRange): string | null { return range.startsWith('day:') ? range.slice(4) : null; } +/** Exact-day ranges cannot provide a distinct "today" comparison column. */ +export function isUsageHistorySingleDay(range: UsageHistoryRange): boolean { + return range === 'today' || usageRangeDay(range) !== null; +} + export function isUsageDayInRange( day: string, todayKey: string, From 4d344e3f35441d5c89b05b9d02e95fdee5b91f96 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:55:52 +1200 Subject: [PATCH 13/30] fix(usage): use pill cells for clickable heatmap Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../components/new-chat/UsageHeatmap.tsx | 2 +- .../__tests__/usageHeatmapMetric.test.tsx | 17 +++++++++++++++++ docs/design-rules/design-inventory.md | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index 530c2e75ffc..dce958b7230 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -283,7 +283,7 @@ export function UsageHeatmap({ : '' }` } - className="rounded-[3px]" + className={onDayClick ? 'rounded-full' : 'rounded-[3px]'} style={{ width: CELL_PX, height: CELL_PX, diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index 4aa947f8679..cd2163e627c 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -162,4 +162,21 @@ describe('UsageHeatmap metric', () => { fireEvent.click(getByRole('button', { name: '2026-08-21' })); expect(onDayClick).toHaveBeenCalledWith('2026-08-21'); }); + + it('可点击日期格的可见彩色表面使用 pill 圆角', () => { + const onDayClick = vi.fn(); + const { getByRole } = render( + , + ); + + expect(getByRole('button', { name: '2026-08-21' }).firstElementChild?.className).toContain( + 'rounded-full', + ); + }); }); diff --git a/docs/design-rules/design-inventory.md b/docs/design-rules/design-inventory.md index 0ae92454d0b..6afca0fcd17 100644 --- a/docs/design-rules/design-inventory.md +++ b/docs/design-rules/design-inventory.md @@ -20,9 +20,9 @@ | `desktop.auth.login` | desktop | 登录页 | hash `/login`(GuestRoute → LoginPage) | LoginBrandStage, LoginCaptchaOverlay, LoginControls, LoginPage | apps/desktop/src/renderer/components/login/LoginBrandStage.tsx, apps/desktop/src/renderer/components/login/LoginCaptchaOverlay.tsx, apps/desktop/src/renderer/components/login/LoginControls.tsx, apps/desktop/src/renderer/components/login/LoginPage.tsx, apps/desktop/src/renderer/components/login/loginDesignTokens.ts | 59 | 0 | 21 | | `desktop.chat.files` | desktop | 工作目录文件浏览 | hash `/cc-agent/files/:sessionId`(WorkdirBrowseRoute) | FileBodyView, FileTreeView, WorkdirBrowseRoute | apps/desktop/src/renderer/features/cc-agent/workdir-browse/DocSearchBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/DrawioPreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileBodyView.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileFilterInput.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileTabsBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FileTreeView.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/FilterResultList.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/ImagePreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/OpenInSystemActions.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/PdfPreview.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/SessionTabsBar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/UnrenderablePlaceholder.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/WorkdirBrowseRoute.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/WorkdirBrowseSidebar.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useChatRailCollapsed.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useChatRailResize.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useConfirmSwitchAwayIfDirty.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useDelayedFlag.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useFileContent.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useFileTree.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useOpenTabs.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useProjectFileList.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/hooks/useRevealFileInTree.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/activeFileBodyHandle.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/docModeSwitchProjects.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/drawioExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/expandedStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileContentCache.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileIcon.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileMeta.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileScrollStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/fileSelectionParams.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/filterFiles.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/imageExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/markdownAutosave.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/openTabsStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/pdfExt.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/lib/selectedFileStore.ts, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchInput.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchPanel.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/SearchResults.tsx, apps/desktop/src/renderer/features/cc-agent/workdir-browse/search/hooks/useProjectSearch.ts | 22 | 7 | 64 | | `desktop.chat.new-draft` | desktop | 新建任务草稿页 | hash `/cc-agent/new`(NewMakerDraftRoute) | NewMakerDraftRoute | apps/desktop/src/renderer/features/cc-agent/NewMakerDraftRoute.tsx | 15 | 0 | 2 | -| `desktop.chat.orca-workflow` | desktop | Orca 协同工作台 | hash `/cc-agent/orca/:sessionId`(OrcaWorkflowRoute) | CCAgentSessionView, OrcaSplitView, OrcaWorkerPanel, OrcaWorkflowRoute | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaSplitView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkerPanel.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkflowRoute.tsx | 266 | 56 | 412 | +| `desktop.chat.orca-workflow` | desktop | Orca 协同工作台 | hash `/cc-agent/orca/:sessionId`(OrcaWorkflowRoute) | CCAgentSessionView, OrcaSplitView, OrcaWorkerPanel, OrcaWorkflowRoute | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaSplitView.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkerPanel.tsx, apps/desktop/src/renderer/features/cc-agent/OrcaWorkflowRoute.tsx | 266 | 56 | 413 | | `desktop.chat.scheduled` | desktop | 调度任务列表 | hash `/cc-agent/scheduled`(SchedulerPage) | SchedulerPage | apps/desktop/src/renderer/features/scheduler/SchedulerPage.tsx, apps/desktop/src/renderer/features/scheduler/components/AgentMark.tsx, apps/desktop/src/renderer/features/scheduler/components/BoundSessionCard.tsx, apps/desktop/src/renderer/features/scheduler/components/ProjectAutomationNotifyBridge.tsx, apps/desktop/src/renderer/features/scheduler/components/RunHistoryCard.tsx, apps/desktop/src/renderer/features/scheduler/components/RunHistoryPane.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleChips.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleFormDialog.tsx, apps/desktop/src/renderer/features/scheduler/components/ScheduleStatusChip.tsx, apps/desktop/src/renderer/features/scheduler/components/ScriptCapabilityMultiSelect.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListCell.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListFilterPopover.tsx, apps/desktop/src/renderer/features/scheduler/components/TaskListPane.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateCard.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateGallery.tsx, apps/desktop/src/renderer/features/scheduler/components/TemplateParamForm.tsx, apps/desktop/src/renderer/features/scheduler/hooks/useDeleteScheduleWithSessions.tsx, apps/desktop/src/renderer/features/scheduler/hooks/useRunNowBusyGuard.ts, apps/desktop/src/renderer/features/scheduler/hooks/useRuns.ts, apps/desktop/src/renderer/features/scheduler/hooks/useScheduleForm.ts, apps/desktop/src/renderer/features/scheduler/hooks/useScheduleUnreadRunCounts.ts, apps/desktop/src/renderer/features/scheduler/hooks/useSchedules.ts, apps/desktop/src/renderer/features/scheduler/hooks/useSessionReferences.ts, apps/desktop/src/renderer/features/scheduler/hooks/useTemplates.ts, apps/desktop/src/renderer/features/scheduler/index.ts, apps/desktop/src/renderer/features/scheduler/lib/cronCodexPreset.ts, apps/desktop/src/renderer/features/scheduler/lib/cronToHuman.ts, apps/desktop/src/renderer/features/scheduler/lib/formatters.ts, apps/desktop/src/renderer/features/scheduler/lib/pluginScheduleCreateIntent.ts, apps/desktop/src/renderer/features/scheduler/lib/projectAutomationConfig.ts, apps/desktop/src/renderer/features/scheduler/lib/runHistoryGrouping.ts, apps/desktop/src/renderer/features/scheduler/lib/runUnread.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleFormLogic.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleRunReadSync.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleSessionBinding.ts, apps/desktop/src/renderer/features/scheduler/lib/scheduleSidebarIndexRuns.ts, apps/desktop/src/renderer/features/scheduler/lib/schedulesStore.ts, apps/desktop/src/renderer/features/scheduler/lib/statusFilter.ts, apps/desktop/src/renderer/features/scheduler/lib/statusFilterStorage.ts, apps/desktop/src/renderer/features/scheduler/lib/templateLocalization.ts, apps/desktop/src/renderer/features/scheduler/lib/usageLimitScheduleCreateIntent.ts | 44 | 0 | 118 | -| `desktop.chat.session` | desktop | 会话工作台 | hash `/cc-agent/:sessionId`;副窗经 `/cc-agent/boot` 解析后落到同一会话视图 | AssistantMessage, CCAgentFeatureLayout, CCAgentSessionView, ChatInput, PermissionPrompt | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx | 266 | 56 | 411 | +| `desktop.chat.session` | desktop | 会话工作台 | hash `/cc-agent/:sessionId`;副窗经 `/cc-agent/boot` 解析后落到同一会话视图 | AssistantMessage, CCAgentFeatureLayout, CCAgentSessionView, ChatInput, PermissionPrompt | apps/desktop/src/renderer/components/chat/AgentActionRow.tsx, apps/desktop/src/renderer/components/chat/AgentActionsBlock.tsx, apps/desktop/src/renderer/components/chat/AgentTaskCard.tsx, apps/desktop/src/renderer/components/chat/AskUserQuestionBubble.tsx, apps/desktop/src/renderer/components/chat/AssistantMessage.tsx, apps/desktop/src/renderer/components/chat/AutomationOriginBadge.tsx, apps/desktop/src/renderer/components/chat/ChatAudioCard.tsx, apps/desktop/src/renderer/components/chat/ChatDisplaySnapshotContext.tsx, apps/desktop/src/renderer/components/chat/ChatImageView.tsx, apps/desktop/src/renderer/components/chat/ChatSessionFileContext.tsx, apps/desktop/src/renderer/components/chat/ChatSoundEffectCard.tsx, apps/desktop/src/renderer/components/chat/ChatVideoView.tsx, apps/desktop/src/renderer/components/chat/CopyAsImageBlock.tsx, apps/desktop/src/renderer/components/chat/CredentialSwitchWaitBanner.tsx, apps/desktop/src/renderer/components/chat/DiffView.tsx, apps/desktop/src/renderer/components/chat/ErrorBanner.tsx, apps/desktop/src/renderer/components/chat/ErrorMessageCard.tsx, apps/desktop/src/renderer/components/chat/GeneratedFilesCard.tsx, apps/desktop/src/renderer/components/chat/GhostSummonCard.tsx, apps/desktop/src/renderer/components/chat/GhostToolCard.tsx, apps/desktop/src/renderer/components/chat/HookTaskCard.tsx, apps/desktop/src/renderer/components/chat/ImageGalleryContext.tsx, apps/desktop/src/renderer/components/chat/ImageHoverPreview.tsx, apps/desktop/src/renderer/components/chat/ImageLightbox.tsx, apps/desktop/src/renderer/components/chat/ImageMissingPlaceholder.tsx, apps/desktop/src/renderer/components/chat/InlineReferenceChip.tsx, apps/desktop/src/renderer/components/chat/InterruptedTurnBanner.tsx, apps/desktop/src/renderer/components/chat/JumpToBottomChip.tsx, apps/desktop/src/renderer/components/chat/MarkdownDiffBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownMermaidBlock.tsx, apps/desktop/src/renderer/components/chat/MarkdownRenderer.tsx, apps/desktop/src/renderer/components/chat/MermaidLightbox.tsx, apps/desktop/src/renderer/components/chat/MessageActionBar.tsx, apps/desktop/src/renderer/components/chat/MessageNavRail.tsx, apps/desktop/src/renderer/components/chat/MessageStream.tsx, apps/desktop/src/renderer/components/chat/ModelLightbox.tsx, apps/desktop/src/renderer/components/chat/NewMessageIndicator.tsx, apps/desktop/src/renderer/components/chat/PlanReviewBubble.tsx, apps/desktop/src/renderer/components/chat/PrevMessageJumpChip.tsx, apps/desktop/src/renderer/components/chat/ProjectLinkChip.tsx, apps/desktop/src/renderer/components/chat/QuoteChip.tsx, apps/desktop/src/renderer/components/chat/RewindPreviewDialog.tsx, apps/desktop/src/renderer/components/chat/SelectionQuoteButton.tsx, apps/desktop/src/renderer/components/chat/SentAgentReferenceChip.tsx, apps/desktop/src/renderer/components/chat/SentInlineAtomBody.tsx, apps/desktop/src/renderer/components/chat/SessionHandoffCard.tsx, apps/desktop/src/renderer/components/chat/SessionLinkChip.tsx, apps/desktop/src/renderer/components/chat/ShareMessageCheckbox.tsx, apps/desktop/src/renderer/components/chat/ShareSelectionBar.tsx, apps/desktop/src/renderer/components/chat/SlackIcon.tsx, apps/desktop/src/renderer/components/chat/StreamFadeSpan.tsx, apps/desktop/src/renderer/components/chat/StreamingMarkdownChunk.tsx, apps/desktop/src/renderer/components/chat/SystemCard.tsx, apps/desktop/src/renderer/components/chat/TextLightbox.tsx, apps/desktop/src/renderer/components/chat/ThinkingCard.tsx, apps/desktop/src/renderer/components/chat/ThinkingText.tsx, apps/desktop/src/renderer/components/chat/TodoListCard.tsx, apps/desktop/src/renderer/components/chat/ToolCallCard.tsx, apps/desktop/src/renderer/components/chat/ToolPayloadLightbox.tsx, apps/desktop/src/renderer/components/chat/TopRightChipStack.tsx, apps/desktop/src/renderer/components/chat/TurnChangesCard.tsx, apps/desktop/src/renderer/components/chat/UpgradeBanner.tsx, apps/desktop/src/renderer/components/chat/UserMessage.tsx, apps/desktop/src/renderer/components/chat/UserMessageEditBox.tsx, apps/desktop/src/renderer/components/chat/UserMessageUrlLink.tsx, apps/desktop/src/renderer/components/chat/VideoLightbox.tsx, apps/desktop/src/renderer/components/chat/WorkGroupBlock.tsx, apps/desktop/src/renderer/components/chat/WorktreeRestoreBanner.tsx, apps/desktop/src/renderer/components/chat/XIcon.tsx, apps/desktop/src/renderer/components/chat/activityRowChrome.ts, apps/desktop/src/renderer/components/chat/autoFollowIntent.ts, apps/desktop/src/renderer/components/chat/errorReasonI18n.ts, apps/desktop/src/renderer/components/chat/imageDisplaySize.ts, apps/desktop/src/renderer/components/chat/lightboxAnnotations.ts, apps/desktop/src/renderer/components/chat/lightboxGestures.ts, apps/desktop/src/renderer/components/chat/markdownImageTargets.ts, apps/desktop/src/renderer/components/chat/messageNavRailModel.ts, apps/desktop/src/renderer/components/chat/messageNavRailWheel.ts, apps/desktop/src/renderer/components/chat/messageStreamIndicatorPosition.ts, apps/desktop/src/renderer/components/chat/rehypeFencedCodeMarker.ts, apps/desktop/src/renderer/components/chat/rehypeMathBlockMarker.ts, apps/desktop/src/renderer/components/chat/rehypeStreamWordFade.ts, apps/desktop/src/renderer/components/chat/remarkHtmlImages.ts, apps/desktop/src/renderer/components/chat/remarkLocalPathLinks.ts, apps/desktop/src/renderer/components/chat/remarkPreserveRawLocalDestinations.ts, apps/desktop/src/renderer/components/chat/remarkSessionLinks.ts, apps/desktop/src/renderer/components/chat/remarkStrictInlineMath.ts, apps/desktop/src/renderer/components/chat/remarkTruncateCjkUrls.ts, apps/desktop/src/renderer/components/chat/repairStreamingMarkdown.ts, apps/desktop/src/renderer/components/chat/scrollAnchoringDetect.ts, apps/desktop/src/renderer/components/chat/shareSelectionStore.ts, apps/desktop/src/renderer/components/chat/streamingMarkdownChunks.ts, apps/desktop/src/renderer/components/chat/toolLoopI18n.ts, apps/desktop/src/renderer/components/chat/unreadCount.ts, apps/desktop/src/renderer/components/chat/useAutomaticHistoryLoadBudget.ts, apps/desktop/src/renderer/components/chat/useCopyAsImage.tsx, apps/desktop/src/renderer/components/chat/useDeleteMessage.ts, apps/desktop/src/renderer/components/chat/useFileChipContextMenu.tsx, apps/desktop/src/renderer/components/chat/useForkAtMessage.ts, apps/desktop/src/renderer/components/chat/useNavigationKeyListener.ts, apps/desktop/src/renderer/components/chat/useOpenWithMenu.tsx, apps/desktop/src/renderer/components/chat/usePrevUserMessageInView.ts, apps/desktop/src/renderer/components/chat/userMessageCollapse.ts, apps/desktop/src/renderer/components/chat/userMessageDisplayText.ts, apps/desktop/src/renderer/components/chat/userMessageLinkify.ts, apps/desktop/src/renderer/components/chat/viewportFillDetect.ts, apps/desktop/src/renderer/components/new-chat/AddRemoteProjectDialog.tsx, apps/desktop/src/renderer/components/new-chat/AgentSelect.tsx, apps/desktop/src/renderer/components/new-chat/AskUserQuestionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/AtMentionPanel.tsx, apps/desktop/src/renderer/components/new-chat/AttachmentTypeThumb.tsx, apps/desktop/src/renderer/components/new-chat/ChatInput.tsx, apps/desktop/src/renderer/components/new-chat/CjkPunctDecoration.ts, apps/desktop/src/renderer/components/new-chat/CjkPunctuationUtils.ts, apps/desktop/src/renderer/components/new-chat/ComposerListIndentDecoration.ts, apps/desktop/src/renderer/components/new-chat/ComposerListNodes.ts, apps/desktop/src/renderer/components/new-chat/ComposerQuoteNode.tsx, apps/desktop/src/renderer/components/new-chat/DeviceSwitcherPill.tsx, apps/desktop/src/renderer/components/new-chat/EffortSlider.tsx, apps/desktop/src/renderer/components/new-chat/EmptyDocSelectionGuard.ts, apps/desktop/src/renderer/components/new-chat/ExtraDirsButton.tsx, apps/desktop/src/renderer/components/new-chat/FastModeToggle.tsx, apps/desktop/src/renderer/components/new-chat/FolderPickerPopover.tsx, apps/desktop/src/renderer/components/new-chat/FullAccessConfirmContent.tsx, apps/desktop/src/renderer/components/new-chat/GhostCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/GoalAdvancedLimits.tsx, apps/desktop/src/renderer/components/new-chat/GoalIndicator.tsx, apps/desktop/src/renderer/components/new-chat/HomeUsageDashboard.tsx, apps/desktop/src/renderer/components/new-chat/ListComposerTextarea.tsx, apps/desktop/src/renderer/components/new-chat/MentionChipNode.tsx, apps/desktop/src/renderer/components/new-chat/MentionDragCaretDecoration.ts, apps/desktop/src/renderer/components/new-chat/ModelConfigFlyout.tsx, apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx, apps/desktop/src/renderer/components/new-chat/NewGoalDialog.tsx, apps/desktop/src/renderer/components/new-chat/PastedTextChipNode.tsx, apps/desktop/src/renderer/components/new-chat/PendingQueuePanel.tsx, apps/desktop/src/renderer/components/new-chat/PermissionPrompt.tsx, apps/desktop/src/renderer/components/new-chat/PermissionSelector.tsx, apps/desktop/src/renderer/components/new-chat/PinnedPlanPanel.tsx, apps/desktop/src/renderer/components/new-chat/PlanActionCard.tsx, apps/desktop/src/renderer/components/new-chat/PlanModeIndicator.tsx, apps/desktop/src/renderer/components/new-chat/PlanViewerCard.tsx, apps/desktop/src/renderer/components/new-chat/PluginSetupPrompt.tsx, apps/desktop/src/renderer/components/new-chat/QuickStartPillMark.ts, apps/desktop/src/renderer/components/new-chat/SendButton.tsx, apps/desktop/src/renderer/components/new-chat/SlashCommandDecoration.ts, apps/desktop/src/renderer/components/new-chat/SlashCommandPalette.tsx, apps/desktop/src/renderer/components/new-chat/TakeoverMask.tsx, apps/desktop/src/renderer/components/new-chat/ThinkingToggle.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedFlyoutHost.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelPanel.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRail.tsx, apps/desktop/src/renderer/components/new-chat/UnifiedModelRow.tsx, apps/desktop/src/renderer/components/new-chat/UsageDailyBars.tsx, apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx, apps/desktop/src/renderer/components/new-chat/VendorSegmentedSwitcher.tsx, apps/desktop/src/renderer/components/new-chat/VoiceInputDraftDecoration.ts, apps/desktop/src/renderer/components/new-chat/WindowsSelectionReplacement.ts, apps/desktop/src/renderer/components/new-chat/WorktreeChipsRow.tsx, apps/desktop/src/renderer/components/new-chat/WorktreeCreatingOverlay.tsx, apps/desktop/src/renderer/components/new-chat/agentOptions.ts, apps/desktop/src/renderer/components/new-chat/agentSwitchConfirmation.ts, apps/desktop/src/renderer/components/new-chat/branchPick.ts, apps/desktop/src/renderer/components/new-chat/composerBlankPointerFocus.ts, apps/desktop/src/renderer/components/new-chat/composerContentSerialization.ts, apps/desktop/src/renderer/components/new-chat/composerDocState.ts, apps/desktop/src/renderer/components/new-chat/composerFrameScheduler.ts, apps/desktop/src/renderer/components/new-chat/composerHistoryProjection.ts, apps/desktop/src/renderer/components/new-chat/composerPromptRecommendation.ts, apps/desktop/src/renderer/components/new-chat/composerRenderGate.ts, apps/desktop/src/renderer/components/new-chat/composerSendOwnership.ts, apps/desktop/src/renderer/components/new-chat/composerSendSnapshot.ts, apps/desktop/src/renderer/components/new-chat/effortChangeQueue.ts, apps/desktop/src/renderer/components/new-chat/extraDirsActions.ts, apps/desktop/src/renderer/components/new-chat/ghostComposerPlacement.ts, apps/desktop/src/renderer/components/new-chat/localAttachmentPicker.ts, apps/desktop/src/renderer/components/new-chat/mentionChipInsertion.ts, apps/desktop/src/renderer/components/new-chat/pastePipeline.ts, apps/desktop/src/renderer/components/new-chat/pathPaste.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueLocks.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowActivity.ts, apps/desktop/src/renderer/components/new-chat/pendingQueueRowPresentation.ts, apps/desktop/src/renderer/components/new-chat/planModeComposerCommand.ts, apps/desktop/src/renderer/components/new-chat/remoteBrowseAdapters.ts, apps/desktop/src/renderer/components/new-chat/remoteExistingProjects.ts, apps/desktop/src/renderer/components/new-chat/sessionLinkPaste.ts, apps/desktop/src/renderer/components/new-chat/sourceSwitch.ts, apps/desktop/src/renderer/components/new-chat/unifiedModelSelection.ts, apps/desktop/src/renderer/components/new-chat/usagePalette.ts, apps/desktop/src/renderer/components/new-chat/useComposerSendFocusRestore.ts, apps/desktop/src/renderer/components/new-chat/useModelDiscoveryPending.ts, apps/desktop/src/renderer/components/new-chat/useUnifiedRowActions.ts, apps/desktop/src/renderer/features/cc-agent/CCAgentFeatureLayout.tsx, apps/desktop/src/renderer/features/cc-agent/CCAgentSessionView.tsx | 266 | 56 | 412 | | `desktop.dev.maker-experimental` | desktop | Maker 实验诊断页 | hash `/maker-experimental`(MakerExperimentalView;EXPERIMENTAL_FEATURES 现为空,仍是生产路由) | MakerExperimentalView | apps/desktop/src/renderer/features/maker-experimental/MakerExperimentalView.tsx | 1 | 39 | 11 | | `desktop.issues.guide` | desktop | Issue 引导页 | hash `/issues`(IssueTrackerFeatureLayout,已迁 GitHub 后的引导页) | IssueTrackerFeatureLayout | apps/desktop/src/renderer/features/issue-tracker/IssueTrackerFeatureLayout.tsx, apps/desktop/src/renderer/features/issue-tracker/MyIssueList.tsx, apps/desktop/src/renderer/features/issue-tracker/hooks/useMyIssues.ts, apps/desktop/src/renderer/features/issue-tracker/lib/myIssuesNotices.ts, apps/desktop/src/renderer/features/issue-tracker/lib/startIssueChat.ts | 0 | 0 | 8 | | `desktop.native.app-menu` | desktop | 应用菜单 | Menu.setApplicationMenu(用户可见出口,不展开业务逻辑) | applicationMenuLabels | apps/desktop/src/main/applicationMenuLabels.ts | 0 | 0 | 0 | From bc86a8c2b7ff38721e6cbf05c73ea93ea775f08c Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:17:26 +1200 Subject: [PATCH 14/30] fix(usage): scope task snapshots to account lifecycle Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../settings/usage/UsageTaskTable.tsx | 70 +++++++++++++++---- .../usage/__tests__/usageHistoryStats.test.ts | 31 +++++++- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 172e64df2d5..968009ed402 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -25,7 +25,7 @@ * 不计算消息预览,避免把统计页变成整库 messages 扫描。 */ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; @@ -35,6 +35,7 @@ import * as sessionService from '@/lib/sessionService'; import type { Session } from '@/lib/ccAgent.types'; import { formatSidebarTime } from '@/features/cc-agent/lib/formatSidebarTime'; import { useProviders } from '@/hooks/useProviders'; +import { isDataOwnerPushCurrent } from '@/contexts/dataOwnerGeneration'; import { usageRankColor } from '@/components/new-chat/usagePalette'; import { providerDisplayNameById } from '@/lib/providerDisplayName'; import { formatUsagePercent } from './formatUsagePercent'; @@ -110,8 +111,31 @@ export function shouldHideUsageTaskTable(range: UsageHistoryRange): boolean { return isUsageHistorySingleDay(range); } -type UsageSessionsState = - { status: 'loading' } | { status: 'ready'; sessions: Session[] } | { status: 'error' }; +export type UsageSessionsState = + | { scopeKey: string | null; status: 'loading' } + | { scopeKey: string | null; status: 'ready'; sessions: Session[] } + | { scopeKey: string | null; status: 'error' }; + +/** Only a snapshot belonging to the current data owner may feed the table. */ +export function usageSessionsForScope( + state: UsageSessionsState, + scopeKey: string | null, +): Session[] { + return state.scopeKey === scopeKey && state.status === 'ready' ? state.sessions : []; +} + +/** Consume an existing deleted-session push without weakening the full-query scope. */ +export function removeUsageSessionForScope( + state: UsageSessionsState, + scopeKey: string | null, + sessionId: string, +): UsageSessionsState { + if (state.scopeKey !== scopeKey || state.status !== 'ready') return state; + return { + ...state, + sessions: state.sessions.filter((session) => session.id !== sessionId), + }; +} /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 @@ -124,34 +148,56 @@ export function useTopTokenSessions( ): Session[] { // 归档的任务同样消耗过 token, 统计口径不该因为用户归档而变。 const { sessions: recentSessions } = useCCSessions({ includeArchived: 'all' }); - const [usageSessions, setUsageSessions] = useState({ status: 'loading' }); + const scopeKey = accountScopeKey ?? null; + const deletedSessionIdsRef = useRef>(new Set()); + const [usageSessions, setUsageSessions] = useState(() => ({ + scopeKey, + status: 'loading', + })); useEffect(() => { let cancelled = false; - setUsageSessions({ status: 'loading' }); + deletedSessionIdsRef.current = new Set(); + setUsageSessions({ scopeKey, status: 'loading' }); + const sessionsPush = window.electronAPI?.localDb?.sessionsPush; + const unsubscribe = sessionsPush?.onPatched(({ sessionId, patch }, ownerStamp) => { + if (!isDataOwnerPushCurrent(ownerStamp) || patch.status !== 'deleted') return; + deletedSessionIdsRef.current.add(sessionId); + setUsageSessions((state) => removeUsageSessionForScope(state, scopeKey, sessionId)); + }); void sessionService .list(20, 'all', { usageHistory: true }) .then((rows) => { - if (!cancelled) setUsageSessions({ status: 'ready', sessions: rows }); + if (cancelled) return; + const deletedSessionIds = deletedSessionIdsRef.current; + setUsageSessions({ + scopeKey, + status: 'ready', + sessions: rows.filter((session) => !deletedSessionIds.has(session.id)), + }); }) .catch(() => { - if (!cancelled) setUsageSessions({ status: 'error' }); + if (!cancelled) setUsageSessions({ scopeKey, status: 'error' }); }); return () => { cancelled = true; + unsubscribe?.(); }; - }, [accountScopeKey]); + }, [scopeKey]); const sessions = useMemo(() => { // The sidebar snapshot is capped at 1000 rows and cannot stand in for the // full usage-history query. Hide the aggregate until that query succeeds; // showing the truncated or failed fallback would mislabel the ranking. - if (usageSessions.status !== 'ready') return []; - const byId = new Map(usageSessions.sessions.map((session) => [session.id, session])); + if (usageSessions.scopeKey !== scopeKey || usageSessions.status !== 'ready') return []; + const scopedSessions = usageSessionsForScope(usageSessions, scopeKey); + const byId = new Map(scopedSessions.map((session) => [session.id, session])); // 当前列表快照包含最近的实时 token/status patch,优先覆盖全量查询的旧行。 - for (const session of recentSessions) byId.set(session.id, session); + for (const session of recentSessions) { + if (!deletedSessionIdsRef.current.has(session.id)) byId.set(session.id, session); + } return [...byId.values()]; - }, [recentSessions, usageSessions]); + }, [recentSessions, scopeKey, usageSessions]); return useMemo(() => { // Sessions retain only their latest activity timestamp, so they cannot diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index 63409844e72..409b8587d43 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import type { Session } from '@/lib/ccAgent.types'; import type { UsageHistoryPayload } from '@/hooks/useUsageHistory'; import { buildAgentRows, @@ -13,7 +14,13 @@ import { isUsageHistoryEmpty, toUsageDays, } from '../usageHistoryStats'; -import { shouldHideUsageTaskTable, usageActivityIso } from '../UsageTaskTable'; +import { + removeUsageSessionForScope, + shouldHideUsageTaskTable, + usageActivityIso, + usageSessionsForScope, + type UsageSessionsState, +} from '../UsageTaskTable'; const zeroMoney = { amount: 0, @@ -73,6 +80,28 @@ describe('isUsageHistorySingleDay', () => { }); }); +describe('usage task session scope', () => { + const readyState = (scopeKey: string): UsageSessionsState => ({ + scopeKey, + status: 'ready', + sessions: [{ id: 'session-a' } as Session], + }); + + it('账号切换的过渡帧不暴露旧账号的全量快照', () => { + expect(usageSessionsForScope(readyState('owner-a'), 'owner-b')).toEqual([]); + }); + + it('只从同一账号的快照消费 deleted 事件', () => { + const state = readyState('owner-a'); + expect(removeUsageSessionForScope(state, 'owner-a', 'session-a')).toMatchObject({ + scopeKey: 'owner-a', + status: 'ready', + sessions: [], + }); + expect(removeUsageSessionForScope(state, 'owner-b', 'session-a')).toBe(state); + }); +}); + describe('cacheHitRate', () => { it('输出 token 不进分母 (与逐轮卡片同一公式)', () => { expect( From 08e4af7baf0f99c2cd47f4ee95e549c58ca12fb1 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:42:59 +1200 Subject: [PATCH 15/30] fix(usage): close pending data and bridge contracts Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- apps/desktop/src/preload/preload.ts | 14 ++++++- .../homeUsageDashboardSource.test.ts | 11 ++++++ .../settings/usage/UsageHistorySection.tsx | 1 + .../src/renderer/hooks/useUsageHistory.ts | 39 +++++++++++++++---- apps/desktop/src/renderer/vite-env.d.ts | 2 +- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index de44f55363c..ee9aa465c67 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -386,6 +386,13 @@ type DiscordBotSessionAuthCheckWire = { providerId: string | null; providerLabel: string | null; }; + +/** Public shape of the local session-list bridge options. */ +type LocalDbSessionListOptions = { + includePinned?: boolean; + fresh?: boolean; + usageHistory?: boolean; +}; /** * 个人 Telegram bot 的传输状态(与 @cindy/im 的 IMStatus 同形; preload 不引包, * 就地声明)。offline = 凭证保留但用户主动下线, 与 idle(未配置)严格区分。 @@ -5011,8 +5018,11 @@ contextBridge.exposeInMainWorld('electronAPI', { }), }, sessions: { - list: (limit?: number, status?: string, options?: unknown): Promise => - ipcRenderer.invoke('local-db:sessions:list', limit, status, options), + list: ( + limit?: number, + status?: string, + options?: LocalDbSessionListOptions, + ): Promise => ipcRenderer.invoke('local-db:sessions:list', limit, status, options), create: (body?: unknown): Promise => ipcRenderer.invoke('local-db:sessions:create', body), get: (id: string): Promise => ipcRenderer.invoke('local-db:sessions:get', id), diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 9b0c365d54e..64d09694f34 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -169,4 +169,15 @@ describe('HomeUsageDashboard source contract', () => { "next.models.some((m) => m.agentKind === 'codex' && m.estimatedCostUsd === null)", ); }); + + it('lets the token-only settings page consume pending estimates without changing the home gate', () => { + const hookSource = readFileSync(resolve(__dirname, '../hooks/useUsageHistory.ts'), 'utf8'); + const settingsSource = readFileSync( + resolve(__dirname, '../components/settings/usage/UsageHistorySection.tsx'), + 'utf8', + ); + expect(settingsSource).toContain('allowPendingEstimates: true'); + expect(hookSource).toContain('allowPendingEstimates?: boolean;'); + expect(hookSource).toContain('if (!scope.request.allowPendingEstimates) return scope.cache;'); + }); }); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index b20fb871df0..6d6c0f320a5 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -82,6 +82,7 @@ export function UsageHistorySection(): React.JSX.Element { userId: user?.id, days: 'all', modelDays: 'all', + allowPendingEstimates: true, }); const [range, setRange] = useState('30d'); const [heatmapWeeks, setHeatmapWeeks] = useState(20); diff --git a/apps/desktop/src/renderer/hooks/useUsageHistory.ts b/apps/desktop/src/renderer/hooks/useUsageHistory.ts index 103fbc218a6..c2a178c5571 100644 --- a/apps/desktop/src/renderer/hooks/useUsageHistory.ts +++ b/apps/desktop/src/renderer/hooks/useUsageHistory.ts @@ -324,7 +324,11 @@ interface UsageHistoryScopeState { listeners: Set<(p: UsageHistoryPayload | null) => void>; statusListeners: Set<(refreshing: boolean) => void>; refreshing: boolean; - request: { days: number | 'all'; modelDays: number | 'all' }; + request: { + days: number | 'all'; + modelDays: number | 'all'; + allowPendingEstimates: boolean; + }; } const scopes = new Map(); @@ -335,9 +339,14 @@ function normalizeScopeKey(userId?: string | null): string { function getScope( scopeKey: string, - request: { days: number | 'all'; modelDays: number | 'all' } = { + request: { + days: number | 'all'; + modelDays: number | 'all'; + allowPendingEstimates: boolean; + } = { days: HISTORY_WINDOW_DAYS, modelDays: MODEL_WINDOW_DAYS, + allowPendingEstimates: false, }, ): UsageHistoryScopeState { let state = scopes.get(scopeKey); @@ -431,9 +440,14 @@ async function load(scopeKey: string, opts?: { forceRefresh?: boolean; resetPric void load(scopeKey, { forceRefresh: true }); }, PRICING_RETRY_DELAY_MS); } - return scope.cache; + // Amount-bearing consumers wait for pricing so they never present a + // misleading value. Token-only consumers can render this payload now; + // the delayed force-refresh above still replaces it once estimates are + // available. + if (!scope.request.allowPendingEstimates) return scope.cache; + } else { + scope.pricingRetryDone = false; } - scope.pricingRetryDone = false; // dirty-check: 数据未变 (仅 generatedAt 不同) → 不换 cache 引用、不写快照、 // 不通知监听 — 等价数据不触发重渲染与 JSON 落盘 const nextSerialized = serializeForCompare(nextForCache); @@ -464,12 +478,20 @@ function normalizeWindow(value: UsageHistoryWindow | undefined, fallback: number function scopeKeyForRequest( scopeKey: string, - request: { days: number | 'all'; modelDays: number | 'all' }, + request: { + days: number | 'all'; + modelDays: number | 'all'; + allowPendingEstimates: boolean; + }, ): string { - if (request.days === HISTORY_WINDOW_DAYS && request.modelDays === MODEL_WINDOW_DAYS) { + if ( + request.days === HISTORY_WINDOW_DAYS && + request.modelDays === MODEL_WINDOW_DAYS && + !request.allowPendingEstimates + ) { return scopeKey; } - return `${scopeKey}|days=${request.days}|modelDays=${request.modelDays}`; + return `${scopeKey}|days=${request.days}|modelDays=${request.modelDays}|allowPendingEstimates=${request.allowPendingEstimates ? 1 : 0}`; } export function useUsageHistory(opts?: { @@ -477,6 +499,8 @@ export function useUsageHistory(opts?: { userId?: string | null; days?: UsageHistoryWindow; modelDays?: UsageHistoryWindow; + /** Token-only consumers may render the payload while prices finish loading. */ + allowPendingEstimates?: boolean; }): { history: UsageHistoryPayload | null; refreshing: boolean; @@ -486,6 +510,7 @@ export function useUsageHistory(opts?: { const request = { days: normalizeWindow(opts?.days, HISTORY_WINDOW_DAYS), modelDays: normalizeWindow(opts?.modelDays, MODEL_WINDOW_DAYS), + allowPendingEstimates: opts?.allowPendingEstimates ?? false, }; const scopedKey = scopeKeyForRequest(scopeKey, request); const scope = getScope(scopedKey, request); diff --git a/apps/desktop/src/renderer/vite-env.d.ts b/apps/desktop/src/renderer/vite-env.d.ts index c034c614b8a..01c2f75b3e2 100644 --- a/apps/desktop/src/renderer/vite-env.d.ts +++ b/apps/desktop/src/renderer/vite-env.d.ts @@ -4375,7 +4375,7 @@ interface ElectronAPI { list: ( limit?: number, status?: 'active' | 'archived' | 'all', - options?: { includePinned?: boolean; fresh?: boolean }, + options?: { includePinned?: boolean; fresh?: boolean; usageHistory?: boolean }, ) => Promise; create: (body?: { id?: string; From 08733ae031d35eb4587cbfc739ad3f5f048001b7 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:08:28 +1200 Subject: [PATCH 16/30] fix(usage): align heatmap metric and selection states Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../homeUsageDashboardSource.test.ts | 11 +++++ .../components/new-chat/UsageHeatmap.tsx | 21 ++++++++-- .../__tests__/usageHeatmapMetric.test.tsx | 42 +++++++++++++++++++ .../settings/usage/UsageTokenBars.tsx | 4 +- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 64d09694f34..0564cf452d9 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -15,6 +15,10 @@ const heatmapSource = readFileSync( resolve(__dirname, '../components/new-chat/UsageHeatmap.tsx'), 'utf8', ); +const tokenBarsSource = readFileSync( + resolve(__dirname, '../components/settings/usage/UsageTokenBars.tsx'), + 'utf8', +); describe('HomeUsageDashboard source contract', () => { it('uses the Claude account daily spend for the visible today amount when available', () => { @@ -67,6 +71,13 @@ describe('HomeUsageDashboard source contract', () => { expect(dailyBarsSource).not.toContain("money.currency ?? 'USD'"); }); + it('keeps selected usage chart days flat without ad-hoc shadows', () => { + expect(heatmapSource).not.toContain('boxShadow'); + expect(tokenBarsSource).not.toContain('boxShadow'); + expect(heatmapSource).toContain("'2px solid var(--focus-ring-soft)'"); + expect(tokenBarsSource).toContain("'2px solid var(--focus-ring-soft)'"); + }); + it('shows cached usage immediately while marking background refresh', () => { expect(source).toContain( 'const { history, refreshing: usageRefreshing } = useUsageHistory({ paused: collapsed, userId: user?.id });', diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index dce958b7230..d81396a5d72 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -84,12 +84,14 @@ export function resolveHeatmapWeeks({ availableWidth, minimumWeeks = MIN_HEATMAP_WEEKS, windowDays, + metric, }: { - days: Array<{ day: string }>; + days: Array<{ day: string; money?: RegionalMoney; tokens?: number }>; todayKey: string; availableWidth: number; minimumWeeks?: number; windowDays?: number; + metric?: 'money' | 'tokens'; }): number { const minWeeks = Math.max( MIN_HEATMAP_WEEKS, @@ -99,9 +101,16 @@ export function resolveHeatmapWeeks({ const today = parseDayKey(todayKey); if (!todayKey || Number.isNaN(today.getTime())) return minWeeks; + const intensityOf = (day: { money?: RegionalMoney; tokens?: number }): number => + metric === 'tokens' ? (day.tokens ?? 0) : (day.money?.amount ?? 0); const earliestDay = days + .filter( + (day) => + day.day && + day.day <= todayKey && + (metric === undefined || intensityOf(day) > 0), + ) .map((day) => day.day) - .filter((day) => day && day <= todayKey) .sort()[0]; const dataWeeks = earliestDay ? Math.floor( @@ -169,6 +178,7 @@ export function UsageHeatmap({ availableWidth, minimumWeeks, windowDays, + metric, }); useLayoutEffect(() => { @@ -291,8 +301,11 @@ export function UsageHeatmap({ cell.level === 0 ? 'var(--surface-chip)' : `color-mix(in srgb, var(--accent-emphasis) ${LEVEL_MIX[cell.level - 1] * 100}%, var(--surface-chip))`, - boxShadow: - selectedDay === cell.day ? '0 0 0 2px var(--focus-ring-soft)' : undefined, + outline: + selectedDay === cell.day + ? '2px solid var(--focus-ring-soft)' + : undefined, + outlineOffset: selectedDay === cell.day ? '1px' : undefined, }} /> diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index cd2163e627c..3fe7e5a96c1 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -73,6 +73,30 @@ describe('UsageHeatmap metric', () => { ).toBe(60); }); + it('按当前 metric 过滤历史起点,不让零值日期扩展热力图', () => { + const history = [ + { day: '2025-01-01', money: money(100), tokens: 0 }, + { day: '2026-08-20', money: money(0), tokens: 1_000 }, + ]; + + expect( + resolveHeatmapWeeks({ + days: history, + todayKey: '2026-08-22', + availableWidth: 900, + metric: 'money', + }), + ).toBe(60); + expect( + resolveHeatmapWeeks({ + days: history, + todayKey: '2026-08-22', + availableWidth: 900, + metric: 'tokens', + }), + ).toBe(20); + }); + it('周日锚点也覆盖完整的 windowDays 历史,不把未来占位挤掉最早日期', () => { expect(heatmapWeeksForWindow('2026-08-22', 140)).toBe(20); expect(heatmapWeeksForWindow('2026-08-23', 140)).toBe(21); @@ -179,4 +203,22 @@ describe('UsageHeatmap metric', () => { 'rounded-full', ); }); + + it('选中日期使用 outline 而不是页面内阴影', () => { + const { getByRole } = render( + , + ); + + const selectedCell = getByRole('button', { name: '2026-08-21' }) + .firstElementChild as HTMLElement; + expect(selectedCell.style.outline).toBe('2px solid var(--focus-ring-soft)'); + expect(selectedCell.style.boxShadow).toBe(''); + }); }); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx index 85c1cd47499..e3fa968606a 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx @@ -168,7 +168,9 @@ export function UsageTokenBars({ style={{ height, backgroundColor: b.segments.length === 0 ? 'var(--surface-chip)' : undefined, - boxShadow: selectedDay === b.day ? '0 0 0 2px var(--focus-ring-soft)' : undefined, + outline: + selectedDay === b.day ? '2px solid var(--focus-ring-soft)' : undefined, + outlineOffset: selectedDay === b.day ? '1px' : undefined, }} > {[...b.segments].reverse().map((s) => ( From 9b91a4695be05c7bed9b4bd488496314380629f2 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:32:31 +1200 Subject: [PATCH 17/30] fix(usage): keep model colors aligned across ranges Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../homeUsageDashboardSource.test.ts | 14 +++++ .../settings/usage/UsageBreakdownTables.tsx | 51 ++++++++++--------- .../settings/usage/UsageHistorySection.tsx | 6 ++- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 0564cf452d9..8dee2255bbd 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -19,6 +19,14 @@ const tokenBarsSource = readFileSync( resolve(__dirname, '../components/settings/usage/UsageTokenBars.tsx'), 'utf8', ); +const breakdownTablesSource = readFileSync( + resolve(__dirname, '../components/settings/usage/UsageBreakdownTables.tsx'), + 'utf8', +); +const usageHistorySectionSource = readFileSync( + resolve(__dirname, '../components/settings/usage/UsageHistorySection.tsx'), + 'utf8', +); describe('HomeUsageDashboard source contract', () => { it('uses the Claude account daily spend for the visible today amount when available', () => { @@ -78,6 +86,12 @@ describe('HomeUsageDashboard source contract', () => { expect(tokenBarsSource).toContain("'2px solid var(--focus-ring-soft)'"); }); + it('uses the shared model key rank for chart and filtered model-table colors', () => { + expect(usageHistorySectionSource).toContain('colorOrder={colorOrder}'); + expect(breakdownTablesSource).toContain('usageRankOf(colorOrder, row.key)'); + expect(breakdownTablesSource).toContain('colorOrder: string[];'); + }); + it('shows cached usage immediately while marking background refresh', () => { expect(source).toContain( 'const { history, refreshing: usageRefreshing } = useUsageHistory({ paused: collapsed, userId: user?.id });', diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx index 5af7596e767..8b1485f68d8 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx @@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; import { formatCompactTokens, formatModelShort } from '@/lib/usageFormat'; -import { usageRankColor } from '@/components/new-chat/usagePalette'; +import { usageRankColor, usageRankOf } from '@/components/new-chat/usagePalette'; import { type AgentTokenRow, type ModelTokenRow, type UsageAgentKind } from './usageHistoryStats'; import { formatUsagePercent } from './formatUsagePercent'; @@ -149,9 +149,11 @@ export function UsageAgentTable({ export function UsageModelTable({ rows, rangeLabel, + colorOrder, }: { rows: ModelTokenRow[]; rangeLabel: string; + colorOrder: string[]; }): React.JSX.Element { const { t } = useTranslation(); @@ -176,29 +178,32 @@ export function UsageModelTable({ - {rows.map((row, index) => ( - - - - - - {formatModelShort(row.model)} - - {/* 同一模型 id 可能跨 agent 撞名, 标签让两行区分得开 */} - - {row.agentKind} + {rows.map((row) => { + const rank = usageRankOf(colorOrder, row.key); + return ( + + + + + + {formatModelShort(row.model)} + + {/* 同一模型 id 可能跨 agent 撞名, 标签让两行区分得开 */} + + {row.agentKind} + - - - {formatCompactTokens(row.tokens)} - - {formatCompactTokens(row.inputTokens)} - {formatCompactTokens(row.outputTokens)} - {formatCompactTokens(row.cacheReadTokens)} - {formatCompactTokens(row.cacheCreateTokens)} - - - ))} + + {formatCompactTokens(row.tokens)} + + {formatCompactTokens(row.inputTokens)} + {formatCompactTokens(row.outputTokens)} + {formatCompactTokens(row.cacheReadTokens)} + {formatCompactTokens(row.cacheCreateTokens)} + + + ); + })} ); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index 6d6c0f320a5..67f6c9d362a 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -253,7 +253,11 @@ export function UsageHistorySection(): React.JSX.Element { {modelRows.length > 0 && (
- +
)} From 18f8077319c2bd6f2524cbe0123f5a36374ad878 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:01:38 +1200 Subject: [PATCH 18/30] fix(usage): preserve canonical task token totals Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../settings/usage/UsageTaskTable.tsx | 18 +++++++++++++++-- .../usage/__tests__/usageHistoryStats.test.ts | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 968009ed402..e2c32703701 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -137,6 +137,15 @@ export function removeUsageSessionForScope( }; } +/** + * Merge live sidebar metadata into the full usage-history row without letting + * the sidebar's potentially stale token snapshot replace the database total. + */ +export function mergeUsageSessionSnapshots(fullSession: Session, liveSession: Session): Session { + const merged = { ...fullSession, ...liveSession }; + return { ...merged, totalTokenUsage: fullSession.totalTokenUsage }; +} + /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 * 组件内部返回 null 会留下一张只有标题、正文全空的卡片 (会话被删光 / 列表首次加载中)。 @@ -192,9 +201,14 @@ export function useTopTokenSessions( if (usageSessions.scopeKey !== scopeKey || usageSessions.status !== 'ready') return []; const scopedSessions = usageSessionsForScope(usageSessions, scopeKey); const byId = new Map(scopedSessions.map((session) => [session.id, session])); - // 当前列表快照包含最近的实时 token/status patch,优先覆盖全量查询的旧行。 + // 侧栏快照只补充实时元数据;其 totalTokenUsage 可能滞后,保留全量查询的累计值。 for (const session of recentSessions) { - if (!deletedSessionIdsRef.current.has(session.id)) byId.set(session.id, session); + if (deletedSessionIdsRef.current.has(session.id)) continue; + const usageSession = byId.get(session.id); + byId.set( + session.id, + usageSession ? mergeUsageSessionSnapshots(usageSession, session) : session, + ); } return [...byId.values()]; }, [recentSessions, scopeKey, usageSessions]); diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index 409b8587d43..f047902a4d8 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -16,6 +16,7 @@ import { } from '../usageHistoryStats'; import { removeUsageSessionForScope, + mergeUsageSessionSnapshots, shouldHideUsageTaskTable, usageActivityIso, usageSessionsForScope, @@ -100,6 +101,25 @@ describe('usage task session scope', () => { }); expect(removeUsageSessionForScope(state, 'owner-b', 'session-a')).toBe(state); }); + + it('合并实时元数据时保留全量查询的累计 token', () => { + const fullSession = { + id: 'session-a', + title: '历史标题', + totalTokenUsage: 1000, + } as Session; + const liveSession = { + id: 'session-a', + title: '最新标题', + totalTokenUsage: 100, + } as Session; + + expect(mergeUsageSessionSnapshots(fullSession, liveSession)).toMatchObject({ + id: 'session-a', + title: '最新标题', + totalTokenUsage: 1000, + }); + }); }); describe('cacheHitRate', () => { From 252796d626e78fb561db9b9135be786472676d0d Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:22:54 +1200 Subject: [PATCH 19/30] fix(usage): refresh task totals on token updates Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../settings/usage/UsageTaskTable.tsx | 32 +++++++++++++++++++ .../usage/__tests__/usageHistoryStats.test.ts | 22 +++++++++++++ 2 files changed, 54 insertions(+) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index e2c32703701..2b00f5fc1ec 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -146,6 +146,23 @@ export function mergeUsageSessionSnapshots(fullSession: Session, liveSession: Se return { ...merged, totalTokenUsage: fullSession.totalTokenUsage }; } +/** Apply the authoritative per-session token push to the matching full-query row. */ +export function updateUsageSessionTokenForScope( + state: UsageSessionsState, + scopeKey: string | null, + sessionId: string, + totalTokenUsage: number, +): UsageSessionsState { + if (state.scopeKey !== scopeKey || state.status !== 'ready') return state; + let changed = false; + const sessions = state.sessions.map((session) => { + if (session.id !== sessionId || session.totalTokenUsage === totalTokenUsage) return session; + changed = true; + return { ...session, totalTokenUsage }; + }); + return changed ? { ...state, sessions } : state; +} + /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 * 组件内部返回 null 会留下一张只有标题、正文全空的卡片 (会话被删光 / 列表首次加载中)。 @@ -174,6 +191,20 @@ export function useTopTokenSessions( deletedSessionIdsRef.current.add(sessionId); setUsageSessions((state) => removeUsageSessionForScope(state, scopeKey, sessionId)); }); + const unsubscribeTokens = window.electronAPI?.onUsageSessionTokensChanged?.( + ({ sessionId, totalTokens }, ownerStamp) => { + if ( + !isDataOwnerPushCurrent(ownerStamp) || + !Number.isFinite(totalTokens) || + totalTokens < 0 + ) { + return; + } + setUsageSessions((state) => + updateUsageSessionTokenForScope(state, scopeKey, sessionId, totalTokens), + ); + }, + ); void sessionService .list(20, 'all', { usageHistory: true }) .then((rows) => { @@ -191,6 +222,7 @@ export function useTopTokenSessions( return () => { cancelled = true; unsubscribe?.(); + unsubscribeTokens?.(); }; }, [scopeKey]); diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index f047902a4d8..02fd6c82951 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -18,6 +18,7 @@ import { removeUsageSessionForScope, mergeUsageSessionSnapshots, shouldHideUsageTaskTable, + updateUsageSessionTokenForScope, usageActivityIso, usageSessionsForScope, type UsageSessionsState, @@ -120,6 +121,27 @@ describe('usage task session scope', () => { totalTokenUsage: 1000, }); }); + + it('同账号的 token 推送更新全量快照,其他范围保持不变', () => { + const state: UsageSessionsState = { + scopeKey: 'owner-a', + status: 'ready', + sessions: [ + { id: 'session-a', totalTokenUsage: 100 } as Session, + { id: 'session-b', totalTokenUsage: 200 } as Session, + ], + }; + + expect(updateUsageSessionTokenForScope(state, 'owner-a', 'session-a', 150)).toMatchObject({ + scopeKey: 'owner-a', + status: 'ready', + sessions: [ + { id: 'session-a', totalTokenUsage: 150 }, + { id: 'session-b', totalTokenUsage: 200 }, + ], + }); + expect(updateUsageSessionTokenForScope(state, 'owner-b', 'session-a', 150)).toBe(state); + }); }); describe('cacheHitRate', () => { From 4c5abccf98f65e340cadbcea20e717f873cdcbd4 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:53:16 +1200 Subject: [PATCH 20/30] fix(usage): narrow task snapshot live update scope Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../homeUsageDashboardSource.test.ts | 5 ++ .../components/new-chat/UsageHeatmap.tsx | 79 ++++++++++--------- .../__tests__/usageHeatmapMetric.test.tsx | 17 +++- .../settings/usage/UsageTaskTable.tsx | 36 +-------- .../usage/__tests__/usageHistoryStats.test.ts | 21 ----- 5 files changed, 64 insertions(+), 94 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 8dee2255bbd..c743cc3c37b 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -86,6 +86,11 @@ describe('HomeUsageDashboard source contract', () => { expect(tokenBarsSource).toContain("'2px solid var(--focus-ring-soft)'"); }); + it('keeps the home heatmap non-interactive when no day callback is supplied', () => { + expect(heatmapSource).not.toContain('disabled={!onDayClick}'); + expect(heatmapSource).toContain('return onDayClick ? ('); + }); + it('uses the shared model key rank for chart and filtered model-table colors', () => { expect(usageHistorySectionSource).toContain('colorOrder={colorOrder}'); expect(breakdownTablesSource).toContain('usageRankOf(colorOrder, row.key)'); diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index d81396a5d72..cbc2cb6a9c6 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -264,53 +264,54 @@ export function UsageHeatmap({
{columns.map((col, ci) => (
- {col.map((cell, ri) => - cell.placeholder ? ( -
- ) : ( + {col.map((cell, ri) => { + if (cell.placeholder) { + return
; + } + + const title = + metric === 'tokens' + ? `${cell.day} · ${ + cell.tokens > 0 + ? t('usageDashboard.tokensOnly', { + tokens: formatCompactTokens(cell.tokens), + }) + : t('usageHistory.heatmap.emptyCell') + }` + : `${cell.day} · ${formatMoney(cell.money)}${ + cell.tokens > 0 + ? ` · ${t('usageDashboard.tokensOnly', { tokens: formatCompactTokens(cell.tokens) })}` + : '' + }`; + const className = onDayClick ? 'rounded-full' : 'rounded-[3px]'; + const style = { + width: CELL_PX, + height: CELL_PX, + backgroundColor: + cell.level === 0 + ? 'var(--surface-chip)' + : `color-mix(in srgb, var(--accent-emphasis) ${LEVEL_MIX[cell.level - 1] * 100}%, var(--surface-chip))`, + outline: + selectedDay === cell.day ? '2px solid var(--focus-ring-soft)' : undefined, + outlineOffset: selectedDay === cell.day ? '1px' : undefined, + }; + const visual =
; + + return onDayClick ? ( - ), - )} + ) : ( +
{visual}
+ ); + })}
))}
diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index 3fe7e5a96c1..e9305ee1eac 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -146,14 +146,18 @@ describe('UsageHeatmap metric', () => { const baseline = render( , ); - const baselineCell = baseline.getByRole('button', { name: '2026-08-21' }).firstElementChild; + const baselineCell = baseline.container.querySelector( + 'div[title^="2026-08-21"]', + ); const baselineColor = (baselineCell as HTMLElement).style.backgroundColor; baseline.unmount(); const withOutlier = render( , ); - const outlierCell = withOutlier.getByRole('button', { name: '2026-08-21' }).firstElementChild; + const outlierCell = withOutlier.container.querySelector( + 'div[title^="2026-08-21"]', + ); expect((outlierCell as HTMLElement).style.backgroundColor).toBe(baselineColor); }); @@ -187,6 +191,15 @@ describe('UsageHeatmap metric', () => { expect(onDayClick).toHaveBeenCalledWith('2026-08-21'); }); + it('首页非交互日期格不渲染为禁用按钮', () => { + const { container, queryByRole } = render( + , + ); + + expect(queryByRole('button', { name: '2026-08-21' })).toBeNull(); + expect(container.querySelector('div[title^="2026-08-21"]')).toBeTruthy(); + }); + it('可点击日期格的可见彩色表面使用 pill 圆角', () => { const onDayClick = vi.fn(); const { getByRole } = render( diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 2b00f5fc1ec..20c4655b6bc 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -146,23 +146,6 @@ export function mergeUsageSessionSnapshots(fullSession: Session, liveSession: Se return { ...merged, totalTokenUsage: fullSession.totalTokenUsage }; } -/** Apply the authoritative per-session token push to the matching full-query row. */ -export function updateUsageSessionTokenForScope( - state: UsageSessionsState, - scopeKey: string | null, - sessionId: string, - totalTokenUsage: number, -): UsageSessionsState { - if (state.scopeKey !== scopeKey || state.status !== 'ready') return state; - let changed = false; - const sessions = state.sessions.map((session) => { - if (session.id !== sessionId || session.totalTokenUsage === totalTokenUsage) return session; - changed = true; - return { ...session, totalTokenUsage }; - }); - return changed ? { ...state, sessions } : state; -} - /** * 候选行 —— 单独暴露成 hook, 让调用方能在**渲染卡片之前**知道有没有行。 * 组件内部返回 null 会留下一张只有标题、正文全空的卡片 (会话被删光 / 列表首次加载中)。 @@ -191,20 +174,10 @@ export function useTopTokenSessions( deletedSessionIdsRef.current.add(sessionId); setUsageSessions((state) => removeUsageSessionForScope(state, scopeKey, sessionId)); }); - const unsubscribeTokens = window.electronAPI?.onUsageSessionTokensChanged?.( - ({ sessionId, totalTokens }, ownerStamp) => { - if ( - !isDataOwnerPushCurrent(ownerStamp) || - !Number.isFinite(totalTokens) || - totalTokens < 0 - ) { - return; - } - setUsageSessions((state) => - updateUsageSessionTokenForScope(state, scopeKey, sessionId, totalTokens), - ); - }, - ); + // The task table intentionally uses the full-query snapshot as its + // lifecycle-total source. It does not maintain a second partial live-update + // path: the usage charts own dated live refresh, while this aggregate is + // refreshed when its account scope changes or the page remounts. void sessionService .list(20, 'all', { usageHistory: true }) .then((rows) => { @@ -222,7 +195,6 @@ export function useTopTokenSessions( return () => { cancelled = true; unsubscribe?.(); - unsubscribeTokens?.(); }; }, [scopeKey]); diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index 02fd6c82951..496e2bec9ee 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -18,7 +18,6 @@ import { removeUsageSessionForScope, mergeUsageSessionSnapshots, shouldHideUsageTaskTable, - updateUsageSessionTokenForScope, usageActivityIso, usageSessionsForScope, type UsageSessionsState, @@ -122,26 +121,6 @@ describe('usage task session scope', () => { }); }); - it('同账号的 token 推送更新全量快照,其他范围保持不变', () => { - const state: UsageSessionsState = { - scopeKey: 'owner-a', - status: 'ready', - sessions: [ - { id: 'session-a', totalTokenUsage: 100 } as Session, - { id: 'session-b', totalTokenUsage: 200 } as Session, - ], - }; - - expect(updateUsageSessionTokenForScope(state, 'owner-a', 'session-a', 150)).toMatchObject({ - scopeKey: 'owner-a', - status: 'ready', - sessions: [ - { id: 'session-a', totalTokenUsage: 150 }, - { id: 'session-b', totalTokenUsage: 200 }, - ], - }); - expect(updateUsageSessionTokenForScope(state, 'owner-b', 'session-a', 150)).toBe(state); - }); }); describe('cacheHitRate', () => { From 6dd9dbd80bfc259de0de4d6d3d3a8ada98b037a0 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:11:21 +1200 Subject: [PATCH 21/30] fix(usage): narrow history session payload Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../sessionsListIncludePinned.test.ts | 29 ++++++++++++- apps/desktop/src/main/localDb/ipc/sessions.ts | 43 +++++++++++++------ apps/desktop/src/main/localDb/mapper.ts | 33 ++++++++++++++ .../settings/usage/UsageTaskTable.tsx | 30 +++++++++---- .../desktop/src/renderer/lib/ccAgent.types.ts | 19 ++++++++ 5 files changed, 131 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts index a7d995c5c5f..54ed9cd6840 100644 --- a/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts +++ b/apps/desktop/src/main/__tests__/sessionsListIncludePinned.test.ts @@ -202,13 +202,38 @@ describe('local-db:sessions:list includePinned', () => { it('returns the full sessions set for the usage-history query without message projections', async () => { const handler = sessionsListHandler(); h.queryResults.push([ - { session: sessionRow('recent', { totalTokenUsage: 20 }) }, - { session: sessionRow('old', { totalTokenUsage: 200 }) }, + sessionRow('recent', { totalTokenUsage: 20 }), + sessionRow('old', { totalTokenUsage: 200 }), ]); const result = await handler({}, 20, 'all', { usageHistory: true }); expect(result.map((s) => s.id)).toEqual(['recent', 'old']); + expect(result[0]).toEqual({ + id: 'recent', + title: 'recent', + model: 'sonnet', + providerId: null, + totalTokenUsage: 20, + contextTokens: 0, + contextWindow: 0, + userSendAt: null, + updatedAt: new Date(1_700_000_000_000).toISOString(), + }); + expect(result[0]).not.toHaveProperty('workingDir'); + const selectCalls = h.fakeDb.select.mock.calls as unknown as Array; + const projection = selectCalls[0]?.[0] as Record | undefined; + expect(Object.keys(projection ?? {})).toEqual([ + 'id', + 'title', + 'model', + 'providerId', + 'totalTokenUsage', + 'contextTokens', + 'contextWindow', + 'userSendAt', + 'updatedAt', + ]); expect(h.fakeDb.select).toHaveBeenCalledTimes(1); expect(h.listQuery).not.toHaveBeenCalled(); expect(h.queryResults).toHaveLength(0); diff --git a/apps/desktop/src/main/localDb/ipc/sessions.ts b/apps/desktop/src/main/localDb/ipc/sessions.ts index c8a4b113573..325a1e4fceb 100644 --- a/apps/desktop/src/main/localDb/ipc/sessions.ts +++ b/apps/desktop/src/main/localDb/ipc/sessions.ts @@ -22,7 +22,7 @@ import { DEFAULT_DRAFT_SESSION_TITLE, normalizeAutoTitle } from '@cindy/maker-sh import { getDbClient } from '../client/current'; import * as currentDb from '../client/current'; import type { DbClient } from '../client/DbClient'; -import { sessions, messages } from '../schema'; +import { sessions } from '../schema'; import { LIST_PREVIEW_EXTRACT_SQL, LATEST_VISIBLE_PREVIEW_FILTER_SQL, @@ -36,6 +36,7 @@ import { resolveBusinessSessionId } from '../../sessionIds'; import { normalizeDbAgentKind } from '../../../shared/agentKindConversion'; import { sessionToCamel, + sessionUsageToCamel, sessionCreateToRow, sessionPatchToRow, persistableSessionEffort, @@ -1132,14 +1133,7 @@ export function registerSessionIpc( const statusWhere = () => statusFilter ? eq(sessions.status, statusFilter) : ne(sessions.status, 'deleted'); const rows = await selectSessionUsageRows(db, and(sourceFilter, statusWhere())); - return rows.map((session) => - sessionToCamel({ - ...session, - messageCount: 0, - latestMessageExtract: null, - latestMessageRole: null, - }), - ); + return rows.map(sessionUsageToCamel); }; // key 用同一快照上的 userId + clientEpoch + 归一化参数。 // forceRefresh / status 重拉带 fresh,不并入写前那次查询。 @@ -2346,13 +2340,38 @@ interface SessionListRow { function selectSessionUsageRows( db: DbClient['drizzle'], where: SQL | undefined, -): Promise> { +): Promise< + Array< + Pick< + typeof sessions.$inferSelect, + | 'id' + | 'title' + | 'model' + | 'providerId' + | 'totalTokenUsage' + | 'contextTokens' + | 'contextWindow' + | 'userSendAt' + | 'updatedAt' + > + > +> { return db - .select({ session: sessions }) + .select({ + id: sessions.id, + title: sessions.title, + model: sessions.model, + providerId: sessions.providerId, + totalTokenUsage: sessions.totalTokenUsage, + contextTokens: sessions.contextTokens, + contextWindow: sessions.contextWindow, + userSendAt: sessions.userSendAt, + updatedAt: sessions.updatedAt, + }) .from(sessions) .where(where) .orderBy(desc(sessions.updatedAt)) - .then((rows) => rows.map((row) => row.session)); + .then((rows) => rows); } /** diff --git a/apps/desktop/src/main/localDb/mapper.ts b/apps/desktop/src/main/localDb/mapper.ts index b4ddc3e0995..053a099524e 100644 --- a/apps/desktop/src/main/localDb/mapper.ts +++ b/apps/desktop/src/main/localDb/mapper.ts @@ -20,6 +20,7 @@ import type { // 类型从 renderer 共享(仅 type-only import,运行时无 import 副作用) import type { Session, + UsageHistorySession, SessionStatus, Message, MessageRole, @@ -54,6 +55,18 @@ import { type SessionRow = typeof sessions.$inferSelect; type SessionInsert = typeof sessions.$inferInsert; +type SessionUsageRow = Pick< + SessionRow, + | 'id' + | 'title' + | 'model' + | 'providerId' + | 'totalTokenUsage' + | 'contextTokens' + | 'contextWindow' + | 'userSendAt' + | 'updatedAt' +>; /** * 运行时固定 effort 模型用 `null`;`sessions.effort` 是 NOT NULL 枚举。 @@ -480,6 +493,26 @@ function msToIso(ms: number | null | undefined): string | null { return new Date(ms).toISOString(); } +/** + * 用量历史榜单的最小行映射。 + * + * 与 sessionToCamel 分开,避免全量 usageHistory 查询把 workingDir、SDK + * 标识、远程主机等只属于会话管理的字段泄露到 Renderer。 + */ +export function sessionUsageToCamel(row: SessionUsageRow): UsageHistorySession { + return { + id: row.id, + title: row.title, + model: row.model, + providerId: row.providerId, + totalTokenUsage: row.totalTokenUsage, + contextTokens: row.contextTokens, + contextWindow: row.contextWindow, + userSendAt: msToIso(row.userSendAt), + updatedAt: new Date(row.updatedAt).toISOString(), + }; +} + function isoToMs(iso: string | null): number | null { if (iso === null || iso === undefined) return null; const t = new Date(iso).getTime(); diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx index 20c4655b6bc..2b6ff61c490 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx @@ -32,7 +32,7 @@ import { cn } from '@/lib/utils'; import { formatCompactTokens, formatModelShort } from '@/lib/usageFormat'; import { useCCSessions } from '@/hooks/useCCSessions'; import * as sessionService from '@/lib/sessionService'; -import type { Session } from '@/lib/ccAgent.types'; +import type { Session, UsageHistorySession } from '@/lib/ccAgent.types'; import { formatSidebarTime } from '@/features/cc-agent/lib/formatSidebarTime'; import { useProviders } from '@/hooks/useProviders'; import { isDataOwnerPushCurrent } from '@/contexts/dataOwnerGeneration'; @@ -113,14 +113,14 @@ export function shouldHideUsageTaskTable(range: UsageHistoryRange): boolean { export type UsageSessionsState = | { scopeKey: string | null; status: 'loading' } - | { scopeKey: string | null; status: 'ready'; sessions: Session[] } + | { scopeKey: string | null; status: 'ready'; sessions: UsageHistorySession[] } | { scopeKey: string | null; status: 'error' }; /** Only a snapshot belonging to the current data owner may feed the table. */ export function usageSessionsForScope( state: UsageSessionsState, scopeKey: string | null, -): Session[] { +): UsageHistorySession[] { return state.scopeKey === scopeKey && state.status === 'ready' ? state.sessions : []; } @@ -141,9 +141,21 @@ export function removeUsageSessionForScope( * Merge live sidebar metadata into the full usage-history row without letting * the sidebar's potentially stale token snapshot replace the database total. */ -export function mergeUsageSessionSnapshots(fullSession: Session, liveSession: Session): Session { - const merged = { ...fullSession, ...liveSession }; - return { ...merged, totalTokenUsage: fullSession.totalTokenUsage }; +export function mergeUsageSessionSnapshots( + fullSession: UsageHistorySession, + liveSession: Session, +): UsageHistorySession { + return { + ...fullSession, + title: liveSession.title, + model: liveSession.model, + providerId: liveSession.providerId, + contextTokens: liveSession.contextTokens, + contextWindow: liveSession.contextWindow, + userSendAt: liveSession.userSendAt, + updatedAt: liveSession.updatedAt, + totalTokenUsage: fullSession.totalTokenUsage, + }; } /** @@ -154,7 +166,7 @@ export function useTopTokenSessions( range: UsageHistoryRange = '30d', todayKeyOverride?: string, accountScopeKey?: string, -): Session[] { +): UsageHistorySession[] { // 归档的任务同样消耗过 token, 统计口径不该因为用户归档而变。 const { sessions: recentSessions } = useCCSessions({ includeArchived: 'all' }); const scopeKey = accountScopeKey ?? null; @@ -242,8 +254,8 @@ export function useTopTokenSessions( export function UsageTaskTable({ rows, rangeLabel, -}: { - rows: Session[]; + }: { + rows: UsageHistorySession[]; rangeLabel: string; }): React.JSX.Element { const { t } = useTranslation(); diff --git a/apps/desktop/src/renderer/lib/ccAgent.types.ts b/apps/desktop/src/renderer/lib/ccAgent.types.ts index 88cf05337b6..057ffd0c087 100644 --- a/apps/desktop/src/renderer/lib/ccAgent.types.ts +++ b/apps/desktop/src/renderer/lib/ccAgent.types.ts @@ -325,6 +325,25 @@ export interface Session { summary?: string | null; } +/** + * 用量历史任务榜单的最小 wire DTO。 + * + * 该查询会覆盖整个本地 sessions 表,因此不能把完整 Session 行送进 + * Renderer;榜单只需要这些统计与展示字段。 + */ +export type UsageHistorySession = Pick< + Session, + | 'id' + | 'title' + | 'model' + | 'providerId' + | 'totalTokenUsage' + | 'contextTokens' + | 'contextWindow' + | 'userSendAt' + | 'updatedAt' +>; + export interface SessionRuntimeProfileProjection { agentKind: 'claude-code' | 'codex' | 'pi'; model: string; From c589cbb7737f4c39242de870624ee9b056cfa754 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:33:42 +1200 Subject: [PATCH 22/30] fix(usage): narrow history session bridge types Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- apps/desktop/src/preload/preload.ts | 43 ++++++++++++++++--- .../src/renderer/lib/sessionService.ts | 32 +++++++++++++- apps/desktop/src/renderer/vite-env.d.ts | 43 ++++++++++++++++--- 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index ee9aa465c67..18c1eee7a8a 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -393,6 +393,43 @@ type LocalDbSessionListOptions = { fresh?: boolean; usageHistory?: boolean; }; +type LocalDbSessionListRegularOptions = Omit & { + usageHistory?: false | undefined; +}; +type LocalDbSessionListUsageOptions = Omit & { + usageHistory: true; +}; +type LocalDbSessionListResult = + | import('../renderer/lib/ccAgent.types').Session[] + | import('../renderer/lib/ccAgent.types').UsageHistorySession[]; + +function listLocalDbSessions( + limit?: number, + status?: string, + options?: LocalDbSessionListRegularOptions, +): Promise; +function listLocalDbSessions( + limit?: number, + status?: string, + options?: LocalDbSessionListUsageOptions, +): Promise; +function listLocalDbSessions( + limit?: number, + status?: string, + options?: LocalDbSessionListOptions, +): Promise; +function listLocalDbSessions( + limit?: number, + status?: string, + options?: LocalDbSessionListOptions, +): Promise { + return ipcRenderer.invoke( + 'local-db:sessions:list', + limit, + status, + options, + ) as Promise; +} /** * 个人 Telegram bot 的传输状态(与 @cindy/im 的 IMStatus 同形; preload 不引包, * 就地声明)。offline = 凭证保留但用户主动下线, 与 idle(未配置)严格区分。 @@ -5018,11 +5055,7 @@ contextBridge.exposeInMainWorld('electronAPI', { }), }, sessions: { - list: ( - limit?: number, - status?: string, - options?: LocalDbSessionListOptions, - ): Promise => ipcRenderer.invoke('local-db:sessions:list', limit, status, options), + list: listLocalDbSessions, create: (body?: unknown): Promise => ipcRenderer.invoke('local-db:sessions:create', body), get: (id: string): Promise => ipcRenderer.invoke('local-db:sessions:get', id), diff --git a/apps/desktop/src/renderer/lib/sessionService.ts b/apps/desktop/src/renderer/lib/sessionService.ts index 70746471451..8d7093f9cb1 100644 --- a/apps/desktop/src/renderer/lib/sessionService.ts +++ b/apps/desktop/src/renderer/lib/sessionService.ts @@ -6,7 +6,13 @@ * 与现有 httpClient 错误对齐。 */ -import type { OrcaRole, Session, SessionStatus, WorkspaceKind } from '@/lib/ccAgent.types'; +import type { + OrcaRole, + Session, + SessionStatus, + UsageHistorySession, + WorkspaceKind, +} from '@/lib/ccAgent.types'; import { ApiError } from '@/lib/httpClient'; import { extractIpcError } from '@/utils/ipcError'; // device-link 透明对等:fork / rewind 按 sessionId 来源路由(本机 → 本地 maker, @@ -56,11 +62,33 @@ export type SessionListOptions = { usageHistory?: boolean; }; +type SessionListRegularOptions = Omit & { + usageHistory?: false | undefined; +}; +type SessionListUsageOptions = Omit & { + usageHistory: true; +}; + +export function list( + limit?: number, + status?: ListStatusFilter, + options?: SessionListRegularOptions, +): Promise; +export function list( + limit?: number, + status?: ListStatusFilter, + options?: SessionListUsageOptions, +): Promise; +export function list( + limit?: number, + status?: ListStatusFilter, + options?: SessionListOptions, +): Promise; export async function list( limit: number = 20, status?: ListStatusFilter, options?: SessionListOptions, -): Promise { +): Promise { return wrap(window.electronAPI.localDb.sessions.list(limit, status, options)); } diff --git a/apps/desktop/src/renderer/vite-env.d.ts b/apps/desktop/src/renderer/vite-env.d.ts index 01c2f75b3e2..6fdebc86087 100644 --- a/apps/desktop/src/renderer/vite-env.d.ts +++ b/apps/desktop/src/renderer/vite-env.d.ts @@ -1144,6 +1144,24 @@ type CindyMediaPreferenceKind = { defaultModel: CindyMediaPreferenceOption | null; }; +type ElectronLocalDbSessionListOptions = { + includePinned?: boolean; + fresh?: boolean; + usageHistory?: boolean; +}; +type ElectronLocalDbSessionListRegularOptions = Omit< + ElectronLocalDbSessionListOptions, + 'usageHistory' +> & { + usageHistory?: false | undefined; +}; +type ElectronLocalDbSessionListUsageOptions = Omit< + ElectronLocalDbSessionListOptions, + 'usageHistory' +> & { + usageHistory: true; +}; + interface ElectronAPI { platform: string; /** 当前 Desktop 构建是否具备 Beta 更新渠道。 */ @@ -4372,11 +4390,26 @@ interface ElectronAPI { ) => () => void; }; sessions: { - list: ( - limit?: number, - status?: 'active' | 'archived' | 'all', - options?: { includePinned?: boolean; fresh?: boolean; usageHistory?: boolean }, - ) => Promise; + list: { + ( + limit?: number, + status?: 'active' | 'archived' | 'all', + options?: ElectronLocalDbSessionListRegularOptions, + ): Promise; + ( + limit?: number, + status?: 'active' | 'archived' | 'all', + options?: ElectronLocalDbSessionListUsageOptions, + ): Promise; + ( + limit?: number, + status?: 'active' | 'archived' | 'all', + options?: ElectronLocalDbSessionListOptions, + ): Promise< + | import('@/lib/ccAgent.types').Session[] + | import('@/lib/ccAgent.types').UsageHistorySession[] + >; + }; create: (body?: { id?: string; workingDir?: string; From c8d7794f34a9cb4b10272711cf5d6f81cc1628e0 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:56:58 +1200 Subject: [PATCH 23/30] fix(usage): keep today heatmap cell clickable Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../components/new-chat/UsageHeatmap.tsx | 5 ++++- .../__tests__/usageHeatmapMetric.test.tsx | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index cbc2cb6a9c6..d2a28da35c2 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -210,7 +210,10 @@ export function UsageHeatmap({ const cursor = new Date(start); for (let index = 0; index < visibleWeeks * 7; index += 1) { const key = toDayKey(cursor); - const placeholder = cursor > today; + // `cursor` stays at noon so DST transitions cannot skip a calendar day; + // compare the rendered day keys instead of Date instants so today's cell + // is not mistaken for a future placeholder. + const placeholder = key > todayKey; const money = spendByDay.get(key) ?? { amount: 0, currency: days[0]?.money.currency ?? EMPTY_MONEY_CURRENCY, diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index e9305ee1eac..818bea8eeb0 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -191,6 +191,22 @@ describe('UsageHeatmap metric', () => { expect(onDayClick).toHaveBeenCalledWith('2026-08-21'); }); + it('今天的日期格不是未来占位,并且可以点击', () => { + const onDayClick = vi.fn(); + const { getByRole } = render( + , + ); + + fireEvent.click(getByRole('button', { name: '2026-08-22' })); + expect(onDayClick).toHaveBeenCalledWith('2026-08-22'); + }); + it('首页非交互日期格不渲染为禁用按钮', () => { const { container, queryByRole } = render( , From 8861d8fa67a7988f06165acc8060f4308a198f4f Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:22:06 +1200 Subject: [PATCH 24/30] fix(usage): sync today chart selection Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../components/settings/usage/UsageHistorySection.tsx | 4 +++- .../settings/usage/__tests__/usageHistoryStats.test.ts | 9 +++++++++ .../components/settings/usage/usageHistoryStats.ts | 5 +++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index 67f6c9d362a..ae5a7172230 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -87,7 +87,9 @@ export function UsageHistorySection(): React.JSX.Element { const [range, setRange] = useState('30d'); const [heatmapWeeks, setHeatmapWeeks] = useState(20); - const selectedDay = usageRangeDay(range); + // The Today option is an exact-day range too: use the payload's main-side + // date anchor so both dropdown and chart entry points show the same selection. + const selectedDay = usageRangeDay(range, history?.todayKey); const rangeLabel = useMemo(() => { if (selectedDay) { const [year, month, day] = selectedDay.split('-').map(Number); diff --git a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts index 496e2bec9ee..57a36f0a0b8 100644 --- a/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts +++ b/apps/desktop/src/renderer/components/settings/usage/__tests__/usageHistoryStats.test.ts @@ -12,6 +12,7 @@ import { filterUsageHistoryPayload, isUsageHistorySingleDay, isUsageHistoryEmpty, + usageRangeDay, toUsageDays, } from '../usageHistoryStats'; import { @@ -81,6 +82,14 @@ describe('isUsageHistorySingleDay', () => { }); }); +describe('usageRangeDay', () => { + it('Today 使用 payload 的日期锚点,和图表点击当天保持同一 selectedDay', () => { + expect(usageRangeDay('today', '2026-08-22')).toBe('2026-08-22'); + expect(usageRangeDay('today')).toBeNull(); + expect(usageRangeDay('day:2026-08-20', '2026-08-22')).toBe('2026-08-20'); + }); +}); + describe('usage task session scope', () => { const readyState = (scopeKey: string): UsageSessionsState => ({ scopeKey, diff --git a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts index e2fd1617b2f..b050b685cf3 100644 --- a/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts +++ b/apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts @@ -77,7 +77,8 @@ function shiftDayKey(dayKey: string, deltaDays: number): string { return `${date.getFullYear()}-${mm}-${dd}`; } -export function usageRangeDay(range: UsageHistoryRange): string | null { +export function usageRangeDay(range: UsageHistoryRange, todayKey?: string): string | null { + if (range === 'today') return todayKey ?? null; return range.startsWith('day:') ? range.slice(4) : null; } @@ -110,7 +111,7 @@ export function filterUsageHistoryPayload( ): UsageHistoryPayload | null { if (!payload) return null; - const selectedDay = usageRangeDay(range); + const selectedDay = usageRangeDay(range, payload.todayKey); const rangeTodayKey = selectedDay ?? payload.todayKey; const modelByKey = new Map(); for (const row of payload.modelDaily) { From 504756c5012453ffa295099c5c11acba62611840 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:47:54 +1200 Subject: [PATCH 25/30] fix(usage): avoid duplicate today option Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../renderer/components/settings/usage/UsageHistorySection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx index ae5a7172230..1d615db4c60 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx @@ -182,7 +182,7 @@ export function UsageHistorySection(): React.JSX.Element { ))} - {selectedDay ? ( + {range.startsWith('day:') && selectedDay ? ( Date: Fri, 4 Sep 2026 03:35:20 +1200 Subject: [PATCH 26/30] fix(usage): distinguish loading from empty history Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../__tests__/homeUsageDashboardSource.test.ts | 10 ++++++++++ .../settings/usage/UsageHistorySection.tsx | 13 +++++++++++-- docs/design-rules/design-inventory.md | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index c743cc3c37b..fbdd7f59f67 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -210,4 +210,14 @@ describe('HomeUsageDashboard source contract', () => { expect(hookSource).toContain('allowPendingEstimates?: boolean;'); expect(hookSource).toContain('if (!scope.request.allowPendingEstimates) return scope.cache;'); }); + + it('keeps the settings usage loading state separate from a loaded empty payload', () => { + expect(usageHistorySectionSource).toContain('const loading = history === null;'); + expect(usageHistorySectionSource).toContain( + 'const empty = !loading && isUsageHistoryEmpty(history);', + ); + expect(usageHistorySectionSource).toMatch( + /\{loading \? \(\s* { setRange(value as UsageHistoryRange); @@ -199,7 +200,15 @@ export function UsageHistorySection(): React.JSX.Element {
- {empty ? ( + {loading ? ( +
+ +
+ ) : empty ? (
{t('usageHistory.empty')}
diff --git a/docs/design-rules/design-inventory.md b/docs/design-rules/design-inventory.md index 6afca0fcd17..6bba11019b5 100644 --- a/docs/design-rules/design-inventory.md +++ b/docs/design-rules/design-inventory.md @@ -37,7 +37,7 @@ | `desktop.overlay.toast` | desktop | Toast | App 常驻 ToastContainer(用户可见出口,不展开业务逻辑) | Toast, ToastContainer | apps/desktop/src/renderer/components/ui/toast/Toast.tsx, apps/desktop/src/renderer/components/ui/toast/ToastContainer.tsx, apps/desktop/src/renderer/components/ui/toast/index.ts | 4 | 4 | 2 | | `desktop.plugins.app-main` | desktop | 插件主视图 | hash `/apps/:ghostId`(GhostMainViewFeatureLayout) | GhostMainViewFeatureLayout, GhostMainViewHost | apps/desktop/src/renderer/features/plugin/GhostMainViewFeatureLayout.tsx, apps/desktop/src/renderer/features/plugin/GhostMainViewHost.tsx | 0 | 0 | 0 | | `desktop.plugins.installed` | desktop | 已装插件 | hash `/plugins`(GhostPluginPage) | AddMarketplaceDialog, GhostPluginDetailView, GhostPluginPage, MarketPluginDetailView, MyPublishesSection, PluginManagementLayout, PluginScopePicker, UpdateAllDialog | apps/desktop/src/renderer/features/plugin/AddMarketplaceDialog.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginPage.tsx, apps/desktop/src/renderer/features/plugin/MarketPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/MyPublishesSection.tsx, apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/plugin/PluginScopePicker.tsx, apps/desktop/src/renderer/features/plugin/UpdateAllDialog.tsx, apps/desktop/src/renderer/features/plugin/plugin-motion.css | 49 | 0 | 81 | -| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 726 | +| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 727 | | `desktop.shell.main-layout` | desktop | 主窗口壳(标题栏 / 左右栏 / 内容区) | main BrowserWindow → renderer/index.tsx → main-entry.tsx → App → MainLayout (`#/` 受保护壳) | ChromeActions, FeishuConflictDialogHost, GhostMediaLightboxHost, MainLayout, RightSidebar, RightSidebarShell, SessionShareImportWizard, Sidebar, UpdateNoticeDialog, WindowControls | apps/desktop/src/renderer/cindy-brain/GhostMediaLightboxHost.tsx, apps/desktop/src/renderer/components/UpdateNoticeDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialogHost.tsx, apps/desktop/src/renderer/components/layout/BrowserWebviewPool.tsx, apps/desktop/src/renderer/components/layout/ChromeActions.tsx, apps/desktop/src/renderer/components/layout/ContentHeader.tsx, apps/desktop/src/renderer/components/layout/CredentialStoreBanner.tsx, apps/desktop/src/renderer/components/layout/FadeSwitcher.tsx, apps/desktop/src/renderer/components/layout/GhostPanelWindowLayout.tsx, apps/desktop/src/renderer/components/layout/GlobalDropImportListener.tsx, apps/desktop/src/renderer/components/layout/MainLayout.tsx, apps/desktop/src/renderer/components/layout/RightSidebar.tsx, apps/desktop/src/renderer/components/layout/RightSidebarDetach.tsx, apps/desktop/src/renderer/components/layout/RightSidebarMaximize.tsx, apps/desktop/src/renderer/components/layout/RightSidebarToggle.tsx, apps/desktop/src/renderer/components/layout/SidebarWindowLayout.tsx, apps/desktop/src/renderer/components/layout/chromeActionsGeometry.ts, apps/desktop/src/renderer/components/layout/chromeActionsLayout.ts, apps/desktop/src/renderer/components/layout/railChromeActions.ts, apps/desktop/src/renderer/components/layout/windowDrag.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/sidebar/AccountSwitcherDialog.tsx, apps/desktop/src/renderer/components/sidebar/AttentionDot.tsx, apps/desktop/src/renderer/components/sidebar/GhostMainViewNavEntries.tsx, apps/desktop/src/renderer/components/sidebar/MobileDownloadDialog.tsx, apps/desktop/src/renderer/components/sidebar/Sidebar.tsx, apps/desktop/src/renderer/components/sidebar/SidebarIconButton.tsx, apps/desktop/src/renderer/components/sidebar/SidebarTopNav.tsx, apps/desktop/src/renderer/components/sidebar/SortableList.tsx, apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx, apps/desktop/src/renderer/components/sidebar/UserInfoSection.tsx, apps/desktop/src/renderer/components/sidebar/VendorIcon.tsx, apps/desktop/src/renderer/components/sidebar/VendorReadinessBadge.tsx, apps/desktop/src/renderer/components/sidebar/WorktreeBadge.tsx, apps/desktop/src/renderer/components/title-bar/ChromeIconButton.tsx, apps/desktop/src/renderer/components/title-bar/MenuButton.tsx, apps/desktop/src/renderer/components/title-bar/WindowControls.tsx, apps/desktop/src/renderer/features/right-sidebar/AddTabDropdown.tsx, apps/desktop/src/renderer/features/right-sidebar/EmptyState.tsx, apps/desktop/src/renderer/features/right-sidebar/RightSidebarShell.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBar.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBodyErrorBoundary.tsx, apps/desktop/src/renderer/features/right-sidebar/hooks/useBrowserWebview.ts, apps/desktop/src/renderer/features/right-sidebar/hooks/useNativePopupSurface.ts, apps/desktop/src/renderer/features/right-sidebar/iosSimulatorPluginAvailability.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserPartition.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserWebviewPool.ts, apps/desktop/src/renderer/features/right-sidebar/lib/detachedSidebarRouting.ts, apps/desktop/src/renderer/features/right-sidebar/lib/executeSidebarCommand.ts, apps/desktop/src/renderer/features/right-sidebar/lib/iosSimulatorFocusBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/lastTurnChangedFiles.ts, apps/desktop/src/renderer/features/right-sidebar/lib/nativePopupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openBackgroundTasksTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarFileBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openSubagentsTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTerminalShortcut.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTurnReview.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupRouter.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/rsbBrowserBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarCommands.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarHostSession.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/BackgroundTasksBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowAgentStrip.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowProgressTree.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/chatTaskFocusIntent.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/listSessionTasks.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/workflowProgressModel.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.css, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/dropExternalFile.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/fileTreeImagePreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/useSessionScopedTreeWidth.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/index.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorInstanceGrid.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/iosSimulatorH264Decoder.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersAttentionIcon.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/actions.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/closeDecision.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/ResourceUsageBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/resource-usage.css, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/subscription.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/ImageDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/MarkdownDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/PlainUnifiedDiff.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/diffRows.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/highlight.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/inlineDiff.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/useDiffHighlights.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/ReviewTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/diffExpansionPreference.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/fileTree.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/gitApplyCommand.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/markdownPreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useLastTurnFilter.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewGitState.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewSource.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/ConversationStream.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/DetailView.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/RunList.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentToolCard.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentsBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentChangeFence.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentConversation.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentFormat.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/TerminalTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/lib/xtermPool.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserCommentPopover.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/browserCommentEditorDraft.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/lib/parseOmnibox.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useBrowserComment.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useLocalHtmlAutoReload.ts, apps/desktop/src/renderer/features/right-sidebar/registry.ts, apps/desktop/src/renderer/features/right-sidebar/store.ts, apps/desktop/src/renderer/features/right-sidebar/types.ts, apps/desktop/src/renderer/layout/LayoutRoot.tsx, apps/desktop/src/renderer/layout/PanelDragController.tsx, apps/desktop/src/renderer/layout/PanelDragPrototype.tsx, apps/desktop/src/renderer/layout/collapsePrefs.ts, apps/desktop/src/renderer/layout/layoutDevTools.ts, apps/desktop/src/renderer/layout/panePlacement.tsx, apps/desktop/src/renderer/layout/paneWidths.tsx, apps/desktop/src/renderer/layout/panelMaximize.tsx, apps/desktop/src/renderer/styles/globals.css | 145 | 72 | 288 | | `desktop.skillhub.local` | desktop | SkillHub 本地技能 | hash `/skillhub/local` 及详情 `/skillhub/local/:kind/global/:name`、`/skillhub/local/:kind/project/:projectHash/:name` | InstallTargetPicker, PluginManagementLayout, SkillhubDetailView, SkillhubFeatureLayout, SkillhubHomeView, SkillhubMarketPreviewPanel | apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubDetailView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubFeatureLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubHomeView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx | 41 | 0 | 71 | | `desktop.skillhub.market` | desktop | SkillHub 市场 | hash `/skillhub/market`(SkillhubMarketListView) | InstallTargetPicker, MarketCard, MarketInfoEditDialog, SkillhubMarketListView, SkillhubMarketPreviewPanel, VisibilityEditorDialog | apps/desktop/src/renderer/features/skillhub/SkillhubMarketListView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketCard.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketInfoEditDialog.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketPreviewTree.tsx, apps/desktop/src/renderer/features/skillhub/components/PlatformTagSelector.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillCategoryFilterBar.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillIcon.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillTagList.tsx, apps/desktop/src/renderer/features/skillhub/components/TeamScopePicker.tsx, apps/desktop/src/renderer/features/skillhub/components/VisibilityEditorDialog.tsx | 46 | 0 | 59 | From 17fe3705f28bb6fc01a9e5ee433c7ed2d1ee1f74 Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:04:41 +1200 Subject: [PATCH 27/30] fix(usage): distinguish load failure from loading Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../homeUsageDashboardSource.test.ts | 23 +++++++++++++++---- .../settings/usage/UsageHistorySection.tsx | 12 ++++++++-- .../src/renderer/hooks/useUsageHistory.ts | 15 +++++++----- .../src/renderer/i18n/locales/en/common.json | 1 + .../src/renderer/i18n/locales/ja/common.json | 1 + .../src/renderer/i18n/locales/ko/common.json | 1 + .../renderer/i18n/locales/zh-CN/common.json | 1 + .../renderer/i18n/locales/zh-TW/common.json | 1 + docs/design-rules/design-inventory.md | 2 +- 9 files changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index fbdd7f59f67..7330aed1a5e 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -148,7 +148,7 @@ describe('HomeUsageDashboard source contract', () => { ); expect(hookSource).toContain('setHistoryState({ scopeKey: scopedKey, value });'); expect(hookSource).toContain( - 'history: historyState.scopeKey === scopedKey ? historyState.value : scope.cache,', + 'const history = historyState.scopeKey === scopedKey ? historyState.value : scope.cache;', ); }); @@ -212,12 +212,25 @@ describe('HomeUsageDashboard source contract', () => { }); it('keeps the settings usage loading state separate from a loaded empty payload', () => { - expect(usageHistorySectionSource).toContain('const loading = history === null;'); + const hookSource = readFileSync(resolve(__dirname, '../hooks/useUsageHistory.ts'), 'utf8'); + expect(usageHistorySectionSource).toContain( + 'const loading = history === null && refreshing;', + ); + expect(usageHistorySectionSource).toContain( + 'const loadFailed = history === null && !refreshing;', + ); expect(usageHistorySectionSource).toContain( - 'const empty = !loading && isUsageHistoryEmpty(history);', + 'const empty = history !== null && isUsageHistoryEmpty(history);', ); - expect(usageHistorySectionSource).toMatch( - /\{loading \? \(\s* { setRange(value as UsageHistoryRange); @@ -208,6 +209,13 @@ export function UsageHistorySection(): React.JSX.Element { >
+ ) : loadFailed ? ( +
+ {t('usageHistory.loadFailed')} +
) : empty ? (
{t('usageHistory.empty')} diff --git a/apps/desktop/src/renderer/hooks/useUsageHistory.ts b/apps/desktop/src/renderer/hooks/useUsageHistory.ts index c2a178c5571..85abf9953d6 100644 --- a/apps/desktop/src/renderer/hooks/useUsageHistory.ts +++ b/apps/desktop/src/renderer/hooks/useUsageHistory.ts @@ -518,7 +518,10 @@ export function useUsageHistory(opts?: { scopeKey: scopedKey, value: scope.cache, })); - const [isRefreshing, setIsRefreshing] = useState(scope.refreshing); + // A null payload is pending until the first request settles. Keeping that + // invariant in the hook lets consumers distinguish initial loading from a + // settled load failure without adding a second lifecycle state owner. + const [isRefreshing, setIsRefreshing] = useState(scope.cache === null || scope.refreshing); useEffect(() => { const activeScope = getScope(scopedKey, request); @@ -531,10 +534,10 @@ export function useUsageHistory(opts?: { // load() 在 render 与本 effect 之间更新了 cache, 且后续 load 都命中 dirty-check // (不再广播), 本实例会停在旧值。引用相同时 React 自动 bail out, 无重渲染成本。 setScopedHistory(activeScope.cache); - setIsRefreshing(activeScope.refreshing); // mount 与 paused→false (展开瞬间) 都拉一次 — 折叠摘要 / 图表数据都保持新鲜。 // 收起时跳过后续 push 订阅, 避免每个 turn 触发后台重拉。 void load(scopedKey); + setIsRefreshing(activeScope.refreshing); // paused (卡片收起): 不订阅消费推送, 收起用户的 per-turn 重拉成本归零 if (paused) { @@ -569,8 +572,8 @@ export function useUsageHistory(opts?: { }; }, [paused, scopedKey]); - return { - history: historyState.scopeKey === scopedKey ? historyState.value : scope.cache, - refreshing: isRefreshing, - }; + const history = historyState.scopeKey === scopedKey ? historyState.value : scope.cache; + const refreshing = + historyState.scopeKey === scopedKey ? isRefreshing : scope.cache === null || scope.refreshing; + return { history, refreshing }; } diff --git a/apps/desktop/src/renderer/i18n/locales/en/common.json b/apps/desktop/src/renderer/i18n/locales/en/common.json index 742e8c74d10..90d96785363 100644 --- a/apps/desktop/src/renderer/i18n/locales/en/common.json +++ b/apps/desktop/src/renderer/i18n/locales/en/common.json @@ -5795,6 +5795,7 @@ "usageHistory": { "description": "Token usage produced inside Cindy on this device. Records start accumulating when the feature ships; earlier history is not backfilled.", "empty": "No usage recorded yet. This fills in once you start a chat.", + "loadFailed": "Unable to load usage history. Reopen Settings to try again.", "range": { "label": "Time range", "ariaLabel": "Filter usage time range", diff --git a/apps/desktop/src/renderer/i18n/locales/ja/common.json b/apps/desktop/src/renderer/i18n/locales/ja/common.json index fdb51486269..d410a9c8dbb 100644 --- a/apps/desktop/src/renderer/i18n/locales/ja/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ja/common.json @@ -5793,6 +5793,7 @@ "usageHistory": { "description": "このデバイスの Cindy 内で発生したトークン使用量です。記録は機能の提供開始時から蓄積され、それ以前の履歴は補完されません。", "empty": "まだ使用記録がありません。会話を実行すると蓄積が始まります。", + "loadFailed": "使用履歴を読み込めませんでした。設定を開き直して、もう一度お試しください。", "range": { "label": "期間", "ariaLabel": "使用量の期間を絞り込む", diff --git a/apps/desktop/src/renderer/i18n/locales/ko/common.json b/apps/desktop/src/renderer/i18n/locales/ko/common.json index 153d17e8b2b..af2be802968 100644 --- a/apps/desktop/src/renderer/i18n/locales/ko/common.json +++ b/apps/desktop/src/renderer/i18n/locales/ko/common.json @@ -5793,6 +5793,7 @@ "usageHistory": { "description": "이 기기의 Cindy 내부에서 발생한 토큰 사용량입니다. 기능 출시 시점부터 기록이 쌓이며 이전 기록은 채워지지 않습니다.", "empty": "아직 사용 기록이 없습니다. 대화를 실행하면 쌓이기 시작합니다.", + "loadFailed": "사용량 기록을 불러오지 못했습니다. 설정을 다시 열어 재시도해 주세요.", "range": { "label": "기간", "ariaLabel": "사용량 기간 필터", diff --git a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json index 69450e3aa2e..e1d060e7bc5 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-CN/common.json @@ -5793,6 +5793,7 @@ "usageHistory": { "description": "这台设备上 Cindy 内部产生的 token 用量。记录从该功能上线起积累,不回填更早的历史。", "empty": "还没有用量记录。跑过对话之后,这里会开始积累。", + "loadFailed": "无法加载用量历史。请重新打开设置后重试。", "range": { "label": "时间范围", "ariaLabel": "筛选用量时间范围", diff --git a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json index 93f1ae81523..0d845e3df82 100644 --- a/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json +++ b/apps/desktop/src/renderer/i18n/locales/zh-TW/common.json @@ -5793,6 +5793,7 @@ "usageHistory": { "description": "這台裝置上 Cindy 內部產生的 token 用量。紀錄從該功能上線起累積,不回填更早的歷史。", "empty": "還沒有用量紀錄。跑過對話之後,這裡會開始累積。", + "loadFailed": "無法載入用量歷史。請重新開啟設定後再試一次。", "range": { "label": "時間範圍", "ariaLabel": "篩選用量時間範圍", diff --git a/docs/design-rules/design-inventory.md b/docs/design-rules/design-inventory.md index 6bba11019b5..46af53cf9bb 100644 --- a/docs/design-rules/design-inventory.md +++ b/docs/design-rules/design-inventory.md @@ -37,7 +37,7 @@ | `desktop.overlay.toast` | desktop | Toast | App 常驻 ToastContainer(用户可见出口,不展开业务逻辑) | Toast, ToastContainer | apps/desktop/src/renderer/components/ui/toast/Toast.tsx, apps/desktop/src/renderer/components/ui/toast/ToastContainer.tsx, apps/desktop/src/renderer/components/ui/toast/index.ts | 4 | 4 | 2 | | `desktop.plugins.app-main` | desktop | 插件主视图 | hash `/apps/:ghostId`(GhostMainViewFeatureLayout) | GhostMainViewFeatureLayout, GhostMainViewHost | apps/desktop/src/renderer/features/plugin/GhostMainViewFeatureLayout.tsx, apps/desktop/src/renderer/features/plugin/GhostMainViewHost.tsx | 0 | 0 | 0 | | `desktop.plugins.installed` | desktop | 已装插件 | hash `/plugins`(GhostPluginPage) | AddMarketplaceDialog, GhostPluginDetailView, GhostPluginPage, MarketPluginDetailView, MyPublishesSection, PluginManagementLayout, PluginScopePicker, UpdateAllDialog | apps/desktop/src/renderer/features/plugin/AddMarketplaceDialog.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/GhostPluginPage.tsx, apps/desktop/src/renderer/features/plugin/MarketPluginDetailView.tsx, apps/desktop/src/renderer/features/plugin/MyPublishesSection.tsx, apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/plugin/PluginScopePicker.tsx, apps/desktop/src/renderer/features/plugin/UpdateAllDialog.tsx, apps/desktop/src/renderer/features/plugin/plugin-motion.css | 49 | 0 | 81 | -| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 727 | +| `desktop.settings` | desktop | 设置 | hash `/settings`(SettingsView;tab 含 general / personalization / providers / billing / usage / voice-input / im-bot / shortcuts / agent-island / import / remote-control / ghosts / builtin-tools / computer-use / help / about) | SettingsView | apps/desktop/src/renderer/components/settings/AboutSection.tsx, apps/desktop/src/renderer/components/settings/AccountDeletionSection.tsx, apps/desktop/src/renderer/components/settings/AddProviderWizard.tsx, apps/desktop/src/renderer/components/settings/AgentIslandSection.tsx, apps/desktop/src/renderer/components/settings/AgentResourceSection.tsx, apps/desktop/src/renderer/components/settings/AppearanceSection.tsx, apps/desktop/src/renderer/components/settings/AuxiliaryModelSection.tsx, apps/desktop/src/renderer/components/settings/BetaChannelCell.tsx, apps/desktop/src/renderer/components/settings/BrowserBackendSubsection.tsx, apps/desktop/src/renderer/components/settings/BrowserRealProfileSubsection.tsx, apps/desktop/src/renderer/components/settings/BuiltinToolsSection.tsx, apps/desktop/src/renderer/components/settings/ChatEmbeddingCell.tsx, apps/desktop/src/renderer/components/settings/ChipMetricsSection.tsx, apps/desktop/src/renderer/components/settings/CollaborationSection.tsx, apps/desktop/src/renderer/components/settings/CompactionSection.tsx, apps/desktop/src/renderer/components/settings/ComposerSendShortcutSection.tsx, apps/desktop/src/renderer/components/settings/ComputerPermissionGuideWindow.tsx, apps/desktop/src/renderer/components/settings/ComputerUseSection.tsx, apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx, apps/desktop/src/renderer/components/settings/CustomProviderRuntimeFillOverlay.tsx, apps/desktop/src/renderer/components/settings/DefaultOverrideControls.tsx, apps/desktop/src/renderer/components/settings/DingTalkBotSection.tsx, apps/desktop/src/renderer/components/settings/DiscordBotSection.tsx, apps/desktop/src/renderer/components/settings/DownloadMeter.tsx, apps/desktop/src/renderer/components/settings/ExperimentalSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotNotificationSection.tsx, apps/desktop/src/renderer/components/settings/FeishuBotSection.tsx, apps/desktop/src/renderer/components/settings/FontFamilyPicker.tsx, apps/desktop/src/renderer/components/settings/GenericGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/GitSafetySection.tsx, apps/desktop/src/renderer/components/settings/HelpAssistantPanel.tsx, apps/desktop/src/renderer/components/settings/HelpSection.tsx, apps/desktop/src/renderer/components/settings/HelpThreadView.tsx, apps/desktop/src/renderer/components/settings/HookConnectionsSection.tsx, apps/desktop/src/renderer/components/settings/HookWorkspacePrefsEditor.tsx, apps/desktop/src/renderer/components/settings/ImBotSection.tsx, apps/desktop/src/renderer/components/settings/ImChannelSettingsCard.tsx, apps/desktop/src/renderer/components/settings/ImDefaultSettingsSection.tsx, apps/desktop/src/renderer/components/settings/ImLifecycleAnnouncementSection.tsx, apps/desktop/src/renderer/components/settings/InputDeviceConnectionStatus.tsx, apps/desktop/src/renderer/components/settings/JoyConGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/KeyboardShortcutsSection.tsx, apps/desktop/src/renderer/components/settings/LanguageSection.tsx, apps/desktop/src/renderer/components/settings/LayoutResetControl.tsx, apps/desktop/src/renderer/components/settings/LinkOpenSection.tsx, apps/desktop/src/renderer/components/settings/LocalOllamaInstall.tsx, apps/desktop/src/renderer/components/settings/LocalPackagingTag.tsx, apps/desktop/src/renderer/components/settings/LogoutSection.tsx, apps/desktop/src/renderer/components/settings/LspBetaCell.tsx, apps/desktop/src/renderer/components/settings/McpServerDialog.tsx, apps/desktop/src/renderer/components/settings/McpServersSection.tsx, apps/desktop/src/renderer/components/settings/MemorySection.tsx, apps/desktop/src/renderer/components/settings/MessageNavRailCell.tsx, apps/desktop/src/renderer/components/settings/ModelPriceOverrideDialog.tsx, apps/desktop/src/renderer/components/settings/MyDevicesPanel.tsx, apps/desktop/src/renderer/components/settings/NotificationSection.tsx, apps/desktop/src/renderer/components/settings/OAuthDeviceCodeCard.tsx, apps/desktop/src/renderer/components/settings/OllamaProviderDetail.tsx, apps/desktop/src/renderer/components/settings/PiPackagesSection.tsx, apps/desktop/src/renderer/components/settings/PlayStationGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/ProfileEditDialog.tsx, apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, apps/desktop/src/renderer/components/settings/RemoteControlSection.tsx, apps/desktop/src/renderer/components/settings/RemoteHostDetail.tsx, apps/desktop/src/renderer/components/settings/RemoteSection.tsx, apps/desktop/src/renderer/components/settings/SessionImportSection.tsx, apps/desktop/src/renderer/components/settings/SessionRuntimeFallbackCell.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/settings/SettingsCatalogPanel.tsx, apps/desktop/src/renderer/components/settings/SettingsSidebarNav.tsx, apps/desktop/src/renderer/components/settings/SettingsTextInput.tsx, apps/desktop/src/renderer/components/settings/SettingsView.tsx, apps/desktop/src/renderer/components/settings/SilentEncryptedRetryCell.tsx, apps/desktop/src/renderer/components/settings/SshKeySetupDialog.tsx, apps/desktop/src/renderer/components/settings/StorageManagementCard.tsx, apps/desktop/src/renderer/components/settings/StreamFadeSection.tsx, apps/desktop/src/renderer/components/settings/SubagentModelSection.tsx, apps/desktop/src/renderer/components/settings/SwitchProGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/TelegramBehaviorSettings.tsx, apps/desktop/src/renderer/components/settings/TelegramBotSection.tsx, apps/desktop/src/renderer/components/settings/TelegramRemoteDevices.tsx, apps/desktop/src/renderer/components/settings/TerminalShellSection.tsx, apps/desktop/src/renderer/components/settings/TipsSection.tsx, apps/desktop/src/renderer/components/settings/UnifiedModelList.tsx, apps/desktop/src/renderer/components/settings/UserProfileCard.tsx, apps/desktop/src/renderer/components/settings/UserPromptSection.tsx, apps/desktop/src/renderer/components/settings/VisionBridgeSection.tsx, apps/desktop/src/renderer/components/settings/VoiceInputSection.tsx, apps/desktop/src/renderer/components/settings/WechatBotSection.tsx, apps/desktop/src/renderer/components/settings/WecomBotSection.tsx, apps/desktop/src/renderer/components/settings/WindowBehaviorSection.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeyboardLayout.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexKeycapGlyphs.tsx, apps/desktop/src/renderer/components/settings/WorkLouderCodexSettings.tsx, apps/desktop/src/renderer/components/settings/XUsageGuide.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadLayout.tsx, apps/desktop/src/renderer/components/settings/XboxGamepadSettings.tsx, apps/desktop/src/renderer/components/settings/androidStatusPresentation.ts, apps/desktop/src/renderer/components/settings/billingVisibility.ts, apps/desktop/src/renderer/components/settings/browserOpenForLoginError.ts, apps/desktop/src/renderer/components/settings/computerPermissionFlow.ts, apps/desktop/src/renderer/components/settings/contacts/ContactDetailPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsImportDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsListPane.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsManagerDialog.tsx, apps/desktop/src/renderer/components/settings/contacts/ContactsSection.tsx, apps/desktop/src/renderer/components/settings/contacts/startContactsAiSession.ts, apps/desktop/src/renderer/components/settings/dualSenseSilhouette.ts, apps/desktop/src/renderer/components/settings/feishuBotPresentation.ts, apps/desktop/src/renderer/components/settings/fontFamilyValue.ts, apps/desktop/src/renderer/components/settings/gamepadLayoutPrimitives.tsx, apps/desktop/src/renderer/components/settings/gamepadSilhouetteGeom.ts, apps/desktop/src/renderer/components/settings/hookWorkspacePrefsLogic.ts, apps/desktop/src/renderer/components/settings/imBotVisibility.ts, apps/desktop/src/renderer/components/settings/imDefaultSettingsLogic.ts, apps/desktop/src/renderer/components/settings/joyConSilhouette.ts, apps/desktop/src/renderer/components/settings/myDevicesModel.ts, apps/desktop/src/renderer/components/settings/providerAssetModule.ts, apps/desktop/src/renderer/components/settings/realProfilePermissionGuide.ts, apps/desktop/src/renderer/components/settings/switchProSilhouette.ts, apps/desktop/src/renderer/components/settings/ultimateC1Silhouette.ts, apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx, apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx, apps/desktop/src/renderer/components/settings/usage/UsageStatRow.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTaskTable.tsx, apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx, apps/desktop/src/renderer/components/settings/usage/formatUsagePercent.ts, apps/desktop/src/renderer/components/settings/usage/usageHistoryStats.ts, apps/desktop/src/renderer/components/settings/usageVisibility.ts, apps/desktop/src/renderer/components/settings/wizardRecommend.ts, apps/desktop/src/renderer/components/settings/workLouderCodexCommandCopy.ts, apps/desktop/src/renderer/components/settings/xboxSeriesSilhouette.ts, apps/desktop/src/renderer/styles/sortable.css | 143 | 121 | 728 | | `desktop.shell.main-layout` | desktop | 主窗口壳(标题栏 / 左右栏 / 内容区) | main BrowserWindow → renderer/index.tsx → main-entry.tsx → App → MainLayout (`#/` 受保护壳) | ChromeActions, FeishuConflictDialogHost, GhostMediaLightboxHost, MainLayout, RightSidebar, RightSidebarShell, SessionShareImportWizard, Sidebar, UpdateNoticeDialog, WindowControls | apps/desktop/src/renderer/cindy-brain/GhostMediaLightboxHost.tsx, apps/desktop/src/renderer/components/UpdateNoticeDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialog.tsx, apps/desktop/src/renderer/components/feishuBot/FeishuConflictDialogHost.tsx, apps/desktop/src/renderer/components/layout/BrowserWebviewPool.tsx, apps/desktop/src/renderer/components/layout/ChromeActions.tsx, apps/desktop/src/renderer/components/layout/ContentHeader.tsx, apps/desktop/src/renderer/components/layout/CredentialStoreBanner.tsx, apps/desktop/src/renderer/components/layout/FadeSwitcher.tsx, apps/desktop/src/renderer/components/layout/GhostPanelWindowLayout.tsx, apps/desktop/src/renderer/components/layout/GlobalDropImportListener.tsx, apps/desktop/src/renderer/components/layout/MainLayout.tsx, apps/desktop/src/renderer/components/layout/RightSidebar.tsx, apps/desktop/src/renderer/components/layout/RightSidebarDetach.tsx, apps/desktop/src/renderer/components/layout/RightSidebarMaximize.tsx, apps/desktop/src/renderer/components/layout/RightSidebarToggle.tsx, apps/desktop/src/renderer/components/layout/SidebarWindowLayout.tsx, apps/desktop/src/renderer/components/layout/chromeActionsGeometry.ts, apps/desktop/src/renderer/components/layout/chromeActionsLayout.ts, apps/desktop/src/renderer/components/layout/railChromeActions.ts, apps/desktop/src/renderer/components/layout/windowDrag.tsx, apps/desktop/src/renderer/components/settings/SessionShareImportWizard.tsx, apps/desktop/src/renderer/components/sidebar/AccountSwitcherDialog.tsx, apps/desktop/src/renderer/components/sidebar/AttentionDot.tsx, apps/desktop/src/renderer/components/sidebar/GhostMainViewNavEntries.tsx, apps/desktop/src/renderer/components/sidebar/MobileDownloadDialog.tsx, apps/desktop/src/renderer/components/sidebar/Sidebar.tsx, apps/desktop/src/renderer/components/sidebar/SidebarIconButton.tsx, apps/desktop/src/renderer/components/sidebar/SidebarTopNav.tsx, apps/desktop/src/renderer/components/sidebar/SortableList.tsx, apps/desktop/src/renderer/components/sidebar/UpdateBanner.tsx, apps/desktop/src/renderer/components/sidebar/UserInfoSection.tsx, apps/desktop/src/renderer/components/sidebar/VendorIcon.tsx, apps/desktop/src/renderer/components/sidebar/VendorReadinessBadge.tsx, apps/desktop/src/renderer/components/sidebar/WorktreeBadge.tsx, apps/desktop/src/renderer/components/title-bar/ChromeIconButton.tsx, apps/desktop/src/renderer/components/title-bar/MenuButton.tsx, apps/desktop/src/renderer/components/title-bar/WindowControls.tsx, apps/desktop/src/renderer/features/right-sidebar/AddTabDropdown.tsx, apps/desktop/src/renderer/features/right-sidebar/EmptyState.tsx, apps/desktop/src/renderer/features/right-sidebar/RightSidebarShell.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBar.tsx, apps/desktop/src/renderer/features/right-sidebar/TabBodyErrorBoundary.tsx, apps/desktop/src/renderer/features/right-sidebar/hooks/useBrowserWebview.ts, apps/desktop/src/renderer/features/right-sidebar/hooks/useNativePopupSurface.ts, apps/desktop/src/renderer/features/right-sidebar/iosSimulatorPluginAvailability.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserPartition.ts, apps/desktop/src/renderer/features/right-sidebar/lib/browserWebviewPool.ts, apps/desktop/src/renderer/features/right-sidebar/lib/detachedSidebarRouting.ts, apps/desktop/src/renderer/features/right-sidebar/lib/executeSidebarCommand.ts, apps/desktop/src/renderer/features/right-sidebar/lib/iosSimulatorFocusBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/lastTurnChangedFiles.ts, apps/desktop/src/renderer/features/right-sidebar/lib/nativePopupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openBackgroundTasksTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openInSidebarFileBrowser.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openSubagentsTab.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTerminalShortcut.ts, apps/desktop/src/renderer/features/right-sidebar/lib/openTurnReview.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupRouter.ts, apps/desktop/src/renderer/features/right-sidebar/lib/popupTabs.ts, apps/desktop/src/renderer/features/right-sidebar/lib/rsbBrowserBridge.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarCommands.ts, apps/desktop/src/renderer/features/right-sidebar/lib/sidebarHostSession.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/BackgroundTasksBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowAgentStrip.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/WorkflowProgressTree.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/chatTaskFocusIntent.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/listSessionTasks.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/background-tasks/workflowProgressModel.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.css, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/FileBrowserBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/dropExternalFile.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/fileTreeImagePreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/file-browser/useSessionScopedTreeWidth.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/index.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorInstanceGrid.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/IOSSimulatorTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/ios-simulator/iosSimulatorH264Decoder.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersAttentionIcon.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/OrcaWorkersTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/actions.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/closeDecision.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/orca-workers/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/ResourceUsageBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/resource-usage.css, apps/desktop/src/renderer/features/right-sidebar/plugins/resource-usage/subscription.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/ImageDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/MarkdownDiffPreview.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/PlainUnifiedDiff.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/diffRows.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/highlight.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/inlineDiff.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/DiffViewer/useDiffHighlights.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/ReviewTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/diffExpansionPreference.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/fileTree.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/gitApplyCommand.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/review/markdownPreview.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useLastTurnFilter.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewGitState.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/review/useReviewSource.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/ConversationStream.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/DetailView.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/RunList.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentToolCard.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/SubagentsBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentChangeFence.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentConversation.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/subagents/subagentFormat.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/TerminalTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/terminal/lib/xtermPool.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserChrome.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserCommentPopover.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/BrowserTabBody.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/browserCommentEditorDraft.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/index.tsx, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/lib/parseOmnibox.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useBrowserComment.ts, apps/desktop/src/renderer/features/right-sidebar/plugins/web-browser/useLocalHtmlAutoReload.ts, apps/desktop/src/renderer/features/right-sidebar/registry.ts, apps/desktop/src/renderer/features/right-sidebar/store.ts, apps/desktop/src/renderer/features/right-sidebar/types.ts, apps/desktop/src/renderer/layout/LayoutRoot.tsx, apps/desktop/src/renderer/layout/PanelDragController.tsx, apps/desktop/src/renderer/layout/PanelDragPrototype.tsx, apps/desktop/src/renderer/layout/collapsePrefs.ts, apps/desktop/src/renderer/layout/layoutDevTools.ts, apps/desktop/src/renderer/layout/panePlacement.tsx, apps/desktop/src/renderer/layout/paneWidths.tsx, apps/desktop/src/renderer/layout/panelMaximize.tsx, apps/desktop/src/renderer/styles/globals.css | 145 | 72 | 288 | | `desktop.skillhub.local` | desktop | SkillHub 本地技能 | hash `/skillhub/local` 及详情 `/skillhub/local/:kind/global/:name`、`/skillhub/local/:kind/project/:projectHash/:name` | InstallTargetPicker, PluginManagementLayout, SkillhubDetailView, SkillhubFeatureLayout, SkillhubHomeView, SkillhubMarketPreviewPanel | apps/desktop/src/renderer/features/plugin/PluginManagementLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubDetailView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubFeatureLayout.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubHomeView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx | 41 | 0 | 71 | | `desktop.skillhub.market` | desktop | SkillHub 市场 | hash `/skillhub/market`(SkillhubMarketListView) | InstallTargetPicker, MarketCard, MarketInfoEditDialog, SkillhubMarketListView, SkillhubMarketPreviewPanel, VisibilityEditorDialog | apps/desktop/src/renderer/features/skillhub/SkillhubMarketListView.tsx, apps/desktop/src/renderer/features/skillhub/SkillhubMarketPreviewPanel.tsx, apps/desktop/src/renderer/features/skillhub/components/InstallTargetPicker.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketCard.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketInfoEditDialog.tsx, apps/desktop/src/renderer/features/skillhub/components/MarketPreviewTree.tsx, apps/desktop/src/renderer/features/skillhub/components/PlatformTagSelector.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillCategoryFilterBar.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillIcon.tsx, apps/desktop/src/renderer/features/skillhub/components/SkillTagList.tsx, apps/desktop/src/renderer/features/skillhub/components/TeamScopePicker.tsx, apps/desktop/src/renderer/features/skillhub/components/VisibilityEditorDialog.tsx | 46 | 0 | 59 | From 96ffc933c81395c787dc383a8752b57d1e817e8c Mon Sep 17 00:00:00 2001 From: Dash <125997726+dashhuang@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:50:31 +1200 Subject: [PATCH 28/30] fix(usage): complete interactive chart semantics Signed-off-by: Dash <125997726+dashhuang@users.noreply.github.com> --- .../homeUsageDashboardSource.test.ts | 23 ++++++++++---- .../components/new-chat/UsageHeatmap.tsx | 30 +++++++++--------- .../__tests__/usageHeatmapMetric.test.tsx | 24 +++++++++++--- .../settings/usage/UsageHistorySection.tsx | 10 ++---- .../settings/usage/UsageTokenBars.tsx | 31 +++++++++++++------ 5 files changed, 77 insertions(+), 41 deletions(-) diff --git a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts index 7330aed1a5e..583511018fa 100644 --- a/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts +++ b/apps/desktop/src/renderer/__tests__/homeUsageDashboardSource.test.ts @@ -213,9 +213,7 @@ describe('HomeUsageDashboard source contract', () => { it('keeps the settings usage loading state separate from a loaded empty payload', () => { const hookSource = readFileSync(resolve(__dirname, '../hooks/useUsageHistory.ts'), 'utf8'); - expect(usageHistorySectionSource).toContain( - 'const loading = history === null && refreshing;', - ); + expect(usageHistorySectionSource).toContain('const loading = history === null && refreshing;'); expect(usageHistorySectionSource).toContain( 'const loadFailed = history === null && !refreshing;', ); @@ -223,9 +221,7 @@ describe('HomeUsageDashboard source contract', () => { 'const empty = history !== null && isUsageHistoryEmpty(history);', ); expect(usageHistorySectionSource).toContain("t('usageHistory.loadFailed')"); - expect(hookSource).toContain( - 'useState(scope.cache === null || scope.refreshing)', - ); + expect(hookSource).toContain('useState(scope.cache === null || scope.refreshing)'); expect(hookSource.indexOf('void load(scopedKey);')).toBeLessThan( hookSource.indexOf('setIsRefreshing(activeScope.refreshing);'), ); @@ -233,4 +229,19 @@ describe('HomeUsageDashboard source contract', () => { 'historyState.scopeKey === scopedKey ? isRefreshing : scope.cache === null || scope.refreshing;', ); }); + + it('keeps interactive usage controls discoverable and semantically selected', () => { + expect(usageHistorySectionSource).toContain( + 'data-[state=checked]:bg-[var(--settings-menu-bg-selected)]', + ); + expect(usageHistorySectionSource).toContain( + 'data-[state=checked]:text-[var(--settings-menu-text-selected)]', + ); + expect(heatmapSource).toContain('const accessibleLabel ='); + expect(heatmapSource).toContain('aria-label={accessibleLabel}'); + expect(tokenBarsSource).toContain('function parseDayKeyLocal(dayKey: string): Date'); + expect(tokenBarsSource).toContain( + 'aria-label={`${dateFormatter.format(parseDayKeyLocal(b.day))}', + ); + }); }); diff --git a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx index d2a28da35c2..8c05ee5776d 100644 --- a/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx +++ b/apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx @@ -105,10 +105,7 @@ export function resolveHeatmapWeeks({ metric === 'tokens' ? (day.tokens ?? 0) : (day.money?.amount ?? 0); const earliestDay = days .filter( - (day) => - day.day && - day.day <= todayKey && - (metric === undefined || intensityOf(day) > 0), + (day) => day.day && day.day <= todayKey && (metric === undefined || intensityOf(day) > 0), ) .map((day) => day.day) .sort()[0]; @@ -152,6 +149,11 @@ export function UsageHeatmap({ const { t, i18n } = useTranslation(); const plotRef = useRef(null); const [availableWidth, setAvailableWidth] = useState(0); + const dateFormatter = useMemo( + () => + new Intl.DateTimeFormat(i18n.language, { year: 'numeric', month: 'short', day: 'numeric' }), + [i18n.language], + ); useLayoutEffect(() => { const element = plotRef.current; @@ -272,20 +274,20 @@ export function UsageHeatmap({ return
; } - const title = + const usageSummary = metric === 'tokens' - ? `${cell.day} · ${ - cell.tokens > 0 - ? t('usageDashboard.tokensOnly', { - tokens: formatCompactTokens(cell.tokens), - }) - : t('usageHistory.heatmap.emptyCell') - }` - : `${cell.day} · ${formatMoney(cell.money)}${ + ? cell.tokens > 0 + ? t('usageDashboard.tokensOnly', { + tokens: formatCompactTokens(cell.tokens), + }) + : t('usageHistory.heatmap.emptyCell') + : `${formatMoney(cell.money)}${ cell.tokens > 0 ? ` · ${t('usageDashboard.tokensOnly', { tokens: formatCompactTokens(cell.tokens) })}` : '' }`; + const title = `${cell.day} · ${usageSummary}`; + const accessibleLabel = `${dateFormatter.format(parseDayKey(cell.day))} · ${usageSummary}`; const className = onDayClick ? 'rounded-full' : 'rounded-[3px]'; const style = { width: CELL_PX, @@ -304,7 +306,7 @@ export function UsageHeatmap({ diff --git a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx index a896c69e326..e6d3c5fa145 100644 --- a/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx +++ b/apps/desktop/src/renderer/components/new-chat/__tests__/usageHeatmapMetric.test.tsx @@ -191,6 +191,25 @@ describe('UsageHeatmap metric', () => { expect(onDayClick).toHaveBeenCalledWith('2026-08-21'); }); + it('可点击日期格保留小视觉标记但提供 24px 命中区域', () => { + const { getByRole } = render( + , + ); + + const button = getByRole('button', { name: /Aug 21, 2026/ }); + const visual = button.firstElementChild as HTMLElement; + expect(button.style.width).toBe('24px'); + expect(button.style.height).toBe('24px'); + expect(visual.style.width).toBe('12px'); + expect(visual.style.height).toBe('12px'); + }); + it('今天的日期格不是未来占位,并且可以点击', () => { const onDayClick = vi.fn(); const { getByRole } = render( diff --git a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx index a90109b96a5..068e490d3e1 100644 --- a/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx +++ b/apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx @@ -154,7 +154,9 @@ export function UsageTokenBars({
{bars.list.map((b) => { const ratio = bars.max > 0 ? b.tokens / bars.max : 0; - const height = b.tokens > 0 ? Math.max(3, Math.round(ratio * CHART_HEIGHT_PX)) : 2; + const visualHeight = + b.tokens > 0 ? Math.max(3, Math.round(ratio * CHART_HEIGHT_PX)) : 2; + const hitHeight = Math.max(24, visualHeight); const usageSummary = b.tokens > 0 ? t('usageDashboard.tokensOnly', { tokens: formatCompactTokens(b.tokens) }) @@ -178,23 +180,31 @@ export function UsageTokenBars({ onClick={() => onDayClick?.(b.day)} disabled={!onDayClick} // 列容器只负责高度与圆角裁切; 分段自上而下 = rank 降序 ("其它"在顶, 大头在底) - className="flex min-w-0 flex-1 cursor-pointer flex-col justify-end overflow-hidden rounded-full border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" + className="flex min-w-0 flex-1 cursor-pointer items-end justify-center rounded-full border-0 bg-transparent p-0 outline-none focus-visible:ring-2 focus-visible:ring-[var(--focus-ring-soft)]" style={{ - height, - backgroundColor: b.segments.length === 0 ? 'var(--surface-chip)' : undefined, + height: hitHeight, outline: selectedDay === b.day ? '2px solid var(--focus-ring-soft)' : undefined, outlineOffset: selectedDay === b.day ? '1px' : undefined, }} > - {[...b.segments].reverse().map((s) => ( -
- ))} +