ux: Improve subtask visibility and navigation in history and chat views (#10864)

* Taskheader

* Subtask messages

* View subtask

* subtasks in history items

* i18n

* Table

* Lighter visuals

* bug

* fix: Align tests with implementation behavior

* 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)

* 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.

---------

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Bruno Bergher 2026-01-24 12:21:25 +00:00 committed by GitHub
parent 3b703b8b53
commit 5848db66be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 2350 additions and 371 deletions

View file

@ -1747,43 +1747,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<void> => {
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 associated shadow repository or branch.
// TODO: Store `workspaceDir` in the `HistoryItem` object.
// 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 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 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)}`,
)
}
}
// 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)}`,
)
}
await this.postStateToWebview()
} catch (error) {
// If task is not found, just remove it from state
if (error instanceof Error && error.message === "Task not found") {

View file

@ -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": {

View file

@ -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": {

View file

@ -68,6 +68,9 @@ import {
TerminalSquare,
MessageCircle,
Repeat2,
Split,
ArrowRight,
Check,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
@ -176,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("")
@ -385,6 +389,7 @@ export const ChatRowContent = ({
display: "flex",
alignItems: "center",
gap: "10px",
cursor: "default",
marginBottom: "10px",
wordBreak: "break-word",
}
@ -815,10 +820,34 @@ 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<ClineSayTool>(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 (
<>
<div style={headerStyle}>
{toolIcon("tasklist")}
<Split className="size-4" />
<span style={{ fontWeight: "bold" }}>
<Trans
i18nKey="chat:subtasks.wantsToCreate"
@ -827,32 +856,19 @@ export const ChatRowContent = ({
/>
</span>
</div>
<div
style={{
marginTop: "4px",
backgroundColor: "var(--vscode-badge-background)",
border: "1px solid var(--vscode-badge-background)",
borderRadius: "4px 4px 0 0",
overflow: "hidden",
marginBottom: "2px",
}}>
<div
style={{
padding: "9px 10px 9px 14px",
backgroundColor: "var(--vscode-badge-background)",
borderBottom: "1px solid var(--vscode-editorGroup-border)",
fontWeight: "bold",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-badge-foreground)",
display: "flex",
alignItems: "center",
gap: "6px",
}}>
<span className="codicon codicon-arrow-right"></span>
{t("chat:subtasks.newTaskContent")}
</div>
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
<MarkdownBlock markdown={tool.content} />
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pb-1">
<MarkdownBlock markdown={tool.content} />
<div>
{childTaskId && !isFollowedBySubtaskResult && (
<button
className="cursor-pointer flex gap-1 items-center mt-2 text-vscode-descriptionForeground hover:text-vscode-descriptionForeground hover:underline font-normal"
onClick={() =>
vscode.postMessage({ type: "showTaskWithId", text: childTaskId })
}>
{t("chat:subtasks.goToSubtask")}
<ArrowRight className="size-3" />
</button>
)}
</div>
</div>
</>
@ -864,33 +880,8 @@ export const ChatRowContent = ({
{toolIcon("check-all")}
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.wantsToFinish")}</span>
</div>
<div
style={{
marginTop: "4px",
backgroundColor: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-badge-background)",
borderRadius: "4px",
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
padding: "9px 10px 9px 14px",
backgroundColor: "var(--vscode-badge-background)",
borderBottom: "1px solid var(--vscode-editorGroup-border)",
fontWeight: "bold",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-badge-foreground)",
display: "flex",
alignItems: "center",
gap: "6px",
}}>
<span className="codicon codicon-check"></span>
{t("chat:subtasks.completionContent")}
</div>
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
</div>
<div className="text-muted-foreground pl-6">
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
</div>
</>
)
@ -1019,40 +1010,25 @@ export const ChatRowContent = ({
/>
)
case "subtask_result":
// Get the child task ID that produced this result
const completedChildTaskId = currentTaskItem?.completedByChildId
return (
<div>
<div
style={{
marginTop: "0px",
backgroundColor: "var(--vscode-badge-background)",
border: "1px solid var(--vscode-badge-background)",
borderRadius: "0 0 4px 4px",
overflow: "hidden",
marginBottom: "8px",
}}>
<div
style={{
padding: "9px 10px 9px 14px",
backgroundColor: "var(--vscode-badge-background)",
borderBottom: "1px solid var(--vscode-editorGroup-border)",
fontWeight: "bold",
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-badge-foreground)",
display: "flex",
alignItems: "center",
gap: "6px",
}}>
<span className="codicon codicon-arrow-left"></span>
{t("chat:subtasks.resultContent")}
</div>
<div
style={{
padding: "12px 16px",
backgroundColor: "var(--vscode-editor-background)",
}}>
<MarkdownBlock markdown={message.text} />
</div>
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pt-2 pb-1 -mt-5">
<div style={headerStyle}>
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.resultContent")}</span>
<Check className="size-3" />
</div>
<MarkdownBlock markdown={message.text} />
{completedChildTaskId && (
<button
className="cursor-pointer flex gap-1 items-center mt-2 text-vscode-descriptionForeground hover:text-vscode-descriptionForeground hover:underline font-normal"
onClick={() =>
vscode.postMessage({ type: "showTaskWithId", text: completedChildTaskId })
}>
{t("chat:subtasks.goToSubtask")}
<ArrowRight className="size-3" />
</button>
)}
</div>
)
case "reasoning":

View file

@ -1482,6 +1482,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
aggregatedCostsMap.get(currentTaskItem.id)!.childrenCost > 0
)
}
parentTaskId={currentTaskItem?.parentTaskId}
costBreakdown={
currentTaskItem?.id && aggregatedCostsMap.has(currentTaskItem.id)
? getCostBreakdownIfNeeded(aggregatedCostsMap.get(currentTaskItem.id)!, {

View file

@ -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"
@ -22,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, CircularProgress } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
import { vscode } from "@src/utils/vscode"
@ -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 (
<div className="group pt-2 pb-0 px-3">
{isSubtask && (
<div className="mb-2" onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
onClick={handleBackToParent}
className="flex items-center gap-1.5 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground">
<ArrowLeft className="size-3" />
{t("chat:task.backToParentTask")}
</Button>
</div>
)}
{showLongRunningTaskMessage && !isTaskComplete && (
<DismissibleUpsell
upsellId="longRunningTask"
@ -177,11 +199,8 @@ const TaskHeader = ({
<div className="grow min-w-0">
{isTaskExpanded && <span className="font-bold">{t("chat:task.title")}</span>}
{!isTaskExpanded && (
<div className="flex items-center gap-2">
<SquarePen className="size-3 shrink-0" />
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
<Mention text={task.text} />
</span>
<div className="flex items-center gap-2 whitespace-nowrap overflow-hidden text-ellipsis">
<Mention text={task.text} />
</div>
)}
</div>
@ -205,82 +224,104 @@ const TaskHeader = ({
className="flex items-center justify-between text-sm text-muted-foreground/70"
onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2">
<Coins className="size-3 shrink-0" />
<StandardTooltip
content={
<div className="space-y-1">
<div>
{t("chat:tokenProgress.tokensUsed", {
used: formatLargeNumber(contextTokens || 0),
total: formatLargeNumber(contextWindow),
})}
</div>
{(() => {
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 && (
<div>
{t("chat:tokenProgress.reservedForResponse", {
amount: formatLargeNumber(reservedForOutput),
})}
</div>
)}
{availableSpace > 0 && (
<div>
{t("chat:tokenProgress.availableSpace", {
amount: formatLargeNumber(availableSpace),
})}
</div>
)}
</>
)
})()}
</div>
}
return (
<Table className="text-base ml-1.5">
<TableBody>
<TableRow>
<TableCell className="font-medium whitespace-nowrap">
{t("chat:tokenProgress.tokensUsedLabel")}
</TableCell>
<TableCell className="text-right text-[0.9em] font-mono">
{formatLargeNumber(contextTokens || 0)} /{" "}
{formatLargeNumber(contextWindow)}
</TableCell>
</TableRow>
{reservedForOutput > 0 && (
<TableRow>
<TableCell className="font-medium whitespace-nowrap">
{t("chat:tokenProgress.reservedForResponseLabel")}
</TableCell>
<TableCell className="text-right text-[0.9em] font-mono">
{formatLargeNumber(reservedForOutput)}
</TableCell>
</TableRow>
)}
{availableSpace > 0 && (
<TableRow>
<TableCell className="font-medium whitespace-nowrap">
{t("chat:tokenProgress.availableSpaceLabel")}
</TableCell>
<TableCell className="text-right text-[0.9em] font-mono">
{formatLargeNumber(availableSpace)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
})()}
side="top"
sideOffset={8}>
<span className="mr-1">
{formatLargeNumber(contextTokens || 0)} / {formatLargeNumber(contextWindow)}
<span className="flex items-center gap-1.5">
{(() => {
const percentage = Math.round(((contextTokens || 0) / contextWindow) * 100)
return (
<>
<CircularProgress percentage={percentage} />
<span>{percentage}%</span>
</>
)
})()}
</span>
</StandardTooltip>
{!!totalCost && (
<StandardTooltip
content={
hasSubtasks ? (
<div>
<>
<span>·</span>
<StandardTooltip
content={
hasSubtasks ? (
<div>
{t("chat:costs.totalWithSubtasks", {
cost: (aggregatedCost ?? totalCost).toFixed(2),
})}
<div>
{t("chat:costs.totalWithSubtasks", {
cost: (aggregatedCost ?? totalCost).toFixed(2),
})}
</div>
{costBreakdown && (
<div className="text-xs mt-1">{costBreakdown}</div>
)}
</div>
{costBreakdown && <div className="text-xs mt-1">{costBreakdown}</div>}
</div>
) : (
<div>{t("chat:costs.total", { cost: totalCost.toFixed(2) })}</div>
)
}
side="top"
sideOffset={8}>
<span>
${(aggregatedCost ?? totalCost).toFixed(2)}
{hasSubtasks && (
<span className="text-xs ml-1" title={t("chat:costs.includesSubtasks")}>
*
) : (
<div>{t("chat:costs.total", { cost: totalCost.toFixed(2) })}</div>
)
}
side="top"
sideOffset={8}>
<>
<span>
${(aggregatedCost ?? totalCost).toFixed(2)}
{hasSubtasks && (
<span
className="text-xs ml-1"
title={t("chat:costs.includesSubtasks")}>
*
</span>
)}
</span>
)}
</span>
</StandardTooltip>
</>
</StandardTooltip>
</>
)}
</div>
{showBrowserGlobe && (

View file

@ -0,0 +1,227 @@
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<string, string> = {
"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<HistoryItem> | 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<HistoryItem>, clineMessages?: ClineMessage[]) {
mockCurrentTaskItem = currentTaskItem
mockClineMessages = clineMessages || [message]
return render(
<QueryClientProvider client={queryClient}>
<ChatRowContent
message={message}
isExpanded={false}
isLast={false}
isStreaming={false}
onToggleExpand={() => {}}
onSuggestionClick={() => {}}
onBatchFileResponse={() => {}}
onFollowUpUnmount={() => {}}
isFollowUpAnswered={false}
/>
</QueryClientProvider>,
)
}
describe("ChatRow - subtask links", () => {
beforeEach(() => {
mockPostMessage.mockClear()
})
describe("newTask tool", () => {
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: "code",
content: "Implement feature X",
}),
}
// childIds maps by index to newTask messages - first newTask gets childIds[0]
renderChatRow(message, {
childIds: ["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 using index-matched childId for multiple newTasks", () => {
const message = {
ts: Date.now(),
type: "ask" as const,
ask: "tool" as const,
text: JSON.stringify({
tool: "newTask",
mode: "architect",
content: "Design system architecture",
}),
}
// 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"],
})
const goToSubtaskButton = screen.getByText("Go to subtask")
expect(goToSubtaskButton).toBeInTheDocument()
fireEvent.click(goToSubtaskButton)
// First newTask message maps to first childId
expect(mockPostMessage).toHaveBeenCalledWith({
type: "showTaskWithId",
text: "first-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()
})
})
})

View file

@ -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()
})
})
})

View file

@ -31,8 +31,8 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
size="icon"
data-testid="delete-task-button"
onClick={handleDeleteClick}
className="opacity-70">
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
className="group-hover:opacity-100 opacity-50 transition-opacity">
<span className="codicon codicon-trash scale-80" />
</Button>
</StandardTooltip>
)

View file

@ -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 (
<AlertDialog {...props}>
<AlertDialogContent onEscapeKeyDown={() => onOpenChange?.(false)}>
<AlertDialogHeader>
<AlertDialogTitle>{t("history:deleteTask")}</AlertDialogTitle>
<AlertDialogDescription>{t("history:deleteTaskMessage")}</AlertDialogDescription>
<AlertDialogDescription>{message}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel asChild>

View file

@ -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 (
<div className="flex flex-col gap-1">
<div className="flex flex-wrap items-center justify-between mt-4 mb-2">
@ -25,10 +30,15 @@ const HistoryPreview = () => {
{t("history:viewAllHistory")}
</button>
</div>
{tasks.length !== 0 && (
{displayGroups.length !== 0 && (
<>
{tasks.slice(0, 4).map((item) => (
<TaskItem key={item.id} item={item} variant="compact" />
{displayGroups.map((group) => (
<TaskGroupItem
key={group.parent.id}
group={group}
variant="compact"
onToggleExpand={() => toggleExpand(group.parent.id)}
/>
))}
</>
)}

View file

@ -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<string | null>(null)
const [deleteSubtaskCount, setDeleteSubtaskCount] = useState<number>(0)
const [isSelectionMode, setIsSelectionMode] = useState(false)
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
const [showBatchDeleteDialog, setShowBatchDeleteDialog] = useState<boolean>(false)
// Get subtask count for a task
const getSubtaskCount = useMemo(() => {
const countMap = new Map<string, number>()
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) => {
</TabHeader>
<TabContent className="px-2 py-0">
<Virtuoso
className="flex-1 overflow-y-scroll"
data={tasks}
data-testid="virtuoso-container"
initialTopMostItemIndex={0}
components={{
List: React.forwardRef((props, ref) => (
<div {...props} ref={ref} data-testid="virtuoso-item-list" />
)),
}}
itemContent={(_index, item) => (
<TaskItem
key={item.id}
item={item}
variant="full"
showWorkspace={showAllWorkspaces}
isSelectionMode={isSelectionMode}
isSelected={selectedTaskIds.includes(item.id)}
onToggleSelection={toggleTaskSelection}
onDelete={setDeleteTaskId}
className="m-2"
/>
)}
/>
{isSearchMode && flatTasks ? (
// Search mode: flat list with subtask prefix
<Virtuoso
className="flex-1 overflow-y-scroll"
data={flatTasks}
data-testid="virtuoso-container"
initialTopMostItemIndex={0}
components={{
List: React.forwardRef((props, ref) => (
<div {...props} ref={ref} data-testid="virtuoso-item-list" />
)),
}}
itemContent={(_index, item) => (
<TaskItem
key={item.id}
item={item}
variant="full"
showWorkspace={showAllWorkspaces}
isSelectionMode={isSelectionMode}
isSelected={selectedTaskIds.includes(item.id)}
onToggleSelection={toggleTaskSelection}
onDelete={handleDelete}
className="m-2"
/>
)}
/>
) : (
// Grouped mode: task groups with expandable subtasks
<Virtuoso
className="flex-1 overflow-y-scroll"
data={groups}
data-testid="virtuoso-container"
initialTopMostItemIndex={0}
components={{
List: React.forwardRef((props, ref) => (
<div {...props} ref={ref} data-testid="virtuoso-item-list" />
)),
}}
itemContent={(_index, group) => (
<TaskGroupItem
key={group.parent.id}
group={group}
variant="full"
showWorkspace={showAllWorkspaces}
isSelectionMode={isSelectionMode}
isSelected={selectedTaskIds.includes(group.parent.id)}
onToggleSelection={toggleTaskSelection}
onDelete={handleDelete}
onToggleExpand={() => toggleExpand(group.parent.id)}
className="m-2"
/>
)}
/>
)}
</TabContent>
{/* Fixed action bar at bottom - only shown in selection mode with selected items */}
@ -275,7 +326,17 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{/* Delete dialog */}
{deleteTaskId && (
<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
<DeleteTaskDialog
taskId={deleteTaskId}
subtaskCount={deleteSubtaskCount}
onOpenChange={(open) => {
if (!open) {
setDeleteTaskId(null)
setDeleteSubtaskCount(0)
}
}}
open
/>
)}
{/* Batch delete dialog */}

View file

@ -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 (
<div
data-testid="subtask-collapsible-row"
className={cn(
"flex items-center gap-1 px-3 py-2 -mt-2 cursor-pointer text-xs",
"hover:text-vscode-descriptionForeground",
isExpanded ? "text-vscode-descriptionForeground" : "text-vscode-descriptionForeground/80",
"transition-colors",
className,
)}
onClick={(e) => {
e.stopPropagation()
onToggle()
}}
role="button"
aria-expanded={isExpanded}
aria-label={isExpanded ? t("history:collapseSubtasks") : t("history:expandSubtasks")}>
<ChevronRight className={`size-3 transition-transform ${isExpanded && "rotate-90"}`} />
{t("history:subtasks", { count })}
</div>
)
}
export default memo(SubtaskCollapsibleRow)

View file

@ -0,0 +1,49 @@
import { memo } from "react"
import { ArrowRight } from "lucide-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 (
<div
data-testid={`subtask-row-${item.id}`}
className={cn(
"group flex items-center justify-between gap-2 pl-1 pr-4 py-1 ml-6 cursor-pointer",
"text-vscode-foreground/60 hover:text-vscode-foreground transition-colors",
className,
)}
onClick={handleClick}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleClick()
}
}}>
<StandardTooltip content={item.task} delay={600}>
<span className="text-sm line-clamp-1">{item.task}</span>
</StandardTooltip>
<ArrowRight className="size-3 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</div>
)
}
export default memo(SubtaskRow)

View file

@ -0,0 +1,88 @@
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 (
<div
data-testid={`task-group-${parent.id}`}
className={cn(
"bg-vscode-editor-background rounded-xl border border-transparent overflow-hidden",
className,
)}>
{/* Parent task */}
<TaskItem
item={parent}
variant={variant}
showWorkspace={showWorkspace}
isSelectionMode={isSelectionMode}
isSelected={isSelected}
onToggleSelection={onToggleSelection}
onDelete={onDelete}
hasSubtasks={hasSubtasks}
/>
{/* Subtask collapsible row */}
{hasSubtasks && (
<SubtaskCollapsibleRow count={subtasks.length} isExpanded={isExpanded} onToggle={onToggleExpand} />
)}
{/* Expanded subtasks */}
{hasSubtasks && (
<div
data-testid="subtask-list"
className={cn(
"overflow-clip transition-all duration-500",
isExpanded ? "max-h-100 pb-2" : "max-h-0",
)}>
{subtasks.map((subtask) => (
<SubtaskRow key={subtask.id} item={subtask} />
))}
</div>
)}
</div>
)
}
export default memo(TaskGroupItem)

View file

@ -1,20 +1,19 @@
import { memo } from "react"
import type { HistoryItem } from "@roo-code/types"
import { ArrowRight, Folder } from "lucide-react"
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 +25,7 @@ const TaskItem = ({
item,
variant,
showWorkspace = false,
hasSubtasks = false,
isSelectionMode = false,
isSelected = false,
onToggleSelection,
@ -47,8 +47,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",
"text-vscode-foreground/80 hover:text-vscode-foreground transition-colors",
hasSubtasks ? "rounded-t-xl" : "rounded-xl",
className,
)}
onClick={handleClick}>
@ -69,32 +70,52 @@ const TaskItem = ({
)}
<div className="flex-1 min-w-0">
<div
className={cn(
"overflow-hidden whitespace-pre-wrap font-light text-vscode-foreground text-ellipsis line-clamp-3",
{
"text-base": !isCompact,
},
!isCompact && isSelectionMode ? "mb-1" : "",
<div className="flex items-start gap-1">
{item.highlight ? (
<div
className={cn(
"flex-1 min-w-0 overflow-hidden whitespace-pre-wrap font-light text-ellipsis line-clamp-3",
{
"text-base": !isCompact,
},
!isCompact && isSelectionMode ? "mb-1" : "",
)}
data-testid="task-content"
dangerouslySetInnerHTML={{ __html: item.highlight }}
/>
) : (
<div
className={cn(
"flex-1 min-w-0 overflow-hidden whitespace-pre-wrap font-light text-ellipsis line-clamp-3",
{
"text-base": !isCompact,
},
!isCompact && isSelectionMode ? "mb-1" : "",
)}
data-testid="task-content">
<StandardTooltip content={item.task}>
<span>{item.task}</span>
</StandardTooltip>
</div>
)}
data-testid="task-content"
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
{item.highlight ? undefined : item.task}
{/* Arrow icon that appears on hover */}
<ArrowRight className="size-4 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
{showWorkspace && item.workspace && (
<div className="flex items-center font-mono gap-1 text-vscode-descriptionForeground text-xs mt-1">
<Folder className="size-3" />
<span>{item.workspace}</span>
</div>
)}
<TaskItemFooter
item={item}
variant={variant}
isSelectionMode={isSelectionMode}
isSubtask={item.isSubtask}
onDelete={onDelete}
/>
{showWorkspace && item.workspace && (
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
<span className="codicon codicon-folder scale-80" />
<span>{item.workspace}</span>
</div>
)}
</div>
</div>
</div>

View file

@ -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<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false, onDelete }) => {
const TaskItemFooter: React.FC<TaskItemFooterProps> = ({
item,
variant,
isSelectionMode = false,
isSubtask = false,
onDelete,
}) => {
const { t } = useAppTranslation()
return (
<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center">
<div className="flex gap-1 items-center text-vscode-descriptionForeground/60">
{/* Subtask tag */}
{isSubtask && (
<>
<Split className="size-3" />
<span>{t("history:subtaskTag")}</span>
<span>·</span>
</>
)}
{/* Datetime with time-ago format */}
<StandardTooltip content={new Date(item.ts).toLocaleString()}>
<span className="first-letter:uppercase">{formatTimeAgo(item.ts)}</span>
</StandardTooltip>
<span>·</span>
{/* Cost */}
{!!item.totalCost && (
<span className="flex items-center" data-testid="cost-footer-compact">
{"$" + item.totalCost.toFixed(2)}
</span>
<>
<span>·</span>
<span className="flex items-center" data-testid="cost-footer-compact">
{"$" + item.totalCost.toFixed(2)}
</span>
</>
)}
</div>
{/* Action Buttons for non-compact view */}
{!isSelectionMode && (
<div className="flex flex-row gap-0 -mx-2 items-center text-vscode-descriptionForeground/60 hover:text-vscode-descriptionForeground">
<div className="flex flex-row gap-0 -mx-1.5 items-center text-vscode-descriptionForeground/60 hover:text-vscode-descriptionForeground opacity-0 group-hover:opacity-100">
<CopyButton itemTask={item.task} />
{variant === "full" && <ExportButton itemId={item.id} />}
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}

View file

@ -8,13 +8,17 @@ vi.mock("@/utils/vscode")
vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => {
t: (key: string, options?: Record<string, unknown>) => {
const translations: Record<string, string> = {
"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(
<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} subtaskCount={3} />,
)
expect(screen.getByText("This will also delete 3 subtask(s). Are you sure?")).toBeInTheDocument()
})
it("shows standard message when no subtasks", () => {
render(
<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} subtaskCount={0} />,
)
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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
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(
<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} subtaskCount={1} />,
)
expect(screen.getByText("This will also delete 1 subtask(s). Are you sure?")).toBeInTheDocument()
})
it("still deletes task when cascade warning is shown", () => {
render(
<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} subtaskCount={5} />,
)
const deleteButton = screen.getByText("Delete")
fireEvent.click(deleteButton)
expect(vscode.postMessage).toHaveBeenCalledWith({
type: "deleteTaskWithId",
text: mockTaskId,
})
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
})
})
})

View file

@ -0,0 +1,267 @@
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<string, unknown>) => {
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> = {}): 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> = {}): 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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
expect(screen.getByText("1 Subtask")).toBeInTheDocument()
})
it("does not show subtask row when no subtasks", () => {
const group = createMockGroup({ subtasks: [] })
render(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={onToggleExpand} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
// 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")
})
})
describe("selection mode", () => {
it("handles selection mode correctly", () => {
const onToggleSelection = vi.fn()
const group = createMockGroup({
parent: createMockDisplayHistoryItem({ id: "parent-1" }),
})
render(
<TaskGroupItem
group={group}
variant="full"
isSelectionMode={true}
isSelected={false}
onToggleSelection={onToggleSelection}
onToggleExpand={vi.fn()}
/>,
)
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(
<TaskGroupItem
group={group}
variant="full"
isSelectionMode={true}
isSelected={true}
onToggleSelection={vi.fn()}
onToggleExpand={vi.fn()}
/>,
)
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(<TaskGroupItem group={group} variant="compact" onToggleExpand={vi.fn()} />)
// 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(<TaskGroupItem group={group} variant="full" onToggleExpand={vi.fn()} />)
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(<TaskGroupItem group={group} variant="full" onDelete={onDelete} onToggleExpand={vi.fn()} />)
// 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(<TaskGroupItem group={group} variant="full" showWorkspace={true} onToggleExpand={vi.fn()} />)
// 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(<TaskGroupItem group={group} variant="full" className="custom-class" onToggleExpand={vi.fn()} />)
const container = screen.getByTestId("task-group-parent-1")
expect(container).toHaveClass("custom-class")
})
})
})

View file

@ -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")
})
})

View file

@ -82,4 +82,16 @@ describe("TaskItemFooter", () => {
expect(screen.queryByTestId("delete-task-button")).not.toBeInTheDocument()
})
it("shows subtask tag when isSubtask is true", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSubtask={true} />)
expect(screen.getByText("history:subtaskTag")).toBeInTheDocument()
})
it("does not show subtask tag when isSubtask is false", () => {
render(<TaskItemFooter item={mockItem} variant="full" isSubtask={false} />)
expect(screen.queryByText("history:subtaskTag")).not.toBeInTheDocument()
})
})

View file

@ -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> = {}): 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)
})
})
})

View file

@ -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
}

View file

@ -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<Set<string>>(new Set())
const isSearchMode = searchQuery.trim().length > 0
// Build a map of taskId -> HistoryItem for quick lookup
const taskMap = useMemo(() => {
const map = new Map<string, HistoryItem>()
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<string, HistoryItem[]>()
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,
}
}

View file

@ -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(<CircularProgress percentage={50} />)
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(<CircularProgress percentage={50} size={24} />)
const svg = screen.getByRole("progressbar")
expect(svg).toHaveAttribute("width", "24")
expect(svg).toHaveAttribute("height", "24")
})
it("should render with default size of 16", () => {
render(<CircularProgress percentage={50} />)
const svg = screen.getByRole("progressbar")
expect(svg).toHaveAttribute("width", "16")
expect(svg).toHaveAttribute("height", "16")
})
it("should clamp percentage to 0 when negative", () => {
render(<CircularProgress percentage={-10} />)
const svg = screen.getByRole("progressbar")
expect(svg).toHaveAttribute("aria-valuenow", "0")
})
it("should clamp percentage to 100 when over 100", () => {
render(<CircularProgress percentage={150} />)
const svg = screen.getByRole("progressbar")
expect(svg).toHaveAttribute("aria-valuenow", "100")
})
it("should apply custom className", () => {
render(<CircularProgress percentage={50} className="custom-class" />)
const svg = screen.getByRole("progressbar")
expect(svg).toHaveClass("custom-class")
expect(svg).toHaveClass("shrink-0")
})
it("should render two circles (background and progress)", () => {
render(<CircularProgress percentage={50} />)
const svg = screen.getByRole("progressbar")
const circles = svg.querySelectorAll("circle")
expect(circles).toHaveLength(2)
})
it("should have background circle with 0.2 opacity", () => {
render(<CircularProgress percentage={50} />)
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(<CircularProgress percentage={50} size={16} strokeWidth={2} />)
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(<CircularProgress percentage={0} size={16} strokeWidth={2} />)
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(<CircularProgress percentage={100} size={16} strokeWidth={2} />)
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(<CircularProgress percentage={50} size={16} />)
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(<CircularProgress percentage={50} />)
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(<CircularProgress percentage={50} strokeWidth={4} />)
const svg = screen.getByRole("progressbar")
const circles = svg.querySelectorAll("circle")
circles.forEach((circle) => {
expect(circle).toHaveAttribute("stroke-width", "4")
})
})
})

View file

@ -106,6 +106,29 @@ describe("StandardTooltip", () => {
)
})
it("should render with custom delay", async () => {
const user = userEvent.setup()
render(
<TooltipProvider>
<StandardTooltip content="Tooltip text" delay={0}>
<button>Hover me</button>
</StandardTooltip>
</TooltipProvider>,
)
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()

View file

@ -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
* <CircularProgress percentage={75} />
* <CircularProgress percentage={50} size={24} strokeWidth={3} />
* ```
*/
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 (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className={cn("shrink-0", className)}
role="progressbar"
aria-valuenow={clampedPercentage}
aria-valuemin={0}
aria-valuemax={100}>
{/* Background circle */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
opacity="0.2"
/>
{/* Progress circle */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${center} ${center})`}
/>
</svg>
)
})

View file

@ -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"
@ -16,6 +17,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"

View file

@ -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 */
@ -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 600ms).
* This component wraps the Radix UI tooltip with a standardized delay duration.
*
* @example
@ -38,6 +40,11 @@ interface StandardTooltipProps {
* <IconButton icon="info" />
* </StandardTooltip>
*
* // With custom delay
* <StandardTooltip content="Quick tooltip" delay={100}>
* <Button>Hover me</Button>
* </StandardTooltip>
*
* @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,9 +68,14 @@ export function StandardTooltip({
const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined
return (
<Tooltip>
<Tooltip delayDuration={delay}>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent side={side} align={align} sideOffset={sideOffset} className={className} style={style}>
<TooltipContent
side={side}
align={align}
sideOffset={sideOffset}
className={`rounded-lg p-2 ${className}`}
style={style}>
{content}
</TooltipContent>
</Tooltip>

View file

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => <table ref={ref} className={cn("w-full text-sm", className)} {...props} />,
)
Table.displayName = "Table"
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <tbody ref={ref} className={cn("", className)} {...props} />,
)
TableBody.displayName = "TableBody"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => <tr ref={ref} className={cn("", className)} {...props} />,
)
TableRow.displayName = "TableRow"
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => <td ref={ref} className={cn("py-0.5 pr-3 align-top", className)} {...props} />,
)
TableCell.displayName = "TableCell"
export { Table, TableBody, TableRow, TableCell }

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +262,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +262,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"

View file

@ -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"
}

View file

@ -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",
@ -51,13 +52,16 @@
"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",
"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"
@ -279,12 +283,12 @@
},
"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": "Subtask completed",
"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.",
"goToSubtask": "View task"
},
"questions": {
"hasQuestion": "Roo has a question"

View file

@ -41,5 +41,11 @@
"mostTokens": "Most Tokens",
"mostRelevant": "Most Relevant"
},
"viewAllHistory": "View all"
"viewAllHistory": "View all",
"subtasks_one": "{{count}} subtask",
"subtasks_other": "{{count}} subtasks",
"subtaskTag": "Subtask",
"deleteWithSubtasks": "This will also delete {{count}} subtask(s). Are you sure?",
"expandSubtasks": "Expand subtasks",
"collapseSubtasks": "Collapse subtasks"
}

View file

@ -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",
@ -51,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",
@ -146,14 +150,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 +262,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +262,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"openInCloud": "Roo Code Cloud में कार्य खोलें",
"openInCloudIntro": "कहीं से भी Roo की निगरानी या इंटरैक्ट करना जारी रखें। खोलने के लिए स्कैन करें, क्लिक करें या कॉपी करें।",
"openApiHistory": "API इतिहास खोलें",
"openUiHistory": "UI इतिहास खोलें"
"openUiHistory": "UI इतिहास खोलें",
"backToParentTask": "मूल कार्य"
},
"unpin": "पिन करें",
"pin": "अवपिन करें",
"tokenProgress": {
"availableSpace": "उपलब्ध स्थान: {{amount}} tokens",
"tokensUsed": "प्रयुक्त tokens: {{used}} / {{total}}",
"reservedForResponse": "मॉडल प्रतिक्रिया के लिए आरक्षित: {{amount}} tokens"
"reservedForResponse": "मॉडल प्रतिक्रिया के लिए आरक्षित: {{amount}} tokens",
"tokensUsedLabel": "प्रयुक्त tokens",
"reservedForResponseLabel": "प्रतिक्रिया के लिए आरक्षित",
"availableSpaceLabel": "उपलब्ध स्थान"
},
"retry": {
"title": "पुनः प्रयास करें",
@ -146,14 +150,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 +262,8 @@
"completionContent": "उपकार्य पूर्ण",
"resultContent": "उपकार्य परिणाम",
"defaultResult": "कृपया अगले कार्य पर जारी रखें।",
"completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।"
"completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।",
"goToSubtask": "कार्य देखें"
},
"questions": {
"hasQuestion": "Roo का एक प्रश्न है"

View file

@ -41,5 +41,11 @@
"mostTokens": "सबसे अधिक टोकन",
"mostRelevant": "सबसे प्रासंगिक"
},
"viewAllHistory": "सभी देखें"
"viewAllHistory": "सभी देखें",
"subtasks_one": "{{count}} उप-कार्य",
"subtasks_other": "{{count}} उप-कार्य",
"subtaskTag": "उप-कार्य",
"deleteWithSubtasks": "यह {{count}} उप-कार्य(कों) को भी हटा देगा। क्या आप निश्चित हैं?",
"expandSubtasks": "उप-कार्य विस्तारित करें",
"collapseSubtasks": "उप-कार्य संपीड़ित करें"
}

