mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-08 03:07:53 +00:00
feat: track line changes for delegated subtasks
Compute per-task lines added/removed from tool diffStats, persist on history items, and aggregate recursively across descendants.\n\nRender +/− in todo rows, task header, and expanded task details with diff-like green/red styling. Update i18n labels across locales and adjust layout/alignment.\n\nRelates to PR #10765
This commit is contained in:
parent
3445ca1ecd
commit
235cffad3b
30 changed files with 629 additions and 32 deletions
|
|
@ -16,6 +16,8 @@ export const historyItemSchema = z.object({
|
|||
cacheWrites: z.number().optional(),
|
||||
cacheReads: z.number().optional(),
|
||||
totalCost: z.number(),
|
||||
linesAdded: z.number().optional(),
|
||||
linesRemoved: z.number().optional(),
|
||||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export const todoItemSchema = z.object({
|
|||
subtaskId: z.string().optional(), // ID of the linked subtask (child task) for direct cost/token attribution
|
||||
tokens: z.number().optional(), // Total tokens (in + out) for linked subtask
|
||||
cost: z.number().optional(), // Total cost for linked subtask
|
||||
added: z.number().optional(),
|
||||
removed: z.number().optional(),
|
||||
})
|
||||
|
||||
export type TodoItem = z.infer<typeof todoItemSchema>
|
||||
|
|
|
|||
125
src/core/task-persistence/__tests__/taskMetadata.spec.ts
Normal file
125
src/core/task-persistence/__tests__/taskMetadata.spec.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
|
||||
// Hoisted mocks to avoid initialization ordering issues
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
getTaskDirectoryPathMock: vi.fn().mockResolvedValue("/mock/task/dir"),
|
||||
getFolderSizeLooseMock: vi.fn().mockResolvedValue(0),
|
||||
getApiMetricsMock: vi.fn().mockReturnValue({
|
||||
totalTokensIn: 0,
|
||||
totalTokensOut: 0,
|
||||
totalCacheWrites: 0,
|
||||
totalCacheReads: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("get-folder-size", () => ({
|
||||
default: {
|
||||
loose: hoisted.getFolderSizeLooseMock,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("../../../utils/storage", () => ({
|
||||
getTaskDirectoryPath: hoisted.getTaskDirectoryPathMock,
|
||||
}))
|
||||
|
||||
vi.mock("../../../shared/getApiMetrics", () => ({
|
||||
getApiMetrics: hoisted.getApiMetricsMock,
|
||||
}))
|
||||
|
||||
// Import after mocks
|
||||
import { taskMetadata } from "../taskMetadata"
|
||||
|
||||
describe("taskMetadata() line change parsing", () => {
|
||||
beforeEach(() => {
|
||||
hoisted.getTaskDirectoryPathMock.mockClear()
|
||||
hoisted.getFolderSizeLooseMock.mockClear()
|
||||
hoisted.getApiMetricsMock.mockClear()
|
||||
})
|
||||
|
||||
it("computes linesAdded/linesRemoved from tool message diffStats", async () => {
|
||||
const result = await taskMetadata({
|
||||
taskId: "task-1",
|
||||
taskNumber: 1,
|
||||
globalStoragePath: "/mock/global",
|
||||
workspace: "/mock/workspace",
|
||||
messages: [
|
||||
{ ts: 1, type: "say", say: "text", text: "Task" } as any,
|
||||
{
|
||||
ts: 2,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({ diffStats: { added: 5, removed: 2 } }),
|
||||
} as any,
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.historyItem.linesAdded).toBe(5)
|
||||
expect(result.historyItem.linesRemoved).toBe(2)
|
||||
})
|
||||
|
||||
it("aggregates linesAdded/linesRemoved from batch tool message batchDiffs[].diffStats", async () => {
|
||||
const result = await taskMetadata({
|
||||
taskId: "task-2",
|
||||
taskNumber: 2,
|
||||
globalStoragePath: "/mock/global",
|
||||
workspace: "/mock/workspace",
|
||||
messages: [
|
||||
{ ts: 1, type: "say", say: "text", text: "Task" } as any,
|
||||
{
|
||||
ts: 2,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
text: JSON.stringify({
|
||||
batchDiffs: [
|
||||
{ path: "a.ts", diffStats: { added: 1, removed: 1 } },
|
||||
{ path: "b.ts", diffStats: { added: 2, removed: 3 } },
|
||||
],
|
||||
}),
|
||||
} as any,
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.historyItem.linesAdded).toBe(3)
|
||||
expect(result.historyItem.linesRemoved).toBe(4)
|
||||
})
|
||||
|
||||
it("ignores partial tool messages", async () => {
|
||||
const result = await taskMetadata({
|
||||
taskId: "task-3",
|
||||
taskNumber: 3,
|
||||
globalStoragePath: "/mock/global",
|
||||
workspace: "/mock/workspace",
|
||||
messages: [
|
||||
{ ts: 1, type: "say", say: "text", text: "Task" } as any,
|
||||
{
|
||||
ts: 2,
|
||||
type: "ask",
|
||||
ask: "tool",
|
||||
partial: true,
|
||||
text: JSON.stringify({ diffStats: { added: 10, removed: 10 } }),
|
||||
} as any,
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.historyItem.linesAdded).toBeUndefined()
|
||||
expect(result.historyItem.linesRemoved).toBeUndefined()
|
||||
})
|
||||
|
||||
it("ignores invalid JSON in tool message text gracefully", async () => {
|
||||
const result = await taskMetadata({
|
||||
taskId: "task-4",
|
||||
taskNumber: 4,
|
||||
globalStoragePath: "/mock/global",
|
||||
workspace: "/mock/workspace",
|
||||
messages: [
|
||||
{ ts: 1, type: "say", say: "text", text: "Task" } as any,
|
||||
{ ts: 2, type: "ask", ask: "tool", text: "{not-json" } as any,
|
||||
],
|
||||
})
|
||||
|
||||
expect(result.historyItem.linesAdded).toBeUndefined()
|
||||
expect(result.historyItem.linesRemoved).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
@ -12,6 +12,64 @@ import { t } from "../../i18n"
|
|||
|
||||
const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 })
|
||||
|
||||
type DiffStats = { added: number; removed: number }
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
}
|
||||
|
||||
function isDiffStats(value: unknown): value is DiffStats {
|
||||
if (!value || typeof value !== "object") return false
|
||||
|
||||
const v = value as { added?: unknown; removed?: unknown }
|
||||
return isFiniteNumber(v.added) && isFiniteNumber(v.removed)
|
||||
}
|
||||
|
||||
function getLineStatsFromToolApprovalMessages(messages: ClineMessage[]): {
|
||||
linesAdded: number
|
||||
linesRemoved: number
|
||||
foundAnyStats: boolean
|
||||
} {
|
||||
let linesAdded = 0
|
||||
let linesRemoved = 0
|
||||
let foundAnyStats = false
|
||||
|
||||
for (const m of messages) {
|
||||
// Only count complete tool approval asks (avoid double-counting partial/streaming updates)
|
||||
if (!(m.type === "ask" && m.ask === "tool" && m.partial !== true)) continue
|
||||
if (typeof m.text !== "string" || m.text.length === 0) continue
|
||||
|
||||
let payload: unknown
|
||||
try {
|
||||
payload = JSON.parse(m.text)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!payload || typeof payload !== "object") continue
|
||||
const p = payload as { diffStats?: unknown; batchDiffs?: unknown }
|
||||
|
||||
if (isDiffStats(p.diffStats)) {
|
||||
linesAdded += p.diffStats.added
|
||||
linesRemoved += p.diffStats.removed
|
||||
foundAnyStats = true
|
||||
}
|
||||
|
||||
if (Array.isArray(p.batchDiffs)) {
|
||||
for (const batchDiff of p.batchDiffs) {
|
||||
if (!batchDiff || typeof batchDiff !== "object") continue
|
||||
const bd = batchDiff as { diffStats?: unknown }
|
||||
if (!isDiffStats(bd.diffStats)) continue
|
||||
linesAdded += bd.diffStats.added
|
||||
linesRemoved += bd.diffStats.removed
|
||||
foundAnyStats = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { linesAdded, linesRemoved, foundAnyStats }
|
||||
}
|
||||
|
||||
export type TaskMetadataOptions = {
|
||||
taskId: string
|
||||
rootTaskId?: string
|
||||
|
|
@ -55,6 +113,8 @@ export async function taskMetadata({
|
|||
let tokenUsage: ReturnType<typeof getApiMetrics>
|
||||
let taskDirSize: number
|
||||
let taskMessage: ClineMessage | undefined
|
||||
let linesAdded: number | undefined
|
||||
let linesRemoved: number | undefined
|
||||
|
||||
if (!hasMessages) {
|
||||
// Handle no messages case
|
||||
|
|
@ -93,6 +153,12 @@ export async function taskMetadata({
|
|||
} else {
|
||||
taskDirSize = cachedSize
|
||||
}
|
||||
|
||||
const lineStats = getLineStatsFromToolApprovalMessages(messages)
|
||||
if (lineStats.foundAnyStats) {
|
||||
linesAdded = lineStats.linesAdded
|
||||
linesRemoved = lineStats.linesRemoved
|
||||
}
|
||||
}
|
||||
|
||||
// Create historyItem once with pre-calculated values.
|
||||
|
|
@ -115,6 +181,8 @@ export async function taskMetadata({
|
|||
cacheWrites: tokenUsage.totalCacheWrites,
|
||||
cacheReads: tokenUsage.totalCacheReads,
|
||||
totalCost: tokenUsage.totalCost,
|
||||
...(typeof linesAdded === "number" ? { linesAdded } : {}),
|
||||
...(typeof linesRemoved === "number" ? { linesRemoved } : {}),
|
||||
size: taskDirSize,
|
||||
workspace,
|
||||
mode,
|
||||
|
|
|
|||
|
|
@ -3372,6 +3372,8 @@ export class ClineProvider
|
|||
if (linkedTodo) {
|
||||
linkedTodo.tokens = (childHistoryItem?.tokensIn || 0) + (childHistoryItem?.tokensOut || 0)
|
||||
linkedTodo.cost = childHistoryItem?.totalCost || 0
|
||||
linkedTodo.added = childHistoryItem?.linesAdded || 0
|
||||
linkedTodo.removed = childHistoryItem?.linesRemoved || 0
|
||||
|
||||
parentClineMessages.push({
|
||||
ts: Date.now(),
|
||||
|
|
|
|||
|
|
@ -53,11 +53,15 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
linesAdded: 2,
|
||||
linesRemoved: 1,
|
||||
childIds: ["child-1"],
|
||||
} as unknown as HistoryItem,
|
||||
"child-1": {
|
||||
id: "child-1",
|
||||
totalCost: 0.5,
|
||||
linesAdded: 3,
|
||||
linesRemoved: 2,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
|
@ -69,6 +73,12 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(0.5)
|
||||
expect(result.totalCost).toBe(1.5)
|
||||
expect(result.ownAdded).toBe(2)
|
||||
expect(result.ownRemoved).toBe(1)
|
||||
expect(result.childrenAdded).toBe(3)
|
||||
expect(result.childrenRemoved).toBe(2)
|
||||
expect(result.totalAdded).toBe(5)
|
||||
expect(result.totalRemoved).toBe(3)
|
||||
expect(result.childBreakdown).toHaveProperty("child-1")
|
||||
const child1 = result.childBreakdown?.["child-1"]
|
||||
expect(child1).toBeDefined()
|
||||
|
|
@ -114,16 +124,22 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
linesAdded: 2,
|
||||
linesRemoved: 2,
|
||||
childIds: ["child"],
|
||||
} as unknown as HistoryItem,
|
||||
child: {
|
||||
id: "child",
|
||||
totalCost: 0.5,
|
||||
linesAdded: 3,
|
||||
linesRemoved: 1,
|
||||
childIds: ["grandchild"],
|
||||
} as unknown as HistoryItem,
|
||||
grandchild: {
|
||||
id: "grandchild",
|
||||
totalCost: 0.25,
|
||||
linesAdded: 1,
|
||||
linesRemoved: 4,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
|
@ -136,12 +152,22 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
expect(result.childrenCost).toBe(0.75) // child (0.5) + grandchild (0.25)
|
||||
expect(result.totalCost).toBe(1.75)
|
||||
|
||||
expect(result.ownAdded).toBe(2)
|
||||
expect(result.ownRemoved).toBe(2)
|
||||
// children totals include all descendants
|
||||
expect(result.childrenAdded).toBe(4) // child (3) + grandchild (1)
|
||||
expect(result.childrenRemoved).toBe(5) // child (1) + grandchild (4)
|
||||
expect(result.totalAdded).toBe(6)
|
||||
expect(result.totalRemoved).toBe(7)
|
||||
|
||||
// Verify child breakdown
|
||||
const child = result.childBreakdown?.["child"]
|
||||
expect(child).toBeDefined()
|
||||
expect(child!.ownCost).toBe(0.5)
|
||||
expect(child!.childrenCost).toBe(0.25)
|
||||
expect(child!.totalCost).toBe(0.75)
|
||||
expect(child!.totalAdded).toBe(4)
|
||||
expect(child!.totalRemoved).toBe(5)
|
||||
|
||||
// Verify grandchild breakdown
|
||||
const grandchild = child!.childBreakdown?.["grandchild"]
|
||||
|
|
@ -149,6 +175,8 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
expect(grandchild!.ownCost).toBe(0.25)
|
||||
expect(grandchild!.childrenCost).toBe(0)
|
||||
expect(grandchild!.totalCost).toBe(0.25)
|
||||
expect(grandchild!.totalAdded).toBe(1)
|
||||
expect(grandchild!.totalRemoved).toBe(4)
|
||||
})
|
||||
|
||||
it("should detect and prevent circular references", async () => {
|
||||
|
|
@ -156,11 +184,15 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
"task-a": {
|
||||
id: "task-a",
|
||||
totalCost: 1.0,
|
||||
linesAdded: 2,
|
||||
linesRemoved: 3,
|
||||
childIds: ["task-b"],
|
||||
} as unknown as HistoryItem,
|
||||
"task-b": {
|
||||
id: "task-b",
|
||||
totalCost: 0.5,
|
||||
linesAdded: 4,
|
||||
linesRemoved: 1,
|
||||
childIds: ["task-a"], // Circular reference back to task-a
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
|
@ -173,6 +205,8 @@ describe("aggregateTaskCostsRecursive", () => {
|
|||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(0.5) // Only task-b's own cost, circular ref returns 0
|
||||
expect(result.totalCost).toBe(1.5)
|
||||
expect(result.totalAdded).toBe(6) // task-a (2) + task-b (4)
|
||||
expect(result.totalRemoved).toBe(4) // task-a (3) + task-b (1)
|
||||
|
||||
// Verify warning was logged
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Circular reference detected: task-a"))
|
||||
|
|
@ -333,11 +367,23 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 0.5,
|
||||
childrenCost: 0,
|
||||
totalCost: 0.5,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 10,
|
||||
totalRemoved: 5,
|
||||
},
|
||||
"child-2": {
|
||||
ownCost: 0.3,
|
||||
childrenCost: 0.2,
|
||||
totalCost: 0.5,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 1,
|
||||
totalRemoved: 2,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -369,6 +415,8 @@ describe("buildSubtaskDetails", () => {
|
|||
expect(child1!.name).toBe("First subtask")
|
||||
expect(child1!.tokens).toBe(150) // 100 + 50
|
||||
expect(child1!.cost).toBe(0.5)
|
||||
expect(child1!.added).toBe(10)
|
||||
expect(child1!.removed).toBe(5)
|
||||
expect(child1!.status).toBe("completed")
|
||||
expect(child1!.hasNestedChildren).toBe(false)
|
||||
|
||||
|
|
@ -377,6 +425,8 @@ describe("buildSubtaskDetails", () => {
|
|||
expect(child2!.name).toBe("Second subtask with nested children")
|
||||
expect(child2!.tokens).toBe(300) // 200 + 100
|
||||
expect(child2!.cost).toBe(0.5)
|
||||
expect(child2!.added).toBe(1)
|
||||
expect(child2!.removed).toBe(2)
|
||||
expect(child2!.status).toBe("active")
|
||||
expect(child2!.hasNestedChildren).toBe(true) // childrenCost > 0
|
||||
})
|
||||
|
|
@ -389,6 +439,12 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 1.0,
|
||||
childrenCost: 0,
|
||||
totalCost: 1.0,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -418,6 +474,12 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 1.0,
|
||||
childrenCost: 0,
|
||||
totalCost: 1.0,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -445,11 +507,23 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 0.5,
|
||||
childrenCost: 0,
|
||||
totalCost: 0.5,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
"missing-child": {
|
||||
ownCost: 0.3,
|
||||
childrenCost: 0,
|
||||
totalCost: 0.3,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -488,6 +562,12 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 0.5,
|
||||
childrenCost: 0,
|
||||
totalCost: 0.5,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -514,6 +594,12 @@ describe("buildSubtaskDetails", () => {
|
|||
ownCost: 0.5,
|
||||
childrenCost: 0,
|
||||
totalCost: 0.5,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export interface SubtaskDetail {
|
|||
name: string // First 50 chars of task description
|
||||
tokens: number // tokensIn + tokensOut
|
||||
cost: number // Aggregated total cost
|
||||
added: number // Aggregated total lines added
|
||||
removed: number // Aggregated total lines removed
|
||||
status: "active" | "completed" | "delegated"
|
||||
hasNestedChildren: boolean // Has its own subtasks
|
||||
}
|
||||
|
|
@ -16,6 +18,12 @@ export interface AggregatedCosts {
|
|||
ownCost: number // This task's own API costs
|
||||
childrenCost: number // Sum of all direct children costs (recursive)
|
||||
totalCost: number // ownCost + childrenCost
|
||||
ownAdded: number // This task's own lines added
|
||||
ownRemoved: number // This task's own lines removed
|
||||
childrenAdded: number // Sum of all descendant lines added
|
||||
childrenRemoved: number // Sum of all descendant lines removed
|
||||
totalAdded: number // ownAdded + childrenAdded
|
||||
totalRemoved: number // ownRemoved + childrenRemoved
|
||||
childBreakdown?: {
|
||||
// Optional detailed breakdown
|
||||
[childId: string]: AggregatedCosts
|
||||
|
|
@ -39,7 +47,17 @@ export async function aggregateTaskCostsRecursive(
|
|||
// Prevent infinite loops
|
||||
if (visited.has(taskId)) {
|
||||
console.warn(`[aggregateTaskCostsRecursive] Circular reference detected: ${taskId}`)
|
||||
return { ownCost: 0, childrenCost: 0, totalCost: 0 }
|
||||
return {
|
||||
ownCost: 0,
|
||||
childrenCost: 0,
|
||||
totalCost: 0,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
}
|
||||
}
|
||||
visited.add(taskId)
|
||||
|
||||
|
|
@ -47,11 +65,25 @@ export async function aggregateTaskCostsRecursive(
|
|||
const history = await getTaskHistory(taskId)
|
||||
if (!history) {
|
||||
console.warn(`[aggregateTaskCostsRecursive] Task ${taskId} not found`)
|
||||
return { ownCost: 0, childrenCost: 0, totalCost: 0 }
|
||||
return {
|
||||
ownCost: 0,
|
||||
childrenCost: 0,
|
||||
totalCost: 0,
|
||||
ownAdded: 0,
|
||||
ownRemoved: 0,
|
||||
childrenAdded: 0,
|
||||
childrenRemoved: 0,
|
||||
totalAdded: 0,
|
||||
totalRemoved: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const ownCost = history.totalCost || 0
|
||||
const ownAdded = history.linesAdded || 0
|
||||
const ownRemoved = history.linesRemoved || 0
|
||||
let childrenCost = 0
|
||||
let childrenAdded = 0
|
||||
let childrenRemoved = 0
|
||||
const childBreakdown: { [childId: string]: AggregatedCosts } = {}
|
||||
|
||||
// Recursively aggregate child costs
|
||||
|
|
@ -63,6 +95,8 @@ export async function aggregateTaskCostsRecursive(
|
|||
new Set(visited), // Create new Set to allow sibling traversal
|
||||
)
|
||||
childrenCost += childAggregated.totalCost
|
||||
childrenAdded += childAggregated.totalAdded
|
||||
childrenRemoved += childAggregated.totalRemoved
|
||||
childBreakdown[childId] = childAggregated
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +105,12 @@ export async function aggregateTaskCostsRecursive(
|
|||
ownCost,
|
||||
childrenCost,
|
||||
totalCost: ownCost + childrenCost,
|
||||
ownAdded,
|
||||
ownRemoved,
|
||||
childrenAdded,
|
||||
childrenRemoved,
|
||||
totalAdded: ownAdded + childrenAdded,
|
||||
totalRemoved: ownRemoved + childrenRemoved,
|
||||
childBreakdown,
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +144,8 @@ export async function buildSubtaskDetails(
|
|||
name: truncateTaskName(history.task, 50),
|
||||
tokens: (history.tokensIn || 0) + (history.tokensOut || 0),
|
||||
cost: costs.totalCost,
|
||||
added: costs.totalAdded,
|
||||
removed: costs.totalRemoved,
|
||||
status: history.status || "completed",
|
||||
hasNestedChildren: costs.childrenCost > 0,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ const TaskHeader = ({
|
|||
? (() => {
|
||||
const lastRelevantIndex = findLastIndex(
|
||||
clineMessages,
|
||||
(m) => !(m.ask === "resume_task" || m.ask === "resume_completed_task"),
|
||||
(m) => !((m as any)?.ask === "resume_task" || (m as any)?.ask === "resume_completed_task"),
|
||||
)
|
||||
return lastRelevantIndex !== -1
|
||||
? clineMessages[lastRelevantIndex]?.ask === "completion_result"
|
||||
|
|
@ -160,6 +160,50 @@ const TaskHeader = ({
|
|||
return costs
|
||||
}, [todos, subtaskDetails])
|
||||
|
||||
const aggregatedLineChanges = useMemo(() => {
|
||||
const ownAdded = (currentTaskItem as any)?.linesAdded
|
||||
const ownRemoved = (currentTaskItem as any)?.linesRemoved
|
||||
|
||||
const processedSubtasks = new Set<string>()
|
||||
let childrenAdded = 0
|
||||
let childrenRemoved = 0
|
||||
|
||||
if (Array.isArray(subtaskDetails)) {
|
||||
for (const subtask of subtaskDetails) {
|
||||
if (!subtask?.id || typeof subtask.id !== "string") continue
|
||||
if (processedSubtasks.has(subtask.id)) continue
|
||||
processedSubtasks.add(subtask.id)
|
||||
|
||||
if (typeof subtask.added === "number" && Number.isFinite(subtask.added)) {
|
||||
childrenAdded += subtask.added
|
||||
}
|
||||
if (typeof subtask.removed === "number" && Number.isFinite(subtask.removed)) {
|
||||
childrenRemoved += subtask.removed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalAdded = (typeof ownAdded === "number" && Number.isFinite(ownAdded) ? ownAdded : 0) + childrenAdded
|
||||
const totalRemoved =
|
||||
(typeof ownRemoved === "number" && Number.isFinite(ownRemoved) ? ownRemoved : 0) + childrenRemoved
|
||||
|
||||
const hasAdded = totalAdded > 0
|
||||
const hasRemoved = totalRemoved > 0
|
||||
const hasAnyLineChanges = hasAdded || hasRemoved
|
||||
const formatted = [hasAdded ? `+${totalAdded}` : null, hasRemoved ? `−${totalRemoved}` : null]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return {
|
||||
totalAdded,
|
||||
totalRemoved,
|
||||
hasAdded,
|
||||
hasRemoved,
|
||||
hasAnyLineChanges,
|
||||
formatted,
|
||||
}
|
||||
}, [currentTaskItem, subtaskDetails])
|
||||
|
||||
const tooltipCostData = useMemo(
|
||||
() =>
|
||||
getTaskHeaderCostTooltipData({
|
||||
|
|
@ -313,7 +357,7 @@ const TaskHeader = ({
|
|||
<StandardTooltip
|
||||
content={
|
||||
shouldTreatAsHasSubtasks ? (
|
||||
<div>
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
{t("chat:costs.totalWithSubtasks", {
|
||||
cost: displayTotalCost.toFixed(2),
|
||||
|
|
@ -329,7 +373,7 @@ const TaskHeader = ({
|
|||
}
|
||||
side="top"
|
||||
sideOffset={8}>
|
||||
<span>
|
||||
<span className="tabular-nums">
|
||||
${displayTotalCost.toFixed(2)}
|
||||
{shouldTreatAsHasSubtasks && (
|
||||
<span className="text-xs ml-1" title={t("chat:costs.includesSubtasks")}>
|
||||
|
|
@ -339,6 +383,20 @@ const TaskHeader = ({
|
|||
</span>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{aggregatedLineChanges.hasAnyLineChanges && (
|
||||
<span className="flex items-center gap-2 tabular-nums text-sm">
|
||||
{aggregatedLineChanges.hasAdded && (
|
||||
<span className="font-medium text-vscode-charts-green">
|
||||
+{aggregatedLineChanges.totalAdded}
|
||||
</span>
|
||||
)}
|
||||
{aggregatedLineChanges.hasRemoved && (
|
||||
<span className="font-medium text-vscode-charts-red">
|
||||
−{aggregatedLineChanges.totalRemoved}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{showBrowserGlobe && (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
|
@ -514,6 +572,28 @@ const TaskHeader = ({
|
|||
</tr>
|
||||
)}
|
||||
|
||||
{aggregatedLineChanges.hasAnyLineChanges && (
|
||||
<tr>
|
||||
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]">
|
||||
{t("common:stats.lines")}
|
||||
</th>
|
||||
<td className="font-light align-top">
|
||||
<span className="flex items-center gap-2 tabular-nums">
|
||||
{aggregatedLineChanges.hasAdded && (
|
||||
<span className="font-medium text-vscode-charts-green">
|
||||
+{aggregatedLineChanges.totalAdded}
|
||||
</span>
|
||||
)}
|
||||
{aggregatedLineChanges.hasRemoved && (
|
||||
<span className="font-medium text-vscode-charts-red">
|
||||
−{aggregatedLineChanges.totalRemoved}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Size display */}
|
||||
{!!currentTaskItem?.size && currentTaskItem.size > 0 && (
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface TodoItem {
|
|||
subtaskId?: string
|
||||
tokens?: number
|
||||
cost?: number
|
||||
added?: number
|
||||
removed?: number
|
||||
}
|
||||
|
||||
function getTodoIcon(status: TodoStatus | null) {
|
||||
|
|
@ -84,7 +86,7 @@ export function TodoListDisplay({ todos, subtaskDetails, onSubtaskClick }: TodoL
|
|||
? "text-vscode-charts-yellow"
|
||||
: "text-vscode-foreground",
|
||||
)}
|
||||
onClick={() => setIsCollapsed((v) => !v)}>
|
||||
onClick={() => setIsCollapsed((v: boolean) => !v)}>
|
||||
<ListChecks className="size-3 shrink-0" />
|
||||
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{isCollapsed
|
||||
|
|
@ -113,6 +115,17 @@ export function TodoListDisplay({ todos, subtaskDetails, onSubtaskClick }: TodoL
|
|||
const displayCost = todo.cost ?? subtaskById?.cost
|
||||
const shouldShowCost = typeof displayTokens === "number" && typeof displayCost === "number"
|
||||
|
||||
const displayAdded = todo.added ?? subtaskById?.added
|
||||
const displayRemoved = todo.removed ?? subtaskById?.removed
|
||||
const hasValidSubtaskLink = typeof todo.subtaskId === "string" && todo.subtaskId.length > 0
|
||||
const shouldShowLineChanges =
|
||||
hasValidSubtaskLink && (Number.isFinite(displayAdded) || Number.isFinite(displayRemoved))
|
||||
|
||||
const hasAdded =
|
||||
typeof displayAdded === "number" && Number.isFinite(displayAdded) && displayAdded > 0
|
||||
const hasRemoved =
|
||||
typeof displayRemoved === "number" && Number.isFinite(displayRemoved) && displayRemoved > 0
|
||||
|
||||
return (
|
||||
<li
|
||||
key={todo.id || todo.content}
|
||||
|
|
@ -131,14 +144,36 @@ export function TodoListDisplay({ todos, subtaskDetails, onSubtaskClick }: TodoL
|
|||
{todo.content}
|
||||
</span>
|
||||
{/* Token count and cost display */}
|
||||
{shouldShowCost && (
|
||||
{(shouldShowCost || shouldShowLineChanges) && (
|
||||
<span className="flex items-center gap-2 text-xs text-vscode-descriptionForeground shrink-0">
|
||||
<span className="tabular-nums opacity-70">
|
||||
{formatLargeNumber(displayTokens)}
|
||||
</span>
|
||||
<span className="tabular-nums min-w-[45px] text-right">
|
||||
${displayCost.toFixed(2)}
|
||||
</span>
|
||||
{shouldShowCost && (
|
||||
<>
|
||||
<span className="tabular-nums opacity-70">
|
||||
{formatLargeNumber(displayTokens)}
|
||||
</span>
|
||||
<span className="tabular-nums min-w-[45px] text-right">
|
||||
${displayCost.toFixed(2)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{shouldShowLineChanges && (
|
||||
<span className="tabular-nums ml-2 min-w-[60px] grid grid-cols-2 items-center justify-end">
|
||||
<span
|
||||
className={cn(
|
||||
" text-right",
|
||||
hasAdded ? "font-medium text-vscode-charts-green" : "",
|
||||
)}>
|
||||
{hasAdded ? `+${displayAdded}` : "\u00A0"}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
" text-right",
|
||||
hasRemoved ? "font-medium text-vscode-charts-red" : "",
|
||||
)}>
|
||||
{hasRemoved ? `−${displayRemoved}` : "\u00A0"}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ describe("TodoListDisplay", () => {
|
|||
name: "Task 1: Change background colour",
|
||||
tokens: 95400,
|
||||
cost: 0.22,
|
||||
added: 10,
|
||||
removed: 4,
|
||||
status: "completed",
|
||||
hasNestedChildren: false,
|
||||
},
|
||||
|
|
@ -42,6 +44,8 @@ describe("TodoListDisplay", () => {
|
|||
name: "Task 2: Add timestamp to bottom",
|
||||
tokens: 95000,
|
||||
cost: 0.24,
|
||||
added: 3,
|
||||
removed: 2,
|
||||
status: "completed",
|
||||
hasNestedChildren: false,
|
||||
},
|
||||
|
|
@ -106,7 +110,9 @@ describe("TodoListDisplay", () => {
|
|||
|
||||
// The pending task has no subtaskId, should not show cost
|
||||
const listItems = screen.getAllByRole("listitem")
|
||||
const pendingItem = listItems.find((item) => item.textContent?.includes("Task 3: Pending task"))
|
||||
const pendingItem = listItems.find((item: HTMLElement) =>
|
||||
item.textContent?.includes("Task 3: Pending task"),
|
||||
)
|
||||
expect(pendingItem).toBeDefined()
|
||||
expect(pendingItem?.textContent).not.toContain("$")
|
||||
})
|
||||
|
|
@ -177,6 +183,96 @@ describe("TodoListDisplay", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("line change display", () => {
|
||||
it("uses todo.added/todo.removed when present", () => {
|
||||
const todosWithDirectLineChanges = [
|
||||
{
|
||||
id: "1",
|
||||
content: "Task 1: Change background colour",
|
||||
status: "completed",
|
||||
subtaskId: "subtask-1",
|
||||
added: 7,
|
||||
removed: 9,
|
||||
},
|
||||
]
|
||||
render(<TodoListDisplay todos={todosWithDirectLineChanges} subtaskDetails={subtaskDetails} />)
|
||||
|
||||
// Expand
|
||||
const header = screen.getByText("1 to-dos done")
|
||||
fireEvent.click(header)
|
||||
|
||||
// Line changes are rendered as separate colored spans
|
||||
expect(screen.getByText("+7")).toBeInTheDocument()
|
||||
expect(screen.getByText("−9")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("falls back to subtaskDetails when todo added/removed are missing", () => {
|
||||
const todosMissingDirectLineChanges = [
|
||||
{
|
||||
id: "1",
|
||||
content: "Task 1: Change background colour",
|
||||
status: "completed",
|
||||
subtaskId: "subtask-1",
|
||||
},
|
||||
]
|
||||
render(<TodoListDisplay todos={todosMissingDirectLineChanges} subtaskDetails={subtaskDetails} />)
|
||||
|
||||
// Expand
|
||||
const header = screen.getByText("1 to-dos done")
|
||||
fireEvent.click(header)
|
||||
|
||||
// Line changes are rendered as separate colored spans
|
||||
expect(screen.getByText("+10")).toBeInTheDocument()
|
||||
expect(screen.getByText("−4")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("hides line deltas when no data available (no subtaskId)", () => {
|
||||
const todosNoSubtaskLink = [{ id: "1", content: "No link todo", status: "completed" }]
|
||||
render(<TodoListDisplay todos={todosNoSubtaskLink} subtaskDetails={subtaskDetails} />)
|
||||
|
||||
// Expand
|
||||
const header = screen.getByText("1 to-dos done")
|
||||
fireEvent.click(header)
|
||||
|
||||
expect(screen.queryByText(/\+\d+/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/−\d+/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("hides line deltas when all values are undefined (subtaskId present)", () => {
|
||||
const todosWithLinkButNoLineChanges = [
|
||||
{
|
||||
id: "1",
|
||||
content: "Task 1: Change background colour",
|
||||
status: "completed",
|
||||
subtaskId: "subtask-1",
|
||||
},
|
||||
]
|
||||
const subtaskDetailsWithoutLineChanges: SubtaskDetail[] = [
|
||||
{
|
||||
id: "subtask-1",
|
||||
name: "Task 1: Change background colour",
|
||||
tokens: 95400,
|
||||
cost: 0.22,
|
||||
status: "completed",
|
||||
hasNestedChildren: false,
|
||||
} as unknown as SubtaskDetail,
|
||||
]
|
||||
render(
|
||||
<TodoListDisplay
|
||||
todos={todosWithLinkButNoLineChanges}
|
||||
subtaskDetails={subtaskDetailsWithoutLineChanges}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Expand
|
||||
const header = screen.getByText("1 to-dos done")
|
||||
fireEvent.click(header)
|
||||
|
||||
expect(screen.queryByText(/\+\d+/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/−\d+/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("click handler", () => {
|
||||
it("should call onSubtaskClick when a todo with subtaskId is clicked", () => {
|
||||
const onSubtaskClick = vi.fn()
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/ca/common.json
generated
5
webview-ui/src/i18n/locales/ca/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Esperant la tasca filla {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propi",
|
||||
"own": "Principal",
|
||||
"subtasks": "Subtasques"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Línies"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/de/common.json
generated
5
webview-ui/src/i18n/locales/de/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Warte auf Unteraufgabe {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Eigen",
|
||||
"own": "Haupt",
|
||||
"subtasks": "Unteraufgaben"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Zeilen"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"collapse": "Collapse task",
|
||||
"seeMore": "See more",
|
||||
"seeLess": "See less",
|
||||
"lineChanges": "Lines:",
|
||||
"tokens": "Tokens",
|
||||
"cache": "Cache",
|
||||
"apiCost": "API Cost",
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Awaiting child task {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Own",
|
||||
"own": "Main",
|
||||
"subtasks": "Subtasks"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Lines"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/es/common.json
generated
5
webview-ui/src/i18n/locales/es/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Esperando tarea secundaria {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propio",
|
||||
"own": "Principal",
|
||||
"subtasks": "Subtareas"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Líneas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/fr/common.json
generated
5
webview-ui/src/i18n/locales/fr/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "En attente de la tâche enfant {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propre",
|
||||
"own": "Principal",
|
||||
"subtasks": "Sous-tâches"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Lignes"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/hi/common.json
generated
5
webview-ui/src/i18n/locales/hi/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "चाइल्ड कार्य {{childId}} की प्रतीक्षा में"
|
||||
},
|
||||
"costs": {
|
||||
"own": "स्वयं",
|
||||
"own": "मुख्य",
|
||||
"subtasks": "उपकार्य"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "पंक्तियाँ"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/id/common.json
generated
5
webview-ui/src/i18n/locales/id/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Menunggu tugas anak {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Sendiri",
|
||||
"own": "Utama",
|
||||
"subtasks": "Subtugas"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Baris"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/it/common.json
generated
5
webview-ui/src/i18n/locales/it/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "In attesa dell'attività figlia {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Proprio",
|
||||
"own": "Principale",
|
||||
"subtasks": "Sottoattività"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Linee"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/ja/common.json
generated
5
webview-ui/src/i18n/locales/ja/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "子タスク{{childId}}を待機中"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"own": "メイン",
|
||||
"subtasks": "サブタスク"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "行"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/ko/common.json
generated
5
webview-ui/src/i18n/locales/ko/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "하위 작업 {{childId}} 대기 중"
|
||||
},
|
||||
"costs": {
|
||||
"own": "자체",
|
||||
"own": "메인",
|
||||
"subtasks": "하위작업"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "라인"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/nl/common.json
generated
5
webview-ui/src/i18n/locales/nl/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Wachten op kindtaak {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Eigen",
|
||||
"own": "Hoofd",
|
||||
"subtasks": "Subtaken"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Regels"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/pl/common.json
generated
5
webview-ui/src/i18n/locales/pl/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Oczekiwanie na zadanie podrzędne {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Własne",
|
||||
"own": "Główne",
|
||||
"subtasks": "Podzadania"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Linie"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/pt-BR/common.json
generated
5
webview-ui/src/i18n/locales/pt-BR/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Aguardando tarefa filha {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Próprio",
|
||||
"own": "Principal",
|
||||
"subtasks": "Subtarefas"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Linhas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/ru/common.json
generated
5
webview-ui/src/i18n/locales/ru/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Ожидание дочерней задачи {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Собственные",
|
||||
"own": "Основная",
|
||||
"subtasks": "Подзадачи"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Строки"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/tr/common.json
generated
5
webview-ui/src/i18n/locales/tr/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "{{childId}} alt görevi bekleniyor"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Kendi",
|
||||
"own": "Ana",
|
||||
"subtasks": "Alt görevler"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Satırlar"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/vi/common.json
generated
5
webview-ui/src/i18n/locales/vi/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "Đang chờ nhiệm vụ con {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Riêng",
|
||||
"own": "Chính",
|
||||
"subtasks": "Nhiệm vụ con"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "Dòng"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/zh-CN/common.json
generated
5
webview-ui/src/i18n/locales/zh-CN/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "等待子任务 {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"own": "主要",
|
||||
"subtasks": "子任务"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "行"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
webview-ui/src/i18n/locales/zh-TW/common.json
generated
5
webview-ui/src/i18n/locales/zh-TW/common.json
generated
|
|
@ -108,7 +108,10 @@
|
|||
"awaiting_child": "等待子工作 {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"own": "主要",
|
||||
"subtasks": "子工作"
|
||||
},
|
||||
"stats": {
|
||||
"lines": "行"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export type SubtaskDetail = {
|
|||
name: string
|
||||
/** tokensIn + tokensOut */
|
||||
tokens: number
|
||||
/** Total lines added across the subtask */
|
||||
added: number
|
||||
/** Total lines removed across the subtask */
|
||||
removed: number
|
||||
/** Aggregated total cost */
|
||||
cost: number
|
||||
status: "active" | "completed" | "delegated"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue