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:
7
.changeset/fn-7142-summary-token-cost.md
Normal file
7
.changeset/fn-7142-summary-token-cost.md
Normal 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).
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "취소",
|
||||||
|
|||||||
@@ -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": "取消",
|
||||||
|
|||||||
@@ -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": "取消",
|
||||||
|
|||||||
Reference in New Issue
Block a user