View file

@ -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"
@ -57,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",
@ -155,14 +159,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 +293,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -149,14 +153,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 +262,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"

View file

@ -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à"
}

View file

@ -28,14 +28,18 @@
"openInCloud": "Roo Code Cloudでタスクを開く",
"openInCloudIntro": "どこからでもRooの監視や操作を続けられます。スキャン、クリック、またはコピーして開いてください。",
"openApiHistory": "API履歴を開く",
"openUiHistory": "UI履歴を開く"
"openUiHistory": "UI履歴を開く",
"backToParentTask": "親タスク"
},
"unpin": "ピン留めを解除",
"pin": "ピン留め",
"tokenProgress": {
"availableSpace": "利用可能な空き容量: {{amount}} トークン",
"tokensUsed": "使用トークン: {{used}} / {{total}}",
"reservedForResponse": "モデル応答用に予約: {{amount}} トークン"
"reservedForResponse": "モデル応答用に予約: {{amount}} トークン",
"tokensUsedLabel": "使用トークン",
"reservedForResponseLabel": "応答用に予約",
"availableSpaceLabel": "利用可能な空き容量"
},
"retry": {
"title": "再試行",
@ -146,14 +150,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 +262,8 @@
"completionContent": "サブタスク完了",
"resultContent": "サブタスク結果",
"defaultResult": "次のタスクに進んでください。",
"completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。"
"completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。",
"goToSubtask": "タスクを表示"
},
"questions": {
"hasQuestion": "Rooは質問があります"

View file

@ -41,5 +41,11 @@
"mostTokens": "最多トークン",
"mostRelevant": "最も関連性の高い"
},
"viewAllHistory": "すべて表示"
"viewAllHistory": "すべて表示",
"subtasks_one": "{{count}} サブタスク",
"subtasks_other": "{{count}} サブタスク",
"subtaskTag": "サブタスク",
"deleteWithSubtasks": "これにより {{count}} サブタスクも削除されます。よろしいですか?",
"expandSubtasks": "サブタスクを展開",
"collapseSubtasks": "サブタスクを折りたたむ"
}

