mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Feat/issue 5376 aggregate subtask costs (#10757)
This commit is contained in:
parent
f2b16d400d
commit
f48ea389df
44 changed files with 773 additions and 3 deletions
|
|
@ -94,6 +94,7 @@ export interface ExtensionMessage {
|
|||
| "claudeCodeRateLimits"
|
||||
| "customToolsResult"
|
||||
| "modes"
|
||||
| "taskWithAggregatedCosts"
|
||||
text?: string
|
||||
payload?: any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
checkpointWarning?: {
|
||||
|
|
@ -182,6 +183,13 @@ export interface ExtensionMessage {
|
|||
stepIndex?: number // For browserSessionNavigate: the target step index to display
|
||||
tools?: SerializedCustomToolDefinition[] // For customToolsResult
|
||||
modes?: { slug: string; name: string }[] // For modes response
|
||||
aggregatedCosts?: {
|
||||
// For taskWithAggregatedCosts response
|
||||
totalCost: number
|
||||
ownCost: number
|
||||
childrenCost: number
|
||||
}
|
||||
historyItem?: HistoryItem
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
@ -498,6 +506,7 @@ export interface WebviewMessage {
|
|||
| "getDismissedUpsells"
|
||||
| "updateSettings"
|
||||
| "allowedCommands"
|
||||
| "getTaskWithAggregatedCosts"
|
||||
| "deniedCommands"
|
||||
| "killBrowserSession"
|
||||
| "openBrowserSessionPanel"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import {
|
|||
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
|
||||
getModelId,
|
||||
} from "@roo-code/types"
|
||||
import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud"
|
||||
|
||||
|
|
@ -1705,6 +1706,20 @@ export class ClineProvider
|
|||
throw new Error("Task not found")
|
||||
}
|
||||
|
||||
async getTaskWithAggregatedCosts(taskId: string): Promise<{
|
||||
historyItem: HistoryItem
|
||||
aggregatedCosts: AggregatedCosts
|
||||
}> {
|
||||
const { historyItem } = await this.getTaskWithId(taskId)
|
||||
|
||||
const aggregatedCosts = await aggregateTaskCostsRecursive(taskId, async (id: string) => {
|
||||
const result = await this.getTaskWithId(id)
|
||||
return result.historyItem
|
||||
})
|
||||
|
||||
return { historyItem, aggregatedCosts }
|
||||
}
|
||||
|
||||
async showTaskWithId(id: string) {
|
||||
if (id !== this.getCurrentTask()?.taskId) {
|
||||
// Non-current task.
|
||||
|
|
|
|||
326
src/core/webview/__tests__/aggregateTaskCosts.spec.ts
Normal file
326
src/core/webview/__tests__/aggregateTaskCosts.spec.ts
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { aggregateTaskCostsRecursive } from "../aggregateTaskCosts.js"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
|
||||
describe("aggregateTaskCostsRecursive", () => {
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
it("should calculate cost for task with no children", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
"task-1": {
|
||||
id: "task-1",
|
||||
totalCost: 1.5,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("task-1", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(1.5)
|
||||
expect(result.childrenCost).toBe(0)
|
||||
expect(result.totalCost).toBe(1.5)
|
||||
expect(result.childBreakdown).toEqual({})
|
||||
})
|
||||
|
||||
it("should calculate cost for task with undefined childIds", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
"task-1": {
|
||||
id: "task-1",
|
||||
totalCost: 2.0,
|
||||
// childIds is undefined
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("task-1", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(2.0)
|
||||
expect(result.childrenCost).toBe(0)
|
||||
expect(result.totalCost).toBe(2.0)
|
||||
expect(result.childBreakdown).toEqual({})
|
||||
})
|
||||
|
||||
it("should aggregate parent with one child", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
childIds: ["child-1"],
|
||||
} as unknown as HistoryItem,
|
||||
"child-1": {
|
||||
id: "child-1",
|
||||
totalCost: 0.5,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("parent", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(0.5)
|
||||
expect(result.totalCost).toBe(1.5)
|
||||
expect(result.childBreakdown).toHaveProperty("child-1")
|
||||
const child1 = result.childBreakdown?.["child-1"]
|
||||
expect(child1).toBeDefined()
|
||||
expect(child1!.totalCost).toBe(0.5)
|
||||
})
|
||||
|
||||
it("should aggregate parent with multiple children", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
childIds: ["child-1", "child-2", "child-3"],
|
||||
} as unknown as HistoryItem,
|
||||
"child-1": {
|
||||
id: "child-1",
|
||||
totalCost: 0.5,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
"child-2": {
|
||||
id: "child-2",
|
||||
totalCost: 0.75,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
"child-3": {
|
||||
id: "child-3",
|
||||
totalCost: 0.25,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("parent", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(1.5) // 0.5 + 0.75 + 0.25
|
||||
expect(result.totalCost).toBe(2.5)
|
||||
expect(Object.keys(result.childBreakdown || {})).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("should recursively aggregate multi-level hierarchy", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
childIds: ["child"],
|
||||
} as unknown as HistoryItem,
|
||||
child: {
|
||||
id: "child",
|
||||
totalCost: 0.5,
|
||||
childIds: ["grandchild"],
|
||||
} as unknown as HistoryItem,
|
||||
grandchild: {
|
||||
id: "grandchild",
|
||||
totalCost: 0.25,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("parent", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(0.75) // child (0.5) + grandchild (0.25)
|
||||
expect(result.totalCost).toBe(1.75)
|
||||
|
||||
// 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)
|
||||
|
||||
// Verify grandchild breakdown
|
||||
const grandchild = child!.childBreakdown?.["grandchild"]
|
||||
expect(grandchild).toBeDefined()
|
||||
expect(grandchild!.ownCost).toBe(0.25)
|
||||
expect(grandchild!.childrenCost).toBe(0)
|
||||
expect(grandchild!.totalCost).toBe(0.25)
|
||||
})
|
||||
|
||||
it("should detect and prevent circular references", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
"task-a": {
|
||||
id: "task-a",
|
||||
totalCost: 1.0,
|
||||
childIds: ["task-b"],
|
||||
} as unknown as HistoryItem,
|
||||
"task-b": {
|
||||
id: "task-b",
|
||||
totalCost: 0.5,
|
||||
childIds: ["task-a"], // Circular reference back to task-a
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("task-a", getTaskHistory)
|
||||
|
||||
// Should still process task-b but ignore the circular reference
|
||||
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)
|
||||
|
||||
// Verify warning was logged
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Circular reference detected: task-a"))
|
||||
})
|
||||
|
||||
it("should handle missing task gracefully", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
childIds: ["nonexistent-child"],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("parent", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(1.0)
|
||||
expect(result.childrenCost).toBe(0) // Missing child contributes 0
|
||||
expect(result.totalCost).toBe(1.0)
|
||||
|
||||
// Verify warning was logged
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Task nonexistent-child not found"))
|
||||
})
|
||||
|
||||
it("should return zero costs for completely missing task", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("nonexistent", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(0)
|
||||
expect(result.childrenCost).toBe(0)
|
||||
expect(result.totalCost).toBe(0)
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining("Task nonexistent not found"))
|
||||
})
|
||||
|
||||
it("should handle task with null totalCost", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
"task-1": {
|
||||
id: "task-1",
|
||||
totalCost: null as unknown as number, // Explicitly null (invalid type in prod)
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("task-1", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(0)
|
||||
expect(result.childrenCost).toBe(0)
|
||||
expect(result.totalCost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle task with undefined totalCost", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
"task-1": {
|
||||
id: "task-1",
|
||||
// totalCost is undefined
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("task-1", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(0)
|
||||
expect(result.childrenCost).toBe(0)
|
||||
expect(result.totalCost).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle complex hierarchy with mixed costs", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
root: {
|
||||
id: "root",
|
||||
totalCost: 2.5,
|
||||
childIds: ["child-1", "child-2"],
|
||||
} as unknown as HistoryItem,
|
||||
"child-1": {
|
||||
id: "child-1",
|
||||
totalCost: 1.2,
|
||||
childIds: ["grandchild-1", "grandchild-2"],
|
||||
} as unknown as HistoryItem,
|
||||
"child-2": {
|
||||
id: "child-2",
|
||||
totalCost: 0.8,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
"grandchild-1": {
|
||||
id: "grandchild-1",
|
||||
totalCost: 0.3,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
"grandchild-2": {
|
||||
id: "grandchild-2",
|
||||
totalCost: 0.15,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("root", getTaskHistory)
|
||||
|
||||
expect(result.ownCost).toBe(2.5)
|
||||
// child-1: 1.2 + 0.3 + 0.15 = 1.65
|
||||
// child-2: 0.8
|
||||
// Total children: 2.45
|
||||
expect(result.childrenCost).toBe(2.45)
|
||||
expect(result.totalCost).toBe(4.95) // 2.5 + 2.45
|
||||
})
|
||||
|
||||
it("should handle siblings without cross-contamination", async () => {
|
||||
const mockHistory: Record<string, HistoryItem> = {
|
||||
parent: {
|
||||
id: "parent",
|
||||
totalCost: 1.0,
|
||||
childIds: ["sibling-1", "sibling-2"],
|
||||
} as unknown as HistoryItem,
|
||||
"sibling-1": {
|
||||
id: "sibling-1",
|
||||
totalCost: 0.5,
|
||||
childIds: ["nephew"],
|
||||
} as unknown as HistoryItem,
|
||||
"sibling-2": {
|
||||
id: "sibling-2",
|
||||
totalCost: 0.3,
|
||||
childIds: ["nephew"], // Same child ID as sibling-1
|
||||
} as unknown as HistoryItem,
|
||||
nephew: {
|
||||
id: "nephew",
|
||||
totalCost: 0.1,
|
||||
childIds: [],
|
||||
} as unknown as HistoryItem,
|
||||
}
|
||||
|
||||
const getTaskHistory = vi.fn(async (id: string) => mockHistory[id])
|
||||
|
||||
const result = await aggregateTaskCostsRecursive("parent", getTaskHistory)
|
||||
|
||||
// Both siblings should independently count nephew
|
||||
// sibling-1: 0.5 + 0.1 = 0.6
|
||||
// sibling-2: 0.3 + 0.1 = 0.4
|
||||
// Total: 1.0 + 0.6 + 0.4 = 2.0
|
||||
expect(result.totalCost).toBe(2.0)
|
||||
})
|
||||
})
|
||||
65
src/core/webview/aggregateTaskCosts.ts
Normal file
65
src/core/webview/aggregateTaskCosts.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import type { HistoryItem } from "@roo-code/types"
|
||||
|
||||
export interface AggregatedCosts {
|
||||
ownCost: number // This task's own API costs
|
||||
childrenCost: number // Sum of all direct children costs (recursive)
|
||||
totalCost: number // ownCost + childrenCost
|
||||
childBreakdown?: {
|
||||
// Optional detailed breakdown
|
||||
[childId: string]: AggregatedCosts
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively aggregate costs for a task and all its subtasks.
|
||||
*
|
||||
* @param taskId - The task ID to aggregate costs for
|
||||
* @param getTaskHistory - Function to load HistoryItem by task ID
|
||||
* @param visited - Set to prevent circular references
|
||||
* @returns Aggregated cost information
|
||||
*/
|
||||
export async function aggregateTaskCostsRecursive(
|
||||
taskId: string,
|
||||
getTaskHistory: (id: string) => Promise<HistoryItem | undefined>,
|
||||
visited: Set<string> = new Set(),
|
||||
): Promise<AggregatedCosts> {
|
||||
// Prevent infinite loops
|
||||
if (visited.has(taskId)) {
|
||||
console.warn(`[aggregateTaskCostsRecursive] Circular reference detected: ${taskId}`)
|
||||
return { ownCost: 0, childrenCost: 0, totalCost: 0 }
|
||||
}
|
||||
visited.add(taskId)
|
||||
|
||||
// Load this task's history
|
||||
const history = await getTaskHistory(taskId)
|
||||
if (!history) {
|
||||
console.warn(`[aggregateTaskCostsRecursive] Task ${taskId} not found`)
|
||||
return { ownCost: 0, childrenCost: 0, totalCost: 0 }
|
||||
}
|
||||
|
||||
const ownCost = history.totalCost || 0
|
||||
let childrenCost = 0
|
||||
const childBreakdown: { [childId: string]: AggregatedCosts } = {}
|
||||
|
||||
// Recursively aggregate child costs
|
||||
if (history.childIds && history.childIds.length > 0) {
|
||||
for (const childId of history.childIds) {
|
||||
const childAggregated = await aggregateTaskCostsRecursive(
|
||||
childId,
|
||||
getTaskHistory,
|
||||
new Set(visited), // Create new Set to allow sibling traversal
|
||||
)
|
||||
childrenCost += childAggregated.totalCost
|
||||
childBreakdown[childId] = childAggregated
|
||||
}
|
||||
}
|
||||
|
||||
const result: AggregatedCosts = {
|
||||
ownCost,
|
||||
childrenCost,
|
||||
totalCost: ownCost + childrenCost,
|
||||
childBreakdown,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -784,6 +784,32 @@ export const webviewMessageHandler = async (
|
|||
case "exportTaskWithId":
|
||||
provider.exportTaskWithId(message.text!)
|
||||
break
|
||||
case "getTaskWithAggregatedCosts": {
|
||||
try {
|
||||
const taskId = message.text
|
||||
if (!taskId) {
|
||||
throw new Error("Task ID is required")
|
||||
}
|
||||
const result = await provider.getTaskWithAggregatedCosts(taskId)
|
||||
await provider.postMessageToWebview({
|
||||
type: "taskWithAggregatedCosts",
|
||||
// IMPORTANT: ChatView stores aggregatedCostsMap keyed by message.text (taskId)
|
||||
// so we must include it here.
|
||||
text: taskId,
|
||||
historyItem: result.historyItem,
|
||||
aggregatedCosts: result.aggregatedCosts,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting task with aggregated costs:", error)
|
||||
await provider.postMessageToWebview({
|
||||
type: "taskWithAggregatedCosts",
|
||||
// Include taskId when available for correlation in UI logs.
|
||||
text: message.text,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "importSettings": {
|
||||
await importSettingsWithFeedback({
|
||||
providerSettingsManager: provider.providerSettingsManager,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Trans } from "react-i18next"
|
|||
|
||||
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
|
||||
import { appendImages } from "@src/utils/imageUtils"
|
||||
import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting"
|
||||
|
||||
import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types"
|
||||
|
||||
|
|
@ -166,6 +167,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const autoApproveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const userRespondedRef = useRef<boolean>(false)
|
||||
const [currentFollowUpTs, setCurrentFollowUpTs] = useState<number | null>(null)
|
||||
const [aggregatedCostsMap, setAggregatedCostsMap] = useState<
|
||||
Map<
|
||||
string,
|
||||
{
|
||||
totalCost: number
|
||||
ownCost: number
|
||||
childrenCost: number
|
||||
}
|
||||
>
|
||||
>(new Map())
|
||||
|
||||
const clineAskRef = useRef(clineAsk)
|
||||
useEffect(() => {
|
||||
|
|
@ -467,6 +478,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
userRespondedRef.current = false
|
||||
}, [task?.ts])
|
||||
|
||||
const taskTs = task?.ts
|
||||
|
||||
// Request aggregated costs when task changes and has childIds
|
||||
useEffect(() => {
|
||||
if (taskTs && currentTaskItem?.childIds && currentTaskItem.childIds.length > 0) {
|
||||
vscode.postMessage({
|
||||
type: "getTaskWithAggregatedCosts",
|
||||
text: currentTaskItem.id,
|
||||
})
|
||||
}
|
||||
}, [taskTs, currentTaskItem?.id, currentTaskItem?.childIds])
|
||||
|
||||
useEffect(() => {
|
||||
if (isHidden) {
|
||||
everVisibleMessagesTsRef.current.clear()
|
||||
|
|
@ -889,6 +912,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "interactionRequired":
|
||||
playSound("notification")
|
||||
break
|
||||
case "taskWithAggregatedCosts":
|
||||
if (message.text && message.aggregatedCosts) {
|
||||
setAggregatedCostsMap((prev) => {
|
||||
const newMap = new Map(prev)
|
||||
newMap.set(message.text!, message.aggregatedCosts!)
|
||||
return newMap
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
// textAreaRef.current is not explicitly required here since React
|
||||
// guarantees that ref will be stable across re-renders, and we're
|
||||
|
|
@ -1438,6 +1470,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
cacheWrites={apiMetrics.totalCacheWrites}
|
||||
cacheReads={apiMetrics.totalCacheReads}
|
||||
totalCost={apiMetrics.totalCost}
|
||||
aggregatedCost={
|
||||
currentTaskItem?.id && aggregatedCostsMap.has(currentTaskItem.id)
|
||||
? aggregatedCostsMap.get(currentTaskItem.id)!.totalCost
|
||||
: undefined
|
||||
}
|
||||
hasSubtasks={
|
||||
!!(
|
||||
currentTaskItem?.id &&
|
||||
aggregatedCostsMap.has(currentTaskItem.id) &&
|
||||
aggregatedCostsMap.get(currentTaskItem.id)!.childrenCost > 0
|
||||
)
|
||||
}
|
||||
costBreakdown={
|
||||
currentTaskItem?.id && aggregatedCostsMap.has(currentTaskItem.id)
|
||||
? getCostBreakdownIfNeeded(aggregatedCostsMap.get(currentTaskItem.id)!, {
|
||||
own: t("common:costs.own"),
|
||||
subtasks: t("common:costs.subtasks"),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
contextTokens={apiMetrics.contextTokens}
|
||||
buttonsDisabled={sendingDisabled}
|
||||
handleCondenseContext={handleCondenseContext}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ export interface TaskHeaderProps {
|
|||
cacheWrites?: number
|
||||
cacheReads?: number
|
||||
totalCost: number
|
||||
aggregatedCost?: number
|
||||
hasSubtasks?: boolean
|
||||
costBreakdown?: string
|
||||
contextTokens: number
|
||||
buttonsDisabled: boolean
|
||||
handleCondenseContext: (taskId: string) => void
|
||||
|
|
@ -55,6 +58,9 @@ const TaskHeader = ({
|
|||
cacheWrites,
|
||||
cacheReads,
|
||||
totalCost,
|
||||
aggregatedCost,
|
||||
hasSubtasks,
|
||||
costBreakdown,
|
||||
contextTokens,
|
||||
buttonsDisabled,
|
||||
handleCondenseContext,
|
||||
|
|
@ -248,7 +254,34 @@ const TaskHeader = ({
|
|||
{formatLargeNumber(contextTokens || 0)} / {formatLargeNumber(contextWindow)}
|
||||
</span>
|
||||
</StandardTooltip>
|
||||
{!!totalCost && <span>${totalCost.toFixed(2)}</span>}
|
||||
{!!totalCost && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
hasSubtasks ? (
|
||||
<div>
|
||||
<div>
|
||||
{t("chat:costs.totalWithSubtasks", {
|
||||
cost: (aggregatedCost ?? totalCost).toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
{costBreakdown && <div className="text-xs mt-1">{costBreakdown}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div>{t("chat:costs.total", { cost: totalCost.toFixed(2) })}</div>
|
||||
)
|
||||
}
|
||||
side="top"
|
||||
sideOffset={8}>
|
||||
<span>
|
||||
${(aggregatedCost ?? totalCost).toFixed(2)}
|
||||
{hasSubtasks && (
|
||||
<span className="text-xs ml-1" title={t("chat:costs.includesSubtasks")}>
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
{showBrowserGlobe && (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
|
|
@ -278,7 +311,7 @@ const TaskHeader = ({
|
|||
<span
|
||||
className="text-sm font-medium"
|
||||
style={{ color: "var(--vscode-testing-iconPassed)" }}>
|
||||
Active
|
||||
{t("chat:browser.active")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -386,7 +419,38 @@ const TaskHeader = ({
|
|||
{t("chat:task.apiCost")}
|
||||
</th>
|
||||
<td className="font-light align-top">
|
||||
<span>${totalCost?.toFixed(2)}</span>
|
||||
<StandardTooltip
|
||||
content={
|
||||
hasSubtasks ? (
|
||||
<div>
|
||||
<div>
|
||||
{t("chat:costs.totalWithSubtasks", {
|
||||
cost: (aggregatedCost ?? totalCost).toFixed(2),
|
||||
})}
|
||||
</div>
|
||||
{costBreakdown && (
|
||||
<div className="text-xs mt-1">{costBreakdown}</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="top"
|
||||
sideOffset={8}>
|
||||
<span>
|
||||
${(aggregatedCost ?? totalCost).toFixed(2)}
|
||||
{hasSubtasks && (
|
||||
<span
|
||||
className="text-xs ml-1"
|
||||
title={t("chat:costs.includesSubtasks")}>
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</StandardTooltip>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ca/chat.json
generated
6
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "A més, <careersLink>estem contractant!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Cost total (subtasques incloses): ${{cost}}",
|
||||
"total": "Cost total: ${{cost}}",
|
||||
"includesSubtasks": "Inclou els costos de les subtasques"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sessió del navegador",
|
||||
"active": "Actiu",
|
||||
"rooWantsToUse": "Roo vol utilitzar el navegador",
|
||||
"consoleLogs": "Registres de consola",
|
||||
"noNewLogs": "(Cap registre nou)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ca/common.json
generated
4
webview-ui/src/i18n/locales/ca/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Delegat a la tasca {{childId}}",
|
||||
"delegation_completed": "Subtasca completada, reprenent la tasca principal",
|
||||
"awaiting_child": "Esperant la tasca filla {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propi",
|
||||
"subtasks": "Subtasques"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/de/chat.json
generated
6
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Außerdem, <careersLink>wir stellen ein!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Gesamtkosten (inkl. Unteraufgaben): ${{cost}}",
|
||||
"total": "Gesamtkosten: ${{cost}}",
|
||||
"includesSubtasks": "Enthält Kosten für Unteraufgaben"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Browser-Sitzung",
|
||||
"active": "Aktiv",
|
||||
"rooWantsToUse": "Roo möchte den Browser verwenden",
|
||||
"consoleLogs": "Konsolenprotokolle",
|
||||
"noNewLogs": "(Keine neuen Protokolle)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/de/common.json
generated
4
webview-ui/src/i18n/locales/de/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "An Aufgabe {{childId}} delegiert",
|
||||
"delegation_completed": "Unteraufgabe abgeschlossen, übergeordnete Aufgabe wird fortgesetzt",
|
||||
"awaiting_child": "Warte auf Unteraufgabe {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Eigen",
|
||||
"subtasks": "Unteraufgaben"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -362,8 +362,14 @@
|
|||
"copyToInput": "Copy to input (same as shift + click)",
|
||||
"timerPrefix": "Auto-approve enabled. Selecting in {{seconds}}s…"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Total Cost (including subtasks): ${{cost}}",
|
||||
"total": "Total Cost: ${{cost}}",
|
||||
"includesSubtasks": "Includes subtask costs"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Browser Session",
|
||||
"active": "Active",
|
||||
"rooWantsToUse": "Roo wants to use the browser",
|
||||
"consoleLogs": "Console Logs",
|
||||
"noNewLogs": "(No new logs)",
|
||||
|
|
|
|||
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Delegated to task {{childId}}",
|
||||
"delegation_completed": "Subtask completed, resuming parent",
|
||||
"awaiting_child": "Awaiting child task {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Own",
|
||||
"subtasks": "Subtasks"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/es/chat.json
generated
6
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Además, <careersLink>¡estamos contratando!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Costo total (incluyendo subtareas): ${{cost}}",
|
||||
"total": "Costo total: ${{cost}}",
|
||||
"includesSubtasks": "Incluye costos de subtareas"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sesión del navegador",
|
||||
"active": "Activo",
|
||||
"rooWantsToUse": "Roo quiere usar el navegador",
|
||||
"consoleLogs": "Registros de la consola",
|
||||
"noNewLogs": "(No hay nuevos registros)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/es/common.json
generated
4
webview-ui/src/i18n/locales/es/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Delegado a la tarea {{childId}}",
|
||||
"delegation_completed": "Subtarea completada, reanudando tarea principal",
|
||||
"awaiting_child": "Esperando tarea secundaria {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propio",
|
||||
"subtasks": "Subtareas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/fr/chat.json
generated
6
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Aussi, <careersLink>on recrute !</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Coût total (sous-tâches comprises) : ${{cost}}",
|
||||
"total": "Coût total : ${{cost}}",
|
||||
"includesSubtasks": "Inclut les coûts des sous-tâches"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Session du navigateur",
|
||||
"active": "Actif",
|
||||
"rooWantsToUse": "Roo veut utiliser le navigateur",
|
||||
"consoleLogs": "Journaux de console",
|
||||
"noNewLogs": "(Pas de nouveaux journaux)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/fr/common.json
generated
4
webview-ui/src/i18n/locales/fr/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Délégué à la tâche {{childId}}",
|
||||
"delegation_completed": "Sous-tâche terminée, reprise de la tâche parent",
|
||||
"awaiting_child": "En attente de la tâche enfant {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Propre",
|
||||
"subtasks": "Sous-tâches"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/hi/chat.json
generated
6
webview-ui/src/i18n/locales/hi/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "साथ ही, <careersLink>हम भर्ती कर रहे हैं!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "कुल लागत (उप-कार्यों सहित): ${{cost}}",
|
||||
"total": "कुल लागत: ${{cost}}",
|
||||
"includesSubtasks": "उप-कार्यों की लागत शामिल है"
|
||||
},
|
||||
"browser": {
|
||||
"session": "ब्राउज़र सत्र",
|
||||
"active": "सक्रिय",
|
||||
"rooWantsToUse": "Roo ब्राउज़र का उपयोग करना चाहता है",
|
||||
"consoleLogs": "कंसोल लॉग",
|
||||
"noNewLogs": "(कोई नया लॉग नहीं)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/hi/common.json
generated
4
webview-ui/src/i18n/locales/hi/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "कार्य {{childId}} को सौंपा गया",
|
||||
"delegation_completed": "उप-कार्य पूर्ण, मुख्य कार्य फिर से शुरू हो रहा है",
|
||||
"awaiting_child": "चाइल्ड कार्य {{childId}} की प्रतीक्षा में"
|
||||
},
|
||||
"costs": {
|
||||
"own": "स्वयं",
|
||||
"subtasks": "उपकार्य"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/id/chat.json
generated
6
webview-ui/src/i18n/locales/id/chat.json
generated
|
|
@ -368,8 +368,14 @@
|
|||
"copyToInput": "Salin ke input (sama dengan shift + klik)",
|
||||
"timerPrefix": "Persetujuan otomatis diaktifkan. Memilih dalam {{seconds}}s…"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Total Biaya (termasuk subtugas): ${{cost}}",
|
||||
"total": "Total Biaya: ${{cost}}",
|
||||
"includesSubtasks": "Termasuk biaya subtugas"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sesi Browser",
|
||||
"active": "Aktif",
|
||||
"rooWantsToUse": "Roo ingin menggunakan browser",
|
||||
"consoleLogs": "Log Konsol",
|
||||
"noNewLogs": "(Tidak ada log baru)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/id/common.json
generated
4
webview-ui/src/i18n/locales/id/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Didelegasikan ke tugas {{childId}}",
|
||||
"delegation_completed": "Subtugas selesai, melanjutkan tugas induk",
|
||||
"awaiting_child": "Menunggu tugas anak {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Sendiri",
|
||||
"subtasks": "Subtugas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/it/chat.json
generated
6
webview-ui/src/i18n/locales/it/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Inoltre, <careersLink>stiamo assumendo!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Costo totale (sottoattività incluse): ${{cost}}",
|
||||
"total": "Costo totale: ${{cost}}",
|
||||
"includesSubtasks": "Include i costi delle sottoattività"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sessione del browser",
|
||||
"active": "Attivo",
|
||||
"rooWantsToUse": "Roo vuole utilizzare il browser",
|
||||
"consoleLogs": "Log della console",
|
||||
"noNewLogs": "(Nessun nuovo log)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/it/common.json
generated
4
webview-ui/src/i18n/locales/it/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Delegato all'attività {{childId}}",
|
||||
"delegation_completed": "Sottoattività completata, ripresa attività padre",
|
||||
"awaiting_child": "In attesa dell'attività figlia {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Proprio",
|
||||
"subtasks": "Sottoattività"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ja/chat.json
generated
6
webview-ui/src/i18n/locales/ja/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "また、<careersLink>採用中です!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "合計コスト(サブタスク含む): ${{cost}}",
|
||||
"total": "合計コスト: ${{cost}}",
|
||||
"includesSubtasks": "サブタスクのコストを含む"
|
||||
},
|
||||
"browser": {
|
||||
"session": "ブラウザセッション",
|
||||
"active": "アクティブ",
|
||||
"rooWantsToUse": "Rooはブラウザを使用したい",
|
||||
"consoleLogs": "コンソールログ",
|
||||
"noNewLogs": "(新しいログはありません)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ja/common.json
generated
4
webview-ui/src/i18n/locales/ja/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "タスク{{childId}}に委任",
|
||||
"delegation_completed": "サブタスク完了、親タスクを再開",
|
||||
"awaiting_child": "子タスク{{childId}}を待機中"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"subtasks": "サブタスク"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ko/chat.json
generated
6
webview-ui/src/i18n/locales/ko/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "그리고, <careersLink>채용 중입니다!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "총 비용 (하위 작업 포함): ${{cost}}",
|
||||
"total": "총 비용: ${{cost}}",
|
||||
"includesSubtasks": "하위 작업 비용 포함"
|
||||
},
|
||||
"browser": {
|
||||
"session": "브라우저 세션",
|
||||
"active": "활성",
|
||||
"rooWantsToUse": "Roo가 브라우저를 사용하고 싶어합니다",
|
||||
"consoleLogs": "콘솔 로그",
|
||||
"noNewLogs": "(새 로그 없음)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ko/common.json
generated
4
webview-ui/src/i18n/locales/ko/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "작업 {{childId}}에 위임됨",
|
||||
"delegation_completed": "하위 작업 완료, 상위 작업 재개",
|
||||
"awaiting_child": "하위 작업 {{childId}} 대기 중"
|
||||
},
|
||||
"costs": {
|
||||
"own": "자체",
|
||||
"subtasks": "하위작업"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/nl/chat.json
generated
6
webview-ui/src/i18n/locales/nl/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
"copyToInput": "Kopiëren naar invoer (zelfde als shift + klik)",
|
||||
"timerPrefix": "Automatisch goedkeuren ingeschakeld. Selecteren in {{seconds}}s…"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Totale kosten (inclusief subtaken): ${{cost}}",
|
||||
"total": "Totale kosten: ${{cost}}",
|
||||
"includesSubtasks": "Inclusief kosten van subtaken"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Browsersessie",
|
||||
"active": "Actief",
|
||||
"rooWantsToUse": "Roo wil de browser gebruiken",
|
||||
"consoleLogs": "Console-logboeken",
|
||||
"noNewLogs": "(Geen nieuwe logboeken)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/nl/common.json
generated
4
webview-ui/src/i18n/locales/nl/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Gedelegeerd naar taak {{childId}}",
|
||||
"delegation_completed": "Subtaak voltooid, hoofdtaak wordt hervat",
|
||||
"awaiting_child": "Wachten op kindtaak {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Eigen",
|
||||
"subtasks": "Subtaken"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/pl/chat.json
generated
6
webview-ui/src/i18n/locales/pl/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Dodatkowo, <careersLink>zatrudniamy!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Całkowity koszt (w tym podzadania): ${{cost}}",
|
||||
"total": "Całkowity koszt: ${{cost}}",
|
||||
"includesSubtasks": "Zawiera koszty podzadań"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sesja przeglądarki",
|
||||
"active": "Aktywna",
|
||||
"rooWantsToUse": "Roo chce użyć przeglądarki",
|
||||
"consoleLogs": "Logi konsoli",
|
||||
"noNewLogs": "(Brak nowych logów)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pl/common.json
generated
4
webview-ui/src/i18n/locales/pl/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Przekazano do zadania {{childId}}",
|
||||
"delegation_completed": "Podzadanie ukończone, wznowienie zadania nadrzędnego",
|
||||
"awaiting_child": "Oczekiwanie na zadanie podrzędne {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Własne",
|
||||
"subtasks": "Podzadania"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
6
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
|
|
@ -347,8 +347,14 @@
|
|||
},
|
||||
"careers": "Além disso, <careersLink>estamos contratando!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Custo Total (incluindo subtarefas): ${{cost}}",
|
||||
"total": "Custo Total: ${{cost}}",
|
||||
"includesSubtasks": "Inclui custos de subtarefas"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Sessão do Navegador",
|
||||
"active": "Ativo",
|
||||
"rooWantsToUse": "Roo quer usar o navegador",
|
||||
"consoleLogs": "Logs do console",
|
||||
"noNewLogs": "(Sem novos logs)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/pt-BR/common.json
generated
4
webview-ui/src/i18n/locales/pt-BR/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Delegado para tarefa {{childId}}",
|
||||
"delegation_completed": "Subtarefa concluída, retomando tarefa pai",
|
||||
"awaiting_child": "Aguardando tarefa filha {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Próprio",
|
||||
"subtasks": "Subtarefas"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/ru/chat.json
generated
6
webview-ui/src/i18n/locales/ru/chat.json
generated
|
|
@ -348,8 +348,14 @@
|
|||
"copyToInput": "Скопировать во ввод (то же, что shift + клик)",
|
||||
"timerPrefix": "Автоматическое одобрение включено. Выбор через {{seconds}}s…"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Общая стоимость (включая подзадачи): ${{cost}}",
|
||||
"total": "Общая стоимость: ${{cost}}",
|
||||
"includesSubtasks": "Включает стоимость подзадач"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Сеанс браузера",
|
||||
"active": "Активен",
|
||||
"rooWantsToUse": "Roo хочет использовать браузер",
|
||||
"consoleLogs": "Логи консоли",
|
||||
"noNewLogs": "(Новых логов нет)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/ru/common.json
generated
4
webview-ui/src/i18n/locales/ru/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Делегировано задаче {{childId}}",
|
||||
"delegation_completed": "Подзадача завершена, возобновление родительской задачи",
|
||||
"awaiting_child": "Ожидание дочерней задачи {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Собственные",
|
||||
"subtasks": "Подзадачи"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/tr/chat.json
generated
6
webview-ui/src/i18n/locales/tr/chat.json
generated
|
|
@ -348,8 +348,14 @@
|
|||
},
|
||||
"careers": "Ayrıca, <careersLink>işe alım yapıyoruz!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Toplam Maliyet (alt görevler dahil): ${{cost}}",
|
||||
"total": "Toplam Maliyet: ${{cost}}",
|
||||
"includesSubtasks": "Alt görev maliyetlerini içerir"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Tarayıcı Oturumu",
|
||||
"active": "Aktif",
|
||||
"rooWantsToUse": "Roo tarayıcıyı kullanmak istiyor",
|
||||
"consoleLogs": "Konsol Kayıtları",
|
||||
"noNewLogs": "(Yeni kayıt yok)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/tr/common.json
generated
4
webview-ui/src/i18n/locales/tr/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "{{childId}} görevine devredildi",
|
||||
"delegation_completed": "Alt görev tamamlandı, üst görev devam ediyor",
|
||||
"awaiting_child": "{{childId}} alt görevi bekleniyor"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Kendi",
|
||||
"subtasks": "Alt görevler"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/vi/chat.json
generated
6
webview-ui/src/i18n/locales/vi/chat.json
generated
|
|
@ -348,8 +348,14 @@
|
|||
},
|
||||
"careers": "Ngoài ra, <careersLink>chúng tôi đang tuyển dụng!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "Tổng chi phí (bao gồm các tác vụ phụ): ${{cost}}",
|
||||
"total": "Tổng chi phí: ${{cost}}",
|
||||
"includesSubtasks": "Bao gồm chi phí của các tác vụ phụ"
|
||||
},
|
||||
"browser": {
|
||||
"session": "Phiên trình duyệt",
|
||||
"active": "Đang hoạt động",
|
||||
"rooWantsToUse": "Roo muốn sử dụng trình duyệt",
|
||||
"consoleLogs": "Nhật ký bảng điều khiển",
|
||||
"noNewLogs": "(Không có nhật ký mới)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/vi/common.json
generated
4
webview-ui/src/i18n/locales/vi/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "Ủy quyền cho nhiệm vụ {{childId}}",
|
||||
"delegation_completed": "Nhiệm vụ con hoàn thành, tiếp tục nhiệm vụ cha",
|
||||
"awaiting_child": "Đang chờ nhiệm vụ con {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "Riêng",
|
||||
"subtasks": "Nhiệm vụ con"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
6
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -348,8 +348,14 @@
|
|||
},
|
||||
"careers": "此外,<careersLink>我们正在招聘!</careersLink>"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "总成本(包括子任务): ${{cost}}",
|
||||
"total": "总成本: ${{cost}}",
|
||||
"includesSubtasks": "包括子任务成本"
|
||||
},
|
||||
"browser": {
|
||||
"session": "浏览器会话",
|
||||
"active": "活动中",
|
||||
"rooWantsToUse": "Roo想使用浏览器",
|
||||
"consoleLogs": "控制台日志",
|
||||
"noNewLogs": "(没有新日志)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-CN/common.json
generated
4
webview-ui/src/i18n/locales/zh-CN/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "已委托给任务 {{childId}}",
|
||||
"delegation_completed": "子任务已完成,恢复父任务",
|
||||
"awaiting_child": "等待子任务 {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"subtasks": "子任务"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
6
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
|
|
@ -366,8 +366,14 @@
|
|||
"copyToInput": "複製到輸入框 (或按住 Shift 並點選)",
|
||||
"timerPrefix": "自動批准已啟用。{{seconds}}秒後選擇中…"
|
||||
},
|
||||
"costs": {
|
||||
"totalWithSubtasks": "總成本(包括子任務): ${{cost}}",
|
||||
"total": "總成本: ${{cost}}",
|
||||
"includesSubtasks": "包括子任務成本"
|
||||
},
|
||||
"browser": {
|
||||
"session": "瀏覽器會話",
|
||||
"active": "活動中",
|
||||
"rooWantsToUse": "Roo 想要使用瀏覽器",
|
||||
"consoleLogs": "主控台記錄",
|
||||
"noNewLogs": "(沒有新記錄)",
|
||||
|
|
|
|||
4
webview-ui/src/i18n/locales/zh-TW/common.json
generated
4
webview-ui/src/i18n/locales/zh-TW/common.json
generated
|
|
@ -106,5 +106,9 @@
|
|||
"delegated_to": "已委派給工作 {{childId}}",
|
||||
"delegation_completed": "子工作已完成,繼續父工作",
|
||||
"awaiting_child": "等待子工作 {{childId}}"
|
||||
},
|
||||
"costs": {
|
||||
"own": "自身",
|
||||
"subtasks": "子工作"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
webview-ui/src/utils/costFormatting.ts
Normal file
33
webview-ui/src/utils/costFormatting.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Format a cost breakdown string for display.
|
||||
* This mirrors the backend formatCostBreakdown function but uses the webview's i18n.
|
||||
*
|
||||
* @param ownCost - The task's own cost
|
||||
* @param childrenCost - The sum of subtask costs
|
||||
* @param labels - Labels for "Own" and "Subtasks" (from i18n)
|
||||
* @returns Formatted breakdown string like "Own: $1.00 + Subtasks: $0.50"
|
||||
*/
|
||||
export function formatCostBreakdown(
|
||||
ownCost: number,
|
||||
childrenCost: number,
|
||||
labels: { own: string; subtasks: string },
|
||||
): string {
|
||||
return `${labels.own}: $${ownCost.toFixed(2)} + ${labels.subtasks}: $${childrenCost.toFixed(2)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cost breakdown string if the task has children with costs.
|
||||
*
|
||||
* @param costs - Object containing ownCost and childrenCost
|
||||
* @param labels - Labels for "Own" and "Subtasks" (from i18n)
|
||||
* @returns Formatted breakdown string or undefined if no children costs
|
||||
*/
|
||||
export function getCostBreakdownIfNeeded(
|
||||
costs: { ownCost: number; childrenCost: number } | undefined,
|
||||
labels: { own: string; subtasks: string },
|
||||
): string | undefined {
|
||||
if (!costs || costs.childrenCost <= 0) {
|
||||
return undefined
|
||||
}
|
||||
return formatCostBreakdown(costs.ownCost, costs.childrenCost, labels)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue