diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b728db6..c2260486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Work status: the session cost now counts what its subagents spent, with a line under the context meter splitting the session's own cost from the subagents' share, and each subagent's cost shown next to it in the Subagents list. Previously a session that delegated most of its work looked far cheaper than it was. - Files: opening a file over 5,000 lines is no longer blocked — the line-count guard now allows up to 20,000 lines, letting large files reach the virtualized full-file preview instead of being rejected at the open step (thanks @gaojunran). - Settings: fixed the Cloudflare Tunnel download link shown when cloudflared is not installed (thanks to @AyoubAchour). diff --git a/packages/ui/src/components/chat/work-status/DOCUMENTATION.md b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md index 0c310f50..f23be5cc 100644 --- a/packages/ui/src/components/chat/work-status/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/work-status/DOCUMENTATION.md @@ -95,11 +95,11 @@ which requests only providers enabled for this panel. | Block | Source | Notes | |---|---|---| -| Context + cost | `contextUsage.ts` over `useSessionMessages`, `Session.cost` | see below — the store getters cannot serve this | +| Context + cost | `contextUsage.ts` over `useSessionMessages`; cost via `useSubagentCostRollup` (own cost + every descendant subagent, recursively) | see below — the store getters cannot serve this | | Branch, ahead/behind, attention | `useGitStore` directory state | warmed via `runBackgroundNetworkTask(ensureStatus)` and refreshed from Git mutation hints | | Changed files | `useGitStore` status `files` + `diffStats` | working tree, not session-authored edits | | PR + checks | `useFreshestPrVisualSummaryForBranch` | **read-only**; follows the freshest remote-keyed entry for the branch | -| Subagents | child sessions from `useAllLiveSessions` (`parentID`) + `useAllSessionStatuses` | | +| Subagents | child sessions from `useAllLiveSessions` (`parentID`) + `useAllSessionStatuses`; per-row cost from `useSubagentCostRollup`'s `perChildCost` (each child's own subtree total, so nested subagent-of-subagent cost rolls up under its immediate parent row) | | | Subagent blockers | directory `permission` / `question` maps | one subscription covers every child | | Usage | `components/usage/usageGroups.ts` over `useQuotaStore` | grouping shared with the mobile popover; presentation is not | | Linked threads | `lib/linkedIssues.ts` over session metadata | written by the flows that attach an issue or PR | diff --git a/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx b/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx index 4db88c6c..bfe046cf 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx @@ -4,7 +4,7 @@ import { useGitStore } from '@/stores/useGitStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { runBackgroundNetworkTask } from '@/lib/background-network'; import { useFreshestPrVisualSummaryForBranch } from '@/stores/useGitHubPrStatusStore'; -import { useSession, useSessionMessages } from '@/sync/sync-context'; +import { useSessionMessages } from '@/sync/sync-context'; import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -14,6 +14,8 @@ import { resolveUsageTone } from '@/lib/quota'; import { sessionEvents } from '@/lib/sessionEvents'; import { normalizePath } from '@/lib/pathNormalization'; import { computeContextUsage } from './contextUsage'; +import { formatCost } from './subagentCost'; +import { useSubagentCostRollup } from './useSubagentCostRollup'; import { WorkStatusCallout, WorkStatusMeter, @@ -33,11 +35,6 @@ type Props = { showRepository: boolean; }; -// Spend is read against a budget, so it keeps its real precision instead of -// collapsing to two decimals. Trailing zeros are dropped so exact values stay -// short. -const trimZeros = (value: string): string => (value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value); -const formatCost = (cost: number): string => `$${trimZeros(cost.toFixed(4))}`; // Matches the header readout exactly: one decimal, capped the same way, so the // two places that report context fill never disagree by a rounding step. const formatPercent = (percent: number): string => `${Math.min(percent, 999).toFixed(1)}%`; @@ -49,7 +46,6 @@ const formatPercent = (percent: number): string => `${Math.min(percent, 999).toF */ export const WorkStatusPrimaryGroup: React.FC = ({ sessionId, directory, goalRow, showSession, showRepository }) => { const { t } = useI18n(); - const session = useSession(sessionId ?? '', directory ?? undefined); const { git } = useRuntimeAPIs(); const ensureStatus = useGitStore((state) => state.ensureStatus); const fetchStatus = useGitStore((state) => state.fetchStatus); @@ -195,7 +191,16 @@ export const WorkStatusPrimaryGroup: React.FC = ({ sessionId, directory, : usageTone === 'warn' ? 'var(--status-warning)' : 'var(--status-success)'; - const cost = typeof session?.cost === 'number' && session.cost > 0 ? session.cost : null; + // Rollup total: own cost plus every descendant subagent's cost, recursively + // (see useSubagentCostRollup). Shown here instead of session.cost alone, so + // spend that ran in a spawned subagent doesn't hide from the reader. + const { totalCost, ownCost, subagentCost, subagentCount } = useSubagentCostRollup(sessionId); + const cost = totalCost !== null && totalCost > 0 ? totalCost : null; + // The total answers "what has this cost"; the split answers "why is it more + // than the session I am looking at". Only worth a line once subagents exist — + // without them the total *is* the session's own cost and the row would + // restate the number directly above it. + const showCostBreakdown = cost !== null && subagentCount > 0 && subagentCost > 0; const hasSession = showSession && (usagePercent !== null || cost !== null || Boolean(goalRow)); const hasRepository = showRepository && Boolean(branch || changed || prSummary || attentionLabel); @@ -224,6 +229,17 @@ export const WorkStatusPrimaryGroup: React.FC = ({ sessionId, directory, )} /> + {/* Caption, not a row: it explains the figure above it rather + than reporting a reading of its own, so it carries no icon + and no label column. */} + {showCostBreakdown ? ( +

+ {t('chat.workStatus.cost.breakdown', { + session: formatCost(ownCost), + subagents: formatCost(subagentCost), + })} +

+ ) : null} ) : null} {/* Below the context readout: the goal is a standing instruction, diff --git a/packages/ui/src/components/chat/work-status/WorkStatusSubagentsSection.tsx b/packages/ui/src/components/chat/work-status/WorkStatusSubagentsSection.tsx index 6c354e76..f37ab1c5 100644 --- a/packages/ui/src/components/chat/work-status/WorkStatusSubagentsSection.tsx +++ b/packages/ui/src/components/chat/work-status/WorkStatusSubagentsSection.tsx @@ -7,6 +7,8 @@ import { isVSCodeRuntime } from '@/lib/desktop'; import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat'; import { WorkStatusCollapsibleSection, WorkStatusRow, WorkStatusValue } from './WorkStatusPrimitives'; import { useReportWorkStatusPresence } from './presenceContext'; +import { formatCost } from './subagentCost'; +import { useSubagentCostRollup } from './useSubagentCostRollup'; import type { State } from '@/sync/types'; type Props = { @@ -32,6 +34,11 @@ export const WorkStatusSubagentsSection: React.FC = ({ sessionId, directo [liveSessions, sessionId], ); + // Each child's own subtree total (its cost plus every descendant of its + // own), so nested subagent-of-subagent cost rolls up under the immediate + // child row shown here rather than disappearing. + const { perChildCost } = useSubagentCostRollup(sessionId); + // One subscription covers every child: per-session hooks would multiply // store subscriptions by the number of subagents. const permissions = useDirectorySync(React.useCallback((state: State) => state.permission, [])); @@ -88,20 +95,26 @@ export const WorkStatusSubagentsSection: React.FC = ({ sessionId, directo const asked = (questions[child.id]?.length ?? 0) > 0; const busy = statuses[child.id]?.type === 'busy'; const label = child.title?.trim() || t('chat.workStatus.subagent.untitled'); + const childCost = perChildCost.get(child.id) ?? 0; return ( openChildSession(child.id, label) : undefined} ariaLabel={t('chat.workStatus.action.openSubagent', { name: label })} label={label} - value={blocked ? ( - {t('chat.workStatus.subagent.needsPermission')} - ) : asked ? ( - {t('chat.workStatus.subagent.askedQuestion')} - ) : busy ? ( - {t('chat.workStatus.subagent.working')} - ) : ( - {t('chat.workStatus.subagent.done')} + value={( + <> + {blocked ? ( + {t('chat.workStatus.subagent.needsPermission')} + ) : asked ? ( + {t('chat.workStatus.subagent.askedQuestion')} + ) : busy ? ( + {t('chat.workStatus.subagent.working')} + ) : ( + {t('chat.workStatus.subagent.done')} + )} + {childCost > 0 ? {formatCost(childCost)} : null} + )} /> ); diff --git a/packages/ui/src/components/chat/work-status/subagentCost.test.ts b/packages/ui/src/components/chat/work-status/subagentCost.test.ts new file mode 100644 index 00000000..059f2724 --- /dev/null +++ b/packages/ui/src/components/chat/work-status/subagentCost.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from 'bun:test'; +import type { Session } from '@opencode-ai/sdk/v2'; +import { buildChildrenIndex, computeSubtreeCost, formatCost } from './subagentCost'; + +function makeSession(id: string, cost: number, parentID?: string): Session { + return { id, cost, parentID } as unknown as Session; +} + +describe('buildChildrenIndex', () => { + test('groups sessions by parentID', () => { + const root = makeSession('root', 1); + const childA = makeSession('a', 2, 'root'); + const childB = makeSession('b', 3, 'root'); + const index = buildChildrenIndex([root, childA, childB]); + expect(index.get('root')).toEqual([childA, childB]); + }); +}); + +describe('formatCost', () => { + test('prefixes with $ and trims trailing zeros', () => { + expect(formatCost(1.5)).toBe('$1.5'); + expect(formatCost(0.0001)).toBe('$0.0001'); + expect(formatCost(2)).toBe('$2'); + }); +}); + +describe('computeSubtreeCost', () => { + test('sums a flat root with two direct children', () => { + const root = makeSession('root', 1); + const childA = makeSession('a', 2, 'root'); + const childB = makeSession('b', 3, 'root'); + const sessions = [root, childA, childB]; + const sessionsById = new Map(sessions.map((s) => [s.id, s])); + const childrenByParent = buildChildrenIndex(sessions); + expect(computeSubtreeCost('root', sessionsById, childrenByParent)).toBe(6); + }); + + test('rolls up cost through nested descendants', () => { + const root = makeSession('root', 1); + const child = makeSession('child', 2, 'root'); + const grandchild = makeSession('grandchild', 4, 'child'); + const sessions = [root, child, grandchild]; + const sessionsById = new Map(sessions.map((s) => [s.id, s])); + const childrenByParent = buildChildrenIndex(sessions); + expect(computeSubtreeCost('root', sessionsById, childrenByParent)).toBe(7); + expect(computeSubtreeCost('child', sessionsById, childrenByParent)).toBe(6); + }); + + test('does not double-count or infinite-loop on a cycle', () => { + const a = makeSession('a', 1, 'b'); + const b = makeSession('b', 2, 'a'); + const sessions = [a, b]; + const sessionsById = new Map(sessions.map((s) => [s.id, s])); + const childrenByParent = buildChildrenIndex(sessions); + expect(computeSubtreeCost('a', sessionsById, childrenByParent)).toBe(3); + }); + + test('treats zero and undefined cost as zero, not a break', () => { + const root = makeSession('root', 0); + const child = { id: 'child', parentID: 'root' } as unknown as Session; + const sessions = [root, child]; + const sessionsById = new Map(sessions.map((s) => [s.id, s])); + const childrenByParent = buildChildrenIndex(sessions); + expect(computeSubtreeCost('root', sessionsById, childrenByParent)).toBe(0); + }); + + test('returns 0 for an unknown id', () => { + const sessionsById = new Map(); + const childrenByParent = new Map(); + expect(computeSubtreeCost('missing', sessionsById, childrenByParent)).toBe(0); + }); +}); diff --git a/packages/ui/src/components/chat/work-status/subagentCost.ts b/packages/ui/src/components/chat/work-status/subagentCost.ts new file mode 100644 index 00000000..f02f54e8 --- /dev/null +++ b/packages/ui/src/components/chat/work-status/subagentCost.ts @@ -0,0 +1,57 @@ +import type { Session } from '@opencode-ai/sdk/v2'; + +// Spend is read against a budget, so it keeps its real precision instead of +// collapsing to two decimals. Trailing zeros are dropped so exact values stay +// short. Relocated from WorkStatusPrimaryGroup.tsx so both that component and +// WorkStatusSubagentsSection share one implementation. +const trimZeros = (value: string): string => + (value.includes('.') ? value.replace(/0+$/, '').replace(/\.$/, '') : value); + +export const formatCost = (cost: number): string => `$${trimZeros(cost.toFixed(4))}`; + +/** + * Groups a flat live-session list by parentID. One pass, O(n). Sessions + * without a parentID (roots) are simply absent as keys — callers look up a + * specific id's children via `.get(id) ?? []`. + */ +export function buildChildrenIndex(sessions: Session[]): Map { + const index = new Map(); + for (const session of sessions) { + const parentID = (session as unknown as { parentID?: string }).parentID; + if (!parentID) continue; + const existing = index.get(parentID); + if (existing) { + existing.push(session); + } else { + index.set(parentID, [session]); + } + } + return index; +} + +function sessionCost(session: Session | undefined): number { + const cost = (session as unknown as { cost?: number } | undefined)?.cost; + return typeof cost === 'number' ? cost : 0; +} + +/** + * Own cost plus every descendant's cost, recursively. Cycle-guarded with a + * visited set: parentID should form a tree, but this does not trust that + * invariant blindly (mirrors opencode-session-cost's src/cost.ts). + */ +export function computeSubtreeCost( + id: string, + sessionsById: Map, + childrenByParent: Map, + visited: Set = new Set(), +): number { + if (visited.has(id)) return 0; + visited.add(id); + + let total = sessionCost(sessionsById.get(id)); + const children = childrenByParent.get(id) ?? []; + for (const child of children) { + total += computeSubtreeCost(child.id, sessionsById, childrenByParent, visited); + } + return total; +} diff --git a/packages/ui/src/components/chat/work-status/useSubagentCostRollup.test.ts b/packages/ui/src/components/chat/work-status/useSubagentCostRollup.test.ts new file mode 100644 index 00000000..c9d83187 --- /dev/null +++ b/packages/ui/src/components/chat/work-status/useSubagentCostRollup.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from 'bun:test'; +import type { Session } from '@opencode-ai/sdk/v2'; +import { computeRollup } from './useSubagentCostRollup'; + +function makeSession(id: string, cost: number, parentID?: string): Session { + return { id, cost, parentID } as unknown as Session; +} + +const sessions: Session[] = [ + makeSession('root', 1), + makeSession('a', 2, 'root'), + makeSession('b', 3, 'root'), + makeSession('a1', 5, 'a'), +]; + +describe('computeRollup', () => { + test('sums own cost plus every descendant', () => { + const result = computeRollup(sessions, 'root'); + expect(result.totalCost).toBe(11); + expect(result.subagentCount).toBe(3); + }); + + test('splits the total into the session own cost and the subagent share', () => { + const result = computeRollup(sessions, 'root'); + expect(result.ownCost).toBe(1); + expect(result.subagentCost).toBe(10); + expect(result.ownCost + result.subagentCost).toBe(result.totalCost); + }); + + test('reports a zero subagent share for a session with no children', () => { + const result = computeRollup(sessions, 'a1'); + expect(result.ownCost).toBe(5); + expect(result.subagentCost).toBe(0); + expect(result.totalCost).toBe(5); + }); + + test('maps each direct child to its own subtree cost', () => { + const result = computeRollup(sessions, 'root'); + expect(result.perChildCost.get('a')).toBe(7); + expect(result.perChildCost.get('b')).toBe(3); + }); + + test('returns null total for a null sessionId', () => { + const result = computeRollup(sessions, null); + expect(result.totalCost).toBeNull(); + expect(result.subagentCount).toBe(0); + }); + + test('returns null total for an unknown sessionId', () => { + const result = computeRollup(sessions, 'missing'); + expect(result.totalCost).toBeNull(); + }); + + test('sum of perChildCost plus root cost equals totalCost', () => { + const result = computeRollup(sessions, 'root'); + const childSum = Array.from(result.perChildCost.values()).reduce((sum, v) => sum + v, 0); + const rootOwnCost = 1; + expect(childSum + rootOwnCost).toBe(result.totalCost); + }); +}); diff --git a/packages/ui/src/components/chat/work-status/useSubagentCostRollup.ts b/packages/ui/src/components/chat/work-status/useSubagentCostRollup.ts new file mode 100644 index 00000000..9b21f801 --- /dev/null +++ b/packages/ui/src/components/chat/work-status/useSubagentCostRollup.ts @@ -0,0 +1,71 @@ +import React from 'react'; +import type { Session } from '@opencode-ai/sdk/v2'; +import { useAllLiveSessions } from '@/sync/sync-context'; +import { buildChildrenIndex, computeSubtreeCost } from './subagentCost'; + +export type SubagentCostRollup = { + totalCost: number | null; + /** The root session's own spend, excluding every subagent. */ + ownCost: number; + /** Everything the subagents cost between them: `totalCost - ownCost`. */ + subagentCost: number; + subagentCount: number; + perChildCost: Map; +}; + +const EMPTY_ROLLUP: SubagentCostRollup = { + totalCost: null, + ownCost: 0, + subagentCost: 0, + subagentCount: 0, + perChildCost: new Map(), +}; + +function countDescendants(id: string, childrenByParent: Map, visited: Set): number { + if (visited.has(id)) return 0; + visited.add(id); + const kids = childrenByParent.get(id) ?? []; + let count = kids.length; + for (const kid of kids) count += countDescendants(kid.id, childrenByParent, visited); + return count; +} + +/** + * Pure core of useSubagentCostRollup, kept separate so it can be unit-tested + * directly against a plain session array instead of rendering the hook. + */ +export function computeRollup(liveSessions: Session[], sessionId: string | null): SubagentCostRollup { + if (!sessionId) return EMPTY_ROLLUP; + + const sessionsById = new Map(liveSessions.map((session) => [session.id, session])); + if (!sessionsById.has(sessionId)) return EMPTY_ROLLUP; + + const childrenByParent = buildChildrenIndex(liveSessions); + const totalCost = computeSubtreeCost(sessionId, sessionsById, childrenByParent); + + const perChildCost = new Map(); + let subagentCost = 0; + for (const child of childrenByParent.get(sessionId) ?? []) { + const childSubtree = computeSubtreeCost(child.id, sessionsById, childrenByParent); + perChildCost.set(child.id, childSubtree); + subagentCost += childSubtree; + } + + // Derived by subtraction rather than read back off the session, so the split + // always adds up to the total the panel shows even if a cycle guard trimmed + // part of the walk. + const ownCost = totalCost - subagentCost; + const subagentCount = countDescendants(sessionId, childrenByParent, new Set()); + + return { totalCost, ownCost, subagentCost, subagentCount, perChildCost }; +} + +/** + * Own cost plus every descendant subagent's cost, recursively summed, for a + * given root session. Reads the same `useAllLiveSessions()` subscription + * WorkStatusSubagentsSection already holds — no new store subscription. + */ +export function useSubagentCostRollup(sessionId: string | null): SubagentCostRollup { + const liveSessions = useAllLiveSessions(); + return React.useMemo(() => computeRollup(liveSessions, sessionId), [liveSessions, sessionId]); +} diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index 59dec89c..4433e3b0 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -5,7 +5,8 @@ import { SessionDialogs } from '@/components/session/SessionDialogs'; import { ChatView } from '@/components/views/ChatView'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; -import { useSessions, useDirectorySync, useSession, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; +import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; +import { useSubagentCostRollup } from '@/components/chat/work-status/useSubagentCostRollup'; import { useConfigStore } from '@/stores/useConfigStore'; import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { contextTokensFromBreakdown } from '@/stores/utils/tokenUtils'; @@ -671,7 +672,9 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on const providers = useConfigStore((state) => state.providers); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const activeProjectId = useProjectsStore((state) => state.activeProjectId); - const currentSession = useSession(currentSessionId ?? ''); + // Same rollup the work-status panel reports, so the header and the panel + // never disagree about what this session has cost. + const { totalCost: sessionTotalCost } = useSubagentCostRollup(currentSessionId ?? null); const currentSessionMessages = useSessionMessages(currentSessionId ?? ''); const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? ''); const quotaResults = useQuotaStore((state) => state.results); @@ -1028,7 +1031,7 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on percentage={stableContextUsage.percentage} contextLimit={stableContextUsage.contextLimit} outputLimit={stableContextUsage.outputLimit ?? 0} - cost={(currentSession?.cost ?? 0) > 0 ? currentSession?.cost : null} + cost={(sessionTotalCost ?? 0) > 0 ? sessionTotalCost : null} className="h-9 shrink-0 pl-1 pr-1 typography-ui-label" valueClassName="font-semibold leading-none" hideIcon diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 5ae8eb02..81f66485 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -3113,6 +3113,7 @@ export const dict = { 'updateDialog.changelog.title': 'Neuigkeiten', 'chat.workStatus.ariaLabel': 'Arbeitsstatus', 'chat.workStatus.context.label': 'Kontext', + 'chat.workStatus.cost.breakdown': 'Sitzung {session} · Unteragenten {subagents}', 'chat.workStatus.git.changedFileSingle': '{count} Datei geändert', 'chat.workStatus.git.changedFilePlural': '{count} Dateien geändert', 'chat.workStatus.pr.untitled': 'Pull Request ohne Titel', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 27ddb09b..e8e697aa 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -3124,6 +3124,7 @@ export const dict = { 'quota.window.premiumInteractions': 'AI Credits', 'chat.workStatus.ariaLabel': 'Work status', 'chat.workStatus.context.label': 'Context', + 'chat.workStatus.cost.breakdown': 'Session {session} · Subagents {subagents}', 'chat.workStatus.git.changedFileSingle': '{count} file changed', 'chat.workStatus.git.changedFilePlural': '{count} files changed', 'chat.workStatus.pr.untitled': 'Untitled pull request', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index e0fdb627..ebc2a2ad 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -3125,6 +3125,7 @@ export const dict: Record = { "quota.window.premiumInteractions": "Créditos de IA", 'chat.workStatus.ariaLabel': 'Estado del trabajo', 'chat.workStatus.context.label': 'Contexto', + 'chat.workStatus.cost.breakdown': "Sesión {session} · Subagentes {subagents}", 'chat.workStatus.git.changedFileSingle': '{count} archivo modificado', 'chat.workStatus.git.changedFilePlural': '{count} archivos modificados', 'chat.workStatus.pr.untitled': 'Pull request sin título', diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 85744cb9..46d97256 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -3122,6 +3122,7 @@ export const dict = { 'vscodeLayout.actions.cancel': 'Annuler', 'chat.workStatus.ariaLabel': 'État du travail', 'chat.workStatus.context.label': 'Contexte', + 'chat.workStatus.cost.breakdown': 'Session {session} · Sous-agents {subagents}', 'chat.workStatus.git.changedFileSingle': '{count} fichier modifié', 'chat.workStatus.git.changedFilePlural': '{count} fichiers modifiés', 'chat.workStatus.pr.untitled': 'Pull request sans titre', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index b5a25ea8..fa54004a 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -3126,6 +3126,7 @@ export const dict: Record = { 'chat.workStatus.ariaLabel': '作業状況', 'chat.workStatus.context.label': 'コンテキスト', + 'chat.workStatus.cost.breakdown': 'セッション {session} · サブエージェント {subagents}', 'chat.workStatus.git.changedFileSingle': '{count} 件のファイルを変更', 'chat.workStatus.git.changedFilePlural': '{count} 件のファイルを変更', 'chat.workStatus.pr.untitled': 'タイトルなしのプルリクエスト', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index ea578da3..18ebfa49 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -3124,6 +3124,7 @@ export const dict: Record = { 'quota.window.premiumInteractions': 'AI 크레딧', 'chat.workStatus.ariaLabel': '작업 상태', 'chat.workStatus.context.label': '컨텍스트', + 'chat.workStatus.cost.breakdown': '세션 {session} · 서브 에이전트 {subagents}', 'chat.workStatus.git.changedFileSingle': '파일 {count}개 변경됨', 'chat.workStatus.git.changedFilePlural': '파일 {count}개 변경됨', 'chat.workStatus.pr.untitled': '제목 없는 풀 리퀘스트', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index bf5d59b3..9938ac81 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -3141,6 +3141,7 @@ export const dict: Record = { 'quota.window.premiumInteractions': 'Kredyty AI', 'chat.workStatus.ariaLabel': 'Stan pracy', 'chat.workStatus.context.label': 'Kontekst', + 'chat.workStatus.cost.breakdown': 'Sesja {session} · Podagenci {subagents}', 'chat.workStatus.git.changedFileSingle': 'Zmieniono {count} plik', 'chat.workStatus.git.changedFilePlural': 'Zmieniono {count} plików', 'chat.workStatus.pr.untitled': 'Pull request bez tytułu', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index bd002a96..73c923ec 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -3125,6 +3125,7 @@ export const dict: Record = { "quota.window.premiumInteractions": "Créditos de IA", 'chat.workStatus.ariaLabel': 'Status do trabalho', 'chat.workStatus.context.label': 'Contexto', + 'chat.workStatus.cost.breakdown': "Sessão {session} · Subagentes {subagents}", 'chat.workStatus.git.changedFileSingle': '{count} arquivo alterado', 'chat.workStatus.git.changedFilePlural': '{count} arquivos alterados', 'chat.workStatus.pr.untitled': 'Pull request sem título', diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 7387957e..dfe28604 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -3125,6 +3125,7 @@ export const dict: Record = { "quota.window.premiumInteractions": "Кредити ШІ", 'chat.workStatus.ariaLabel': 'Стан роботи', 'chat.workStatus.context.label': 'Контекст', + 'chat.workStatus.cost.breakdown': "Сеанс {session} · Субагенти {subagents}", 'chat.workStatus.git.changedFileSingle': 'Змінено {count} файл', 'chat.workStatus.git.changedFilePlural': 'Змінено {count} файлів', 'chat.workStatus.pr.untitled': 'Pull request без назви', diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 3f36a23e..191110b9 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -3125,6 +3125,7 @@ export const dict: Record = { 'quota.window.premiumInteractions': 'AI 点数', 'chat.workStatus.ariaLabel': '工作状态', 'chat.workStatus.context.label': '上下文', + 'chat.workStatus.cost.breakdown': '会话 {session} · 子智能体 {subagents}', 'chat.workStatus.git.changedFileSingle': '已更改 {count} 个文件', 'chat.workStatus.git.changedFilePlural': '已更改 {count} 个文件', 'chat.workStatus.pr.untitled': '未命名的拉取请求', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index d4234ec9..7338818a 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -3124,6 +3124,7 @@ export const dict: Record = { 'quota.window.premiumInteractions': 'AI 點數', 'chat.workStatus.ariaLabel': '工作狀態', 'chat.workStatus.context.label': '上下文', + 'chat.workStatus.cost.breakdown': '工作階段 {session} · 子 Agent {subagents}', 'chat.workStatus.git.changedFileSingle': '已變更 {count} 個檔案', 'chat.workStatus.git.changedFilePlural': '已變更 {count} 個檔案', 'chat.workStatus.pr.untitled': '未命名的提取請求', diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index 9e857c0a..24afa2ed 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +- The context usage readout now reports the session cost including everything its subagents spent, matching the work status panel instead of showing a lower figure. + ## [1.21.0] - 2026-08-26 - **Chat context attachments:** diff and file comments, terminal selections, and linked issues/PRs now show in the conversation as compact context cards — source header, captured content behind an expander, your comment below — instead of raw text inside the message.