View file

@ -28,14 +28,18 @@
"openInCloud": "Roo Code Cloud에서 작업 열기",
"openInCloudIntro": "어디서나 Roo를 계속 모니터링하거나 상호작용할 수 있습니다. 스캔, 클릭 또는 복사하여 열기.",
"openApiHistory": "API 기록 열기",
"openUiHistory": "UI 기록 열기"
"openUiHistory": "UI 기록 열기",
"backToParentTask": "상위 작업"
},
"unpin": "고정 해제하기",
"pin": "고정하기",
"tokenProgress": {
"availableSpace": "사용 가능한 공간: {{amount}} 토큰",
"tokensUsed": "사용된 토큰: {{used}} / {{total}}",
"reservedForResponse": "모델 응답용 예약: {{amount}} 토큰"
"reservedForResponse": "모델 응답용 예약: {{amount}} 토큰",
"tokensUsedLabel": "사용된 토큰",
"reservedForResponseLabel": "응답용으로 예약됨",
"availableSpaceLabel": "사용 가능한 공간"
},
"retry": {
"title": "다시 시도",
@ -146,14 +150,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 +262,8 @@
"completionContent": "하위 작업 완료",
"resultContent": "하위 작업 결과",
"defaultResult": "다음 작업을 계속 진행해주세요.",
"completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요."
"completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요.",
"goToSubtask": "작업 보기"
},
"questions": {
"hasQuestion": "Roo에게 질문이 있습니다"

View file

@ -41,5 +41,11 @@
"mostTokens": "토큰 많은순",
"mostRelevant": "관련성 높은순"
},
"viewAllHistory": "모두 보기"
"viewAllHistory": "모두 보기",
"subtasks_one": "{{count}} 부분작업",
"subtasks_other": "{{count}} 부분작업",
"subtaskTag": "부분작업",
"deleteWithSubtasks": "이는 {{count}} 부분작업도 삭제합니다. 확실하십니까?",
"expandSubtasks": "부분작업 확장",
"collapseSubtasks": "부분작업 축소"
}

