Merge pull request #3106 from igorvelho/feat/subagent-cost-rollup

feat(ui): count subagent spend in the session cost readout
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 01:26:25 +03:00
committed by GitHub
21 changed files with 329 additions and 21 deletions
+1
View File
@@ -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).
@@ -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 |
@@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ sessionId, directory,
)}
/>
<WorkStatusMeter percent={usagePercent} color={meterColor} />
{/* 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 ? (
<p className="mx-1 mb-1 truncate text-[11px] leading-4 text-muted-foreground tabular-nums">
{t('chat.workStatus.cost.breakdown', {
session: formatCost(ownCost),
subagents: formatCost(subagentCost),
})}
</p>
) : null}
</>
) : null}
{/* Below the context readout: the goal is a standing instruction,
@@ -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<Props> = ({ 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<Props> = ({ 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 (
<WorkStatusRow
key={child.id}
onClick={directory ? () => openChildSession(child.id, label) : undefined}
ariaLabel={t('chat.workStatus.action.openSubagent', { name: label })}
label={label}
value={blocked ? (
<WorkStatusValue tone="warning">{t('chat.workStatus.subagent.needsPermission')}</WorkStatusValue>
) : asked ? (
<WorkStatusValue tone="warning">{t('chat.workStatus.subagent.askedQuestion')}</WorkStatusValue>
) : busy ? (
<WorkStatusValue tone="info">{t('chat.workStatus.subagent.working')}</WorkStatusValue>
) : (
<WorkStatusValue tone="muted">{t('chat.workStatus.subagent.done')}</WorkStatusValue>
value={(
<>
{blocked ? (
<WorkStatusValue tone="warning">{t('chat.workStatus.subagent.needsPermission')}</WorkStatusValue>
) : asked ? (
<WorkStatusValue tone="warning">{t('chat.workStatus.subagent.askedQuestion')}</WorkStatusValue>
) : busy ? (
<WorkStatusValue tone="info">{t('chat.workStatus.subagent.working')}</WorkStatusValue>
) : (
<WorkStatusValue tone="muted">{t('chat.workStatus.subagent.done')}</WorkStatusValue>
)}
{childCost > 0 ? <WorkStatusValue tone="muted">{formatCost(childCost)}</WorkStatusValue> : null}
</>
)}
/>
);
@@ -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<string, Session>();
const childrenByParent = new Map<string, Session[]>();
expect(computeSubtreeCost('missing', sessionsById, childrenByParent)).toBe(0);
});
});
@@ -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<string, Session[]> {
const index = new Map<string, Session[]>();
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<string, Session>,
childrenByParent: Map<string, Session[]>,
visited: Set<string> = 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;
}
@@ -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);
});
});
@@ -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<string, number>;
};
const EMPTY_ROLLUP: SubagentCostRollup = {
totalCost: null,
ownCost: 0,
subagentCost: 0,
subagentCount: 0,
perChildCost: new Map(),
};
function countDescendants(id: string, childrenByParent: Map<string, Session[]>, visited: Set<string>): 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<string, number>();
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]);
}
@@ -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<VSCodeHeaderProps> = ({ 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<VSCodeHeaderProps> = ({ 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
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -3125,6 +3125,7 @@ export const dict: Record<I18nKey, string> = {
"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',
+1
View File
@@ -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',
+1
View File
@@ -3126,6 +3126,7 @@ export const dict: Record<I18nKey, string> = {
'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': 'タイトルなしのプルリクエスト',
+1
View File
@@ -3124,6 +3124,7 @@ export const dict: Record<I18nKey, string> = {
'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': '제목 없는 풀 리퀘스트',
+1
View File
@@ -3141,6 +3141,7 @@ export const dict: Record<I18nKey, string> = {
'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',
@@ -3125,6 +3125,7 @@ export const dict: Record<I18nKey, string> = {
"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',
+1
View File
@@ -3125,6 +3125,7 @@ export const dict: Record<I18nKey, string> = {
"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 без назви',
@@ -3125,6 +3125,7 @@ export const dict: Record<I18nKey, string> = {
'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': '未命名的拉取请求',
@@ -3124,6 +3124,7 @@ export const dict: Record<I18nKey, string> = {
'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': '未命名的提取請求',
+4
View File
@@ -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.