FN-7142: show task summary token costs

Add task summary visibility into token usage, model-level costs, and locale coverage.

- Render done-task Summary token usage by model with input, output, cached, total, and estimated cost columns.
- Thread global model pricing overrides into the summary cost calculation and mark unpriced models as unavailable.
- Add responsive token-cost styling, summary-tab coverage, dashboard docs, release notes, and locale keys.

Files changed:
 .changeset/fn-7142-summary-token-cost.md           |   7 +
 docs/dashboard-guide.md                            |   2 +-
 .../dashboard/app/components/TaskDetailModal.css   | 125 +++++++++++
 .../dashboard/app/components/TaskDetailModal.tsx   |   2 +-
 .../dashboard/app/components/TaskSummaryTab.tsx    | 177 ++++++++++++++-
 .../__tests__/TaskDetailModal.summary-tab.test.tsx | 241 ++++++++++++++++++++-
 packages/i18n/locales/en/app.json                  |  11 +
 packages/i18n/locales/es/app.json                  |  52 ++++-
 packages/i18n/locales/fr/app.json                  |  52 ++++-
 packages/i18n/locales/ko/app.json                  |  52 ++++-
 packages/i18n/locales/zh-CN/app.json               |  52 ++++-
 packages/i18n/locales/zh-TW/app.json               |  52 ++++-
 12 files changed, 794 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-7142
Fusion-Task-Lineage: ec1c7244-4779-46e7-be7c-b0e00b361fd7
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-27 17:56:32 -07:00
parent e8163ab81b
commit 63d1079d44
12 changed files with 794 additions and 31 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Done-task Summary tab now shows token usage by model with estimated cost per model and a task total.
category: feature
dev: TaskSummaryTab derives per-model USD cost client-side via costFor + global modelPricingOverrides from task.tokenUsage.perModel; unpriced models render "—" (never $0).

View File