View file

@ -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",
@ -51,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",
@ -253,7 +257,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -258,7 +262,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +262,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"

View file

@ -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"
}

View file

@ -28,7 +28,8 @@
"openInCloud": "Открыть задачу в Roo Code Cloud",
"openInCloudIntro": "Продолжай отслеживать или взаимодействовать с Roo откуда угодно. Отсканируй, нажми или скопируй для открытия.",
"openApiHistory": "Открыть историю API",
"openUiHistory": "Открыть историю UI"
"openUiHistory": "Открыть историю UI",
"backToParentTask": "Родительская задача"
},
"unpin": "Открепить",
"pin": "Закрепить",
@ -51,7 +52,10 @@
"tokenProgress": {
"availableSpace": "Доступно места: {{amount}} токенов",
"tokensUsed": "Использовано токенов: {{used}} из {{total}}",
"reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов"
"reservedForResponse": "Зарезервировано для ответа модели: {{amount}} токенов",
"tokensUsedLabel": "Использовано токенов",
"reservedForResponseLabel": "Зарезервировано для ответа",
"availableSpaceLabel": "Доступно места"
},
"reject": {
"title": "Отклонить",
@ -141,14 +145,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 +258,8 @@
"completionContent": "Подзадача завершена",
"resultContent": "Результаты подзадачи",
"defaultResult": "Пожалуйста, переходите к следующей задаче.",
"completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу."
"completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу.",
"goToSubtask": "Просмотреть задачу"
},
"questions": {
"hasQuestion": "У Roo есть вопрос"

View file

@ -41,5 +41,11 @@
"mostTokens": "Больше всего токенов",
"mostRelevant": "Наиболее релевантные"
},
"viewAllHistory": "Посмотреть все"
"viewAllHistory": "Посмотреть все",
"subtasks_one": "{{count}} подзадача",
"subtasks_other": "{{count}} подзадач",
"subtaskTag": "Подзадача",
"deleteWithSubtasks": "Это также удалит {{count}} подзадачу(и). Вы уверены?",
"expandSubtasks": "Развернуть подзадачи",
"collapseSubtasks": "Свернуть подзадачи"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +263,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"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",
"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",
@ -146,14 +150,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 +263,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"

