From 7954fb6085c46c9e1bae05a481f3cca2093cf2b5 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 20 Jan 2026 13:23:45 +0000 Subject: [PATCH 01/11] Taskheader --- webview-ui/src/components/chat/ChatView.tsx | 1 + webview-ui/src/components/chat/TaskHeader.tsx | 92 +++++++++++++++---- .../chat/__tests__/TaskHeader.spec.tsx | 49 +++++++++- .../src/components/ui/standard-tooltip.tsx | 7 +- webview-ui/src/i18n/locales/en/chat.json | 3 +- 5 files changed, 129 insertions(+), 23 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 81f6cbebf66..e90e3da4e49 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1482,6 +1482,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction 0 ) } + parentTaskId={currentTaskItem?.parentTaskId} costBreakdown={ currentTaskItem?.id && aggregatedCostsMap.has(currentTaskItem.id) ? getCostBreakdownIfNeeded(aggregatedCostsMap.get(currentTaskItem.id)!, { diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 5dca11b9634..fe4e85e02e6 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -6,12 +6,11 @@ import DismissibleUpsell from "@src/components/common/DismissibleUpsell" import { ChevronUp, ChevronDown, - SquarePen, - Coins, HardDriveDownload, HardDriveUpload, FoldVertical, Globe, + ArrowLeft, } from "lucide-react" import prettyBytes from "pretty-bytes" @@ -44,6 +43,7 @@ export interface TaskHeaderProps { totalCost: number aggregatedCost?: number hasSubtasks?: boolean + parentTaskId?: string costBreakdown?: string contextTokens: number buttonsDisabled: boolean @@ -60,6 +60,7 @@ const TaskHeader = ({ totalCost, aggregatedCost, hasSubtasks, + parentTaskId, costBreakdown, contextTokens, buttonsDisabled, @@ -126,8 +127,29 @@ const TaskHeader = ({ const hasTodos = todos && Array.isArray(todos) && todos.length > 0 + // Determine if this is a subtask (has a parent) + const isSubtask = !!parentTaskId + + const handleBackToParent = () => { + if (parentTaskId) { + vscode.postMessage({ type: "showTaskWithId", text: parentTaskId }) + } + } + return (
+ {isSubtask && ( +
e.stopPropagation()}> + +
+ )} {showLongRunningTaskMessage && !isTaskComplete && ( {isTaskExpanded && {t("chat:task.title")}} {!isTaskExpanded && ( -
- - - - +
+
)}
@@ -205,10 +224,9 @@ const TaskHeader = ({ className="flex items-center justify-between text-sm text-muted-foreground/70" onClick={(e) => e.stopPropagation()}>
- +
{t("chat:tokenProgress.tokensUsed", { used: formatLargeNumber(contextTokens || 0), @@ -250,10 +268,44 @@ const TaskHeader = ({ } side="top" sideOffset={8}> - - {formatLargeNumber(contextTokens || 0)} / {formatLargeNumber(contextWindow)} + + {(() => { + const percentage = Math.round(((contextTokens || 0) / contextWindow) * 100) + const radius = 6 + const circumference = 2 * Math.PI * radius + const strokeDashoffset = circumference - (percentage / 100) * circumference + return ( + <> + + + + + {percentage}% + + ) + })()} + · {!!totalCost && ( - - ${(aggregatedCost ?? totalCost).toFixed(2)} - {hasSubtasks && ( - - * - - )} - + <> + + ${(aggregatedCost ?? totalCost).toFixed(2)} + {hasSubtasks && ( + + * + + )} + + )}
diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 6cdbeaf0c68..07aa5480aff 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -20,10 +20,13 @@ vi.mock("react-i18next", () => ({ }, })) -// Mock the vscode API +// Mock the vscode API - use vi.hoisted to ensure the mock is available when vi.mock is hoisted +const { mockPostMessage } = vi.hoisted(() => ({ + mockPostMessage: vi.fn(), +})) vi.mock("@/utils/vscode", () => ({ vscode: { - postMessage: vi.fn(), + postMessage: mockPostMessage, }, })) @@ -357,4 +360,46 @@ describe("TaskHeader", () => { expect(screen.getByTestId("dismissible-upsell")).toBeInTheDocument() }) }) + + describe("Back to parent task button", () => { + beforeEach(() => { + mockPostMessage.mockClear() + }) + + it("should not show back button when parentTaskId is not provided", () => { + renderTaskHeader() + expect(screen.queryByText("chat:task.backToParentTask")).not.toBeInTheDocument() + }) + + it("should not show back button when parentTaskId is undefined", () => { + renderTaskHeader({ parentTaskId: undefined }) + expect(screen.queryByText("chat:task.backToParentTask")).not.toBeInTheDocument() + }) + + it("should show back button when parentTaskId is provided", () => { + renderTaskHeader({ parentTaskId: "parent-task-123" }) + expect(screen.getByText("chat:task.backToParentTask")).toBeInTheDocument() + }) + + it("should call vscode.postMessage with showTaskWithId when back button is clicked", () => { + renderTaskHeader({ parentTaskId: "parent-task-123" }) + + const backButton = screen.getByText("chat:task.backToParentTask") + fireEvent.click(backButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "parent-task-123", + }) + }) + + it("should show back button with ArrowLeft icon", () => { + renderTaskHeader({ parentTaskId: "parent-task-123" }) + + // Find the button containing the back text and verify it has the ArrowLeft icon + const backButton = screen.getByText("chat:task.backToParentTask").closest("button") + expect(backButton).toBeInTheDocument() + expect(backButton?.querySelector("svg.lucide-arrow-left")).toBeInTheDocument() + }) + }) }) diff --git a/webview-ui/src/components/ui/standard-tooltip.tsx b/webview-ui/src/components/ui/standard-tooltip.tsx index a2b05386e66..7ec2e3205b3 100644 --- a/webview-ui/src/components/ui/standard-tooltip.tsx +++ b/webview-ui/src/components/ui/standard-tooltip.tsx @@ -62,7 +62,12 @@ export function StandardTooltip({ return ( {children} - + {content} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index d92957916b3..86c1e44c28d 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Open task in Roo Code Cloud", "openInCloudIntro": "Keep monitoring or interacting with Roo from anywhere. Scan, click or copy to open.", "openApiHistory": "Open API History", - "openUiHistory": "Open UI History" + "openUiHistory": "Open UI History", + "backToParentTask": "Parent task" }, "unpin": "Unpin", "pin": "Pin", From 276f16a68b5d1d122847755c7c055a548e444d68 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 11:44:15 +0000 Subject: [PATCH 02/11] Subtask messages --- webview-ui/src/components/chat/ChatRow.tsx | 97 ++----------------- webview-ui/src/components/chat/TaskHeader.tsx | 60 ++++++------ webview-ui/src/i18n/locales/en/chat.json | 9 +- 3 files changed, 47 insertions(+), 119 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index e71f92dc415..347b6e16732 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -68,6 +68,7 @@ import { TerminalSquare, MessageCircle, Repeat2, + Split, } from "lucide-react" import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" @@ -818,7 +819,7 @@ export const ChatRowContent = ({ return ( <>
- {toolIcon("tasklist")} +
-
-
- - {t("chat:subtasks.newTaskContent")} -
-
- -
+
+
) @@ -864,33 +840,8 @@ export const ChatRowContent = ({ {toolIcon("check-all")} {t("chat:subtasks.wantsToFinish")}
-
-
- - {t("chat:subtasks.completionContent")} -
-
- -
+
+
) @@ -1020,39 +971,11 @@ export const ChatRowContent = ({ ) case "subtask_result": return ( -
-
-
- - {t("chat:subtasks.resultContent")} -
-
- -
+
+
+ {t("chat:subtasks.resultContent")}
+
) case "reasoning": diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index fe4e85e02e6..d4ff773d26c 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -305,36 +305,42 @@ const TaskHeader = ({ })()} - · {!!totalCost && ( - + <> + · + - {t("chat:costs.totalWithSubtasks", { - cost: (aggregatedCost ?? totalCost).toFixed(2), - })} +
+ {t("chat:costs.totalWithSubtasks", { + cost: (aggregatedCost ?? totalCost).toFixed(2), + })} +
+ {costBreakdown && ( +
{costBreakdown}
+ )}
- {costBreakdown &&
{costBreakdown}
} -
- ) : ( -
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
- ) - } - side="top" - sideOffset={8}> - <> - - ${(aggregatedCost ?? totalCost).toFixed(2)} - {hasSubtasks && ( - - * - - )} - - - + ) : ( +
{t("chat:costs.total", { cost: totalCost.toFixed(2) })}
+ ) + } + side="top" + sideOffset={8}> + <> + + ${(aggregatedCost ?? totalCost).toFixed(2)} + {hasSubtasks && ( + + * + + )} + + + + )}
{showBrowserGlobe && ( diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 86c1e44c28d..724229af219 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -58,7 +58,7 @@ "title": "Deny", "tooltip": "Prevent this action from occurring" }, - "completeSubtaskAndReturn": "Complete Subtask and Return", + "completeSubtaskAndReturn": "Complete subtask and keep going", "approve": { "title": "Approve", "tooltip": "Allow this action to happen" @@ -280,12 +280,11 @@ }, "subtasks": { "wantsToCreate": "Roo wants to create a new subtask in {{mode}} mode", - "wantsToFinish": "Roo wants to finish this subtask", + "wantsToFinish": "Roo wants to mark this as completed", "newTaskContent": "Subtask Instructions", - "completionContent": "Subtask Completed", - "resultContent": "Subtask Results", + "resultContent": "Task result", "defaultResult": "Please continue to the next task.", - "completionInstructions": "Subtask completed! You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task." + "completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task." }, "questions": { "hasQuestion": "Roo has a question" From 9ea2406c9659f0080e75b4e0a31d181746f0c676 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 12:12:45 +0000 Subject: [PATCH 03/11] View subtask --- webview-ui/src/components/chat/ChatRow.tsx | 55 ++++- .../__tests__/ChatRow.subtask-links.spec.tsx | 224 ++++++++++++++++++ webview-ui/src/i18n/locales/en/chat.json | 5 +- 3 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 347b6e16732..cd28a9ef8bc 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -69,6 +69,8 @@ import { MessageCircle, Repeat2, Split, + ArrowRight, + Check, } from "lucide-react" import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" @@ -177,7 +179,8 @@ export const ChatRowContent = ({ }: ChatRowContentProps) => { const { t, i18n } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState() + const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, currentTaskItem } = + useExtensionState() const { info: model } = useSelectedModel(apiConfiguration) const [isEditing, setIsEditing] = useState(false) const [editedContent, setEditedContent] = useState("") @@ -386,6 +389,7 @@ export const ChatRowContent = ({ display: "flex", alignItems: "center", gap: "10px", + cursor: "default", marginBottom: "10px", wordBreak: "break-word", } @@ -816,6 +820,30 @@ export const ChatRowContent = ({ ) case "newTask": + // Find all newTask messages to determine which child task ID corresponds to this message + const newTaskMessages = clineMessages.filter((msg) => { + if (msg.type === "ask" && msg.ask === "tool") { + const t = safeJsonParse(msg.text) + return t?.tool === "newTask" + } + return false + }) + const thisNewTaskIndex = newTaskMessages.findIndex((msg) => msg.ts === message.ts) + const childIds = currentTaskItem?.childIds || [] + + // Only get the child task ID if this newTask has been approved (has a corresponding entry in childIds) + // This prevents showing a link to a previous task when the current newTask is still awaiting approval + // Note: We don't use delegatedToId here because it persists after child tasks complete and would + // incorrectly point to the previous task when a new newTask is awaiting approval + const childTaskId = + thisNewTaskIndex >= 0 && thisNewTaskIndex < childIds.length ? childIds[thisNewTaskIndex] : undefined + + // Check if the next message is a subtask_result - if so, don't show the button + // since the result is displayed right after this message + const currentMessageIndex = clineMessages.findIndex((msg) => msg.ts === message.ts) + const nextMessage = currentMessageIndex >= 0 ? clineMessages[currentMessageIndex + 1] : undefined + const isFollowedBySubtaskResult = nextMessage?.type === "say" && nextMessage?.say === "subtask_result" + return ( <>
@@ -830,6 +858,18 @@ export const ChatRowContent = ({
+
+ {childTaskId && !isFollowedBySubtaskResult && ( + + )} +
) @@ -970,12 +1010,25 @@ export const ChatRowContent = ({ /> ) case "subtask_result": + // Get the child task ID that produced this result + const completedChildTaskId = currentTaskItem?.completedByChildId return (
{t("chat:subtasks.resultContent")} +
+ {completedChildTaskId && ( + + )}
) case "reasoning": diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx new file mode 100644 index 00000000000..a1e897808f0 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx @@ -0,0 +1,224 @@ +import React from "react" +import { render, screen, fireEvent } from "@/utils/test-utils" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ChatRowContent } from "../ChatRow" +import type { HistoryItem, ClineMessage } from "@roo-code/types" + +// Mock vscode API +const mockPostMessage = vi.fn() +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: (msg: unknown) => mockPostMessage(msg), + }, +})) + +// Mock i18n +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => { + const map: Record = { + "chat:subtasks.wantsToCreate": "Roo wants to create a new subtask", + "chat:subtasks.resultContent": "Task result", + "chat:subtasks.goToSubtask": "Go to subtask", + } + return map[key] ?? key + }, + i18n: { exists: () => true }, + }), + Trans: ({ children }: { children?: React.ReactNode }) => <>{children}, + initReactI18next: { type: "3rdParty", init: () => {} }, +})) + +// Mock extension state context +let mockCurrentTaskItem: Partial | undefined = undefined +let mockClineMessages: ClineMessage[] = [] + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + mcpServers: [], + alwaysAllowMcp: false, + currentCheckpoint: null, + mode: "code", + apiConfiguration: {}, + clineMessages: mockClineMessages, + currentTaskItem: mockCurrentTaskItem, + }), +})) + +// Mock useSelectedModel hook +vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: () => ({ info: { supportsImages: true } }), +})) + +const queryClient = new QueryClient() + +function renderChatRow(message: any, currentTaskItem?: Partial, clineMessages?: ClineMessage[]) { + mockCurrentTaskItem = currentTaskItem + mockClineMessages = clineMessages || [message] + + return render( + + {}} + onSuggestionClick={() => {}} + onBatchFileResponse={() => {}} + onFollowUpUnmount={() => {}} + isFollowUpAnswered={false} + /> + , + ) +} + +describe("ChatRow - subtask links", () => { + beforeEach(() => { + mockPostMessage.mockClear() + }) + + describe("newTask tool", () => { + it("should display 'Go to subtask' link when currentTaskItem has delegatedToId", () => { + const message = { + ts: Date.now(), + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ + tool: "newTask", + mode: "code", + content: "Implement feature X", + }), + } + + renderChatRow(message, { + delegatedToId: "child-task-123", + }) + + const goToSubtaskButton = screen.getByText("Go to subtask") + expect(goToSubtaskButton).toBeInTheDocument() + + fireEvent.click(goToSubtaskButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "child-task-123", + }) + }) + + it("should display 'Go to subtask' link when currentTaskItem has childIds", () => { + const message = { + ts: Date.now(), + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ + tool: "newTask", + mode: "architect", + content: "Design system architecture", + }), + } + + renderChatRow(message, { + childIds: ["first-child", "second-child"], + }) + + const goToSubtaskButton = screen.getByText("Go to subtask") + expect(goToSubtaskButton).toBeInTheDocument() + + fireEvent.click(goToSubtaskButton) + + // Should use the last child ID + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "second-child", + }) + }) + + it("should not display 'Go to subtask' link when no child task exists", () => { + const message = { + ts: Date.now(), + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ + tool: "newTask", + mode: "code", + content: "Implement feature X", + }), + } + + renderChatRow(message, undefined) + + const goToSubtaskButton = screen.queryByText("Go to subtask") + expect(goToSubtaskButton).toBeNull() + }) + + it("should not display 'Go to subtask' link when directly followed by subtask_result", () => { + const newTaskMessage = { + ts: 1000, + type: "ask" as const, + ask: "tool" as const, + text: JSON.stringify({ + tool: "newTask", + mode: "code", + content: "Implement feature X", + }), + } + + const subtaskResultMessage = { + ts: 1001, + type: "say" as const, + say: "subtask_result" as const, + text: "The subtask has been completed successfully.", + } + + // Pass both messages in the clineMessages array + renderChatRow(newTaskMessage, { delegatedToId: "child-task-123" }, [ + newTaskMessage, + subtaskResultMessage, + ] as ClineMessage[]) + + // Button should be hidden because next message is subtask_result + const goToSubtaskButton = screen.queryByText("Go to subtask") + expect(goToSubtaskButton).toBeNull() + }) + }) + + describe("subtask_result say message", () => { + it("should display 'Go to subtask' link when currentTaskItem has completedByChildId", () => { + const message = { + ts: Date.now(), + type: "say" as const, + say: "subtask_result" as const, + text: "The subtask has been completed successfully.", + } + + renderChatRow(message, { + completedByChildId: "completed-child-456", + }) + + const goToSubtaskButton = screen.getByText("Go to subtask") + expect(goToSubtaskButton).toBeInTheDocument() + + fireEvent.click(goToSubtaskButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "showTaskWithId", + text: "completed-child-456", + }) + }) + + it("should not display 'Go to subtask' link when no completedByChildId exists", () => { + const message = { + ts: Date.now(), + type: "say" as const, + say: "subtask_result" as const, + text: "The subtask has been completed successfully.", + } + + renderChatRow(message, undefined) + + const goToSubtaskButton = screen.queryByText("Go to subtask") + expect(goToSubtaskButton).toBeNull() + }) + }) +}) diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 724229af219..a99c50e0363 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -282,9 +282,10 @@ "wantsToCreate": "Roo wants to create a new subtask in {{mode}} mode", "wantsToFinish": "Roo wants to mark this as completed", "newTaskContent": "Subtask Instructions", - "resultContent": "Task result", + "resultContent": "Subtask completed", "defaultResult": "Please continue to the next task.", - "completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task." + "completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task.", + "goToSubtask": "View task" }, "questions": { "hasQuestion": "Roo has a question" From b9cd5cf9b345af2884a1fce7033f6069f721672c Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 13:12:31 +0000 Subject: [PATCH 04/11] subtasks in history items --- src/core/webview/ClineProvider.ts | 90 ++-- .../components/history/DeleteTaskDialog.tsx | 10 +- .../src/components/history/HistoryPreview.tsx | 20 +- .../src/components/history/HistoryView.tsx | 113 +++-- .../history/SubtaskCollapsibleRow.tsx | 51 +++ .../src/components/history/SubtaskRow.tsx | 47 +++ .../src/components/history/TaskGroupItem.tsx | 83 ++++ .../src/components/history/TaskItem.tsx | 27 +- .../src/components/history/TaskItemFooter.tsx | 34 +- .../__tests__/DeleteTaskDialog.spec.tsx | 57 ++- .../history/__tests__/TaskGroupItem.spec.tsx | 265 ++++++++++++ .../history/__tests__/TaskItemFooter.spec.tsx | 12 + .../history/__tests__/useGroupedTasks.spec.ts | 397 ++++++++++++++++++ webview-ui/src/components/history/types.ts | 37 ++ .../src/components/history/useGroupedTasks.ts | 101 +++++ .../components/ui/__tests__/tooltip.spec.tsx | 23 + .../src/components/ui/standard-tooltip.tsx | 12 +- webview-ui/src/i18n/locales/en/history.json | 8 +- 18 files changed, 1308 insertions(+), 79 deletions(-) create mode 100644 webview-ui/src/components/history/SubtaskCollapsibleRow.tsx create mode 100644 webview-ui/src/components/history/SubtaskRow.tsx create mode 100644 webview-ui/src/components/history/TaskGroupItem.tsx create mode 100644 webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx create mode 100644 webview-ui/src/components/history/__tests__/useGroupedTasks.spec.ts create mode 100644 webview-ui/src/components/history/types.ts create mode 100644 webview-ui/src/components/history/useGroupedTasks.ts diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 52845543edf..a474d1b1032 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1751,43 +1751,79 @@ export class ClineProvider await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId }) } - // this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder - async deleteTaskWithId(id: string) { + // this function deletes a task from task history, and deletes its checkpoints and delete the task folder + // If the task has subtasks (childIds), they will also be deleted recursively + async deleteTaskWithId(id: string, cascadeSubtasks: boolean = true) { try { - // get the task directory full path - const { taskDirPath } = await this.getTaskWithId(id) + // get the task directory full path and history item + const { taskDirPath, historyItem } = await this.getTaskWithId(id) - // remove task from stack if it's the current task - if (id === this.getCurrentTask()?.taskId) { - // Close the current task instance; delegation flows will be handled via metadata if applicable. - await this.removeClineFromStack() + // Collect all task IDs to delete (parent + all subtasks) + const allIdsToDelete: string[] = [id] + + if (cascadeSubtasks) { + // Recursively collect all child IDs + const collectChildIds = async (taskId: string): Promise => { + try { + const { historyItem: item } = await this.getTaskWithId(taskId) + if (item.childIds && item.childIds.length > 0) { + for (const childId of item.childIds) { + allIdsToDelete.push(childId) + await collectChildIds(childId) + } + } + } catch (error) { + // Child task may already be deleted or not found, continue + console.log(`[deleteTaskWithId] child task ${taskId} not found, skipping`) + } + } + + await collectChildIds(id) } - // delete task from the task history state - await this.deleteTaskFromState(id) + // Remove from stack if any of the tasks to delete are in the current task stack + for (const taskId of allIdsToDelete) { + if (taskId === this.getCurrentTask()?.taskId) { + // Close the current task instance; delegation flows will be handled via metadata if applicable. + await this.removeClineFromStack() + break + } + } + + // Delete all tasks from state in one batch + const taskHistory = this.getGlobalState("taskHistory") ?? [] + const updatedTaskHistory = taskHistory.filter((task) => !allIdsToDelete.includes(task.id)) + await this.updateGlobalState("taskHistory", updatedTaskHistory) + this.recentTasksCache = undefined - // Delete associated shadow repository or branch. - // TODO: Store `workspaceDir` in the `HistoryItem` object. + // Delete associated shadow repositories or branches and task directories const globalStorageDir = this.contextProxy.globalStorageUri.fsPath const workspaceDir = this.cwd + const { getTaskDirectoryPath } = await import("../../utils/storage") + const globalStoragePath = this.contextProxy.globalStorageUri.fsPath - try { - await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir }) - } catch (error) { - console.error( - `[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`, - ) - } + for (const taskId of allIdsToDelete) { + try { + await ShadowCheckpointService.deleteTask({ taskId, globalStorageDir, workspaceDir }) + } catch (error) { + console.error( + `[deleteTaskWithId${taskId}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`, + ) + } - // delete the entire task directory including checkpoints and all content - try { - await fs.rm(taskDirPath, { recursive: true, force: true }) - console.log(`[deleteTaskWithId${id}] removed task directory`) - } catch (error) { - console.error( - `[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`, - ) + // Delete the task directory + try { + const dirPath = await getTaskDirectoryPath(globalStoragePath, taskId) + await fs.rm(dirPath, { recursive: true, force: true }) + console.log(`[deleteTaskWithId${taskId}] removed task directory`) + } catch (error) { + console.error( + `[deleteTaskWithId${taskId}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`, + ) + } } + + await this.postStateToWebview() } catch (error) { // If task is not found, just remove it from state if (error instanceof Error && error.message === "Task not found") { diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx index d0e3ab16a4d..5ff93f4ed35 100644 --- a/webview-ui/src/components/history/DeleteTaskDialog.tsx +++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx @@ -19,9 +19,11 @@ import { vscode } from "@/utils/vscode" interface DeleteTaskDialogProps extends AlertDialogProps { taskId: string + /** Number of subtasks that will also be deleted (for cascade delete warning) */ + subtaskCount?: number } -export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => { +export const DeleteTaskDialog = ({ taskId, subtaskCount = 0, ...props }: DeleteTaskDialogProps) => { const { t } = useAppTranslation() const [isEnterPressed] = useKeyPress("Enter") @@ -40,12 +42,16 @@ export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => } }, [taskId, isEnterPressed, onDelete]) + // Determine the message to show + const message = + subtaskCount > 0 ? t("history:deleteWithSubtasks", { count: subtaskCount }) : t("history:deleteTaskMessage") + return ( onOpenChange?.(false)}> {t("history:deleteTask")} - {t("history:deleteTaskMessage")} + {message} diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx index 2169b1d96e4..02464e69c0d 100644 --- a/webview-ui/src/components/history/HistoryPreview.tsx +++ b/webview-ui/src/components/history/HistoryPreview.tsx @@ -4,16 +4,21 @@ import { vscode } from "@src/utils/vscode" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useTaskSearch } from "./useTaskSearch" -import TaskItem from "./TaskItem" +import { useGroupedTasks } from "./useGroupedTasks" +import TaskGroupItem from "./TaskGroupItem" const HistoryPreview = () => { - const { tasks } = useTaskSearch() + const { tasks, searchQuery } = useTaskSearch() + const { groups, toggleExpand } = useGroupedTasks(tasks, searchQuery) const { t } = useAppTranslation() const handleViewAllHistory = () => { vscode.postMessage({ type: "switchTab", tab: "history" }) } + // Show up to 4 groups (parent + subtasks count as 1 block) + const displayGroups = groups.slice(0, 4) + return (
@@ -25,10 +30,15 @@ const HistoryPreview = () => { {t("history:viewAllHistory")}
- {tasks.length !== 0 && ( + {displayGroups.length !== 0 && ( <> - {tasks.slice(0, 4).map((item) => ( - + {displayGroups.map((group) => ( + toggleExpand(group.parent.id)} + /> ))} )} diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx index d8ee4315938..88b65518812 100644 --- a/webview-ui/src/components/history/HistoryView.tsx +++ b/webview-ui/src/components/history/HistoryView.tsx @@ -1,4 +1,4 @@ -import React, { memo, useState } from "react" +import React, { memo, useState, useMemo } from "react" import { ArrowLeft } from "lucide-react" import { DeleteTaskDialog } from "./DeleteTaskDialog" import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog" @@ -20,7 +20,9 @@ import { useAppTranslation } from "@/i18n/TranslationContext" import { Tab, TabContent, TabHeader } from "../common/Tab" import { useTaskSearch } from "./useTaskSearch" +import { useGroupedTasks } from "./useGroupedTasks" import TaskItem from "./TaskItem" +import TaskGroupItem from "./TaskGroupItem" type HistoryViewProps = { onDone: () => void @@ -41,11 +43,30 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { } = useTaskSearch() const { t } = useAppTranslation() + // Use grouped tasks hook + const { groups, flatTasks, toggleExpand, isSearchMode } = useGroupedTasks(tasks, searchQuery) + const [deleteTaskId, setDeleteTaskId] = useState(null) + const [deleteSubtaskCount, setDeleteSubtaskCount] = useState(0) const [isSelectionMode, setIsSelectionMode] = useState(false) const [selectedTaskIds, setSelectedTaskIds] = useState([]) const [showBatchDeleteDialog, setShowBatchDeleteDialog] = useState(false) + // Get subtask count for a task + const getSubtaskCount = useMemo(() => { + const countMap = new Map() + for (const group of groups) { + countMap.set(group.parent.id, group.subtasks.length) + } + return (taskId: string) => countMap.get(taskId) || 0 + }, [groups]) + + // Handle delete with subtask count + const handleDelete = (taskId: string) => { + setDeleteTaskId(taskId) + setDeleteSubtaskCount(getSubtaskCount(taskId)) + } + // Toggle selection mode const toggleSelectionMode = () => { setIsSelectionMode(!isSelectionMode) @@ -230,30 +251,60 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { - ( -
- )), - }} - itemContent={(_index, item) => ( - - )} - /> + {isSearchMode && flatTasks ? ( + // Search mode: flat list with subtask prefix + ( +
+ )), + }} + itemContent={(_index, item) => ( + + )} + /> + ) : ( + // Grouped mode: task groups with expandable subtasks + ( +
+ )), + }} + itemContent={(_index, group) => ( + toggleExpand(group.parent.id)} + className="m-2" + /> + )} + /> + )} {/* Fixed action bar at bottom - only shown in selection mode with selected items */} @@ -275,7 +326,17 @@ const HistoryView = ({ onDone }: HistoryViewProps) => { {/* Delete dialog */} {deleteTaskId && ( - !open && setDeleteTaskId(null)} open /> + { + if (!open) { + setDeleteTaskId(null) + setDeleteSubtaskCount(0) + } + }} + open + /> )} {/* Batch delete dialog */} diff --git a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx new file mode 100644 index 00000000000..1dc52945d62 --- /dev/null +++ b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx @@ -0,0 +1,51 @@ +import { memo } from "react" +import { ChevronRight } from "lucide-react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@/lib/utils" + +interface SubtaskCollapsibleRowProps { + /** Number of subtasks */ + count: number + /** Whether the subtask list is expanded */ + isExpanded: boolean + /** Callback when the row is clicked to toggle expand/collapse */ + onToggle: () => void + /** Optional className for styling */ + className?: string +} + +/** + * A clickable row that displays the subtask count with an expand/collapse chevron. + * Clicking this row toggles the visibility of the subtask list. + */ +const SubtaskCollapsibleRow = ({ count, isExpanded, onToggle, className }: SubtaskCollapsibleRowProps) => { + const { t } = useAppTranslation() + + if (count === 0) { + return null + } + + return ( +
{ + e.stopPropagation() + onToggle() + }} + role="button" + aria-expanded={isExpanded} + aria-label={isExpanded ? t("history:collapseSubtasks") : t("history:expandSubtasks")}> + + {t("history:subtasks", { count })} +
+ ) +} + +export default memo(SubtaskCollapsibleRow) diff --git a/webview-ui/src/components/history/SubtaskRow.tsx b/webview-ui/src/components/history/SubtaskRow.tsx new file mode 100644 index 00000000000..831cf54742b --- /dev/null +++ b/webview-ui/src/components/history/SubtaskRow.tsx @@ -0,0 +1,47 @@ +import { memo } from "react" +import { vscode } from "@/utils/vscode" +import { cn } from "@/lib/utils" +import type { DisplayHistoryItem } from "./types" +import { StandardTooltip } from "../ui" + +interface SubtaskRowProps { + /** The subtask to display */ + item: DisplayHistoryItem + /** Optional className for styling */ + className?: string +} + +/** + * Displays an individual subtask row when the parent's subtask list is expanded. + * Shows the task name and token/cost info in an indented format. + */ +const SubtaskRow = ({ item, className }: SubtaskRowProps) => { + const handleClick = () => { + vscode.postMessage({ type: "showTaskWithId", text: item.id }) + } + + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + handleClick() + } + }}> + + {item.task} + +
+ ) +} + +export default memo(SubtaskRow) diff --git a/webview-ui/src/components/history/TaskGroupItem.tsx b/webview-ui/src/components/history/TaskGroupItem.tsx new file mode 100644 index 00000000000..e14b13db5e9 --- /dev/null +++ b/webview-ui/src/components/history/TaskGroupItem.tsx @@ -0,0 +1,83 @@ +import { memo } from "react" +import { cn } from "@/lib/utils" +import type { TaskGroup } from "./types" +import TaskItem from "./TaskItem" +import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow" +import SubtaskRow from "./SubtaskRow" + +interface TaskGroupItemProps { + /** The task group to render */ + group: TaskGroup + /** Display variant - compact (preview) or full (history view) */ + variant: "compact" | "full" + /** Whether to show workspace info */ + showWorkspace?: boolean + /** Whether selection mode is active */ + isSelectionMode?: boolean + /** Whether this group's parent is selected */ + isSelected?: boolean + /** Callback when selection state changes */ + onToggleSelection?: (taskId: string, isSelected: boolean) => void + /** Callback when delete is requested */ + onDelete?: (taskId: string) => void + /** Callback when expand/collapse is toggled */ + onToggleExpand: () => void + /** Optional className for styling */ + className?: string +} + +/** + * Renders a task group consisting of a parent task and its collapsible subtask list. + * When expanded, shows individual subtask rows. + */ +const TaskGroupItem = ({ + group, + variant, + showWorkspace = false, + isSelectionMode = false, + isSelected = false, + onToggleSelection, + onDelete, + onToggleExpand, + className, +}: TaskGroupItemProps) => { + const { parent, subtasks, isExpanded } = group + const hasSubtasks = subtasks.length > 0 + + return ( +
+ {/* Parent task */} + + + {/* Subtask collapsible row */} + {hasSubtasks && ( + + )} + + {/* Expanded subtasks */} + {hasSubtasks && isExpanded && ( +
+ {subtasks.map((subtask) => ( + + ))} +
+ )} +
+ ) +} + +export default memo(TaskGroupItem) diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index 087a790013b..223d70e8453 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -1,20 +1,18 @@ import { memo } from "react" -import type { HistoryItem } from "@roo-code/types" +import type { DisplayHistoryItem } from "./types" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import { Checkbox } from "@/components/ui/checkbox" import TaskItemFooter from "./TaskItemFooter" - -interface DisplayHistoryItem extends HistoryItem { - highlight?: string -} +import { StandardTooltip } from "../ui" interface TaskItemProps { item: DisplayHistoryItem variant: "compact" | "full" showWorkspace?: boolean + hasSubtasks?: boolean isSelectionMode?: boolean isSelected?: boolean onToggleSelection?: (taskId: string, isSelected: boolean) => void @@ -26,6 +24,7 @@ const TaskItem = ({ item, variant, showWorkspace = false, + hasSubtasks = false, isSelectionMode = false, isSelected = false, onToggleSelection, @@ -47,8 +46,9 @@ const TaskItem = ({ key={item.id} data-testid={`task-item-${item.id}`} className={cn( - "cursor-pointer group bg-vscode-editor-background rounded-xl relative overflow-hidden border hover:bg-vscode-editor-foreground/10 transition-colors", - "border-transparent", + "cursor-pointer group relative overflow-hidden", + "hover:bg-vscode-editor-foreground/10 transition-colors", + hasSubtasks ? "rounded-t-xl" : "rounded-xl", className, )} onClick={handleClick}> @@ -78,14 +78,23 @@ const TaskItem = ({ !isCompact && isSelectionMode ? "mb-1" : "", )} data-testid="task-content" - {...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}> - {item.highlight ? undefined : item.task} + {...(item.highlight + ? { + dangerouslySetInnerHTML: { + __html: item.highlight, + }, + } + : {})}> + + {item.highlight ? undefined : item.task} +
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index a79467758cc..c9705555981 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -5,34 +5,56 @@ import { CopyButton } from "./CopyButton" import { ExportButton } from "./ExportButton" import { DeleteButton } from "./DeleteButton" import { StandardTooltip } from "../ui/standard-tooltip" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Split } from "lucide-react" export interface TaskItemFooterProps { item: HistoryItem variant: "compact" | "full" isSelectionMode?: boolean + isSubtask?: boolean onDelete?: (taskId: string) => void } -const TaskItemFooter: React.FC = ({ item, variant, isSelectionMode = false, onDelete }) => { +const TaskItemFooter: React.FC = ({ + item, + variant, + isSelectionMode = false, + isSubtask = false, + onDelete, +}) => { + const { t } = useAppTranslation() + return (
+ {/* Subtask tag */} + {isSubtask && ( + <> + + {t("history:subtaskTag")} + · + + )} {/* Datetime with time-ago format */} {formatTimeAgo(item.ts)} - · + {/* Cost */} {!!item.totalCost && ( - - {"$" + item.totalCost.toFixed(2)} - + <> + · + + {"$" + item.totalCost.toFixed(2)} + + )}
{/* Action Buttons for non-compact view */} {!isSelectionMode && ( -
+
{variant === "full" && } {onDelete && } diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx index f8e244e9bfa..1a43dbb4c01 100644 --- a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx +++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx @@ -8,13 +8,17 @@ vi.mock("@/utils/vscode") vi.mock("@/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ - t: (key: string) => { + t: (key: string, options?: Record) => { const translations: Record = { "history:deleteTask": "Delete Task", "history:deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.", "history:cancel": "Cancel", "history:delete": "Delete", } + // Handle deleteWithSubtasks with interpolation + if (key === "history:deleteWithSubtasks" && options?.count !== undefined) { + return `This will also delete ${options.count} subtask(s). Are you sure?` + } return translations[key] || key }, }), @@ -143,4 +147,55 @@ describe("DeleteTaskDialog", () => { text: mockTaskId, }) }) + + describe("cascade delete warning", () => { + it("shows warning message when deleting parent with subtasks", () => { + render( + , + ) + + expect(screen.getByText("This will also delete 3 subtask(s). Are you sure?")).toBeInTheDocument() + }) + + it("shows standard message when no subtasks", () => { + render( + , + ) + + expect( + screen.getByText("Are you sure you want to delete this task? This action cannot be undone."), + ).toBeInTheDocument() + }) + + it("shows standard message when subtaskCount is not provided", () => { + render() + + expect( + screen.getByText("Are you sure you want to delete this task? This action cannot be undone."), + ).toBeInTheDocument() + }) + + it("shows singular subtask warning for single subtask", () => { + render( + , + ) + + expect(screen.getByText("This will also delete 1 subtask(s). Are you sure?")).toBeInTheDocument() + }) + + it("still deletes task when cascade warning is shown", () => { + render( + , + ) + + const deleteButton = screen.getByText("Delete") + fireEvent.click(deleteButton) + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "deleteTaskWithId", + text: mockTaskId, + }) + expect(mockOnOpenChange).toHaveBeenCalledWith(false) + }) + }) }) diff --git a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx new file mode 100644 index 00000000000..d7c70b8293f --- /dev/null +++ b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx @@ -0,0 +1,265 @@ +import { render, screen, fireEvent } from "@/utils/test-utils" + +import TaskGroupItem from "../TaskGroupItem" +import type { TaskGroup, DisplayHistoryItem } from "../types" + +vi.mock("@src/utils/vscode") +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, options?: Record) => { + if (key === "history:subtasks" && options?.count !== undefined) { + return `${options.count} Subtask${options.count === 1 ? "" : "s"}` + } + if (key === "history:subtaskTag") return "Subtask: " + return key + }, + }), +})) + +vi.mock("@/utils/format", () => ({ + formatTimeAgo: vi.fn(() => "2 hours ago"), + formatDate: vi.fn(() => "January 15 at 2:30 PM"), + formatLargeNumber: vi.fn((num: number) => num.toString()), +})) + +const createMockDisplayHistoryItem = (overrides: Partial = {}): DisplayHistoryItem => ({ + id: "task-1", + number: 1, + task: "Test task", + ts: Date.now(), + tokensIn: 100, + tokensOut: 50, + totalCost: 0.01, + workspace: "/workspace/project", + ...overrides, +}) + +const createMockGroup = (overrides: Partial = {}): TaskGroup => ({ + parent: createMockDisplayHistoryItem({ id: "parent-1", task: "Parent task" }), + subtasks: [], + isExpanded: false, + ...overrides, +}) + +describe("TaskGroupItem", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("parent task rendering", () => { + it("renders parent task content", () => { + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ + id: "parent-1", + task: "Test parent task content", + }), + }) + + render() + + expect(screen.getByText("Test parent task content")).toBeInTheDocument() + }) + + it("renders group container with correct test id", () => { + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ id: "my-parent-id" }), + }) + + render() + + expect(screen.getByTestId("task-group-my-parent-id")).toBeInTheDocument() + }) + }) + + describe("subtask count display", () => { + it("shows correct subtask count", () => { + const group = createMockGroup({ + subtasks: [ + createMockDisplayHistoryItem({ id: "child-1", task: "Child 1" }), + createMockDisplayHistoryItem({ id: "child-2", task: "Child 2" }), + createMockDisplayHistoryItem({ id: "child-3", task: "Child 3" }), + ], + }) + + render() + + expect(screen.getByText("3 Subtasks")).toBeInTheDocument() + }) + + it("shows singular subtask text for single subtask", () => { + const group = createMockGroup({ + subtasks: [createMockDisplayHistoryItem({ id: "child-1", task: "Child 1" })], + }) + + render() + + expect(screen.getByText("1 Subtask")).toBeInTheDocument() + }) + + it("does not show subtask row when no subtasks", () => { + const group = createMockGroup({ subtasks: [] }) + + render() + + expect(screen.queryByTestId("subtask-collapsible-row")).not.toBeInTheDocument() + }) + }) + + describe("expand/collapse behavior", () => { + it("calls onToggleExpand when chevron row is clicked", () => { + const onToggleExpand = vi.fn() + const group = createMockGroup({ + subtasks: [createMockDisplayHistoryItem({ id: "child-1", task: "Child 1" })], + }) + + render() + + const collapsibleRow = screen.getByTestId("subtask-collapsible-row") + fireEvent.click(collapsibleRow) + + expect(onToggleExpand).toHaveBeenCalledTimes(1) + }) + + it("shows subtasks when expanded", () => { + const group = createMockGroup({ + isExpanded: true, + subtasks: [ + createMockDisplayHistoryItem({ id: "child-1", task: "Subtask content 1" }), + createMockDisplayHistoryItem({ id: "child-2", task: "Subtask content 2" }), + ], + }) + + render() + + expect(screen.getByTestId("subtask-list")).toBeInTheDocument() + expect(screen.getByText("Subtask content 1")).toBeInTheDocument() + expect(screen.getByText("Subtask content 2")).toBeInTheDocument() + }) + + it("hides subtasks when collapsed", () => { + const group = createMockGroup({ + isExpanded: false, + subtasks: [createMockDisplayHistoryItem({ id: "child-1", task: "Subtask content" })], + }) + + render() + + expect(screen.queryByTestId("subtask-list")).not.toBeInTheDocument() + expect(screen.queryByText("Subtask content")).not.toBeInTheDocument() + }) + }) + + describe("selection mode", () => { + it("handles selection mode correctly", () => { + const onToggleSelection = vi.fn() + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ id: "parent-1" }), + }) + + render( + , + ) + + const checkbox = screen.getByRole("checkbox") + fireEvent.click(checkbox) + + expect(onToggleSelection).toHaveBeenCalledWith("parent-1", true) + }) + + it("shows selected state when isSelected is true", () => { + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ id: "parent-1" }), + }) + + render( + , + ) + + const checkbox = screen.getByRole("checkbox") + // Radix checkbox uses data-state instead of checked attribute + expect(checkbox).toHaveAttribute("data-state", "checked") + }) + }) + + describe("variant handling", () => { + it("passes compact variant to TaskItem", () => { + const group = createMockGroup() + + render() + + // TaskItem should be rendered with compact styling + const taskItem = screen.getByTestId("task-item-parent-1") + expect(taskItem).toBeInTheDocument() + }) + + it("passes full variant to TaskItem", () => { + const group = createMockGroup() + + render() + + const taskItem = screen.getByTestId("task-item-parent-1") + expect(taskItem).toBeInTheDocument() + }) + }) + + describe("delete handling", () => { + it("passes onDelete to TaskItem", () => { + const onDelete = vi.fn() + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ id: "parent-1", task: "Parent task" }), + }) + + render() + + // Delete button uses "delete-task-button" as testid + const deleteButton = screen.getByTestId("delete-task-button") + fireEvent.click(deleteButton) + + expect(onDelete).toHaveBeenCalledWith("parent-1") + }) + }) + + describe("workspace display", () => { + it("passes showWorkspace to TaskItem", () => { + const group = createMockGroup({ + parent: createMockDisplayHistoryItem({ + id: "parent-1", + workspace: "/test/workspace/path", + }), + }) + + render() + + // Workspace should be displayed in TaskItem + const taskItem = screen.getByTestId("task-item-parent-1") + expect(taskItem).toBeInTheDocument() + // Check that workspace folder is shown + expect(screen.getByText("/test/workspace/path")).toBeInTheDocument() + }) + }) + + describe("custom className", () => { + it("applies custom className to container", () => { + const group = createMockGroup() + + render() + + const container = screen.getByTestId("task-group-parent-1") + expect(container).toHaveClass("custom-class") + }) + }) +}) diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index 5c568bb65bc..aa334d94c26 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -82,4 +82,16 @@ describe("TaskItemFooter", () => { expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument() }) + + it("shows subtask tag when isSubtask is true", () => { + render() + + expect(screen.getByText("history:subtaskTag")).toBeInTheDocument() + }) + + it("does not show subtask tag when isSubtask is false", () => { + render() + + expect(screen.queryByText("history:subtaskTag")).not.toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/history/__tests__/useGroupedTasks.spec.ts b/webview-ui/src/components/history/__tests__/useGroupedTasks.spec.ts new file mode 100644 index 00000000000..4f280e72d40 --- /dev/null +++ b/webview-ui/src/components/history/__tests__/useGroupedTasks.spec.ts @@ -0,0 +1,397 @@ +import { renderHook, act } from "@/utils/test-utils" + +import type { HistoryItem } from "@roo-code/types" + +import { useGroupedTasks } from "../useGroupedTasks" + +const createMockTask = (overrides: Partial = {}): HistoryItem => ({ + id: "task-1", + number: 1, + task: "Test task", + ts: Date.now(), + tokensIn: 100, + tokensOut: 50, + totalCost: 0.01, + workspace: "/workspace/project", + ...overrides, +}) + +describe("useGroupedTasks", () => { + describe("grouping behavior", () => { + it("groups tasks correctly by parentTaskId", () => { + const parentTask = createMockTask({ + id: "parent-1", + task: "Parent task", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const childTask1 = createMockTask({ + id: "child-1", + task: "Child task 1", + parentTaskId: "parent-1", + ts: new Date("2024-01-15T13:00:00").getTime(), + }) + const childTask2 = createMockTask({ + id: "child-2", + task: "Child task 2", + parentTaskId: "parent-1", + ts: new Date("2024-01-15T14:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([parentTask, childTask1, childTask2], "")) + + expect(result.current.groups).toHaveLength(1) + expect(result.current.groups[0].parent.id).toBe("parent-1") + expect(result.current.groups[0].subtasks).toHaveLength(2) + expect(result.current.groups[0].subtasks[0].id).toBe("child-2") // Newest first + expect(result.current.groups[0].subtasks[1].id).toBe("child-1") + }) + + it("handles tasks with no children", () => { + const task1 = createMockTask({ + id: "task-1", + task: "Task 1", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const task2 = createMockTask({ + id: "task-2", + task: "Task 2", + ts: new Date("2024-01-16T12:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([task1, task2], "")) + + expect(result.current.groups).toHaveLength(2) + expect(result.current.groups[0].parent.id).toBe("task-2") // Newest first + expect(result.current.groups[0].subtasks).toHaveLength(0) + expect(result.current.groups[1].parent.id).toBe("task-1") + expect(result.current.groups[1].subtasks).toHaveLength(0) + }) + + it("handles orphaned subtasks (parent not in list)", () => { + const orphanedTask = createMockTask({ + id: "orphan-1", + task: "Orphaned task", + parentTaskId: "non-existent-parent", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const regularTask = createMockTask({ + id: "regular-1", + task: "Regular task", + ts: new Date("2024-01-16T12:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([orphanedTask, regularTask], "")) + + // Orphaned task should be treated as a root task + expect(result.current.groups).toHaveLength(2) + expect(result.current.groups.find((g) => g.parent.id === "orphan-1")).toBeTruthy() + expect(result.current.groups.find((g) => g.parent.id === "regular-1")).toBeTruthy() + }) + + it("sorts groups by parent timestamp (newest first)", () => { + const oldTask = createMockTask({ + id: "old-1", + task: "Old task", + ts: new Date("2024-01-10T12:00:00").getTime(), + }) + const middleTask = createMockTask({ + id: "middle-1", + task: "Middle task", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const newTask = createMockTask({ + id: "new-1", + task: "New task", + ts: new Date("2024-01-20T12:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([oldTask, newTask, middleTask], "")) + + expect(result.current.groups).toHaveLength(3) + expect(result.current.groups[0].parent.id).toBe("new-1") + expect(result.current.groups[1].parent.id).toBe("middle-1") + expect(result.current.groups[2].parent.id).toBe("old-1") + }) + + it("handles empty task list", () => { + const { result } = renderHook(() => useGroupedTasks([], "")) + + expect(result.current.groups).toHaveLength(0) + expect(result.current.flatTasks).toBeNull() + expect(result.current.isSearchMode).toBe(false) + }) + + it("handles deeply nested tasks (grandchildren treated as children of their direct parent)", () => { + const rootTask = createMockTask({ + id: "root-1", + task: "Root task", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "root-1", + ts: new Date("2024-01-15T13:00:00").getTime(), + }) + const grandchildTask = createMockTask({ + id: "grandchild-1", + task: "Grandchild task", + parentTaskId: "child-1", + ts: new Date("2024-01-15T14:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([rootTask, childTask, grandchildTask], "")) + + // Root task is the only group at top level + expect(result.current.groups).toHaveLength(1) + expect(result.current.groups[0].parent.id).toBe("root-1") + expect(result.current.groups[0].subtasks).toHaveLength(1) + expect(result.current.groups[0].subtasks[0].id).toBe("child-1") + + // Note: grandchild is a child of child-1, not root-1 + // The current implementation only shows direct children in subtasks + }) + }) + + describe("expand/collapse behavior", () => { + it("starts with all groups collapsed", () => { + const parentTask = createMockTask({ + id: "parent-1", + task: "Parent task", + }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "parent-1", + }) + + const { result } = renderHook(() => useGroupedTasks([parentTask, childTask], "")) + + expect(result.current.groups[0].isExpanded).toBe(false) + }) + + it("expands groups correctly", () => { + const parentTask = createMockTask({ + id: "parent-1", + task: "Parent task", + }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "parent-1", + }) + + const { result } = renderHook(() => useGroupedTasks([parentTask, childTask], "")) + + expect(result.current.groups[0].isExpanded).toBe(false) + + act(() => { + result.current.toggleExpand("parent-1") + }) + + expect(result.current.groups[0].isExpanded).toBe(true) + }) + + it("collapses expanded groups", () => { + const parentTask = createMockTask({ + id: "parent-1", + task: "Parent task", + }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "parent-1", + }) + + const { result } = renderHook(() => useGroupedTasks([parentTask, childTask], "")) + + // Expand first + act(() => { + result.current.toggleExpand("parent-1") + }) + expect(result.current.groups[0].isExpanded).toBe(true) + + // Collapse + act(() => { + result.current.toggleExpand("parent-1") + }) + expect(result.current.groups[0].isExpanded).toBe(false) + }) + + it("expands/collapses multiple groups independently", () => { + const parent1 = createMockTask({ + id: "parent-1", + task: "Parent 1", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const child1 = createMockTask({ + id: "child-1", + task: "Child 1", + parentTaskId: "parent-1", + ts: new Date("2024-01-15T13:00:00").getTime(), + }) + const parent2 = createMockTask({ + id: "parent-2", + task: "Parent 2", + ts: new Date("2024-01-16T12:00:00").getTime(), + }) + const child2 = createMockTask({ + id: "child-2", + task: "Child 2", + parentTaskId: "parent-2", + ts: new Date("2024-01-16T13:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([parent1, child1, parent2, child2], "")) + + // Expand parent-1 + act(() => { + result.current.toggleExpand("parent-1") + }) + + const group1 = result.current.groups.find((g) => g.parent.id === "parent-1") + const group2 = result.current.groups.find((g) => g.parent.id === "parent-2") + + expect(group1?.isExpanded).toBe(true) + expect(group2?.isExpanded).toBe(false) + + // Expand parent-2 + act(() => { + result.current.toggleExpand("parent-2") + }) + + const group1After = result.current.groups.find((g) => g.parent.id === "parent-1") + const group2After = result.current.groups.find((g) => g.parent.id === "parent-2") + + expect(group1After?.isExpanded).toBe(true) + expect(group2After?.isExpanded).toBe(true) + }) + }) + + describe("search mode behavior", () => { + it("returns flat list in search mode with isSubtask flag", () => { + const parentTask = createMockTask({ + id: "parent-1", + task: "Parent task", + ts: new Date("2024-01-15T12:00:00").getTime(), + }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "parent-1", + ts: new Date("2024-01-15T13:00:00").getTime(), + }) + + const { result } = renderHook(() => useGroupedTasks([parentTask, childTask], "search query")) + + expect(result.current.isSearchMode).toBe(true) + expect(result.current.groups).toHaveLength(0) + expect(result.current.flatTasks).not.toBeNull() + expect(result.current.flatTasks).toHaveLength(2) + + const parentInFlat = result.current.flatTasks?.find((t) => t.id === "parent-1") + const childInFlat = result.current.flatTasks?.find((t) => t.id === "child-1") + + expect(parentInFlat?.isSubtask).toBe(false) + expect(childInFlat?.isSubtask).toBe(true) + }) + + it("returns empty groups in search mode", () => { + const task = createMockTask({ id: "task-1", task: "Test task" }) + + const { result } = renderHook(() => useGroupedTasks([task], "search")) + + expect(result.current.groups).toHaveLength(0) + }) + + it("marks orphaned subtasks as non-subtasks in flat list", () => { + const orphanedTask = createMockTask({ + id: "orphan-1", + task: "Orphaned task", + parentTaskId: "non-existent-parent", + }) + + const { result } = renderHook(() => useGroupedTasks([orphanedTask], "search")) + + expect(result.current.flatTasks?.[0].isSubtask).toBe(false) + }) + + it("handles whitespace-only search query as non-search mode", () => { + const task = createMockTask({ id: "task-1", task: "Test task" }) + + const { result } = renderHook(() => useGroupedTasks([task], " ")) + + expect(result.current.isSearchMode).toBe(false) + expect(result.current.groups).toHaveLength(1) + expect(result.current.flatTasks).toBeNull() + }) + + it("returns flatTasks as null when not in search mode", () => { + const task = createMockTask({ id: "task-1", task: "Test task" }) + + const { result } = renderHook(() => useGroupedTasks([task], "")) + + expect(result.current.flatTasks).toBeNull() + }) + }) + + describe("edge cases", () => { + it("handles tasks with same timestamp", () => { + const sameTime = new Date("2024-01-15T12:00:00").getTime() + const task1 = createMockTask({ id: "task-1", task: "Task 1", ts: sameTime }) + const task2 = createMockTask({ id: "task-2", task: "Task 2", ts: sameTime }) + + const { result } = renderHook(() => useGroupedTasks([task1, task2], "")) + + expect(result.current.groups).toHaveLength(2) + }) + + it("handles task list re-render with new data", () => { + const initialTasks = [createMockTask({ id: "task-1", task: "Task 1" })] + + const { result, rerender } = renderHook(({ tasks, query }) => useGroupedTasks(tasks, query), { + initialProps: { tasks: initialTasks, query: "" }, + }) + + expect(result.current.groups).toHaveLength(1) + + // Add more tasks + const updatedTasks = [...initialTasks, createMockTask({ id: "task-2", task: "Task 2" })] + + rerender({ tasks: updatedTasks, query: "" }) + + expect(result.current.groups).toHaveLength(2) + }) + + it("preserves expand state when tasks change", () => { + const parentTask = createMockTask({ id: "parent-1", task: "Parent task" }) + const childTask = createMockTask({ + id: "child-1", + task: "Child task", + parentTaskId: "parent-1", + }) + + const { result, rerender } = renderHook(({ tasks, query }) => useGroupedTasks(tasks, query), { + initialProps: { tasks: [parentTask, childTask], query: "" }, + }) + + // Expand the group + act(() => { + result.current.toggleExpand("parent-1") + }) + expect(result.current.groups[0].isExpanded).toBe(true) + + // Add a new child task + const newChildTask = createMockTask({ + id: "child-2", + task: "Child task 2", + parentTaskId: "parent-1", + }) + + rerender({ tasks: [parentTask, childTask, newChildTask], query: "" }) + + // Expand state should be preserved + expect(result.current.groups[0].isExpanded).toBe(true) + }) + }) +}) diff --git a/webview-ui/src/components/history/types.ts b/webview-ui/src/components/history/types.ts new file mode 100644 index 00000000000..a12dfbce630 --- /dev/null +++ b/webview-ui/src/components/history/types.ts @@ -0,0 +1,37 @@ +import type { HistoryItem } from "@roo-code/types" + +/** + * Extended HistoryItem with display-related fields for search highlighting and subtask indication + */ +export interface DisplayHistoryItem extends HistoryItem { + /** HTML string with search match highlighting */ + highlight?: string + /** Whether this task is a subtask (has a parent in the current task list) */ + isSubtask?: boolean +} + +/** + * A group of tasks consisting of a parent task and its subtasks + */ +export interface TaskGroup { + /** The parent task */ + parent: DisplayHistoryItem + /** List of direct subtasks */ + subtasks: DisplayHistoryItem[] + /** Whether the subtask list is expanded */ + isExpanded: boolean +} + +/** + * Result from the useGroupedTasks hook + */ +export interface GroupedTasksResult { + /** Groups of tasks (parent + subtasks) - used in normal view */ + groups: TaskGroup[] + /** Flat list of tasks with isSubtask flag - used in search mode */ + flatTasks: DisplayHistoryItem[] | null + /** Function to toggle expand/collapse state of a group */ + toggleExpand: (taskId: string) => void + /** Whether search mode is active */ + isSearchMode: boolean +} diff --git a/webview-ui/src/components/history/useGroupedTasks.ts b/webview-ui/src/components/history/useGroupedTasks.ts new file mode 100644 index 00000000000..9d7085881eb --- /dev/null +++ b/webview-ui/src/components/history/useGroupedTasks.ts @@ -0,0 +1,101 @@ +import { useState, useMemo, useCallback } from "react" +import type { HistoryItem } from "@roo-code/types" +import type { DisplayHistoryItem, TaskGroup, GroupedTasksResult } from "./types" + +/** + * Hook to transform a flat task list into grouped structure based on parent-child relationships. + * In search mode, returns a flat list with isSubtask flag for each item. + * + * @param tasks - The list of tasks to group + * @param searchQuery - Current search query (empty string means not searching) + * @returns GroupedTasksResult with groups, flatTasks, toggleExpand, and isSearchMode + */ +export function useGroupedTasks(tasks: HistoryItem[], searchQuery: string): GroupedTasksResult { + const [expandedIds, setExpandedIds] = useState>(new Set()) + + const isSearchMode = searchQuery.trim().length > 0 + + // Build a map of taskId -> HistoryItem for quick lookup + const taskMap = useMemo(() => { + const map = new Map() + for (const task of tasks) { + map.set(task.id, task) + } + return map + }, [tasks]) + + // Group tasks by parent-child relationship + const groups = useMemo((): TaskGroup[] => { + if (isSearchMode) { + // In search mode, we don't group - return empty groups + return [] + } + + // Build children map: parentId -> children[] + const childrenMap = new Map() + + for (const task of tasks) { + if (task.parentTaskId && taskMap.has(task.parentTaskId)) { + const siblings = childrenMap.get(task.parentTaskId) || [] + siblings.push(task) + childrenMap.set(task.parentTaskId, siblings) + } + } + + // Identify root tasks - tasks that either: + // 1. Have no parentTaskId + // 2. Have a parentTaskId that doesn't exist in our task list + const rootTasks = tasks.filter((task) => !task.parentTaskId || !taskMap.has(task.parentTaskId)) + + // Build groups from root tasks + const taskGroups: TaskGroup[] = rootTasks.map((parent) => { + // Get direct children (sorted by timestamp, newest first) + const subtasks = (childrenMap.get(parent.id) || []) + .slice() + .sort((a, b) => b.ts - a.ts) as DisplayHistoryItem[] + + return { + parent: parent as DisplayHistoryItem, + subtasks, + isExpanded: expandedIds.has(parent.id), + } + }) + + // Sort groups by parent timestamp (newest first) + taskGroups.sort((a, b) => b.parent.ts - a.parent.ts) + + return taskGroups + }, [tasks, taskMap, isSearchMode, expandedIds]) + + // Flatten tasks for search mode with isSubtask flag + const flatTasks = useMemo((): DisplayHistoryItem[] | null => { + if (!isSearchMode) { + return null + } + + return tasks.map((task) => ({ + ...task, + isSubtask: !!task.parentTaskId && taskMap.has(task.parentTaskId), + })) as DisplayHistoryItem[] + }, [tasks, taskMap, isSearchMode]) + + // Toggle expand/collapse for a group + const toggleExpand = useCallback((taskId: string) => { + setExpandedIds((prev) => { + const newSet = new Set(prev) + if (newSet.has(taskId)) { + newSet.delete(taskId) + } else { + newSet.add(taskId) + } + return newSet + }) + }, []) + + return { + groups, + flatTasks, + toggleExpand, + isSearchMode, + } +} diff --git a/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx index 97d6e379773..eff3eb4fff6 100644 --- a/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx +++ b/webview-ui/src/components/ui/__tests__/tooltip.spec.tsx @@ -106,6 +106,29 @@ describe("StandardTooltip", () => { ) }) + it("should render with custom delay", async () => { + const user = userEvent.setup() + + render( + + + + + , + ) + + const trigger = screen.getByText("Hover me") + await user.hover(trigger) + + await waitFor( + () => { + const tooltips = screen.getAllByText("Tooltip text") + expect(tooltips.length).toBeGreaterThan(0) + }, + { timeout: 500 }, + ) + }) + it("should apply custom maxWidth", async () => { const user = userEvent.setup() diff --git a/webview-ui/src/components/ui/standard-tooltip.tsx b/webview-ui/src/components/ui/standard-tooltip.tsx index 7ec2e3205b3..c7b819fdad2 100644 --- a/webview-ui/src/components/ui/standard-tooltip.tsx +++ b/webview-ui/src/components/ui/standard-tooltip.tsx @@ -21,10 +21,12 @@ interface StandardTooltipProps { asChild?: boolean /** Maximum width of the tooltip content */ maxWidth?: number | string + /** Delay in milliseconds before showing the tooltip */ + delay?: number } /** - * StandardTooltip component that enforces consistent 300ms delay across the application. + * StandardTooltip component with a configurable delay (defaults to 300ms). * This component wraps the Radix UI tooltip with a standardized delay duration. * * @example @@ -38,6 +40,11 @@ interface StandardTooltipProps { * * * + * // With custom delay + * + * + * + * * @note This replaces native HTML title attributes for consistent timing. * @note Requires a TooltipProvider to be present in the component tree (typically at the app root). * @note Do not nest StandardTooltip components as this can cause UI issues. @@ -51,6 +58,7 @@ export function StandardTooltip({ className, asChild = true, maxWidth, + delay = STANDARD_TOOLTIP_DELAY, }: StandardTooltipProps) { // Don't render tooltip if content is empty or only whitespace. if (!content || (typeof content === "string" && !content.trim())) { @@ -60,7 +68,7 @@ export function StandardTooltip({ const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined return ( - + {children} Date: Wed, 21 Jan 2026 13:22:58 +0000 Subject: [PATCH 05/11] i18n --- webview-ui/src/i18n/locales/ca/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/ca/history.json | 8 +++++++- webview-ui/src/i18n/locales/de/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/de/history.json | 8 +++++++- webview-ui/src/i18n/locales/es/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/es/history.json | 8 +++++++- webview-ui/src/i18n/locales/fr/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/fr/history.json | 8 +++++++- webview-ui/src/i18n/locales/hi/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/hi/history.json | 8 +++++++- webview-ui/src/i18n/locales/id/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/id/history.json | 8 +++++++- webview-ui/src/i18n/locales/it/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/it/history.json | 8 +++++++- webview-ui/src/i18n/locales/ja/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/ja/history.json | 8 +++++++- webview-ui/src/i18n/locales/ko/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/ko/history.json | 8 +++++++- webview-ui/src/i18n/locales/nl/chat.json | 6 ++++-- webview-ui/src/i18n/locales/nl/history.json | 8 +++++++- webview-ui/src/i18n/locales/pl/chat.json | 6 ++++-- webview-ui/src/i18n/locales/pl/history.json | 8 +++++++- webview-ui/src/i18n/locales/pt-BR/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/pt-BR/history.json | 8 +++++++- webview-ui/src/i18n/locales/ru/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/ru/history.json | 8 +++++++- webview-ui/src/i18n/locales/tr/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/tr/history.json | 8 +++++++- webview-ui/src/i18n/locales/vi/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/vi/history.json | 8 +++++++- webview-ui/src/i18n/locales/zh-CN/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/zh-CN/history.json | 8 +++++++- webview-ui/src/i18n/locales/zh-TW/chat.json | 10 ++++++---- webview-ui/src/i18n/locales/zh-TW/history.json | 8 +++++++- 34 files changed, 217 insertions(+), 81 deletions(-) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index c989cc030bd..1ccfcfd6380 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Obrir tasca a Roo Code Cloud", "openInCloudIntro": "Continua monitoritzant o interactuant amb Roo des de qualsevol lloc. Escaneja, fes clic o copia per obrir.", "openApiHistory": "Obrir historial d'API", - "openUiHistory": "Obrir historial d'UI" + "openUiHistory": "Obrir historial d'UI", + "backToParentTask": "Tasca principal" }, "unpin": "Desfixar", "pin": "Fixar", @@ -146,14 +147,14 @@ "rateLimitWait": "Limitació de taxa", "errorTitle": "Error de proveïdor {{code}}", "errorMessage": { - "docs": "Documentació", - "goToSettings": "Configuració", "400": "El proveïdor no ha pogut processar la sol·licitud tal com es va fer. Interromp la tasca i prova una abordatge diferent.", "401": "No s'ha pogut autenticar amb el proveïdor. Si us plau, verifica la teva configuració de clau API.", "402": "Sembla que se t'han acabat els fons/crèdits al teu compte. Vés al teu proveïdor i afegeix més per continuar.", "403": "No autoritzat. La teva clau API és vàlida, però el proveïdor ha rebutjat completar aquesta sol·licitud.", "429": "Massa sol·licituds. Estàs sent limitat pel proveïdor. Si us plau espera una mica abans de la teva propera crida API.", "500": "Error del servidor del proveïdor. Quelcom va malament del costat del proveïdor, no hi ha res de malament amb la teva sol·licitud.", + "docs": "Documentació", + "goToSettings": "Configuració", "unknown": "Error API desconegut. Si us plau contacta amb el suport de Roo Code.", "connection": "Error de connexió. Assegureu-vos que teniu una connexió a Internet funcional.", "claudeCodeNotAuthenticated": "Has d'iniciar sessió per utilitzar Claude Code. Vés a Configuració i fes clic a \"Iniciar sessió a Claude Code\" per autenticar-te." @@ -258,7 +259,8 @@ "completionContent": "Subtasca completada", "resultContent": "Resultats de la subtasca", "defaultResult": "Si us plau, continua amb la següent tasca.", - "completionInstructions": "Subtasca completada! Pots revisar els resultats i suggerir correccions o següents passos. Si tot sembla correcte, confirma per tornar el resultat a la tasca principal." + "completionInstructions": "Subtasca completada! Pots revisar els resultats i suggerir correccions o següents passos. Si tot sembla correcte, confirma per tornar el resultat a la tasca principal.", + "goToSubtask": "Veure tasca" }, "questions": { "hasQuestion": "Roo té una pregunta" diff --git a/webview-ui/src/i18n/locales/ca/history.json b/webview-ui/src/i18n/locales/ca/history.json index 2edecbde047..ab1bba7582e 100644 --- a/webview-ui/src/i18n/locales/ca/history.json +++ b/webview-ui/src/i18n/locales/ca/history.json @@ -48,5 +48,11 @@ "mostTokens": "Més tokens", "mostRelevant": "Més rellevants" }, - "viewAllHistory": "Veure-ho tot" + "viewAllHistory": "Veure-ho tot", + "subtasks_one": "{{count}} subtasca", + "subtasks_other": "{{count}} subtasques", + "subtaskTag": "Subtasca", + "deleteWithSubtasks": "Això també eliminarà {{count}} subtasca(s). Estàs segur?", + "expandSubtasks": "Expandir subtasques", + "collapseSubtasks": "Contreure subtasques" } diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 254052278bd..c7a656d21e4 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Aufgabe in Roo Code Cloud öffnen", "openInCloudIntro": "Überwache oder interagiere mit Roo von überall aus. Scanne, klicke oder kopiere zum Öffnen.", "openApiHistory": "API-Verlauf öffnen", - "openUiHistory": "UI-Verlauf öffnen" + "openUiHistory": "UI-Verlauf öffnen", + "backToParentTask": "Übergeordnete Aufgabe" }, "unpin": "Lösen von oben", "pin": "Anheften", @@ -146,14 +147,14 @@ "rateLimitWait": "Ratenbegrenzung", "errorTitle": "Anbieter-Fehler {{code}}", "errorMessage": { - "docs": "Dokumentation", - "goToSettings": "Einstellungen", "400": "Der Anbieter konnte die Anfrage nicht wie gestellt verarbeiten. Beende die Aufgabe und versuche einen anderen Ansatz.", "401": "Authentifizierung beim Anbieter fehlgeschlagen. Bitte überprüfe deine API-Schlüssel-Konfiguration.", "402": "Es sieht so aus, als ob dir die Guthaben/Credits in deinem Konto ausgegangen sind. Gehe zu deinem Anbieter und füge mehr hinzu, um fortzufahren.", "403": "Nicht autorisiert. Dein API-Schlüssel ist gültig, aber der Anbieter hat sich geweigert, diese Anfrage zu erfüllen.", "429": "Zu viele Anfragen. Du wirst vom Anbieter rate-limitiert. Bitte warte ein wenig, bevor du den nächsten API-Aufruf machst.", "500": "Fehler auf dem Server des Anbieters. Es stimmt etwas mit der Anbieterseite nicht, mit deiner Anfrage stimmt alles.", + "docs": "Dokumentation", + "goToSettings": "Einstellungen", "unknown": "Unbekannter API-Fehler. Bitte kontaktiere den Roo Code Support.", "connection": "Verbindungsfehler. Stelle sicher, dass du eine funktionierende Internetverbindung hast.", "claudeCodeNotAuthenticated": "Du musst dich anmelden, um Claude Code zu verwenden. Gehe zu den Einstellungen und klicke auf \"Bei Claude Code anmelden\", um dich zu authentifizieren." @@ -258,7 +259,8 @@ "completionContent": "Teilaufgabe abgeschlossen", "resultContent": "Teilaufgabenergebnisse", "defaultResult": "Bitte fahre mit der nächsten Aufgabe fort.", - "completionInstructions": "Teilaufgabe abgeschlossen! Du kannst die Ergebnisse überprüfen und Korrekturen oder nächste Schritte vorschlagen. Wenn alles gut aussieht, bestätige, um das Ergebnis an die übergeordnete Aufgabe zurückzugeben." + "completionInstructions": "Teilaufgabe abgeschlossen! Du kannst die Ergebnisse überprüfen und Korrekturen oder nächste Schritte vorschlagen. Wenn alles gut aussieht, bestätige, um das Ergebnis an die übergeordnete Aufgabe zurückzugeben.", + "goToSubtask": "Aufgabe anzeigen" }, "questions": { "hasQuestion": "Roo hat eine Frage" diff --git a/webview-ui/src/i18n/locales/de/history.json b/webview-ui/src/i18n/locales/de/history.json index 0ac423aff6b..46064d6e2ea 100644 --- a/webview-ui/src/i18n/locales/de/history.json +++ b/webview-ui/src/i18n/locales/de/history.json @@ -48,5 +48,11 @@ "mostTokens": "Meiste Tokens", "mostRelevant": "Relevanteste" }, - "viewAllHistory": "Alle anzeigen" + "viewAllHistory": "Alle anzeigen", + "subtasks_one": "{{count}} Teilaufgabe", + "subtasks_other": "{{count}} Teilaufgaben", + "subtaskTag": "Teilaufgabe", + "deleteWithSubtasks": "Dies löscht auch {{count}} Teilaufgabe(n). Bist du sicher?", + "expandSubtasks": "Teilaufgaben erweitern", + "collapseSubtasks": "Teilaufgaben einklappen" } diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index ae0d02232f6..295d5f94822 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Abrir tarea en Roo Code Cloud", "openInCloudIntro": "Continúa monitoreando o interactuando con Roo desde cualquier lugar. Escanea, haz clic o copia para abrir.", "openApiHistory": "Abrir historial de API", - "openUiHistory": "Abrir historial de UI" + "openUiHistory": "Abrir historial de UI", + "backToParentTask": "Tarea principal" }, "unpin": "Desfijar", "pin": "Fijar", @@ -146,14 +147,14 @@ "rateLimitWait": "Limitación de tasa", "errorTitle": "Error del proveedor {{code}}", "errorMessage": { - "docs": "Documentación", - "goToSettings": "Configuración", "400": "El proveedor no pudo procesar la solicitud tal como se hizo. Detén la tarea e intenta un enfoque diferente.", "401": "No se pudo autenticar con el proveedor. Por favor verifica tu configuración de clave API.", "402": "Parece que se te han acabado los fondos/créditos en tu cuenta. Ve a tu proveedor y agrega más para continuar.", "403": "No autorizado. Tu clave API es válida, pero el proveedor se negó a completar esta solicitud.", "429": "Demasiadas solicitudes. Te estás viendo limitado por el proveedor. Por favor espera un poco antes de tu próxima llamada API.", "500": "Error del servidor del proveedor. Algo está mal en el lado del proveedor, no hay nada mal con tu solicitud.", + "docs": "Documentación", + "goToSettings": "Configuración", "unknown": "Error API desconocido. Por favor contacta al soporte de Roo Code.", "connection": "Error de conexión. Asegúrate de tener una conexión a Internet funcional.", "claudeCodeNotAuthenticated": "Debes iniciar sesión para usar Claude Code. Ve a Configuración y haz clic en \"Iniciar sesión en Claude Code\" para autenticarte." @@ -258,7 +259,8 @@ "completionContent": "Subtarea completada", "resultContent": "Resultados de la subtarea", "defaultResult": "Por favor, continúa con la siguiente tarea.", - "completionInstructions": "¡Subtarea completada! Puedes revisar los resultados y sugerir correcciones o próximos pasos. Si todo se ve bien, confirma para devolver el resultado a la tarea principal." + "completionInstructions": "¡Subtarea completada! Puedes revisar los resultados y sugerir correcciones o próximos pasos. Si todo se ve bien, confirma para devolver el resultado a la tarea principal.", + "goToSubtask": "Ver tarea" }, "questions": { "hasQuestion": "Roo tiene una pregunta" diff --git a/webview-ui/src/i18n/locales/es/history.json b/webview-ui/src/i18n/locales/es/history.json index 2d0c45d7507..820f003d402 100644 --- a/webview-ui/src/i18n/locales/es/history.json +++ b/webview-ui/src/i18n/locales/es/history.json @@ -48,5 +48,11 @@ "mostTokens": "Más tokens", "mostRelevant": "Más relevantes" }, - "viewAllHistory": "Ver todo" + "viewAllHistory": "Ver todo", + "subtasks_one": "{{count}} subtarea", + "subtasks_other": "{{count}} subtareas", + "subtaskTag": "Subtarea", + "deleteWithSubtasks": "Esto también eliminará {{count}} subtarea(s). ¿Estás seguro?", + "expandSubtasks": "Expandir subtareas", + "collapseSubtasks": "Contraer subtareas" } diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 56eec51e96b..16d6db318c5 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Ouvrir la tâche dans Roo Code Cloud", "openInCloudIntro": "Continue à surveiller ou interagir avec Roo depuis n'importe où. Scanne, clique ou copie pour ouvrir.", "openApiHistory": "Ouvrir l'historique de l'API", - "openUiHistory": "Ouvrir l'historique de l'UI" + "openUiHistory": "Ouvrir l'historique de l'UI", + "backToParentTask": "Tâche parente" }, "unpin": "Désépingler", "pin": "Épingler", @@ -146,14 +147,14 @@ "rateLimitWait": "Limitation du débit", "errorTitle": "Erreur du fournisseur {{code}}", "errorMessage": { - "docs": "Documentation", - "goToSettings": "Paramètres", "400": "Le fournisseur n'a pas pu traiter la demande telle que présentée. Arrête la tâche et essaie une approche différente.", "401": "Impossible de s'authentifier auprès du fournisseur. Veuillez vérifier la configuration de votre clé API.", "402": "Il semble que vous ayez épuisé vos fonds/crédits sur votre compte. Allez chez votre fournisseur et ajoutez-en plus pour continuer.", "403": "Non autorisé. Votre clé API est valide, mais le fournisseur a refusé de compléter cette demande.", "429": "Trop de demandes. Vous êtes limité par le fournisseur. Veuillez attendre un peu avant votre prochain appel API.", "500": "Erreur du serveur du fournisseur. Quelque chose ne va pas du côté du fournisseur, il n'y a rien de mal avec votre demande.", + "docs": "Documentation", + "goToSettings": "Paramètres", "unknown": "Erreur API inconnue. Veuillez contacter le support Roo Code.", "connection": "Erreur de connexion. Assurez-vous que vous avez une connexion Internet fonctionnelle.", "claudeCodeNotAuthenticated": "Vous devez vous connecter pour utiliser Claude Code. Allez dans les Paramètres et cliquez sur \"Se connecter à Claude Code\" pour vous authentifier." @@ -258,7 +259,8 @@ "completionContent": "Sous-tâche terminée", "resultContent": "Résultats de la sous-tâche", "defaultResult": "Veuillez continuer avec la tâche suivante.", - "completionInstructions": "Sous-tâche terminée ! Vous pouvez examiner les résultats et suggérer des corrections ou les prochaines étapes. Si tout semble bon, confirmez pour retourner le résultat à la tâche parente." + "completionInstructions": "Sous-tâche terminée ! Vous pouvez examiner les résultats et suggérer des corrections ou les prochaines étapes. Si tout semble bon, confirmez pour retourner le résultat à la tâche parente.", + "goToSubtask": "Afficher la tâche" }, "questions": { "hasQuestion": "Roo a une question" diff --git a/webview-ui/src/i18n/locales/fr/history.json b/webview-ui/src/i18n/locales/fr/history.json index a4448edea15..d84cfcb190a 100644 --- a/webview-ui/src/i18n/locales/fr/history.json +++ b/webview-ui/src/i18n/locales/fr/history.json @@ -48,5 +48,11 @@ "mostTokens": "Plus de tokens", "mostRelevant": "Plus pertinentes" }, - "viewAllHistory": "Voir tout" + "viewAllHistory": "Voir tout", + "subtasks_one": "{{count}} sous-tâche", + "subtasks_other": "{{count}} sous-tâches", + "subtaskTag": "Sous-tâche", + "deleteWithSubtasks": "Cela supprimera aussi {{count}} sous-tâche(s). Êtes-vous sûr ?", + "expandSubtasks": "Développer les sous-tâches", + "collapseSubtasks": "Réduire les sous-tâches" } diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index a58e3abff44..dfdbf38b821 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Roo Code Cloud में कार्य खोलें", "openInCloudIntro": "कहीं से भी Roo की निगरानी या इंटरैक्ट करना जारी रखें। खोलने के लिए स्कैन करें, क्लिक करें या कॉपी करें।", "openApiHistory": "API इतिहास खोलें", - "openUiHistory": "UI इतिहास खोलें" + "openUiHistory": "UI इतिहास खोलें", + "backToParentTask": "मूल कार्य" }, "unpin": "पिन करें", "pin": "अवपिन करें", @@ -146,14 +147,14 @@ "rateLimitWait": "दर सीमा", "errorTitle": "प्रदाता त्रुटि {{code}}", "errorMessage": { - "docs": "डॉक्स", - "goToSettings": "सेटिंग्स", "400": "प्रदाता अनुरोध को जैसे बनाया गया था उसे प्रोसेस नहीं कर सका। कार्य को रोकें और एक अलग तरीका आजमाएं।", "401": "प्रदाता के साथ प्रमाणित नहीं किए जा सके। अपने API कुंजी कॉन्फ़िगरेशन की जाँच करें।", "402": "ऐसा लगता है कि आपके खाते में फंड/क्रेडिट समाप्त हो गए हैं। अपने प्रदाता के पास जाएं और जारी रखने के लिए और अधिक जोड़ें।", "403": "अनुमति नहीं। आपकी API कुंजी वैध है, लेकिन प्रदाता ने इस अनुरोध को पूरा करने से इनकार कर दिया।", "429": "बहुत सारे अनुरोध। आप प्रदाता द्वारा दर-सीमित हो रहे हैं। कृपया अपनी अगली API कॉल से पहले थोड़ी देर प्रतीक्षा करें।", "500": "प्रदाता सर्वर त्रुटि। प्रदाता की ओर से कुछ गलत है, आपके अनुरोध में कुछ गलत नहीं है।", + "docs": "डॉक्स", + "goToSettings": "सेटिंग्स", "unknown": "अज्ञात API त्रुटि। कृपया Roo Code सहायता से संपर्क करें।", "connection": "कनेक्शन त्रुटि। सुनिश्चित करें कि आपके पास कार्यशील इंटरनेट कनेक्शन है।", "claudeCodeNotAuthenticated": "Claude Code का उपयोग करने के लिए आपको साइन इन करना होगा। सेटिंग्स में जाएं और प्रमाणित करने के लिए \"Claude Code में साइन इन करें\" पर क्लिक करें।" @@ -258,7 +259,8 @@ "completionContent": "उपकार्य पूर्ण", "resultContent": "उपकार्य परिणाम", "defaultResult": "कृपया अगले कार्य पर जारी रखें।", - "completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।" + "completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।", + "goToSubtask": "कार्य देखें" }, "questions": { "hasQuestion": "Roo का एक प्रश्न है" diff --git a/webview-ui/src/i18n/locales/hi/history.json b/webview-ui/src/i18n/locales/hi/history.json index 2a4b3369b33..0d4cb40dd9e 100644 --- a/webview-ui/src/i18n/locales/hi/history.json +++ b/webview-ui/src/i18n/locales/hi/history.json @@ -41,5 +41,11 @@ "mostTokens": "सबसे अधिक टोकन", "mostRelevant": "सबसे प्रासंगिक" }, - "viewAllHistory": "सभी देखें" + "viewAllHistory": "सभी देखें", + "subtasks_one": "{{count}} उप-कार्य", + "subtasks_other": "{{count}} उप-कार्य", + "subtaskTag": "उप-कार्य", + "deleteWithSubtasks": "यह {{count}} उप-कार्य(कों) को भी हटा देगा। क्या आप निश्चित हैं?", + "expandSubtasks": "उप-कार्य विस्तारित करें", + "collapseSubtasks": "उप-कार्य संपीड़ित करें" } diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index c00a6d5ab3b..d3ff27926db 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Buka tugas di Roo Code Cloud", "openInCloudIntro": "Terus pantau atau berinteraksi dengan Roo dari mana saja. Pindai, klik atau salin untuk membuka.", "openApiHistory": "Buka Riwayat API", - "openUiHistory": "Buka Riwayat UI" + "openUiHistory": "Buka Riwayat UI", + "backToParentTask": "Tugas Induk" }, "history": { "title": "Riwayat" @@ -155,14 +156,14 @@ "rateLimitWait": "Pembatasan rate", "errorTitle": "Kesalahan Penyedia {{code}}", "errorMessage": { - "docs": "Dokumentasi", - "goToSettings": "Pengaturan", "400": "Penyedia tidak dapat memproses permintaan seperti yang dibuat. Hentikan tugas dan coba pendekatan berbeda.", "401": "Tidak dapat mengautentikasi dengan penyedia. Harap periksa konfigurasi kunci API Anda.", "402": "Tampaknya Anda telah kehabisan dana/kredit di akun Anda. Pergi ke penyedia Anda dan tambahkan lebih banyak untuk melanjutkan.", "403": "Tidak sah. Kunci API Anda valid, tetapi penyedia menolak untuk menyelesaikan permintaan ini.", "429": "Terlalu banyak permintaan. Anda dibatasi tingkat oleh penyedia. Harap tunggu sebentar sebelum panggilan API berikutnya Anda.", "500": "Kesalahan server penyedia. Ada yang salah di sisi penyedia, tidak ada yang salah dengan permintaan Anda.", + "docs": "Dokumentasi", + "goToSettings": "Pengaturan", "unknown": "Kesalahan API yang tidak diketahui. Harap hubungi dukungan Roo Code.", "connection": "Kesalahan koneksi. Pastikan Anda memiliki koneksi internet yang berfungsi.", "claudeCodeNotAuthenticated": "Anda perlu masuk untuk menggunakan Claude Code. Buka Pengaturan dan klik \"Masuk ke Claude Code\" untuk mengautentikasi." @@ -289,7 +290,8 @@ "completionContent": "Subtugas Selesai", "resultContent": "Hasil Subtugas", "defaultResult": "Silakan lanjutkan ke tugas berikutnya.", - "completionInstructions": "Subtugas selesai! Kamu bisa meninjau hasilnya dan menyarankan koreksi atau langkah selanjutnya. Jika semuanya terlihat baik, konfirmasi untuk mengembalikan hasil ke tugas induk." + "completionInstructions": "Subtugas selesai! Kamu bisa meninjau hasilnya dan menyarankan koreksi atau langkah selanjutnya. Jika semuanya terlihat baik, konfirmasi untuk mengembalikan hasil ke tugas induk.", + "goToSubtask": "Lihat tugas" }, "questions": { "hasQuestion": "Roo punya pertanyaan" diff --git a/webview-ui/src/i18n/locales/id/history.json b/webview-ui/src/i18n/locales/id/history.json index 4767b109945..7796061107e 100644 --- a/webview-ui/src/i18n/locales/id/history.json +++ b/webview-ui/src/i18n/locales/id/history.json @@ -50,5 +50,11 @@ "mostTokens": "Token Terbanyak", "mostRelevant": "Paling Relevan" }, - "viewAllHistory": "Lihat semua" + "viewAllHistory": "Lihat semua", + "subtasks_one": "{{count}} subtask", + "subtasks_other": "{{count}} subtask", + "subtaskTag": "Subtask", + "deleteWithSubtasks": "Ini juga akan menghapus {{count}} subtask. Apakah Anda yakin?", + "expandSubtasks": "Perluas subtask", + "collapseSubtasks": "Tutup subtask" } diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 3fe64691356..d7f73055371 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Apri attività in Roo Code Cloud", "openInCloudIntro": "Continua a monitorare o interagire con Roo da qualsiasi luogo. Scansiona, clicca o copia per aprire.", "openApiHistory": "Apri cronologia API", - "openUiHistory": "Apri cronologia UI" + "openUiHistory": "Apri cronologia UI", + "backToParentTask": "Attività principale" }, "unpin": "Rilascia", "pin": "Fissa", @@ -149,14 +150,14 @@ "rateLimitWait": "Limitazione della frequenza", "errorTitle": "Errore del fornitore {{code}}", "errorMessage": { - "docs": "Documentazione", - "goToSettings": "Impostazioni", "400": "Il provider non ha potuto elaborare la richiesta come presentata. Interrompi l'attività e prova un approccio diverso.", "401": "Impossibile autenticare con il provider. Verifica la configurazione della tua chiave API.", "402": "Sembra che tu abbia esaurito i fondi/crediti nel tuo account. Vai al tuo provider e aggiungi altro per continuare.", "403": "Non autorizzato. La tua chiave API è valida, ma il provider ha rifiutato di completare questa richiesta.", "429": "Troppe richieste. Sei limitato dal provider. Attendi un po' prima della tua prossima chiamata API.", "500": "Errore del server del provider. C'è qualcosa di sbagliato dal lato del provider, non c'è nulla di sbagliato nella tua richiesta.", + "docs": "Documentazione", + "goToSettings": "Impostazioni", "unknown": "Errore API sconosciuto. Contatta il supporto di Roo Code.", "connection": "Errore di connessione. Assicurati di avere una connessione Internet funzionante.", "claudeCodeNotAuthenticated": "Devi accedere per utilizzare Claude Code. Vai su Impostazioni e clicca su \"Accedi a Claude Code\" per autenticarti." @@ -258,7 +259,8 @@ "completionContent": "Sottoattività completata", "resultContent": "Risultati sottoattività", "defaultResult": "Per favore continua con la prossima attività.", - "completionInstructions": "Sottoattività completata! Puoi rivedere i risultati e suggerire correzioni o prossimi passi. Se tutto sembra a posto, conferma per restituire il risultato all'attività principale." + "completionInstructions": "Sottoattività completata! Puoi rivedere i risultati e suggerire correzioni o prossimi passi. Se tutto sembra a posto, conferma per restituire il risultato all'attività principale.", + "goToSubtask": "Visualizza attività" }, "questions": { "hasQuestion": "Roo ha una domanda" diff --git a/webview-ui/src/i18n/locales/it/history.json b/webview-ui/src/i18n/locales/it/history.json index 9efe4b97c55..aa728ef8f60 100644 --- a/webview-ui/src/i18n/locales/it/history.json +++ b/webview-ui/src/i18n/locales/it/history.json @@ -41,5 +41,11 @@ "mostTokens": "Più token", "mostRelevant": "Più rilevanti" }, - "viewAllHistory": "Visualizza tutto" + "viewAllHistory": "Visualizza tutto", + "subtasks_one": "{{count}} sottoattività", + "subtasks_other": "{{count}} sottoattività", + "subtaskTag": "Sottoattività", + "deleteWithSubtasks": "Questo eliminerà anche {{count}} sottoattività. Sei sicuro?", + "expandSubtasks": "Espandi sottoattività", + "collapseSubtasks": "Comprimi sottoattività" } diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 68f3597396c..fd3dc53f859 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Roo Code Cloudでタスクを開く", "openInCloudIntro": "どこからでもRooの監視や操作を続けられます。スキャン、クリック、またはコピーして開いてください。", "openApiHistory": "API履歴を開く", - "openUiHistory": "UI履歴を開く" + "openUiHistory": "UI履歴を開く", + "backToParentTask": "親タスク" }, "unpin": "ピン留めを解除", "pin": "ピン留め", @@ -146,14 +147,14 @@ "rateLimitWait": "レート制限中", "errorTitle": "プロバイダーエラー {{code}}", "errorMessage": { - "docs": "ドキュメント", - "goToSettings": "設定", "400": "プロバイダーはリクエストをそのまま処理できませんでした。タスクを停止して別のアプローチを試してください。", "401": "プロバイダーで認証できませんでした。API キーの設定を確認してください。", "402": "アカウントの資金/クレジットが不足しているようです。プロバイダーに移動してさらに追加してください。", "403": "認可されていません。API キーは有効ですが、プロバイダーがこのリクエストの完了を拒否しました。", "429": "リクエストが多すぎます。プロバイダーによってレート制限されています。次の API 呼び出しの前にお待ちください。", "500": "プロバイダー サーバー エラー。プロバイダー側に問題があり、リクエスト自体に問題はありません。", + "docs": "ドキュメント", + "goToSettings": "設定", "unknown": "不明な API エラー。Roo Code のサポートにお問い合わせください。", "connection": "接続エラー。インターネット接続が機能していることを確認してください。", "claudeCodeNotAuthenticated": "Claude Codeを使用するにはサインインが必要です。設定に移動して「Claude Codeにサインイン」をクリックして認証してください。" @@ -258,7 +259,8 @@ "completionContent": "サブタスク完了", "resultContent": "サブタスク結果", "defaultResult": "次のタスクに進んでください。", - "completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。" + "completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。", + "goToSubtask": "タスクを表示" }, "questions": { "hasQuestion": "Rooは質問があります" diff --git a/webview-ui/src/i18n/locales/ja/history.json b/webview-ui/src/i18n/locales/ja/history.json index f414a3201c6..b73baa3a763 100644 --- a/webview-ui/src/i18n/locales/ja/history.json +++ b/webview-ui/src/i18n/locales/ja/history.json @@ -41,5 +41,11 @@ "mostTokens": "最多トークン", "mostRelevant": "最も関連性の高い" }, - "viewAllHistory": "すべて表示" + "viewAllHistory": "すべて表示", + "subtasks_one": "{{count}} サブタスク", + "subtasks_other": "{{count}} サブタスク", + "subtaskTag": "サブタスク", + "deleteWithSubtasks": "これにより {{count}} サブタスクも削除されます。よろしいですか?", + "expandSubtasks": "サブタスクを展開", + "collapseSubtasks": "サブタスクを折りたたむ" } diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index a9934a4fd3d..3e7f366cb67 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Roo Code Cloud에서 작업 열기", "openInCloudIntro": "어디서나 Roo를 계속 모니터링하거나 상호작용할 수 있습니다. 스캔, 클릭 또는 복사하여 열기.", "openApiHistory": "API 기록 열기", - "openUiHistory": "UI 기록 열기" + "openUiHistory": "UI 기록 열기", + "backToParentTask": "상위 작업" }, "unpin": "고정 해제하기", "pin": "고정하기", @@ -146,14 +147,14 @@ "rateLimitWait": "속도 제한", "errorTitle": "공급자 오류 {{code}}", "errorMessage": { - "docs": "문서", - "goToSettings": "설정", "400": "공급자가 요청을 처리할 수 없습니다. 작업을 중지하고 다른 방법을 시도하세요.", "401": "공급자로 인증할 수 없습니다. API 키 구성을 확인하세요.", "402": "계정의 자금/크레딧이 부족한 것 같습니다. 공급자에게 가서 더 추가하세요.", "403": "권한 없음. API 키는 유효하지만 공급자가 이 요청을 완료하기를 거부했습니다.", "429": "너무 많은 요청입니다. 공급자에 의해 요청 제한이 적용되고 있습니다. 다음 API 호출 전에 잠깐 기다려주세요.", "500": "공급자 서버 오류입니다. 공급자 쪽에 문제가 있으며 요청에는 문제가 없습니다.", + "docs": "문서", + "goToSettings": "설정", "unknown": "알 수 없는 API 오류입니다. Roo Code 지원팀에 문의하세요.", "connection": "연결 오류입니다. 인터넷 연결이 제대로 작동하는지 확인하세요.", "claudeCodeNotAuthenticated": "Claude Code를 사용하려면 로그인해야 합니다. 설정으로 이동하여 \"Claude Code에 로그인\"을 클릭하여 인증하세요." @@ -258,7 +259,8 @@ "completionContent": "하위 작업 완료", "resultContent": "하위 작업 결과", "defaultResult": "다음 작업을 계속 진행해주세요.", - "completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요." + "completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요.", + "goToSubtask": "작업 보기" }, "questions": { "hasQuestion": "Roo에게 질문이 있습니다" diff --git a/webview-ui/src/i18n/locales/ko/history.json b/webview-ui/src/i18n/locales/ko/history.json index 3947319e5f1..0363feaaffb 100644 --- a/webview-ui/src/i18n/locales/ko/history.json +++ b/webview-ui/src/i18n/locales/ko/history.json @@ -41,5 +41,11 @@ "mostTokens": "토큰 많은순", "mostRelevant": "관련성 높은순" }, - "viewAllHistory": "모두 보기" + "viewAllHistory": "모두 보기", + "subtasks_one": "{{count}} 부분작업", + "subtasks_other": "{{count}} 부분작업", + "subtaskTag": "부분작업", + "deleteWithSubtasks": "이는 {{count}} 부분작업도 삭제합니다. 확실하십니까?", + "expandSubtasks": "부분작업 확장", + "collapseSubtasks": "부분작업 축소" } diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index d1747ed529d..0fede142c03 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Taak openen in Roo Code Cloud", "openInCloudIntro": "Blijf Roo vanaf elke locatie monitoren of ermee interacteren. Scan, klik of kopieer om te openen.", "openApiHistory": "API-geschiedenis openen", - "openUiHistory": "UI-geschiedenis openen" + "openUiHistory": "UI-geschiedenis openen", + "backToParentTask": "Bovenliggende taak" }, "unpin": "Losmaken", "pin": "Vastmaken", @@ -253,7 +254,8 @@ "completionContent": "Subtaak voltooid", "resultContent": "Subtaakresultaten", "defaultResult": "Ga verder met de volgende taak.", - "completionInstructions": "Subtaak voltooid! Je kunt de resultaten bekijken en eventuele correcties of volgende stappen voorstellen. Als alles goed is, bevestig dan om het resultaat terug te sturen naar de hoofdtaak." + "completionInstructions": "Subtaak voltooid! Je kunt de resultaten bekijken en eventuele correcties of volgende stappen voorstellen. Als alles goed is, bevestig dan om het resultaat terug te sturen naar de hoofdtaak.", + "goToSubtask": "Taak weergeven" }, "questions": { "hasQuestion": "Roo heeft een vraag" diff --git a/webview-ui/src/i18n/locales/nl/history.json b/webview-ui/src/i18n/locales/nl/history.json index d99b64565e4..012059ed047 100644 --- a/webview-ui/src/i18n/locales/nl/history.json +++ b/webview-ui/src/i18n/locales/nl/history.json @@ -41,5 +41,11 @@ "mostTokens": "Meeste tokens", "mostRelevant": "Meest relevant" }, - "viewAllHistory": "Alles bekijken" + "viewAllHistory": "Alles bekijken", + "subtasks_one": "{{count}} subtaak", + "subtasks_other": "{{count}} subtaken", + "subtaskTag": "Subtaak", + "deleteWithSubtasks": "Dit zal ook {{count}} subtaak(en) verwijderen. Weet je het zeker?", + "expandSubtasks": "Subtaken uitvouwen", + "collapseSubtasks": "Subtaken samenvouwen" } diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index acdd23f06be..11fb74a3886 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Otwórz zadanie w Roo Code Cloud", "openInCloudIntro": "Kontynuuj monitorowanie lub interakcję z Roo z dowolnego miejsca. Zeskanuj, kliknij lub skopiuj, aby otworzyć.", "openApiHistory": "Otwórz historię API", - "openUiHistory": "Otwórz historię UI" + "openUiHistory": "Otwórz historię UI", + "backToParentTask": "Zadanie nadrzędne" }, "unpin": "Odepnij", "pin": "Przypnij", @@ -258,7 +259,8 @@ "completionContent": "Podzadanie zakończone", "resultContent": "Wyniki podzadania", "defaultResult": "Proszę kontynuować następne zadanie.", - "completionInstructions": "Podzadanie zakończone! Możesz przejrzeć wyniki i zasugerować poprawki lub następne kroki. Jeśli wszystko wygląda dobrze, potwierdź, aby zwrócić wynik do zadania nadrzędnego." + "completionInstructions": "Podzadanie zakończone! Możesz przejrzeć wyniki i zasugerować poprawki lub następne kroki. Jeśli wszystko wygląda dobrze, potwierdź, aby zwrócić wynik do zadania nadrzędnego.", + "goToSubtask": "Wyświetl zadanie" }, "questions": { "hasQuestion": "Roo ma pytanie" diff --git a/webview-ui/src/i18n/locales/pl/history.json b/webview-ui/src/i18n/locales/pl/history.json index d7f8c0610ac..7ec4b40d8f5 100644 --- a/webview-ui/src/i18n/locales/pl/history.json +++ b/webview-ui/src/i18n/locales/pl/history.json @@ -41,5 +41,11 @@ "mostTokens": "Najwięcej tokenów", "mostRelevant": "Najbardziej trafne" }, - "viewAllHistory": "Zobacz wszystko" + "viewAllHistory": "Zobacz wszystko", + "subtasks_one": "{{count}} podzadanie", + "subtasks_other": "{{count}} podzadań", + "subtaskTag": "Podzadanie", + "deleteWithSubtasks": "Spowoduje to usunięcie {{count}} podzadania(ń). Jesteś pewny?", + "expandSubtasks": "Rozwiń podzadania", + "collapseSubtasks": "Zwiń podzadania" } diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 30a86559fa4..60e0573b624 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Abrir tarefa no Roo Code Cloud", "openInCloudIntro": "Continue monitorando ou interagindo com Roo de qualquer lugar. Escaneie, clique ou copie para abrir.", "openApiHistory": "Abrir histórico da API", - "openUiHistory": "Abrir histórico da UI" + "openUiHistory": "Abrir histórico da UI", + "backToParentTask": "Tarefa pai" }, "unpin": "Desfixar", "pin": "Fixar", @@ -146,14 +147,14 @@ "rateLimitWait": "Limitação de taxa", "errorTitle": "Erro do provedor {{code}}", "errorMessage": { - "docs": "Documentação", - "goToSettings": "Configurações", "400": "O provedor não conseguiu processar a solicitação conforme feita. Interrompa a tarefa e tente uma abordagem diferente.", "401": "Não foi possível autenticar com o provedor. Por favor, verifique a configuração da sua chave API.", "402": "Parece que você ficou sem fundos/créditos em sua conta. Vá ao seu provedor e adicione mais para continuar.", "403": "Não autorizado. Sua chave API é válida, mas o provedor se recusou a concluir esta solicitação.", "429": "Muitas solicitações. Você está sendo limitado pelo provedor. Por favor, aguarde um pouco antes de sua próxima chamada de API.", "500": "Erro do servidor do provedor. Algo está errado do lado do provedor, não há nada de errado com sua solicitação.", + "docs": "Documentação", + "goToSettings": "Configurações", "unknown": "Erro de API desconhecido. Por favor, entre em contato com o suporte do Roo Code.", "connection": "Erro de conexão. Certifique-se de que você tem uma conexão de internet funcionando.", "claudeCodeNotAuthenticated": "Você precisa fazer login para usar o Claude Code. Vá para Configurações e clique em \"Entrar no Claude Code\" para autenticar." @@ -258,7 +259,8 @@ "completionContent": "Subtarefa concluída", "resultContent": "Resultados da subtarefa", "defaultResult": "Por favor, continue com a próxima tarefa.", - "completionInstructions": "Subtarefa concluída! Você pode revisar os resultados e sugerir correções ou próximos passos. Se tudo parecer bom, confirme para retornar o resultado à tarefa principal." + "completionInstructions": "Subtarefa concluída! Você pode revisar os resultados e sugerir correções ou próximos passos. Se tudo parecer bom, confirme para retornar o resultado à tarefa principal.", + "goToSubtask": "Ver tarefa" }, "questions": { "hasQuestion": "Roo tem uma pergunta" diff --git a/webview-ui/src/i18n/locales/pt-BR/history.json b/webview-ui/src/i18n/locales/pt-BR/history.json index c2fcdffe8d6..7966df1f463 100644 --- a/webview-ui/src/i18n/locales/pt-BR/history.json +++ b/webview-ui/src/i18n/locales/pt-BR/history.json @@ -41,5 +41,11 @@ "mostTokens": "Mais tokens", "mostRelevant": "Mais relevantes" }, - "viewAllHistory": "Ver tudo" + "viewAllHistory": "Ver tudo", + "subtasks_one": "{{count}} subtarefa", + "subtasks_other": "{{count}} subtarefas", + "subtaskTag": "Subtarefa", + "deleteWithSubtasks": "Isso também excluirá {{count}} subtarefa(s). Tem certeza?", + "expandSubtasks": "Expandir subtarefas", + "collapseSubtasks": "Recolher subtarefas" } diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 008de4397d9..0c5365812b8 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Открыть задачу в Roo Code Cloud", "openInCloudIntro": "Продолжай отслеживать или взаимодействовать с Roo откуда угодно. Отсканируй, нажми или скопируй для открытия.", "openApiHistory": "Открыть историю API", - "openUiHistory": "Открыть историю UI" + "openUiHistory": "Открыть историю UI", + "backToParentTask": "Родительская задача" }, "unpin": "Открепить", "pin": "Закрепить", @@ -141,14 +142,14 @@ "rateLimitWait": "Ограничение частоты", "errorTitle": "Ошибка провайдера {{code}}", "errorMessage": { - "docs": "Документация", - "goToSettings": "Настройки", "400": "Провайдер не смог обработать запрос. Остановите задачу и попробуйте другой подход.", "401": "Не удалось аутентифицироваться у провайдера. Проверьте конфигурацию ключа API.", "402": "Похоже, у вас закончились средства/кредиты на вашем счете. Перейдите к провайдеру и пополните счет, чтобы продолжить.", "403": "Без авторизации. Ваш ключ API действителен, но провайдер отказался выполнить этот запрос.", "429": "Слишком много запросов. Провайдер ограничивает частоту ваших запросов. Пожалуйста, подождите немного перед следующим вызовом API.", "500": "Ошибка сервера провайдера. На стороне провайдера что-то пошло не так, с вашим запросом все в порядке.", + "docs": "Документация", + "goToSettings": "Настройки", "unknown": "Неизвестная ошибка API. Пожалуйста, свяжитесь с поддержкой Roo Code.", "connection": "Ошибка подключения. Убедитесь, что у вас есть рабочее подключение к Интернету.", "claudeCodeNotAuthenticated": "Вам необходимо войти в систему, чтобы использовать Claude Code. Перейдите в Настройки и нажмите «Войти в Claude Code» для аутентификации." @@ -254,7 +255,8 @@ "completionContent": "Подзадача завершена", "resultContent": "Результаты подзадачи", "defaultResult": "Пожалуйста, переходите к следующей задаче.", - "completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу." + "completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу.", + "goToSubtask": "Просмотреть задачу" }, "questions": { "hasQuestion": "У Roo есть вопрос" diff --git a/webview-ui/src/i18n/locales/ru/history.json b/webview-ui/src/i18n/locales/ru/history.json index 099f8d918f8..7852362348b 100644 --- a/webview-ui/src/i18n/locales/ru/history.json +++ b/webview-ui/src/i18n/locales/ru/history.json @@ -41,5 +41,11 @@ "mostTokens": "Больше всего токенов", "mostRelevant": "Наиболее релевантные" }, - "viewAllHistory": "Посмотреть все" + "viewAllHistory": "Посмотреть все", + "subtasks_one": "{{count}} подзадача", + "subtasks_other": "{{count}} подзадач", + "subtaskTag": "Подзадача", + "deleteWithSubtasks": "Это также удалит {{count}} подзадачу(и). Вы уверены?", + "expandSubtasks": "Развернуть подзадачи", + "collapseSubtasks": "Свернуть подзадачи" } diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index f88ed480c0b..1aef8ee5dbb 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Görevi Roo Code Cloud'da aç", "openInCloudIntro": "Roo'yu her yerden izlemeye veya etkileşime devam et. Açmak için tara, tıkla veya kopyala.", "openApiHistory": "API Geçmişini Aç", - "openUiHistory": "UI Geçmişini Aç" + "openUiHistory": "UI Geçmişini Aç", + "backToParentTask": "Üst görev" }, "unpin": "Sabitlemeyi iptal et", "pin": "Sabitle", @@ -146,14 +147,14 @@ "rateLimitWait": "Hız sınırlaması", "errorTitle": "Sağlayıcı Hatası {{code}}", "errorMessage": { - "docs": "Belgeler", - "goToSettings": "Ayarlar", "400": "Sağlayıcı isteği bu şekilde işleyemedi. Görevi durdur ve farklı bir yaklaşım dene.", "401": "Sağlayıcı ile kimlik doğrulaması yapılamadı. Lütfen API anahtarı yapılandırmanızı kontrol edin.", "402": "Hesabınızda para/kredi bitti gibi görünüyor. Sağlayıcıya git ve devam etmek için daha fazla ekle.", "403": "Yetkisiz. API anahtarınız geçerli ama sağlayıcı bu isteği tamamlamayı reddetti.", "429": "Çok fazla istek. Sağlayıcı tarafından oran sınırlaması uygulanıyor. Lütfen sonraki API çağrısından önce biraz bekle.", "500": "Sağlayıcı sunucu hatası. Sağlayıcı tarafında bir sorun var, isteğinizde sorun yok.", + "docs": "Belgeler", + "goToSettings": "Ayarlar", "unknown": "Bilinmeyen API hatası. Lütfen Roo Code desteğiyle iletişime geç.", "connection": "Bağlantı hatası. Çalışan bir internet bağlantınız olduğundan emin olun.", "claudeCodeNotAuthenticated": "Claude Code'u kullanmak için oturum açmanız gerekiyor. Ayarlar'a gidin ve kimlik doğrulaması yapmak için \"Claude Code'da Oturum Aç\" seçeneğine tıklayın." @@ -259,7 +260,8 @@ "completionContent": "Alt Görev Tamamlandı", "resultContent": "Alt Görev Sonuçları", "defaultResult": "Lütfen sonraki göreve devam edin.", - "completionInstructions": "Alt görev tamamlandı! Sonuçları inceleyebilir ve düzeltmeler veya sonraki adımlar önerebilirsiniz. Her şey iyi görünüyorsa, sonucu üst göreve döndürmek için onaylayın." + "completionInstructions": "Alt görev tamamlandı! Sonuçları inceleyebilir ve düzeltmeler veya sonraki adımlar önerebilirsiniz. Her şey iyi görünüyorsa, sonucu üst göreve döndürmek için onaylayın.", + "goToSubtask": "Görevi görüntüle" }, "questions": { "hasQuestion": "Roo'nun bir sorusu var" diff --git a/webview-ui/src/i18n/locales/tr/history.json b/webview-ui/src/i18n/locales/tr/history.json index 60ff183113d..fb7b6c68320 100644 --- a/webview-ui/src/i18n/locales/tr/history.json +++ b/webview-ui/src/i18n/locales/tr/history.json @@ -41,5 +41,11 @@ "mostTokens": "En Çok Token", "mostRelevant": "En İlgili" }, - "viewAllHistory": "Tümünü görüntüle" + "viewAllHistory": "Tümünü görüntüle", + "subtasks_one": "{{count}} alt görev", + "subtasks_other": "{{count}} alt görev", + "subtaskTag": "Alt görev", + "deleteWithSubtasks": "Bu, {{count}} alt görev(i) de silecektir. Emin misiniz?", + "expandSubtasks": "Alt görevleri genişlet", + "collapseSubtasks": "Alt görevleri daralt" } diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index c5724152f88..abcbb906d2b 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -28,7 +28,8 @@ "openInCloud": "Mở tác vụ trong Roo Code Cloud", "openInCloudIntro": "Tiếp tục theo dõi hoặc tương tác với Roo từ bất cứ đâu. Quét, nhấp hoặc sao chép để mở.", "openApiHistory": "Mở lịch sử API", - "openUiHistory": "Mở lịch sử UI" + "openUiHistory": "Mở lịch sử UI", + "backToParentTask": "Nhiệm vụ cha" }, "unpin": "Bỏ ghim khỏi đầu", "pin": "Ghim lên đầu", @@ -146,14 +147,14 @@ "rateLimitWait": "Giới hạn tốc độ", "errorTitle": "Lỗi nhà cung cấp {{code}}", "errorMessage": { - "docs": "Tài liệu", - "goToSettings": "Cài đặt", "400": "Nhà cung cấp không thể xử lý yêu cầu theo cách này. Hãy dừng nhiệm vụ và thử một cách tiếp cận khác.", "401": "Không thể xác thực với nhà cung cấp. Vui lòng kiểm tra cấu hình khóa API của bạn.", "402": "Có vẻ như bạn đã hết tiền/tín dụng trong tài khoản. Hãy truy cập nhà cung cấp và thêm tiền để tiếp tục.", "403": "Truy cập bị từ chối. Khóa API của bạn hợp lệ, nhưng nhà cung cấp từ chối hoàn thành yêu cầu này.", "429": "Quá nhiều yêu cầu. Nhà cung cấp đang giới hạn tốc độ yêu cầu của bạn. Vui lòng chờ một chút trước khi gọi API tiếp theo.", "500": "Lỗi máy chủ của nhà cung cấp. Có sự cố ở phía nhà cung cấp, không có gì sai với yêu cầu của bạn.", + "docs": "Tài liệu", + "goToSettings": "Cài đặt", "unknown": "Lỗi API không xác định. Vui lòng liên hệ hỗ trợ Roo Code.", "connection": "Lỗi kết nối. Đảm bảo rằng bạn có kết nối Internet hoạt động.", "claudeCodeNotAuthenticated": "Bạn cần đăng nhập để sử dụng Claude Code. Vào Cài đặt và nhấp vào \"Đăng nhập vào Claude Code\" để xác thực." @@ -259,7 +260,8 @@ "completionContent": "Nhiệm vụ phụ đã hoàn thành", "resultContent": "Kết quả nhiệm vụ phụ", "defaultResult": "Vui lòng tiếp tục với nhiệm vụ tiếp theo.", - "completionInstructions": "Nhiệm vụ phụ đã hoàn thành! Bạn có thể xem lại kết quả và đề xuất các sửa đổi hoặc bước tiếp theo. Nếu mọi thứ có vẻ tốt, hãy xác nhận để trả kết quả về nhiệm vụ chính." + "completionInstructions": "Nhiệm vụ phụ đã hoàn thành! Bạn có thể xem lại kết quả và đề xuất các sửa đổi hoặc bước tiếp theo. Nếu mọi thứ có vẻ tốt, hãy xác nhận để trả kết quả về nhiệm vụ chính.", + "goToSubtask": "Xem nhiệm vụ" }, "questions": { "hasQuestion": "Roo có một câu hỏi" diff --git a/webview-ui/src/i18n/locales/vi/history.json b/webview-ui/src/i18n/locales/vi/history.json index cac72720334..779953e5406 100644 --- a/webview-ui/src/i18n/locales/vi/history.json +++ b/webview-ui/src/i18n/locales/vi/history.json @@ -41,5 +41,11 @@ "mostTokens": "Nhiều token nhất", "mostRelevant": "Liên quan nhất" }, - "viewAllHistory": "Xem tất cả" + "viewAllHistory": "Xem tất cả", + "subtasks_one": "{{count}} tác vụ con", + "subtasks_other": "{{count}} tác vụ con", + "subtaskTag": "Tác vụ con", + "deleteWithSubtasks": "Điều này cũng sẽ xóa {{count}} tác vụ con. Bạn có chắc không?", + "expandSubtasks": "Mở rộng tác vụ con", + "collapseSubtasks": "Thu gọn tác vụ con" } diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index e13124379e7..bb9c3e3d135 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -28,7 +28,8 @@ "openInCloud": "在 Roo Code Cloud 中打开任务", "openInCloudIntro": "从任何地方继续监控或与 Roo 交互。扫描、点击或复制以打开。", "openApiHistory": "打开 API 历史", - "openUiHistory": "打开 UI 历史" + "openUiHistory": "打开 UI 历史", + "backToParentTask": "父任务" }, "unpin": "取消置顶", "pin": "置顶", @@ -146,14 +147,14 @@ "rateLimitWait": "请求频率限制", "errorTitle": "提供商错误 {{code}}", "errorMessage": { - "docs": "文档", - "goToSettings": "设置", "400": "提供商无法按此方式处理请求。请停止任务并尝试不同方法。", "401": "无法向提供商进行身份验证。请检查您的 API 密钥配置。", "402": "您的账户余额/积分似乎已用尽。请前往提供商处充值以继续。", "403": "无权限。您的 API 密钥有效,但提供商拒绝完成此请求。", "429": "请求过于频繁。提供商已对您的请求进行速率限制。请在下一次 API 调用前稍候。", "500": "提供商服务器错误。提供商端出现问题,您的请求无问题。", + "docs": "文档", + "goToSettings": "设置", "unknown": "未知 API 错误。请联系 Roo Code 支持。", "connection": "连接错误。确保您有可用的互联网连接。", "claudeCodeNotAuthenticated": "你需要登录才能使用 Claude Code。前往设置并点击「登录到 Claude Code」进行身份验证。" @@ -259,7 +260,8 @@ "completionContent": "子任务已完成", "resultContent": "子任务结果", "defaultResult": "请继续下一个任务。", - "completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。" + "completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。", + "goToSubtask": "查看任务" }, "questions": { "hasQuestion": "Roo有一个问题" diff --git a/webview-ui/src/i18n/locales/zh-CN/history.json b/webview-ui/src/i18n/locales/zh-CN/history.json index b88eef3636e..20a73240ea9 100644 --- a/webview-ui/src/i18n/locales/zh-CN/history.json +++ b/webview-ui/src/i18n/locales/zh-CN/history.json @@ -41,5 +41,11 @@ "mostTokens": "最多 Token", "mostRelevant": "最相关" }, - "viewAllHistory": "查看全部" + "viewAllHistory": "查看全部", + "subtasks_one": "{{count}} 子任务", + "subtasks_other": "{{count}} 子任务", + "subtaskTag": "子任务", + "deleteWithSubtasks": "这也将删除 {{count}} 个子任务。您确定吗?", + "expandSubtasks": "展开子任务", + "collapseSubtasks": "收起子任务" } diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 5d20352c573..805f92bad64 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -28,7 +28,8 @@ "openInCloud": "在 Roo Code Cloud 中開啟工作", "openInCloudIntro": "從任何地方繼續監控或與 Roo 互動。掃描、點擊或複製以開啟。", "openApiHistory": "開啟 API 歷史紀錄", - "openUiHistory": "開啟 UI 歷史紀錄" + "openUiHistory": "開啟 UI 歷史紀錄", + "backToParentTask": "上層工作" }, "unpin": "取消釘選", "pin": "釘選", @@ -152,14 +153,14 @@ "rateLimitWait": "速率限制", "errorTitle": "提供商錯誤 {{code}}", "errorMessage": { - "docs": "文件", - "goToSettings": "設定", "400": "提供商無法按照此方式處理請求。請停止工作並嘗試其他方法。", "401": "無法向提供商進行身份驗證。請檢查您的 API 金鑰設定。", "402": "您的帳戶資金/額度似乎已用盡。請前往提供商增加額度以繼續。", "403": "無權存取。您的 API 金鑰有效,但提供商拒絕完成此請求。", "429": "請求次數過多。提供商已對您的請求進行速率限制。請在下一次 API 呼叫前稍候。", "500": "提供商伺服器錯誤。提供商端發生問題,您的請求沒有問題。", + "docs": "文件", + "goToSettings": "設定", "unknown": "未知 API 錯誤。請聯絡 Roo Code 支援。", "connection": "連線錯誤。請確保您有可用的網際網路連線。", "claudeCodeNotAuthenticated": "你需要登入才能使用 Claude Code。前往設定並點擊「登入 Claude Code」以進行驗證。" @@ -287,7 +288,8 @@ "completionContent": "子工作已完成", "resultContent": "子工作結果", "defaultResult": "請繼續下一個工作。", - "completionInstructions": "子工作已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。" + "completionInstructions": "子工作已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。", + "goToSubtask": "查看工作" }, "questions": { "hasQuestion": "Roo 有一個問題" diff --git a/webview-ui/src/i18n/locales/zh-TW/history.json b/webview-ui/src/i18n/locales/zh-TW/history.json index f53bb6aec25..0a1ee01ae54 100644 --- a/webview-ui/src/i18n/locales/zh-TW/history.json +++ b/webview-ui/src/i18n/locales/zh-TW/history.json @@ -41,5 +41,11 @@ "mostTokens": "最多 Token", "mostRelevant": "最相關" }, - "viewAllHistory": "檢視全部" + "viewAllHistory": "檢視全部", + "subtasks_one": "{{count}} 子工作", + "subtasks_other": "{{count}} 子工作", + "subtaskTag": "子工作", + "deleteWithSubtasks": "這也將刪除 {{count}} 個子工作。您確定嗎?", + "expandSubtasks": "展開子工作", + "collapseSubtasks": "收起子工作" } From 54f5afc598506c0a091fb04c0c838a7232d481d6 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 13:35:08 +0000 Subject: [PATCH 06/11] Table --- src/i18n/locales/en/common.json | 4 +- src/i18n/locales/pt-BR/common.json | 4 +- webview-ui/src/components/chat/TaskHeader.tsx | 88 ++++++++++--------- webview-ui/src/components/ui/index.ts | 1 + webview-ui/src/components/ui/table.tsx | 25 ++++++ webview-ui/src/i18n/locales/ca/chat.json | 5 +- webview-ui/src/i18n/locales/de/chat.json | 5 +- webview-ui/src/i18n/locales/en/chat.json | 5 +- webview-ui/src/i18n/locales/es/chat.json | 5 +- webview-ui/src/i18n/locales/fr/chat.json | 5 +- webview-ui/src/i18n/locales/hi/chat.json | 5 +- webview-ui/src/i18n/locales/id/chat.json | 5 +- webview-ui/src/i18n/locales/it/chat.json | 5 +- webview-ui/src/i18n/locales/ja/chat.json | 5 +- webview-ui/src/i18n/locales/ko/chat.json | 5 +- webview-ui/src/i18n/locales/nl/chat.json | 5 +- webview-ui/src/i18n/locales/pl/chat.json | 5 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 5 +- webview-ui/src/i18n/locales/ru/chat.json | 5 +- webview-ui/src/i18n/locales/tr/chat.json | 5 +- webview-ui/src/i18n/locales/vi/chat.json | 5 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 5 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 5 +- 23 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 webview-ui/src/components/ui/table.tsx diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 90c409feb76..79043248baf 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -5,8 +5,8 @@ }, "number_format": { "thousand_suffix": "k", - "million_suffix": "m", - "billion_suffix": "b" + "million_suffix": "M", + "billion_suffix": "B" }, "welcome": "Welcome, {{name}}! You have {{count}} notifications.", "items": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index f41a379acbc..c6af5f8f265 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -9,8 +9,8 @@ }, "number_format": { "thousand_suffix": "k", - "million_suffix": "m", - "billion_suffix": "b" + "million_suffix": "M", + "billion_suffix": "B" }, "welcome": "Bem-vindo(a), {{name}}! Você tem {{count}} notificações.", "items": { diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index d4ff773d26c..c74f44b7428 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -21,7 +21,7 @@ import { findLastIndex } from "@roo/array" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" -import { StandardTooltip, Button } from "@src/components/ui" +import { StandardTooltip, Button, Table, TableBody, TableRow, TableCell } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { vscode } from "@src/utils/vscode" @@ -225,47 +225,53 @@ const TaskHeader = ({ onClick={(e) => e.stopPropagation()}>
-
- {t("chat:tokenProgress.tokensUsed", { - used: formatLargeNumber(contextTokens || 0), - total: formatLargeNumber(contextWindow), - })} -
- {(() => { - const maxTokens = model - ? getModelMaxOutputTokens({ - modelId, - model, - settings: apiConfiguration, - }) - : 0 - const reservedForOutput = maxTokens || 0 - const availableSpace = - contextWindow - (contextTokens || 0) - reservedForOutput + content={(() => { + const maxTokens = model + ? getModelMaxOutputTokens({ + modelId, + model, + settings: apiConfiguration, + }) + : 0 + const reservedForOutput = maxTokens || 0 + const availableSpace = contextWindow - (contextTokens || 0) - reservedForOutput - return ( - <> - {reservedForOutput > 0 && ( -
- {t("chat:tokenProgress.reservedForResponse", { - amount: formatLargeNumber(reservedForOutput), - })} -
- )} - {availableSpace > 0 && ( -
- {t("chat:tokenProgress.availableSpace", { - amount: formatLargeNumber(availableSpace), - })} -
- )} - - ) - })()} -
- } + return ( + + + + + {t("chat:tokenProgress.tokensUsedLabel")} + + + {formatLargeNumber(contextTokens || 0)} /{" "} + {formatLargeNumber(contextWindow)} + + + {reservedForOutput > 0 && ( + + + {t("chat:tokenProgress.reservedForResponseLabel")} + + + {formatLargeNumber(reservedForOutput)} + + + )} + {availableSpace > 0 && ( + + + {t("chat:tokenProgress.availableSpaceLabel")} + + + {formatLargeNumber(availableSpace)} + + + )} + +
+ ) + })()} side="top" sideOffset={8}> diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index ee28b964c54..3f2aacfd931 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -15,6 +15,7 @@ export * from "./separator" export * from "./slider" export * from "./select-dropdown" export * from "./select" +export * from "./table" export * from "./textarea" export * from "./tooltip" export * from "./standard-tooltip" diff --git a/webview-ui/src/components/ui/table.tsx b/webview-ui/src/components/ui/table.tsx new file mode 100644 index 00000000000..d01dbd6f4a4 --- /dev/null +++ b/webview-ui/src/components/ui/table.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => , +) +Table.displayName = "Table" + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => , +) +TableBody.displayName = "TableBody" + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => , +) +TableRow.displayName = "TableRow" + +const TableCell = React.forwardRef>( + ({ className, ...props }, ref) =>
, +) +TableCell.displayName = "TableCell" + +export { Table, TableBody, TableRow, TableCell } diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 1ccfcfd6380..0ef03aeebc9 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Espai disponible: {{amount}} tokens", "tokensUsed": "Tokens utilitzats: {{used}} de {{total}}", - "reservedForResponse": "Reservat per a resposta del model: {{amount}} tokens" + "reservedForResponse": "Reservat per a resposta del model: {{amount}} tokens", + "tokensUsedLabel": "Tokens utilitzats", + "reservedForResponseLabel": "Reservat per a resposta", + "availableSpaceLabel": "Espai disponible" }, "retry": { "title": "Tornar a intentar", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index c7a656d21e4..ceebd2e5846 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Verfügbarer Speicher: {{amount}} Tokens", "tokensUsed": "Verwendete Tokens: {{used}} von {{total}}", - "reservedForResponse": "Reserviert für Modellantwort: {{amount}} Tokens" + "reservedForResponse": "Reserviert für Modellantwort: {{amount}} Tokens", + "tokensUsedLabel": "Verwendete Tokens", + "reservedForResponseLabel": "Für Antwort reserviert", + "availableSpaceLabel": "Verfügbarer Speicher" }, "retry": { "title": "Wiederholen", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index a99c50e0363..10c2a0cdf81 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -52,7 +52,10 @@ "tokenProgress": { "availableSpace": "Available space: {{amount}} tokens", "tokensUsed": "Tokens used: {{used}} of {{total}}", - "reservedForResponse": "Reserved for model response: {{amount}} tokens" + "reservedForResponse": "Reserved for model response: {{amount}} tokens", + "tokensUsedLabel": "Tokens used", + "reservedForResponseLabel": "Reserved for response", + "availableSpaceLabel": "Available space" }, "reject": { "title": "Deny", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 295d5f94822..05f92eeb918 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -52,7 +52,10 @@ "tokenProgress": { "availableSpace": "Espacio disponible: {{amount}} tokens", "tokensUsed": "Tokens utilizados: {{used}} de {{total}}", - "reservedForResponse": "Reservado para respuesta del modelo: {{amount}} tokens" + "reservedForResponse": "Reservado para respuesta del modelo: {{amount}} tokens", + "tokensUsedLabel": "Tokens utilizados", + "reservedForResponseLabel": "Reservado para respuesta", + "availableSpaceLabel": "Espacio disponible" }, "reject": { "title": "Rechazar", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 16d6db318c5..ad220294a51 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Espace disponible : {{amount}} tokens", "tokensUsed": "Tokens utilisés : {{used}} sur {{total}}", - "reservedForResponse": "Réservé pour la réponse du modèle : {{amount}} tokens" + "reservedForResponse": "Réservé pour la réponse du modèle : {{amount}} tokens", + "tokensUsedLabel": "Tokens utilisés", + "reservedForResponseLabel": "Réservé pour la réponse", + "availableSpaceLabel": "Espace disponible" }, "retry": { "title": "Réessayer", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index dfdbf38b821..31af7862883 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "उपलब्ध स्थान: {{amount}} tokens", "tokensUsed": "प्रयुक्त tokens: {{used}} / {{total}}", - "reservedForResponse": "मॉडल प्रतिक्रिया के लिए आरक्षित: {{amount}} tokens" + "reservedForResponse": "मॉडल प्रतिक्रिया के लिए आरक्षित: {{amount}} tokens", + "tokensUsedLabel": "प्रयुक्त tokens", + "reservedForResponseLabel": "प्रतिक्रिया के लिए आरक्षित", + "availableSpaceLabel": "उपलब्ध स्थान" }, "retry": { "title": "पुनः प्रयास करें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index d3ff27926db..04c248fa153 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -58,7 +58,10 @@ "tokenProgress": { "availableSpace": "Ruang tersedia: {{amount}} token", "tokensUsed": "Token digunakan: {{used}} dari {{total}}", - "reservedForResponse": "Dicadangkan untuk respons model: {{amount}} token" + "reservedForResponse": "Dicadangkan untuk respons model: {{amount}} token", + "tokensUsedLabel": "Token digunakan", + "reservedForResponseLabel": "Dicadangkan untuk respons", + "availableSpaceLabel": "Ruang tersedia" }, "reject": { "title": "Tolak", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index d7f73055371..82f0901bdf9 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Spazio disponibile: {{amount}} tokens", "tokensUsed": "Tokens utilizzati: {{used}} di {{total}}", - "reservedForResponse": "Riservato per risposta del modello: {{amount}} tokens" + "reservedForResponse": "Riservato per risposta del modello: {{amount}} tokens", + "tokensUsedLabel": "Tokens utilizzati", + "reservedForResponseLabel": "Riservato per risposta", + "availableSpaceLabel": "Spazio disponibile" }, "retry": { "title": "Riprova", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index fd3dc53f859..329501cc7c7 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "利用可能な空き容量: {{amount}} トークン", "tokensUsed": "使用トークン: {{used}} / {{total}}", - "reservedForResponse": "モデル応答用に予約: {{amount}} トークン" + "reservedForResponse": "モデル応答用に予約: {{amount}} トークン", + "tokensUsedLabel": "使用トークン", + "reservedForResponseLabel": "応答用に予約", + "availableSpaceLabel": "利用可能な空き容量" }, "retry": { "title": "再試行", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 3e7f366cb67..ec6be8ca51c 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "사용 가능한 공간: {{amount}} 토큰", "tokensUsed": "사용된 토큰: {{used}} / {{total}}", - "reservedForResponse": "모델 응답용 예약: {{amount}} 토큰" + "reservedForResponse": "모델 응답용 예약: {{amount}} 토큰", + "tokensUsedLabel": "사용된 토큰", + "reservedForResponseLabel": "응답용으로 예약됨", + "availableSpaceLabel": "사용 가능한 공간" }, "retry": { "title": "다시 시도", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 0fede142c03..20862325d90 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -52,7 +52,10 @@ "tokenProgress": { "availableSpace": "Beschikbare ruimte: {{amount}} tokens", "tokensUsed": "Gebruikte tokens: {{used}} van {{total}}", - "reservedForResponse": "Gereserveerd voor modelantwoord: {{amount}} tokens" + "reservedForResponse": "Gereserveerd voor modelantwoord: {{amount}} tokens", + "tokensUsedLabel": "Gebruikte tokens", + "reservedForResponseLabel": "Gereserveerd voor antwoord", + "availableSpaceLabel": "Beschikbare ruimte" }, "reject": { "title": "Weigeren", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 11fb74a3886..e1b0db36114 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Dostępne miejsce: {{amount}} tokenów", "tokensUsed": "Wykorzystane tokeny: {{used}} z {{total}}", - "reservedForResponse": "Zarezerwowane dla odpowiedzi modelu: {{amount}} tokenów" + "reservedForResponse": "Zarezerwowane dla odpowiedzi modelu: {{amount}} tokenów", + "tokensUsedLabel": "Wykorzystane tokeny", + "reservedForResponseLabel": "Zarezerwowane dla odpowiedzi", + "availableSpaceLabel": "Dostępne miejsce" }, "retry": { "title": "Ponów", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 60e0573b624..5f786481040 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Espaço disponível: {{amount}} tokens", "tokensUsed": "Tokens usados: {{used}} de {{total}}", - "reservedForResponse": "Reservado para resposta do modelo: {{amount}} tokens" + "reservedForResponse": "Reservado para resposta do modelo: {{amount}} tokens", + "tokensUsedLabel": "Tokens usados", + "reservedForResponseLabel": "Reservado para resposta", + "availableSpaceLabel": "Espaço disponível" }, "retry": { "title": "Tentar novamente", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 0c5365812b8..1aa714e6a77 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -52,7 +52,10 @@ "tokenProgress": { "availableSpace": "Доступно места: {{amount}} токенов", "tokensUsed": "Использовано токенов: {{used}} из {{total}}", - "reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов" + "reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов", + "tokensUsedLabel": "Использовано токенов", + "reservedForResponseLabel": "Зарезервировано для ответа", + "availableSpaceLabel": "Доступно места" }, "reject": { "title": "Отклонить", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 1aef8ee5dbb..fcd8f3d1794 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Kullanılabilir alan: {{amount}} token", "tokensUsed": "Kullanılan tokenlar: {{used}} / {{total}}", - "reservedForResponse": "Model yanıtı için ayrılan: {{amount}} token" + "reservedForResponse": "Model yanıtı için ayrılan: {{amount}} token", + "tokensUsedLabel": "Kullanılan tokenlar", + "reservedForResponseLabel": "Yanıt için ayrılan", + "availableSpaceLabel": "Kullanılabilir alan" }, "retry": { "title": "Yeniden Dene", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index abcbb906d2b..aa97f440674 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "Không gian khả dụng: {{amount}} tokens", "tokensUsed": "Tokens đã sử dụng: {{used}} trong {{total}}", - "reservedForResponse": "Dành riêng cho phản hồi mô hình: {{amount}} tokens" + "reservedForResponse": "Dành riêng cho phản hồi mô hình: {{amount}} tokens", + "tokensUsedLabel": "Tokens đã sử dụng", + "reservedForResponseLabel": "Dành riêng cho phản hồi", + "availableSpaceLabel": "Không gian khả dụng" }, "retry": { "title": "Thử lại", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index bb9c3e3d135..64ed215374b 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -36,7 +36,10 @@ "tokenProgress": { "availableSpace": "可用: {{amount}}", "tokensUsed": "已使用: {{used}} / {{total}}", - "reservedForResponse": "已保留: {{amount}}" + "reservedForResponse": "已保留: {{amount}}", + "tokensUsedLabel": "已使用 Token", + "reservedForResponseLabel": "为回复保留", + "availableSpaceLabel": "可用空间" }, "retry": { "title": "重试", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 805f92bad64..ea75c097087 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -52,7 +52,10 @@ "tokenProgress": { "availableSpace": "可用空間:{{amount}} Token", "tokensUsed": "已使用 Token: {{used}} / {{total}}", - "reservedForResponse": "模型回應保留:{{amount}} Token" + "reservedForResponse": "模型回應保留:{{amount}} Token", + "tokensUsedLabel": "已使用 Token", + "reservedForResponseLabel": "為回覆保留", + "availableSpaceLabel": "可用空間" }, "reject": { "title": "拒絕", From ff93d3ad3a01e838ff1e44f82c993b65b47b16a4 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 13:56:16 +0000 Subject: [PATCH 07/11] Lighter visuals --- .../src/components/history/DeleteButton.tsx | 4 +- .../history/SubtaskCollapsibleRow.tsx | 6 +- .../src/components/history/SubtaskRow.tsx | 8 ++- .../src/components/history/TaskGroupItem.tsx | 9 ++- .../src/components/history/TaskItem.tsx | 59 ++++++++++--------- .../src/components/history/TaskItemFooter.tsx | 2 +- .../history/__tests__/TaskItem.spec.tsx | 2 +- 7 files changed, 51 insertions(+), 39 deletions(-) diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx index bd91803627a..8378887c701 100644 --- a/webview-ui/src/components/history/DeleteButton.tsx +++ b/webview-ui/src/components/history/DeleteButton.tsx @@ -31,8 +31,8 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => { size="icon" data-testid="delete-task-button" onClick={handleDeleteClick} - className="opacity-70"> - + className="group-hover:opacity-100 opacity-50 transition-opacity"> + ) diff --git a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx index 1dc52945d62..6e4c74c952f 100644 --- a/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx +++ b/webview-ui/src/components/history/SubtaskCollapsibleRow.tsx @@ -29,9 +29,9 @@ const SubtaskCollapsibleRow = ({ count, isExpanded, onToggle, className }: Subta
{
{ } }}> - {item.task} + {item.task} +
) } diff --git a/webview-ui/src/components/history/TaskGroupItem.tsx b/webview-ui/src/components/history/TaskGroupItem.tsx index e14b13db5e9..6bf2e1a9572 100644 --- a/webview-ui/src/components/history/TaskGroupItem.tsx +++ b/webview-ui/src/components/history/TaskGroupItem.tsx @@ -69,8 +69,13 @@ const TaskGroupItem = ({ )} {/* Expanded subtasks */} - {hasSubtasks && isExpanded && ( -
+ {hasSubtasks && ( +
{subtasks.map((subtask) => ( ))} diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index 223d70e8453..5592f555c88 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -1,4 +1,5 @@ import { memo } from "react" +import { ArrowRight, Folder } from "lucide-react" import type { DisplayHistoryItem } from "./types" import { vscode } from "@/utils/vscode" @@ -47,7 +48,7 @@ const TaskItem = ({ data-testid={`task-item-${item.id}`} className={cn( "cursor-pointer group relative overflow-hidden", - "hover:bg-vscode-editor-foreground/10 transition-colors", + "text-vscode-foreground/80 hover:text-vscode-foreground transition-colors", hasSubtasks ? "rounded-t-xl" : "rounded-xl", className, )} @@ -69,27 +70,38 @@ const TaskItem = ({ )}
-
- - {item.highlight ? undefined : item.task} - +
+
+ + {item.highlight ? undefined : item.task} + +
+ {/* Arrow icon that appears on hover */} +
+ {showWorkspace && item.workspace && ( +
+ + {item.workspace} +
+ )} + - - {showWorkspace && item.workspace && ( -
- - {item.workspace} -
- )}
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index c9705555981..d0dc367e646 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -54,7 +54,7 @@ const TaskItemFooter: React.FC = ({ {/* Action Buttons for non-compact view */} {!isSelectionMode && ( -
+
{variant === "full" && } {onDelete && } diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 1bcc983c6ee..df8fc742d3c 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -107,6 +107,6 @@ describe("TaskItem", () => { ) const taskItem = screen.getByTestId("task-item-1") - expect(taskItem).toHaveClass("hover:bg-vscode-editor-foreground/10") + expect(taskItem).toHaveClass("hover:text-vscode-foreground") }) }) From 0221353136b008a669233b70b4f6388c0b50c1c5 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 13:59:05 +0000 Subject: [PATCH 08/11] bug --- .../src/components/history/TaskItem.tsx | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index 5592f555c88..eba5e59ac94 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -71,26 +71,33 @@ const TaskItem = ({
-
- - {item.highlight ? undefined : item.task} - -
+ {item.highlight ? ( +
+ ) : ( +
+ + {item.task} + +
+ )} {/* Arrow icon that appears on hover */}
From c94364794545631375adcbb17c93973dbe6e084b Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Wed, 21 Jan 2026 14:04:06 +0000 Subject: [PATCH 09/11] fix: Align tests with implementation behavior --- .../chat/__tests__/ChatRow.subtask-links.spec.tsx | 13 ++++++++----- .../history/__tests__/TaskGroupItem.spec.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx index a1e897808f0..3a1971ec68f 100644 --- a/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatRow.subtask-links.spec.tsx @@ -79,7 +79,7 @@ describe("ChatRow - subtask links", () => { }) describe("newTask tool", () => { - it("should display 'Go to subtask' link when currentTaskItem has delegatedToId", () => { + it("should display 'Go to subtask' link when currentTaskItem has childIds", () => { const message = { ts: Date.now(), type: "ask" as const, @@ -91,8 +91,9 @@ describe("ChatRow - subtask links", () => { }), } + // childIds maps by index to newTask messages - first newTask gets childIds[0] renderChatRow(message, { - delegatedToId: "child-task-123", + childIds: ["child-task-123"], }) const goToSubtaskButton = screen.getByText("Go to subtask") @@ -106,7 +107,7 @@ describe("ChatRow - subtask links", () => { }) }) - it("should display 'Go to subtask' link when currentTaskItem has childIds", () => { + it("should display 'Go to subtask' link using index-matched childId for multiple newTasks", () => { const message = { ts: Date.now(), type: "ask" as const, @@ -118,6 +119,8 @@ describe("ChatRow - subtask links", () => { }), } + // The implementation maps newTask messages to childIds by index + // Since this is the first (and only) newTask message, it gets childIds[0] renderChatRow(message, { childIds: ["first-child", "second-child"], }) @@ -127,10 +130,10 @@ describe("ChatRow - subtask links", () => { fireEvent.click(goToSubtaskButton) - // Should use the last child ID + // First newTask message maps to first childId expect(mockPostMessage).toHaveBeenCalledWith({ type: "showTaskWithId", - text: "second-child", + text: "first-child", }) }) diff --git a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx index d7c70b8293f..ff40963a87a 100644 --- a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx @@ -144,8 +144,10 @@ describe("TaskGroupItem", () => { render() - expect(screen.queryByTestId("subtask-list")).not.toBeInTheDocument() - expect(screen.queryByText("Subtask content")).not.toBeInTheDocument() + // The subtask-list element is present but collapsed via CSS (max-h-0) + const subtaskList = screen.queryByTestId("subtask-list") + expect(subtaskList).toBeInTheDocument() + expect(subtaskList).toHaveClass("max-h-0") }) }) From 5d4b258e0c61cd651ec329c16d653b0b9771195c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 21 Jan 2026 14:13:16 +0000 Subject: [PATCH 10/11] refactor: extract CircularProgress component from TaskHeader - Created reusable CircularProgress component for displaying percentage as a ring - Moved inline SVG calculation from TaskHeader.tsx to dedicated component - Added comprehensive tests for CircularProgress component (14 tests) - Component supports customizable size, strokeWidth, and className - Includes proper accessibility attributes (progressbar role, aria-valuenow) --- webview-ui/src/components/chat/TaskHeader.tsx | 29 +--- .../ui/__tests__/circular-progress.spec.tsx | 138 ++++++++++++++++++ .../src/components/ui/circular-progress.tsx | 78 ++++++++++ webview-ui/src/components/ui/index.ts | 1 + 4 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 webview-ui/src/components/ui/__tests__/circular-progress.spec.tsx create mode 100644 webview-ui/src/components/ui/circular-progress.tsx diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index c74f44b7428..3ced8740260 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -21,7 +21,7 @@ import { findLastIndex } from "@roo/array" import { formatLargeNumber } from "@src/utils/format" import { cn } from "@src/lib/utils" -import { StandardTooltip, Button, Table, TableBody, TableRow, TableCell } from "@src/components/ui" +import { StandardTooltip, Button, Table, TableBody, TableRow, TableCell, CircularProgress } from "@src/components/ui" import { useExtensionState } from "@src/context/ExtensionStateContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" import { vscode } from "@src/utils/vscode" @@ -277,34 +277,9 @@ const TaskHeader = ({ {(() => { const percentage = Math.round(((contextTokens || 0) / contextWindow) * 100) - const radius = 6 - const circumference = 2 * Math.PI * radius - const strokeDashoffset = circumference - (percentage / 100) * circumference return ( <> - - - - + {percentage}% ) diff --git a/webview-ui/src/components/ui/__tests__/circular-progress.spec.tsx b/webview-ui/src/components/ui/__tests__/circular-progress.spec.tsx new file mode 100644 index 00000000000..32e9247e448 --- /dev/null +++ b/webview-ui/src/components/ui/__tests__/circular-progress.spec.tsx @@ -0,0 +1,138 @@ +import { render, screen } from "@testing-library/react" +import { CircularProgress } from "../circular-progress" + +describe("CircularProgress", () => { + it("should render with default props", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toBeInTheDocument() + expect(svg).toHaveAttribute("aria-valuenow", "50") + expect(svg).toHaveAttribute("aria-valuemin", "0") + expect(svg).toHaveAttribute("aria-valuemax", "100") + }) + + it("should render with correct size", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toHaveAttribute("width", "24") + expect(svg).toHaveAttribute("height", "24") + }) + + it("should render with default size of 16", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toHaveAttribute("width", "16") + expect(svg).toHaveAttribute("height", "16") + }) + + it("should clamp percentage to 0 when negative", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toHaveAttribute("aria-valuenow", "0") + }) + + it("should clamp percentage to 100 when over 100", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toHaveAttribute("aria-valuenow", "100") + }) + + it("should apply custom className", () => { + render() + + const svg = screen.getByRole("progressbar") + expect(svg).toHaveClass("custom-class") + expect(svg).toHaveClass("shrink-0") + }) + + it("should render two circles (background and progress)", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + expect(circles).toHaveLength(2) + }) + + it("should have background circle with 0.2 opacity", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const backgroundCircle = circles[0] + expect(backgroundCircle).toHaveAttribute("opacity", "0.2") + }) + + it("should render progress circle with correct stroke-dasharray", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const progressCircle = circles[1] + + // With size=16 and strokeWidth=2, radius = (16-2)/2 = 7 + // circumference = 2 * PI * 7 ≈ 43.98 + const expectedCircumference = 2 * Math.PI * 7 + expect(progressCircle).toHaveAttribute("stroke-dasharray", expectedCircumference.toString()) + }) + + it("should render at 0% with full offset", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const progressCircle = circles[1] + + const radius = (16 - 2) / 2 + const circumference = 2 * Math.PI * radius + // At 0%, offset should equal circumference (no progress shown) + expect(progressCircle).toHaveAttribute("stroke-dashoffset", circumference.toString()) + }) + + it("should render at 100% with zero offset", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const progressCircle = circles[1] + + // At 100%, offset should be 0 (full circle shown) + expect(progressCircle).toHaveAttribute("stroke-dashoffset", "0") + }) + + it("should have correct transform on progress circle", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const progressCircle = circles[1] + + // Progress should start from top (rotate -90deg from center) + expect(progressCircle).toHaveAttribute("transform", "rotate(-90 8 8)") + }) + + it("should have round stroke linecap on progress circle", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + const progressCircle = circles[1] + + expect(progressCircle).toHaveAttribute("stroke-linecap", "round") + }) + + it("should apply custom stroke width", () => { + render() + + const svg = screen.getByRole("progressbar") + const circles = svg.querySelectorAll("circle") + + circles.forEach((circle) => { + expect(circle).toHaveAttribute("stroke-width", "4") + }) + }) +}) diff --git a/webview-ui/src/components/ui/circular-progress.tsx b/webview-ui/src/components/ui/circular-progress.tsx new file mode 100644 index 00000000000..40e8be31aa0 --- /dev/null +++ b/webview-ui/src/components/ui/circular-progress.tsx @@ -0,0 +1,78 @@ +import { memo } from "react" +import { cn } from "@/lib/utils" + +interface CircularProgressProps { + /** Progress percentage (0-100) */ + percentage: number + /** Size of the SVG in pixels (default: 16) */ + size?: number + /** Stroke width in pixels (default: 2) */ + strokeWidth?: number + /** Additional CSS classes */ + className?: string +} + +/** + * A circular progress indicator component that displays a percentage as a ring. + * The ring fills clockwise from the top based on the percentage value. + * + * @example + * ```tsx + * + * + * ``` + */ +export const CircularProgress = memo(function CircularProgress({ + percentage, + size = 16, + strokeWidth = 2, + className, +}: CircularProgressProps) { + // Clamp percentage between 0 and 100 + const clampedPercentage = Math.max(0, Math.min(100, percentage)) + + // Calculate the radius based on size and stroke width + // The radius needs to fit within the viewBox accounting for stroke width + const radius = (size - strokeWidth) / 2 + const center = size / 2 + + // Calculate the circumference and dash offset for the progress ring + const circumference = 2 * Math.PI * radius + const strokeDashoffset = circumference - (clampedPercentage / 100) * circumference + + return ( + + {/* Background circle */} + + {/* Progress circle */} + + + ) +}) diff --git a/webview-ui/src/components/ui/index.ts b/webview-ui/src/components/ui/index.ts index 3f2aacfd931..96a45530dc2 100644 --- a/webview-ui/src/components/ui/index.ts +++ b/webview-ui/src/components/ui/index.ts @@ -3,6 +3,7 @@ export * from "./autosize-textarea" export * from "./badge" export * from "./button" export * from "./checkbox" +export * from "./circular-progress" export * from "./collapsible" export * from "./command" export * from "./dialog" From d9c827fb0aa3c0552ea89c23a20cc83cc4ed306f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 21 Jan 2026 15:03:39 +0000 Subject: [PATCH 11/11] chore: update StandardTooltip default delay to 600ms As mentioned in the PR description, increased the tooltip delay to 600ms for less intrusive tooltips. The delay is still configurable via the delay prop for components that need a different value. --- webview-ui/src/components/ui/standard-tooltip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/ui/standard-tooltip.tsx b/webview-ui/src/components/ui/standard-tooltip.tsx index c7b819fdad2..0e5538a128b 100644 --- a/webview-ui/src/components/ui/standard-tooltip.tsx +++ b/webview-ui/src/components/ui/standard-tooltip.tsx @@ -2,7 +2,7 @@ import { ReactNode } from "react" import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip" -export const STANDARD_TOOLTIP_DELAY = 300 +export const STANDARD_TOOLTIP_DELAY = 600 interface StandardTooltipProps { /** The element(s) that trigger the tooltip */ @@ -26,7 +26,7 @@ interface StandardTooltipProps { } /** - * StandardTooltip component with a configurable delay (defaults to 300ms). + * StandardTooltip component with a configurable delay (defaults to 600ms). * This component wraps the Radix UI tooltip with a standardized delay duration. * * @example