Skip to content

Commit 6ac268b

Browse files
committed
feat(history-ui): delegation status badges in history list
1 parent fc5a487 commit 6ac268b

8 files changed

Lines changed: 159 additions & 4 deletions

File tree

webview-ui/src/components/chat/TaskHeader.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { memo, useRef, useState, useMemo } from "react"
22
import { useTranslation } from "react-i18next"
3-
import { ChevronUp, ChevronDown, HardDriveDownload, HardDriveUpload, ListChevronsDownUp, ArrowLeft } from "lucide-react"
3+
import {
4+
ChevronUp,
5+
ChevronDown,
6+
HardDriveDownload,
7+
HardDriveUpload,
8+
ListChevronsDownUp,
9+
ArrowLeft,
10+
ArrowRight,
11+
} from "lucide-react"
412
import prettyBytes from "pretty-bytes"
513

614
import type { ClineMessage } from "@roo-code/types"
@@ -15,7 +23,6 @@ import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
1523
import { vscode } from "@src/utils/vscode"
1624

1725
import Thumbnails from "../common/Thumbnails"
18-
1926
import { TaskActions } from "./TaskActions"
2027
import { ContextWindowProgress } from "./ContextWindowProgress"
2128
import { Mention } from "./Mention"
@@ -99,6 +106,15 @@ const TaskHeader = ({
99106
}
100107
}
101108

109+
// Is this task itself a delegated parent still waiting on a subtask? See #559.
110+
const awaitingChildId = currentTaskItem?.status === "delegated" ? currentTaskItem.awaitingChildId : undefined
111+
112+
const handleGoToSubtask = () => {
113+
if (awaitingChildId) {
114+
vscode.postMessage({ type: "showTaskWithId", text: awaitingChildId })
115+
}
116+
}
117+
102118
return (
103119
<div className="group pt-2 pb-0 px-3">
104120
{isSubtask && (
@@ -113,6 +129,19 @@ const TaskHeader = ({
113129
</Button>
114130
</div>
115131
)}
132+
{awaitingChildId && (
133+
<div className="mb-2" onClick={(e) => e.stopPropagation()}>
134+
<Button
135+
variant="ghost"
136+
size="sm"
137+
onClick={handleGoToSubtask}
138+
className="flex items-center gap-1.5 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground">
139+
<span className="codicon codicon-sync text-[11px]" />
140+
{t("chat:task.waitingOnSubtask")}
141+
<ArrowRight className="size-3" />
142+
</Button>
143+
</div>
144+
)}
116145
<div
117146
className={cn(
118147
"px-3 pt-2.5 pb-2 flex flex-col gap-1.5 relative z-1 cursor-pointer",

webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const mockExtensionState: {
4040
apiConfiguration: ProviderSettings
4141
currentTaskItem: { id: string } | null
4242
clineMessages: any[]
43+
taskHistory: any[]
4344
} = {
4445
apiConfiguration: {
4546
apiProvider: "anthropic",
@@ -48,6 +49,7 @@ const mockExtensionState: {
4849
} as ProviderSettings,
4950
currentTaskItem: { id: "test-task-id" },
5051
clineMessages: [],
52+
taskHistory: [],
5153
}
5254

5355
// Mock the ExtensionStateContext
@@ -223,6 +225,58 @@ describe("TaskHeader", () => {
223225
})
224226
})
225227

228+
describe("Delegated parent waiting-on-subtask banner", () => {
229+
beforeEach(() => {
230+
mockPostMessage.mockClear()
231+
})
232+
233+
afterEach(() => {
234+
mockExtensionState.currentTaskItem = { id: "test-task-id" }
235+
mockExtensionState.taskHistory = []
236+
})
237+
238+
it("does not show the banner when currentTaskItem is not delegated", () => {
239+
mockExtensionState.currentTaskItem = { id: "test-task-id", status: "active" } as any
240+
renderTaskHeader()
241+
expect(screen.queryByText("chat:task.waitingOnSubtask")).not.toBeInTheDocument()
242+
})
243+
244+
it("shows the banner when currentTaskItem is delegated with an awaitingChildId", () => {
245+
mockExtensionState.currentTaskItem = {
246+
id: "parent-1",
247+
status: "delegated",
248+
awaitingChildId: "child-1",
249+
} as any
250+
renderTaskHeader()
251+
expect(screen.getByText("chat:task.waitingOnSubtask")).toBeInTheDocument()
252+
})
253+
254+
it("navigates to the awaited child when the banner is clicked", () => {
255+
mockExtensionState.currentTaskItem = {
256+
id: "parent-1",
257+
status: "delegated",
258+
awaitingChildId: "child-1",
259+
} as any
260+
renderTaskHeader()
261+
262+
fireEvent.click(screen.getByText("chat:task.waitingOnSubtask"))
263+
264+
expect(mockPostMessage).toHaveBeenCalledWith({ type: "showTaskWithId", text: "child-1" })
265+
})
266+
267+
it("does not show an Abandon button (removed: implicit sever on re-delegation)", () => {
268+
mockExtensionState.currentTaskItem = {
269+
id: "parent-1",
270+
status: "delegated",
271+
awaitingChildId: "child-1",
272+
} as any
273+
renderTaskHeader()
274+
275+
expect(screen.getByText("chat:task.waitingOnSubtask")).toBeInTheDocument()
276+
expect(screen.queryByText("history:abandonSubtask")).not.toBeInTheDocument()
277+
})
278+
})
279+
226280
describe("Context window percentage calculation", () => {
227281
// The percentage should be calculated as:
228282
// contextTokens / (contextWindow - reservedForOutput) * 100

webview-ui/src/components/history/SubtaskRow.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { SubtaskTreeNode } from "./types"
66
import { countAllSubtasks } from "./types"
77
import { StandardTooltip } from "../ui"
88
import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow"
9+
import { TaskStatusBadge } from "./TaskStatusBadge"
910

1011
interface SubtaskRowProps {
1112
/** The subtask tree node to display */
@@ -52,6 +53,9 @@ const SubtaskRow = ({ node, depth, onToggleExpand, className }: SubtaskRowProps)
5253
<StandardTooltip content={item.task} delay={600}>
5354
<span className="text-sm line-clamp-1">{item.task}</span>
5455
</StandardTooltip>
56+
{(item.status === "delegated" || item.status === "interrupted") && (
57+
<TaskStatusBadge status={item.status} className="text-xs shrink-0" />
58+
)}
5559
<ArrowRight className="size-3 opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
5660
</div>
5761

webview-ui/src/components/history/TaskItemFooter.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DeleteButton } from "./DeleteButton"
77
import { StandardTooltip } from "../ui/standard-tooltip"
88
import { useAppTranslation } from "@/i18n/TranslationContext"
99
import { Split } from "lucide-react"
10+
import { TaskStatusBadge } from "./TaskStatusBadge"
1011

1112
export interface TaskItemFooterProps {
1213
item: HistoryItem
@@ -36,6 +37,13 @@ const TaskItemFooter: React.FC<TaskItemFooterProps> = ({
3637
<span>·</span>
3738
</>
3839
)}
40+
{/* Delegation status (delegated parent waiting on a child, or interrupted child) */}
41+
{(item.status === "delegated" || item.status === "interrupted") && (
42+
<>
43+
<TaskStatusBadge status={item.status} />
44+
<span>·</span>
45+
</>
46+
)}
3947
{/* Datetime with time-ago format */}
4048
<StandardTooltip content={new Date(item.ts).toLocaleString()}>
4149
<span className="first-letter:uppercase">{formatTimeAgo(item.ts)}</span>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useAppTranslation } from "@/i18n/TranslationContext"
2+
import { cn } from "@/lib/utils"
3+
import { StandardTooltip } from "../ui"
4+
5+
interface TaskStatusBadgeProps {
6+
status: "delegated" | "interrupted"
7+
className?: string
8+
}
9+
10+
/**
11+
* Small inline badge for a task's delegation status: a parent "delegated" and
12+
* waiting on a subtask, or a child that was "interrupted" mid-execution and
13+
* can be resumed rather than silently detached. See #559.
14+
*/
15+
export const TaskStatusBadge = ({ status, className }: TaskStatusBadgeProps) => {
16+
const { t } = useAppTranslation()
17+
18+
const isInterrupted = status === "interrupted"
19+
const icon = isInterrupted ? "codicon-warning" : "codicon-sync"
20+
const label = isInterrupted ? t("history:interruptedTag") : t("history:delegatedTag")
21+
22+
return (
23+
<StandardTooltip content={label}>
24+
<span
25+
data-testid={`task-status-badge-${status}`}
26+
className={cn(
27+
"inline-flex items-center gap-1",
28+
isInterrupted ? "text-vscode-editorWarning-foreground" : "text-vscode-descriptionForeground/60",
29+
className,
30+
)}>
31+
<span className={cn("codicon", icon, "text-[11px]")} />
32+
<span>{label}</span>
33+
</span>
34+
</StandardTooltip>
35+
)
36+
}

webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,24 @@ describe("TaskItemFooter", () => {
9494

9595
expect(screen.queryByText("history:subtaskTag")).not.toBeInTheDocument()
9696
})
97+
98+
it("shows a delegated status badge when status is delegated", () => {
99+
render(
100+
<TaskItemFooter item={{ ...mockItem, status: "delegated", awaitingChildId: "child-1" }} variant="full" />,
101+
)
102+
103+
expect(screen.getByTestId("task-status-badge-delegated")).toBeInTheDocument()
104+
})
105+
106+
it("shows an interrupted status badge when status is interrupted", () => {
107+
render(<TaskItemFooter item={{ ...mockItem, status: "interrupted" }} variant="full" />)
108+
109+
expect(screen.getByTestId("task-status-badge-interrupted")).toBeInTheDocument()
110+
})
111+
112+
it("does not show a status badge for a completed task", () => {
113+
render(<TaskItemFooter item={{ ...mockItem, status: "completed" }} variant="full" />)
114+
115+
expect(screen.queryByTestId(/task-status-badge-/)).not.toBeInTheDocument()
116+
})
97117
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"delete": "Delete Task (Shift + Click to skip confirmation)",
1818
"openApiHistory": "Open API History",
1919
"openUiHistory": "Open UI History",
20-
"backToParentTask": "Parent task"
20+
"backToParentTask": "Parent task",
21+
"waitingOnSubtask": "Waiting on subtask",
22+
"goToSubtask": "Go to subtask"
2123
},
2224
"unpin": "Unpin",
2325
"pin": "Pin",

webview-ui/src/i18n/locales/en/history.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@
4747
"subtaskTag": "Subtask",
4848
"deleteWithSubtasks": "This will also delete {{count}} subtask(s). Are you sure?",
4949
"expandSubtasks": "Expand subtasks",
50-
"collapseSubtasks": "Collapse subtasks"
50+
"collapseSubtasks": "Collapse subtasks",
51+
"delegatedTag": "Waiting on subtask",
52+
"interruptedTag": "Interrupted"
5153
}

0 commit comments

Comments
 (0)