View file

@ -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"
}

View file

@ -28,14 +28,18 @@
"openInCloud": "在 Roo Code Cloud 中打开任务",
"openInCloudIntro": "从任何地方继续监控或与 Roo 交互。扫描、点击或复制以打开。",
"openApiHistory": "打开 API 历史",
"openUiHistory": "打开 UI 历史"
"openUiHistory": "打开 UI 历史",
"backToParentTask": "父任务"
},
"unpin": "取消置顶",
"pin": "置顶",
"tokenProgress": {
"availableSpace": "可用: {{amount}}",
"tokensUsed": "已使用: {{used}} / {{total}}",
"reservedForResponse": "已保留: {{amount}}"
"reservedForResponse": "已保留: {{amount}}",
"tokensUsedLabel": "已使用 Token",
"reservedForResponseLabel": "为回复保留",
"availableSpaceLabel": "可用空间"
},
"retry": {
"title": "重试",
@ -146,14 +150,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 +263,8 @@
"completionContent": "子任务已完成",
"resultContent": "子任务结果",
"defaultResult": "请继续下一个任务。",
"completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。"
"completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。",
"goToSubtask": "查看任务"
},
"questions": {
"hasQuestion": "Roo有一个问题"

View file

@ -41,5 +41,11 @@
"mostTokens": "最多 Token",
"mostRelevant": "最相关"
},
"viewAllHistory": "查看全部"
"viewAllHistory": "查看全部",
"subtasks_one": "{{count}} 子任务",
"subtasks_other": "{{count}} 子任务",
"subtaskTag": "子任务",
"deleteWithSubtasks": "这也将删除 {{count}} 个子任务。您确定吗?",
"expandSubtasks": "展开子任务",
"collapseSubtasks": "收起子任务"
}

View file

@ -28,7 +28,8 @@
"openInCloud": "在 Roo Code Cloud 中開啟工作",
"openInCloudIntro": "從任何地方繼續監控或與 Roo 互動。掃描、點擊或複製以開啟。",
"openApiHistory": "開啟 API 歷史紀錄",
"openUiHistory": "開啟 UI 歷史紀錄"
"openUiHistory": "開啟 UI 歷史紀錄",
"backToParentTask": "上層工作"
},
"unpin": "取消釘選",
"pin": "釘選",
@ -51,7 +52,10 @@
"tokenProgress": {
"availableSpace": "可用空間:{{amount}} Token",
"tokensUsed": "已使用 Token {{used}} / {{total}}",
"reservedForResponse": "模型回應保留:{{amount}} Token"
"reservedForResponse": "模型回應保留:{{amount}} Token",
"tokensUsedLabel": "已使用 Token",
"reservedForResponseLabel": "為回覆保留",
"availableSpaceLabel": "可用空間"
},
"reject": {
"title": "拒絕",
@ -152,14 +156,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 +291,8 @@
"completionContent": "子工作已完成",
"resultContent": "子工作結果",
"defaultResult": "請繼續下一個工作。",
"completionInstructions": "子工作已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。"
"completionInstructions": "子工作已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。",
"goToSubtask": "查看工作"
},
"questions": {
"hasQuestion": "Roo 有一個問題"

View file

@ -41,5 +41,11 @@
"mostTokens": "最多 Token",
"mostRelevant": "最相關"
},
"viewAllHistory": "檢視全部"
"viewAllHistory": "檢視全部",
"subtasks_one": "{{count}} 子工作",
"subtasks_other": "{{count}} 子工作",
"subtaskTag": "子工作",
"deleteWithSubtasks": "這也將刪除 {{count}} 個子工作。您確定嗎?",
"expandSubtasks": "展開子工作",
"collapseSubtasks": "收起子工作"
}