@@ -974,7 +974,7 @@ For related global/project configuration behavior, see [Settings reference](./se
Inspect task definition, logs, review feedback, comments, artifacts, workflow outcomes, model overrides, and task routing from a single modal. Inspect task definition, logs, review feedback, comments, artifacts, workflow outcomes, model overrides, and task routing from a single modal.
- Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form. - Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form.
- The **Summary** tab appears first for `done` tasks and is their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, and retry counts from the already-loaded task detail; non-`done` tasks still open on **Chat** by default. - The **Summary** tab appears first for `done` tasks and is their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. Non-`done` tasks still open on **Chat** by default.
- The **Chat** tab includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Chat** tab includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
- Task-detail Chat messages are persisted as user comments/steering guidance and surfaced to every relevant agent lane: live executor sessions receive steering injection, while planner, reviewer (spec/plan/code), and merger agents (standard and clean-room AI merge/review) receive the latest user comments in their next prompt/pass. - Task-detail Chat messages are persisted as user comments/steering guidance and surfaced to every relevant agent lane: live executor sessions receive steering injection, while planner, reviewer (spec/plan/code), and merger agents (standard and clean-room AI merge/review) receive the latest user comments in their next prompt/pass.
- The priority chip in task metadata is an inline picker: you can change priority directly without entering full edit mode. - The priority chip in task metadata is an inline picker: you can change priority directly without entering full edit mode.

View File

@@ -1642,6 +1642,64 @@ The done-task Summary tab is a scrollable overview composed from existing detail
color: var(--color-error); color: var(--color-error);
} }
/*
FNXC:TaskDetailSummaryTokenCost 2026-06-27-00:00:
The done-task Summary token-cost section must reuse task-summary card hierarchy while showing per-model counts and cost without hardcoded colors or spacing. Mobile converts table rows to stacked cards so the modal and right dock do not trap horizontal scrolling.
*/
.task-summary-token-table-wrap {
width: 100%;
overflow-x: auto;
}
.task-summary-token-table {
width: 100%;
border-collapse: collapse;
color: var(--text);
}
.task-summary-token-table th,
.task-summary-token-table td {
padding: var(--space-sm);
border-bottom: var(--btn-border-width) solid var(--border);
text-align: left;
vertical-align: middle;
}
.task-summary-token-table th {
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.5);
font-weight: 600;
text-transform: uppercase;
letter-spacing: calc(var(--btn-border-width) * 0.5);
}
.task-summary-token-table tbody td:not(:first-child),
.task-summary-token-table tfoot td {
font-family: var(--font-mono);
white-space: nowrap;
}
.task-summary-token-table tfoot th,
.task-summary-token-table tfoot td {
border-bottom: 0;
font-weight: 700;
}
.task-summary-model-label {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
min-width: 0;
}
.task-summary-model-label span:last-child {
overflow-wrap: anywhere;
}
.task-summary-cost-unavailable {
color: var(--color-warning);
}
.task-summary-file-list, .task-summary-file-list,
.task-summary-work-list { .task-summary-work-list {
display: flex; display: flex;
@@ -1731,6 +1789,73 @@ The done-task Summary tab is a scrollable overview composed from existing detail
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.task-summary-token-table-wrap {
overflow-x: visible;
}
.task-summary-token-table,
.task-summary-token-table thead,
.task-summary-token-table tbody,
.task-summary-token-table tfoot,
.task-summary-token-table tr,
.task-summary-token-table th,
.task-summary-token-table td {
display: block;
}
.task-summary-token-table thead {
display: none;
}
.task-summary-token-table tbody tr,
.task-summary-token-table tfoot tr {
padding: var(--space-sm);
background: var(--surface);
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
}
.task-summary-token-table tbody,
.task-summary-token-table tfoot {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.task-summary-token-table td,
.task-summary-token-table tfoot th {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
padding: var(--space-xs) 0;
border-bottom: 0;
text-align: right;
}
.task-summary-token-table td::before {
content: attr(data-label);
color: var(--text-muted);
font-family: var(--font-sans);
font-weight: 600;
text-align: left;
}
.task-summary-token-table tfoot td::before {
content: none;
}
.task-summary-token-table tfoot th {
color: var(--text);
text-align: left;
text-transform: none;
letter-spacing: normal;
}
.task-summary-token-table tfoot th::after {
content: ":";
}
.task-summary-work-list li { .task-summary-work-list li {
align-items: flex-start; align-items: flex-start;
} }

View File

@@ -3223,7 +3223,7 @@ export function TaskDetailContent({
</div> </div>
) : activeTab === "summary" && task.column === "done" ? ( ) : activeTab === "summary" && task.column === "done" ? (
<div className="detail-section detail-section--summary"> <div className="detail-section detail-section--summary">
<TaskSummaryTab task={workingTask} /> <TaskSummaryTab task={workingTask} pricingOverrides={globalSettings?.modelPricingOverrides} />
</div> </div>
) : activeTab === "chat" ? ( ) : activeTab === "chat" ? (
<div className="detail-section detail-section--chat"> <div className="detail-section detail-section--chat">

View File

@@ -3,9 +3,12 @@ import { useTranslation } from "react-i18next";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown"; import type { Components } from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import type { TaskDetail, TaskStep, WorkflowStepResult } from "@fusion/core"; import type { TaskDetail, TaskStep, TaskTokenUsagePerModel, WorkflowStepResult } from "@fusion/core";
import { costFor, type CostResult, type ModelPricingOverrides } from "../../../core/src/model-pricing";
import { createMermaidCodeComponent, sharedRehypePlugins } from "./markdownPipeline"; import { createMermaidCodeComponent, sharedRehypePlugins } from "./markdownPipeline";
import { ProviderIcon } from "./ProviderIcon";
import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify";
import { inferProviderIconKey } from "../utils/providerIconKey";
const EMPTY_MARKDOWN_CHILD_SEPARATOR = ""; const EMPTY_MARKDOWN_CHILD_SEPARATOR = "";
const STRING_OBJECT_TAG = "[object String]"; const STRING_OBJECT_TAG = "[object String]";
@@ -27,6 +30,7 @@ const markdownLinkifyComponents: Components = {
interface TaskSummaryTabProps { interface TaskSummaryTabProps {
task: TaskDetail; task: TaskDetail;
pricingOverrides?: ModelPricingOverrides;
} }
function getCompletedSteps(steps: TaskStep[] | undefined): TaskStep[] { function getCompletedSteps(steps: TaskStep[] | undefined): TaskStep[] {
@@ -37,11 +41,129 @@ function getRenderableWorkflowResults(results: WorkflowStepResult[] | undefined)
return (results ?? []).filter((result) => result.status !== "pending"); return (results ?? []).filter((result) => result.status !== "pending");
} }
interface TokenCostRow {
key: string;
label: string;
modelProvider?: string;
modelId?: string;
inputTokens: number;
outputTokens: number;
cachedTokens: number;
cacheWriteTokens: number;
totalTokens: number;
cost: CostResult;
}
function formatCount(n: number): string {
return Number.isFinite(n) ? Math.round(n).toLocaleString() : "0";
}
function formatCost(usd: number | null, unavailable: boolean): string {
if (unavailable || usd === null || !Number.isFinite(usd)) {
return "—";
}
return `$${usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function toTokenBucketKey(bucket: Pick<TaskTokenUsagePerModel, "modelProvider" | "modelId">): string {
return `${bucket.modelProvider ?? ""}:${bucket.modelId ?? ""}`;
}
function toTokenCostRow(
bucket: Pick<TaskTokenUsagePerModel, "modelProvider" | "modelId" | "inputTokens" | "outputTokens" | "cachedTokens" | "cacheWriteTokens" | "totalTokens">,
unknownLabel: string,
now: number,
pricingOverrides?: ModelPricingOverrides,
): TokenCostRow {
const modelId = bucket.modelId?.trim() || undefined;
const modelProvider = bucket.modelProvider?.trim() || undefined;
const label = modelId ?? unknownLabel;
return {
key: toTokenBucketKey({ modelProvider, modelId }),
label,
modelProvider,
modelId,
inputTokens: bucket.inputTokens,
outputTokens: bucket.outputTokens,
cachedTokens: bucket.cachedTokens,
cacheWriteTokens: bucket.cacheWriteTokens,
totalTokens: bucket.totalTokens,
cost: costFor(
{
inputTokens: bucket.inputTokens,
outputTokens: bucket.outputTokens,
cachedTokens: bucket.cachedTokens,
cacheWriteTokens: bucket.cacheWriteTokens,
},
{ provider: modelProvider, model: modelId },
now,
pricingOverrides,
),
};
}
/**
* FNXC:TaskDetailSummaryTokenCost 2026-06-27-00:00:
* Done-task Summary shows durable token usage broken down by model with derived USD cost. Use already-loaded task.tokenUsage.perModel buckets plus costFor and global pricing overrides threaded from TaskDetailModal; do not fetch or persist cost here. Unpriced models render “—” instead of $0 and make the task total unavailable so estimates are never understated.
*/
function buildTokenCostRows(task: TaskDetail, unknownLabel: string, pricingOverrides?: ModelPricingOverrides): TokenCostRow[] {
const tokenUsage = task.tokenUsage;
if (!tokenUsage) return [];
const buckets = tokenUsage.perModel?.length
? tokenUsage.perModel
: [
{
modelProvider: tokenUsage.modelProvider,
modelId: tokenUsage.modelId,
inputTokens: tokenUsage.inputTokens,
outputTokens: tokenUsage.outputTokens,
cachedTokens: tokenUsage.cachedTokens,
cacheWriteTokens: tokenUsage.cacheWriteTokens,
totalTokens: tokenUsage.totalTokens,
},
];
const merged = new Map<string, Pick<TaskTokenUsagePerModel, "modelProvider" | "modelId" | "inputTokens" | "outputTokens" | "cachedTokens" | "cacheWriteTokens" | "totalTokens">>();
buckets.forEach((bucket) => {
const modelProvider = bucket.modelProvider?.trim() || undefined;
const modelId = bucket.modelId?.trim() || undefined;
const key = toTokenBucketKey({ modelProvider, modelId });
const current = merged.get(key);
if (!current) {
merged.set(key, { ...bucket, modelProvider, modelId });
return;
}
current.inputTokens += bucket.inputTokens;
current.outputTokens += bucket.outputTokens;
current.cachedTokens += bucket.cachedTokens;
current.cacheWriteTokens += bucket.cacheWriteTokens;
current.totalTokens += bucket.totalTokens;
});
const now = Date.now();
return Array.from(merged.values()).map((bucket) => toTokenCostRow(bucket, unknownLabel, now, pricingOverrides));
}
function totalCostForRows(rows: TokenCostRow[]): { usd: number | null; unavailable: boolean } {
let usd = 0;
let unavailable = false;
rows.forEach((row) => {
if (row.totalTokens <= 0) return;
if (row.cost.unavailable || row.cost.usd === null || !Number.isFinite(row.cost.usd)) {
unavailable = true;
return;
}
usd += row.cost.usd;
});
return { usd: unavailable ? null : usd, unavailable };
}
/** /**
* FNXC:TaskDetailSummaryTab 2026-06-27-00:00: * FNXC:TaskDetailSummaryTab 2026-06-27-00:00:
* TaskSummaryTab aggregates read-only completion data already loaded on TaskDetail: agent-written summary, changed-file metadata, implementation steps, workflow-step outcomes, and retry counts. It does not fetch, persist, or generate AI content so done-task details remain a front-end composition only. * TaskSummaryTab aggregates read-only completion data already loaded on TaskDetail: agent-written summary, changed-file metadata, implementation steps, workflow-step outcomes, and retry counts. It does not fetch, persist, or generate AI content so done-task details remain a front-end composition only.
*/ */
export function TaskSummaryTab({ task }: TaskSummaryTabProps) { export function TaskSummaryTab({ task, pricingOverrides }: TaskSummaryTabProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const summary = task.summary?.trim(); const summary = task.summary?.trim();
const changedFiles = task.mergeDetails?.landedFiles?.length const changedFiles = task.mergeDetails?.landedFiles?.length
@@ -55,6 +177,8 @@ export function TaskSummaryTab({ task }: TaskSummaryTabProps) {
|| task.mergeDetails?.deletions != null; || task.mergeDetails?.deletions != null;
const hasChangedContent = changedFiles.length > 0 || hasChangedStats || Boolean(task.mergeDetails?.commitSha); const hasChangedContent = changedFiles.length > 0 || hasChangedStats || Boolean(task.mergeDetails?.commitSha);
const hasAgentWork = completedSteps.length > 0 || workflowResults.length > 0 || retryTotal > 0; const hasAgentWork = completedSteps.length > 0 || workflowResults.length > 0 || retryTotal > 0;
const tokenCostRows = buildTokenCostRows(task, t("taskDetail.summaryTab.unknownModel", "(unknown)"), pricingOverrides);
const totalCost = totalCostForRows(tokenCostRows);
return ( return (
<div className="task-summary-tab" data-testid="task-summary-tab"> <div className="task-summary-tab" data-testid="task-summary-tab">
@@ -114,6 +238,55 @@ export function TaskSummaryTab({ task }: TaskSummaryTabProps) {
</section> </section>
) : null} ) : null}
{task.tokenUsage ? (
<section className="task-summary-section task-summary-section--tokens" data-testid="task-summary-token-cost-section">
<h4>{t("taskDetail.summaryTab.tokenCostHeading", "Token usage & cost")}</h4>
<div className="task-summary-token-table-wrap">
<table className="task-summary-token-table">
<thead>
<tr>
<th>{t("taskDetail.summaryTab.model", "Model")}</th>
<th>{t("taskDetail.summaryTab.inputTokens", "Input")}</th>
<th>{t("taskDetail.summaryTab.outputTokens", "Output")}</th>
<th>{t("taskDetail.summaryTab.cachedTokens", "Cached")}</th>
<th>{t("taskDetail.summaryTab.totalTokens", "Total")}</th>
<th>{t("taskDetail.summaryTab.cost", "Cost")}</th>
</tr>
</thead>
<tbody>
{tokenCostRows.map((row) => (
<tr key={row.key || row.label} data-testid="task-summary-token-row">
<td data-label={t("taskDetail.summaryTab.model", "Model")}>
<span className="task-summary-model-label">
<ProviderIcon provider={inferProviderIconKey(row.modelId ?? "")} size="sm" />
<span>{row.label}</span>
</span>
</td>
<td data-label={t("taskDetail.summaryTab.inputTokens", "Input")}>{formatCount(row.inputTokens)}</td>
<td data-label={t("taskDetail.summaryTab.outputTokens", "Output")}>{formatCount(row.outputTokens)}</td>
<td data-label={t("taskDetail.summaryTab.cachedTokens", "Cached")}>{formatCount(row.cachedTokens)}</td>
<td data-label={t("taskDetail.summaryTab.totalTokens", "Total")}>{formatCount(row.totalTokens)}</td>
<td data-label={t("taskDetail.summaryTab.cost", "Cost")}>
{row.cost.unavailable || row.cost.usd === null ? (
<span className="task-summary-cost-unavailable" title={t("taskDetail.summaryTab.costUnavailable", "No pricing for this model")}>—</span>
) : (
formatCost(row.cost.usd, row.cost.unavailable)
)}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<th scope="row" colSpan={5}>{t("taskDetail.summaryTab.totalCost", "Total cost")}</th>
<td data-label={t("taskDetail.summaryTab.totalCost", "Total cost")}>{formatCost(totalCost.usd, totalCost.unavailable)}</td>
</tr>
</tfoot>
</table>
</div>
</section>
) : null}
{hasAgentWork ? ( {hasAgentWork ? (
<section className="task-summary-section task-summary-section--agent-work"> <section className="task-summary-section task-summary-section--agent-work">
<h4>{t("taskDetail.summaryTab.agentWorkHeading", "Work done by agents")}</h4> <h4>{t("taskDetail.summaryTab.agentWorkHeading", "Work done by agents")}</h4>

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react"; import { render, screen, within } from "@testing-library/react";
import type { Column } from "@fusion/core"; import type { Column, ModelPricingOverrides, TaskTokenUsage } from "@fusion/core";
import { import {
makeTask, makeTask,
noop, noop,
@@ -8,9 +8,11 @@ import {
noopMerge, noopMerge,
noopMove, noopMove,
noopOpenDetail, noopOpenDetail,
readDashboardStylesSource,
setupTaskDetailModalHooks, setupTaskDetailModalHooks,
} from "./TaskDetailModal.test-helpers"; } from "./TaskDetailModal.test-helpers";
import { TaskDetailContent, TaskDetailModal } from "../TaskDetailModal"; import { TaskDetailContent, TaskDetailModal } from "../TaskDetailModal";
import { TaskSummaryTab } from "../TaskSummaryTab";
setupTaskDetailModalHooks(); setupTaskDetailModalHooks();
@@ -18,6 +20,19 @@ function expectButtonActive(button: HTMLElement): void {
expect(button.classList.contains("detail-tab-active")).toBe(true); expect(button.classList.contains("detail-tab-active")).toBe(true);
} }
function tokenUsage(overrides: Partial<TaskTokenUsage> = {}): TaskTokenUsage {
return {
inputTokens: 0,
outputTokens: 0,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 0,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
...overrides,
};
}
function doneTask(overrides = {}) { function doneTask(overrides = {}) {
return makeTask({ return makeTask({
column: "done", column: "done",
@@ -157,6 +172,228 @@ describe("TaskDetailModal Summary tab", () => {
} }
}); });
it("omits the token-cost section when token usage is absent", () => {
render(<TaskSummaryTab task={doneTask({ tokenUsage: undefined })} />);
expect(screen.queryByText("Token usage & cost")).toBeNull();
expect(screen.queryByTestId("task-summary-token-cost-section")).toBeNull();
});
it("renders multi-model token counts with priced, unpriced, and unavailable total cost states", () => {
render(
<TaskSummaryTab
task={doneTask({
tokenUsage: tokenUsage({
inputTokens: 1_000_100,
outputTokens: 1_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 2_000_100,
perModel: [
{
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 2_000_000,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
},
{
modelProvider: "unknown-provider",
modelId: "unpriced-model",
inputTokens: 100,
outputTokens: 0,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 100,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
},
],
}),
})}
/>,
);
const rows = screen.getAllByTestId("task-summary-token-row");
expect(rows).toHaveLength(2);
expect(within(rows[0]).getByText("claude-sonnet-4-6")).toBeTruthy();
expect(within(rows[0]).getAllByText("1,000,000")).toHaveLength(2);
expect(within(rows[0]).getByText("2,000,000")).toBeTruthy();
expect(within(rows[0]).getByText("$18.00")).toBeTruthy();
expect(within(rows[0]).getByTestId("anthropic-icon")).toBeTruthy();
expect(within(rows[1]).getByText("unpriced-model")).toBeTruthy();
expect(within(rows[1]).getByText("—")).toBeTruthy();
expect(screen.getByText("Total cost")).toBeTruthy();
expect(screen.getAllByText("—").length).toBeGreaterThanOrEqual(2);
expect(screen.queryByText("$0.00")).toBeNull();
});
it("renders a numeric task total when all contributing models are priced", () => {
render(
<TaskSummaryTab
task={doneTask({
tokenUsage: tokenUsage({
inputTokens: 2_000_000,
outputTokens: 2_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 4_000_000,
perModel: [
{
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 2_000_000,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
},
{
modelProvider: "openai",
modelId: "gpt-4o-mini",
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 2_000_000,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
},
],
}),
})}
/>,
);
expect(screen.getByText("$18.00")).toBeTruthy();
expect(screen.getByText("$0.75")).toBeTruthy();
expect(screen.getByText("$18.75")).toBeTruthy();
});
it("synthesizes a single aggregate row when per-model buckets are absent", () => {
render(
<TaskSummaryTab
task={doneTask({
tokenUsage: tokenUsage({
inputTokens: 1234,
outputTokens: 5678,
cachedTokens: 90,
cacheWriteTokens: 12,
totalTokens: 7014,
perModel: [],
}),
})}
/>,
);
const rows = screen.getAllByTestId("task-summary-token-row");
expect(rows).toHaveLength(1);
expect(within(rows[0]).getByText("(unknown)")).toBeTruthy();
expect(within(rows[0]).getByText("1,234")).toBeTruthy();
expect(within(rows[0]).getByText("5,678")).toBeTruthy();
expect(within(rows[0]).getByText("90")).toBeTruthy();
expect(within(rows[0]).getByText("7,014")).toBeTruthy();
});
it("applies pricing overrides passed into the summary component", () => {
const pricingOverrides: ModelPricingOverrides = {
"custom:override-model": {
inputPer1M: 2,
outputPer1M: 3,
cacheReadPer1M: 4,
cacheWritePer1M: 5,
source: "test override",
},
};
render(
<TaskSummaryTab
pricingOverrides={pricingOverrides}
task={doneTask({
tokenUsage: tokenUsage({
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 1_000_000,
cacheWriteTokens: 1_000_000,
totalTokens: 4_000_000,
perModel: [
{
modelProvider: "custom",
modelId: "override-model",
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 1_000_000,
cacheWriteTokens: 1_000_000,
totalTokens: 4_000_000,
firstUsedAt: "2026-01-01T00:00:00Z",
lastUsedAt: "2026-01-01T00:00:00Z",
},
],
}),
})}
/>,
);
expect(screen.getAllByText("$14.00").length).toBeGreaterThanOrEqual(1);
});
it("renders the shared token-cost section through embedded and full detail content entrypoints", () => {
const task = doneTask({
tokenUsage: tokenUsage({
inputTokens: 1_000_000,
outputTokens: 1_000_000,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 2_000_000,
modelProvider: "anthropic",
modelId: "claude-sonnet-4-6",
}),
});
const full = render(
<TaskDetailContent
task={task}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
expect(screen.getByText("Token usage & cost")).toBeTruthy();
expect(screen.getByText("claude-sonnet-4-6")).toBeTruthy();
full.unmount();
render(
<TaskDetailContent
task={task}
embedded
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
expect(screen.getByText("Token usage & cost")).toBeTruthy();
expect(screen.getByText("claude-sonnet-4-6")).toBeTruthy();
});
it("keeps token-cost summary styling responsive without hardcoded colors", () => {
const css = readDashboardStylesSource();
expect(css).toContain(".task-summary-token-table");
expect(css).toContain("@media (max-width: 768px)");
expect(css).toContain(".task-summary-token-table td::before");
expect(css).toContain("var(--color-warning)");
expect(css).not.toMatch(/task-summary-token[^{}]*#[0-9a-fA-F]{3,8}/);
});
it("renders graceful empty states without orphaned changed-file headings", () => { it("renders graceful empty states without orphaned changed-file headings", () => {
render( render(
<TaskDetailModal <TaskDetailModal

View File

@@ -7557,17 +7557,28 @@
}, },
"summaryTab": { "summaryTab": {
"agentWorkHeading": "Work done by agents", "agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed", "changedHeading": "What changed",
"commit": "Commit", "commit": "Commit",
"completedSteps": "Completed steps", "completedSteps": "Completed steps",
"completionHeading": "Completion summary", "completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed", "deletions": "Removed",
"filesChanged": "Files", "filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added", "insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.", "noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.", "noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.", "noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.", "retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results" "workflowResults": "Workflow results"
}, },
"tabs": { "tabs": {

View File

@@ -1361,7 +1361,8 @@
"unreadMessages": "Mensajes no leídos", "unreadMessages": "Mensajes no leídos",
"untitledSession": "Sin título", "untitledSession": "Sin título",
"workingStatus": "Trabajando…", "workingStatus": "Trabajando…",
"you": "Tú" "you": "Tú",
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
}, },
"chatRooms": { "chatRooms": {
"error": { "error": {
@@ -5465,7 +5466,16 @@
"webhookPathPlaceholder": "/trigger/my-routine", "webhookPathPlaceholder": "/trigger/my-routine",
"webhookSecretHint": "Secreto HMAC para verificación de firma. Deja vacío para webhooks sin autenticación.", "webhookSecretHint": "Secreto HMAC para verificación de firma. Deja vacío para webhooks sin autenticación.",
"webhookSecretLabel": "Secreto del webhook (opcional)", "webhookSecretLabel": "Secreto del webhook (opcional)",
"webhookSecretPlaceholder": "Opcional: deja vacío para webhooks sin autenticación" "webhookSecretPlaceholder": "Opcional: deja vacío para webhooks sin autenticación",
"allowedToolsHint": "AI prompt steps use all tools by default. Clear tools only when this automation should run without tool access.",
"allowedToolsLabel": "Allowed tools",
"clearTools": "Clear",
"liveOutput": "Live output",
"liveOutputRunning": "Live output — running",
"liveRunComplete": "Run complete",
"liveRunError": "Run failed",
"liveRunStarting": "Starting run…",
"selectAllTools": "Select all"
}, },
"scripts": { "scripts": {
"title": "" "title": ""
@@ -6111,7 +6121,12 @@
"whenEnabledLockFilesPackageLockJsonPnpm2": "", "whenEnabledLockFilesPackageLockJsonPnpm2": "",
"whenEnabledTasksThatPassReviewAreAutomatically": "", "whenEnabledTasksThatPassReviewAreAutomatically": "",
"whenEnabledTheMergedResultIsAutomaticallyPushed": "", "whenEnabledTheMergedResultIsAutomaticallyPushed": "",
"whenUsingSmartPreferMainAutomaticallyPreferThe": "" "whenUsingSmartPreferMainAutomaticallyPreferThe": "",
"planApprovalMode": "Plan approval mode",
"planApprovalModeAutoApproveAll": "Auto-approve all tasks",
"planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.",
"planApprovalModeRequireAll": "Require approval for all tasks",
"planApprovalModeWorkflow": "Use workflow setting"
}, },
"mergeManually": "Fusionar manualmente", "mergeManually": "Fusionar manualmente",
"mobileNav": { "mobileNav": {
@@ -7543,7 +7558,8 @@
"routing": "Enrutamiento", "routing": "Enrutamiento",
"stats": "Estadísticas", "stats": "Estadísticas",
"terminal": "", "terminal": "",
"workflow": "Flujo de trabajo" "workflow": "Flujo de trabajo",
"summary": "Summary"
}, },
"terminal": { "terminal": {
"loading": "" "loading": ""
@@ -7574,7 +7590,33 @@
}, },
"workflowRuntime": "Tiempo de ejecución del flujo de trabajo", "workflowRuntime": "Tiempo de ejecución del flujo de trabajo",
"workflowTimedSteps": "Pasos cronometrados del flujo de trabajo", "workflowTimedSteps": "Pasos cronometrados del flujo de trabajo",
"yes": "Sí" "yes": "Sí",
"summaryTab": {
"agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed",
"commit": "Commit",
"completedSteps": "Completed steps",
"completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed",
"filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
}
}, },
"taskDocuments": { "taskDocuments": {
"cancel": "Cancelar", "cancel": "Cancelar",

View File

@@ -1361,7 +1361,8 @@
"unreadMessages": "Messages non lus", "unreadMessages": "Messages non lus",
"untitledSession": "Sans titre", "untitledSession": "Sans titre",
"workingStatus": "Traitement en cours…", "workingStatus": "Traitement en cours…",
"you": "Vous" "you": "Vous",
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
}, },
"chatRooms": { "chatRooms": {
"error": { "error": {
@@ -5465,7 +5466,16 @@
"webhookPathPlaceholder": "/trigger/my-routine", "webhookPathPlaceholder": "/trigger/my-routine",
"webhookSecretHint": "Secret HMAC pour la vérification de signature. Laisser vide pour les webhooks non authentifiés.", "webhookSecretHint": "Secret HMAC pour la vérification de signature. Laisser vide pour les webhooks non authentifiés.",
"webhookSecretLabel": "Secret webhook (facultatif)", "webhookSecretLabel": "Secret webhook (facultatif)",
"webhookSecretPlaceholder": "Facultatif — laisser vide pour les webhooks non authentifiés" "webhookSecretPlaceholder": "Facultatif — laisser vide pour les webhooks non authentifiés",
"allowedToolsHint": "AI prompt steps use all tools by default. Clear tools only when this automation should run without tool access.",
"allowedToolsLabel": "Allowed tools",
"clearTools": "Clear",
"liveOutput": "Live output",
"liveOutputRunning": "Live output — running",
"liveRunComplete": "Run complete",
"liveRunError": "Run failed",
"liveRunStarting": "Starting run…",
"selectAllTools": "Select all"
}, },
"scripts": { "scripts": {
"title": "" "title": ""
@@ -6111,7 +6121,12 @@
"whenEnabledLockFilesPackageLockJsonPnpm2": "", "whenEnabledLockFilesPackageLockJsonPnpm2": "",
"whenEnabledTasksThatPassReviewAreAutomatically": "", "whenEnabledTasksThatPassReviewAreAutomatically": "",
"whenEnabledTheMergedResultIsAutomaticallyPushed": "", "whenEnabledTheMergedResultIsAutomaticallyPushed": "",
"whenUsingSmartPreferMainAutomaticallyPreferThe": "" "whenUsingSmartPreferMainAutomaticallyPreferThe": "",
"planApprovalMode": "Plan approval mode",
"planApprovalModeAutoApproveAll": "Auto-approve all tasks",
"planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.",
"planApprovalModeRequireAll": "Require approval for all tasks",
"planApprovalModeWorkflow": "Use workflow setting"
}, },
"mergeManually": "Fusionner manuellement", "mergeManually": "Fusionner manuellement",
"mobileNav": { "mobileNav": {
@@ -7543,7 +7558,8 @@
"routing": "Routage", "routing": "Routage",
"stats": "Statistiques", "stats": "Statistiques",
"terminal": "", "terminal": "",
"workflow": "Workflow" "workflow": "Workflow",
"summary": "Summary"
}, },
"terminal": { "terminal": {
"loading": "" "loading": ""
@@ -7574,7 +7590,33 @@
}, },
"workflowRuntime": "Durée d'exécution du workflow", "workflowRuntime": "Durée d'exécution du workflow",
"workflowTimedSteps": "Étapes chronométrées du workflow", "workflowTimedSteps": "Étapes chronométrées du workflow",
"yes": "Oui" "yes": "Oui",
"summaryTab": {
"agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed",
"commit": "Commit",
"completedSteps": "Completed steps",
"completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed",
"filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
}
}, },
"taskDocuments": { "taskDocuments": {
"cancel": "Annuler", "cancel": "Annuler",

View File

@@ -1361,7 +1361,8 @@
"unreadMessages": "읽지 않은 메시지", "unreadMessages": "읽지 않은 메시지",
"untitledSession": "제목 없음", "untitledSession": "제목 없음",
"workingStatus": "작업 중…", "workingStatus": "작업 중…",
"you": "나" "you": "나",
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
}, },
"chatRooms": { "chatRooms": {
"error": { "error": {
@@ -5465,7 +5466,16 @@
"webhookPathPlaceholder": "/trigger/my-routine", "webhookPathPlaceholder": "/trigger/my-routine",
"webhookSecretHint": "서명 검증용 HMAC 시크릿. 미인증 웹훅은 비워두세요.", "webhookSecretHint": "서명 검증용 HMAC 시크릿. 미인증 웹훅은 비워두세요.",
"webhookSecretLabel": "Webhook 시크릿 (선택 사항)", "webhookSecretLabel": "Webhook 시크릿 (선택 사항)",
"webhookSecretPlaceholder": "선택 사항 — 미인증 웹훅은 비워두세요" "webhookSecretPlaceholder": "선택 사항 — 미인증 웹훅은 비워두세요",
"allowedToolsHint": "AI prompt steps use all tools by default. Clear tools only when this automation should run without tool access.",
"allowedToolsLabel": "Allowed tools",
"clearTools": "Clear",
"liveOutput": "Live output",
"liveOutputRunning": "Live output — running",
"liveRunComplete": "Run complete",
"liveRunError": "Run failed",
"liveRunStarting": "Starting run…",
"selectAllTools": "Select all"
}, },
"scripts": { "scripts": {
"title": "" "title": ""
@@ -6111,7 +6121,12 @@
"whenEnabledLockFilesPackageLockJsonPnpm2": "", "whenEnabledLockFilesPackageLockJsonPnpm2": "",
"whenEnabledTasksThatPassReviewAreAutomatically": "", "whenEnabledTasksThatPassReviewAreAutomatically": "",
"whenEnabledTheMergedResultIsAutomaticallyPushed": "", "whenEnabledTheMergedResultIsAutomaticallyPushed": "",
"whenUsingSmartPreferMainAutomaticallyPreferThe": "" "whenUsingSmartPreferMainAutomaticallyPreferThe": "",
"planApprovalMode": "Plan approval mode",
"planApprovalModeAutoApproveAll": "Auto-approve all tasks",
"planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.",
"planApprovalModeRequireAll": "Require approval for all tasks",
"planApprovalModeWorkflow": "Use workflow setting"
}, },
"mergeManually": "수동으로 병합", "mergeManually": "수동으로 병합",
"mobileNav": { "mobileNav": {
@@ -7543,7 +7558,8 @@
"routing": "라우팅", "routing": "라우팅",
"stats": "통계", "stats": "통계",
"terminal": "", "terminal": "",
"workflow": "워크플로" "workflow": "워크플로",
"summary": "Summary"
}, },
"terminal": { "terminal": {
"loading": "" "loading": ""
@@ -7574,7 +7590,33 @@
}, },
"workflowRuntime": "워크플로 런타임", "workflowRuntime": "워크플로 런타임",
"workflowTimedSteps": "워크플로 시간 측정 단계", "workflowTimedSteps": "워크플로 시간 측정 단계",
"yes": "예" "yes": "예",
"summaryTab": {
"agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed",
"commit": "Commit",
"completedSteps": "Completed steps",
"completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed",
"filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
}
}, },
"taskDocuments": { "taskDocuments": {
"cancel": "취소", "cancel": "취소",

View File

@@ -1361,7 +1361,8 @@
"unreadMessages": "未读消息", "unreadMessages": "未读消息",
"untitledSession": "无标题", "untitledSession": "无标题",
"workingStatus": "处理中……", "workingStatus": "处理中……",
"you": "你" "you": "你",
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
}, },
"chatRooms": { "chatRooms": {
"error": { "error": {
@@ -5465,7 +5466,16 @@
"webhookPathPlaceholder": "/trigger/my-routine", "webhookPathPlaceholder": "/trigger/my-routine",
"webhookSecretHint": "用于签名验证的 HMAC 密钥。无需身份验证的 webhook 请留空。", "webhookSecretHint": "用于签名验证的 HMAC 密钥。无需身份验证的 webhook 请留空。",
"webhookSecretLabel": "Webhook 密钥(可选)", "webhookSecretLabel": "Webhook 密钥(可选)",
"webhookSecretPlaceholder": "可选——无需身份验证的 webhook 请留空" "webhookSecretPlaceholder": "可选——无需身份验证的 webhook 请留空",
"allowedToolsHint": "AI prompt steps use all tools by default. Clear tools only when this automation should run without tool access.",
"allowedToolsLabel": "Allowed tools",
"clearTools": "Clear",
"liveOutput": "Live output",
"liveOutputRunning": "Live output — running",
"liveRunComplete": "Run complete",
"liveRunError": "Run failed",
"liveRunStarting": "Starting run…",
"selectAllTools": "Select all"
}, },
"scripts": { "scripts": {
"title": "" "title": ""
@@ -6111,7 +6121,12 @@
"whenEnabledLockFilesPackageLockJsonPnpm2": "", "whenEnabledLockFilesPackageLockJsonPnpm2": "",
"whenEnabledTasksThatPassReviewAreAutomatically": "", "whenEnabledTasksThatPassReviewAreAutomatically": "",
"whenEnabledTheMergedResultIsAutomaticallyPushed": "", "whenEnabledTheMergedResultIsAutomaticallyPushed": "",
"whenUsingSmartPreferMainAutomaticallyPreferThe": "" "whenUsingSmartPreferMainAutomaticallyPreferThe": "",
"planApprovalMode": "Plan approval mode",
"planApprovalModeAutoApproveAll": "Auto-approve all tasks",
"planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.",
"planApprovalModeRequireAll": "Require approval for all tasks",
"planApprovalModeWorkflow": "Use workflow setting"
}, },
"mergeManually": "手动合并", "mergeManually": "手动合并",
"mobileNav": { "mobileNav": {
@@ -7543,7 +7558,8 @@
"routing": "路由", "routing": "路由",
"stats": "统计", "stats": "统计",
"terminal": "", "terminal": "",
"workflow": "工作流" "workflow": "工作流",
"summary": "Summary"
}, },
"terminal": { "terminal": {
"loading": "" "loading": ""
@@ -7574,7 +7590,33 @@
}, },
"workflowRuntime": "工作流运行时间", "workflowRuntime": "工作流运行时间",
"workflowTimedSteps": "工作流计时步骤", "workflowTimedSteps": "工作流计时步骤",
"yes": "是" "yes": "是",
"summaryTab": {
"agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed",
"commit": "Commit",
"completedSteps": "Completed steps",
"completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed",
"filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
}
}, },
"taskDocuments": { "taskDocuments": {
"cancel": "取消", "cancel": "取消",

View File

@@ -1361,7 +1361,8 @@
"unreadMessages": "未讀訊息", "unreadMessages": "未讀訊息",
"untitledSession": "未命名", "untitledSession": "未命名",
"workingStatus": "處理中……", "workingStatus": "處理中……",
"you": "你" "you": "你",
"contextWindowAria": "Estimated {{used}} of {{total}} context tokens"
}, },
"chatRooms": { "chatRooms": {
"error": { "error": {
@@ -5465,7 +5466,16 @@
"webhookPathPlaceholder": "/trigger/my-routine", "webhookPathPlaceholder": "/trigger/my-routine",
"webhookSecretHint": "用於簽章驗證的 HMAC 密鑰。無需驗證的 webhook 請留空。", "webhookSecretHint": "用於簽章驗證的 HMAC 密鑰。無需驗證的 webhook 請留空。",
"webhookSecretLabel": "Webhook 密鑰(可選)", "webhookSecretLabel": "Webhook 密鑰(可選)",
"webhookSecretPlaceholder": "可選——無需驗證的 webhook 請留空" "webhookSecretPlaceholder": "可選——無需驗證的 webhook 請留空",
"allowedToolsHint": "AI prompt steps use all tools by default. Clear tools only when this automation should run without tool access.",
"allowedToolsLabel": "Allowed tools",
"clearTools": "Clear",
"liveOutput": "Live output",
"liveOutputRunning": "Live output — running",
"liveRunComplete": "Run complete",
"liveRunError": "Run failed",
"liveRunStarting": "Starting run…",
"selectAllTools": "Select all"
}, },
"scripts": { "scripts": {
"title": "" "title": ""
@@ -6111,7 +6121,12 @@
"whenEnabledLockFilesPackageLockJsonPnpm2": "", "whenEnabledLockFilesPackageLockJsonPnpm2": "",
"whenEnabledTasksThatPassReviewAreAutomatically": "", "whenEnabledTasksThatPassReviewAreAutomatically": "",
"whenEnabledTheMergedResultIsAutomaticallyPushed": "", "whenEnabledTheMergedResultIsAutomaticallyPushed": "",
"whenUsingSmartPreferMainAutomaticallyPreferThe": "" "whenUsingSmartPreferMainAutomaticallyPreferThe": "",
"planApprovalMode": "Plan approval mode",
"planApprovalModeAutoApproveAll": "Auto-approve all tasks",
"planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.",
"planApprovalModeRequireAll": "Require approval for all tasks",
"planApprovalModeWorkflow": "Use workflow setting"
}, },
"mergeManually": "手動合併", "mergeManually": "手動合併",
"mobileNav": { "mobileNav": {
@@ -7543,7 +7558,8 @@
"routing": "路由", "routing": "路由",
"stats": "統計", "stats": "統計",
"terminal": "", "terminal": "",
"workflow": "工作流程" "workflow": "工作流程",
"summary": "Summary"
}, },
"terminal": { "terminal": {
"loading": "" "loading": ""
@@ -7574,7 +7590,33 @@
}, },
"workflowRuntime": "工作流程執行時間", "workflowRuntime": "工作流程執行時間",
"workflowTimedSteps": "工作流程計時步驟", "workflowTimedSteps": "工作流程計時步驟",
"yes": "是" "yes": "是",
"summaryTab": {
"agentWorkHeading": "Work done by agents",
"cachedTokens": "Cached",
"changedHeading": "What changed",
"commit": "Commit",
"completedSteps": "Completed steps",
"completionHeading": "Completion summary",
"cost": "Cost",
"costUnavailable": "No pricing for this model",
"deletions": "Removed",
"filesChanged": "Files",
"inputTokens": "Input",
"insertions": "Added",
"model": "Model",
"noAgentWork": "No completed steps or workflow results are available for this task.",
"noChangedFiles": "No changed-file list is available for this task.",
"noCompletionSummary": "No completion summary was recorded for this task.",
"noTokenUsage": "No token usage recorded for this task yet.",
"outputTokens": "Output",
"retries": "Agents retried this task {{count}} time{{plural}}.",
"tokenCostHeading": "Token usage & cost",
"totalCost": "Total cost",
"totalTokens": "Total",
"unknownModel": "(unknown)",
"workflowResults": "Workflow results"
}
}, },
"taskDocuments": { "taskDocuments": {
"cancel": "取消", "cancel": "取消",