Skip to content

Commit 480aa1e

Browse files
author
Eric Wheeler
committed
feat: Implement hierarchical task history with completion status
Implement hierarchical display for tasks in the history view, allowing parent tasks to be expanded to show child tasks. - Add `parent_task_id` to `HistoryItem` schema to establish parent-child relationships. - Update frontend to render tasks hierarchically, with child tasks indented and collapsible under their parents. Introduce a `completed` status for tasks: - Add `completed` boolean flag to `HistoryItem` schema. - Backend logic in `Task.ts` now sets `completed: true` when an `attempt_completion` is processed (e.g., `ask: "completion_result"` or `say: "completion_result"` messages) and `completed: false` if new messages are added to a completed task. - Frontend history view now displays completed tasks with a distinct text color (`var(--vscode-testing-iconPassed)`). Enhance UI for child tasks in history: - Child task entries are now more compact, with reduced padding. - Token and cost information is omitted for child tasks to save space. Signed-off-by: Eric Wheeler <[email protected]>
1 parent b6d8d0d commit 480aa1e

File tree

8 files changed

+151
-92
lines changed

8 files changed

+151
-92
lines changed

src/core/config/__tests__/ContextProxy.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ describe("ContextProxy", () => {
128128
tokensIn: 1,
129129
tokensOut: 1,
130130
totalCost: 1,
131+
completed: false,
131132
},
132133
]
133134

@@ -160,6 +161,7 @@ describe("ContextProxy", () => {
160161
tokensIn: 1,
161162
tokensOut: 1,
162163
totalCost: 1,
164+
completed: false,
163165
},
164166
]
165167

src/core/task-persistence/taskMetadata.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export type TaskMetadataOptions = {
1818
globalStoragePath: string
1919
workspace: string
2020
parentTaskId?: string
21+
setCompleted?: boolean
22+
unsetCompleted?: boolean
2123
}
2224

2325
export async function taskMetadata({
@@ -27,6 +29,8 @@ export async function taskMetadata({
2729
globalStoragePath,
2830
workspace,
2931
parentTaskId,
32+
setCompleted,
33+
unsetCompleted,
3034
}: TaskMetadataOptions) {
3135
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
3236
const taskMessage = messages[0] // First message is always the task say.
@@ -47,6 +51,13 @@ export async function taskMetadata({
4751

4852
const tokenUsage = getApiMetrics(combineApiRequests(combineCommandSequences(messages.slice(1))))
4953

54+
let completedValue = false // Default to schema's default
55+
if (setCompleted === true) {
56+
completedValue = true
57+
} else if (unsetCompleted === true) {
58+
completedValue = false
59+
}
60+
5061
const historyItem: HistoryItem = {
5162
id: taskId,
5263
number: taskNumber,
@@ -60,6 +71,7 @@ export async function taskMetadata({
6071
size: taskDirSize,
6172
workspace,
6273
parent_task_id: parentTaskId,
74+
completed: completedValue,
6375
}
6476

6577
return { historyItem, tokenUsage }

src/core/task/Task.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export class Task extends EventEmitter<ClineEvents> {
124124
providerRef: WeakRef<ClineProvider>
125125
private readonly globalStoragePath: string
126126
abort: boolean = false
127+
private isCompleted: boolean = false
127128
didFinishAbortingStream = false
128129
abandoned = false
129130
isInitialized = false
@@ -209,6 +210,7 @@ export class Task extends EventEmitter<ClineEvents> {
209210
}
210211

211212
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
213+
this.isCompleted = historyItem?.completed ?? false
212214
// normal use-case is usually retry similar history task with new workspace
213215
this.workspacePath = parentTask
214216
? parentTask.workspacePath
@@ -342,14 +344,22 @@ export class Task extends EventEmitter<ClineEvents> {
342344
globalStoragePath: this.globalStoragePath,
343345
})
344346

345-
const { historyItem, tokenUsage } = await taskMetadata({
347+
const metadataOptions: Parameters<typeof taskMetadata>[0] = {
346348
messages: this.clineMessages,
347349
taskId: this.taskId,
348350
taskNumber: this.taskNumber,
349351
globalStoragePath: this.globalStoragePath,
350352
workspace: this.cwd,
351353
parentTaskId: this.parentTaskId,
352-
})
354+
}
355+
356+
if (this.isCompleted) {
357+
metadataOptions.setCompleted = true
358+
} else {
359+
metadataOptions.unsetCompleted = true
360+
}
361+
362+
const { historyItem, tokenUsage } = await taskMetadata(metadataOptions)
353363

354364
this.emit("taskTokenUsageUpdated", this.taskId, tokenUsage)
355365

@@ -454,6 +464,12 @@ export class Task extends EventEmitter<ClineEvents> {
454464
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text })
455465
}
456466

467+
// If the AI is asking for a completion_result, it means it has attempted completion.
468+
// Mark as completed now. It will be persisted by saveClineMessages called within addToClineMessages or by the save below.
469+
if (type === "completion_result") {
470+
this.isCompleted = true
471+
}
472+
457473
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
458474

459475
if (this.lastMessageTs !== askTs) {
@@ -464,6 +480,16 @@ export class Task extends EventEmitter<ClineEvents> {
464480
}
465481

466482
const result = { response: this.askResponse!, text: this.askResponseText, images: this.askResponseImages }
483+
484+
// If the task was marked as completed due to a "completion_result" ask,
485+
// but the user did not confirm with "yesButtonClicked" (e.g., they clicked "No" or provided new input),
486+
// then the task is no longer considered completed.
487+
if (type === "completion_result" && result.response !== "yesButtonClicked") {
488+
this.isCompleted = false
489+
// This change will be persisted by the next call to saveClineMessages,
490+
// for example, when user feedback is added as a new message.
491+
}
492+
467493
this.askResponse = undefined
468494
this.askResponseText = undefined
469495
this.askResponseImages = undefined
@@ -472,6 +498,13 @@ export class Task extends EventEmitter<ClineEvents> {
472498
}
473499

474500
async handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) {
501+
const lastAskMessage = this.clineMessages
502+
.slice()
503+
.reverse()
504+
.find((m) => m.type === "ask")
505+
if (this.isCompleted && askResponse === "messageResponse" && lastAskMessage?.ask !== "completion_result") {
506+
this.isCompleted = false
507+
}
475508
this.askResponse = askResponse
476509
this.askResponseText = text
477510
this.askResponseImages = images
@@ -501,8 +534,8 @@ export class Task extends EventEmitter<ClineEvents> {
501534
}
502535

503536
if (partial !== undefined) {
537+
// Handles partial messages
504538
const lastMessage = this.clineMessages.at(-1)
505-
506539
const isUpdatingPreviousPartial =
507540
lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
508541

@@ -521,7 +554,7 @@ export class Task extends EventEmitter<ClineEvents> {
521554
if (!options.isNonInteractive) {
522555
this.lastMessageTs = sayTs
523556
}
524-
557+
// For a new partial message, completion is set only when it's finalized.
525558
await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images, partial })
526559
}
527560
} else {
@@ -532,6 +565,10 @@ export class Task extends EventEmitter<ClineEvents> {
532565
if (!options.isNonInteractive) {
533566
this.lastMessageTs = lastMessage.ts
534567
}
568+
// If this is the final part of a "completion_result" message, mark as completed.
569+
if (type === "completion_result") {
570+
this.isCompleted = true
571+
}
535572

536573
lastMessage.text = text
537574
lastMessage.images = images
@@ -551,7 +588,11 @@ export class Task extends EventEmitter<ClineEvents> {
551588
if (!options.isNonInteractive) {
552589
this.lastMessageTs = sayTs
553590
}
554-
591+
// If this is a new, complete "completion_result" message (being added as partial initially but immediately finalized), mark as completed.
592+
// This case might be rare if "completion_result" is always non-partial or ask.
593+
if (type === "completion_result") {
594+
this.isCompleted = true
595+
}
555596
await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
556597
}
557598
}
@@ -566,7 +607,10 @@ export class Task extends EventEmitter<ClineEvents> {
566607
if (!options.isNonInteractive) {
567608
this.lastMessageTs = sayTs
568609
}
569-
610+
// If this is a new, non-partial "completion_result" message, mark as completed.
611+
if (type === "completion_result") {
612+
this.isCompleted = true
613+
}
570614
await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images, checkpoint })
571615
}
572616
}
@@ -584,6 +628,9 @@ export class Task extends EventEmitter<ClineEvents> {
584628
// Start / Abort / Resume
585629

586630
private async startTask(task?: string, images?: string[]): Promise<void> {
631+
if (this.isCompleted && (task || (images && images.length > 0))) {
632+
this.isCompleted = false
633+
}
587634
// `conversationHistory` (for API) and `clineMessages` (for webview)
588635
// need to be in sync.
589636
// If the extension process were killed, then on restart the
@@ -691,6 +738,9 @@ export class Task extends EventEmitter<ClineEvents> {
691738
let responseImages: string[] | undefined
692739
if (response === "messageResponse") {
693740
await this.say("user_feedback", text, images)
741+
if (this.isCompleted) {
742+
this.isCompleted = false
743+
}
694744
responseText = text
695745
responseImages = images
696746
}

src/exports/roo-code.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type GlobalSettings = {
5555
size?: number | undefined
5656
workspace?: string | undefined
5757
parent_task_id?: string | undefined
58+
completed?: boolean | undefined
5859
}[]
5960
| undefined
6061
autoApprovalEnabled?: boolean | undefined
@@ -769,6 +770,7 @@ type IpcMessage =
769770
size?: number | undefined
770771
workspace?: string | undefined
771772
parent_task_id?: string | undefined
773+
completed?: boolean | undefined
772774
}[]
773775
| undefined
774776
autoApprovalEnabled?: boolean | undefined
@@ -1233,6 +1235,7 @@ type TaskCommand =
12331235
size?: number | undefined
12341236
workspace?: string | undefined
12351237
parent_task_id?: string | undefined
1238+
completed?: boolean | undefined
12361239
}[]
12371240
| undefined
12381241
autoApprovalEnabled?: boolean | undefined

src/exports/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type GlobalSettings = {
5555
size?: number | undefined
5656
workspace?: string | undefined
5757
parent_task_id?: string | undefined
58+
completed?: boolean | undefined
5859
}[]
5960
| undefined
6061
autoApprovalEnabled?: boolean | undefined
@@ -783,6 +784,7 @@ type IpcMessage =
783784
size?: number | undefined
784785
workspace?: string | undefined
785786
parent_task_id?: string | undefined
787+
completed?: boolean | undefined
786788
}[]
787789
| undefined
788790
autoApprovalEnabled?: boolean | undefined
@@ -1249,6 +1251,7 @@ type TaskCommand =
12491251
size?: number | undefined
12501252
workspace?: string | undefined
12511253
parent_task_id?: string | undefined
1254+
completed?: boolean | undefined
12521255
}[]
12531256
| undefined
12541257
autoApprovalEnabled?: boolean | undefined

src/schemas/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const historyItemSchema = z.object({
151151
size: z.number().optional(),
152152
workspace: z.string().optional(),
153153
parent_task_id: z.string().optional(),
154+
completed: z.boolean().optional(),
154155
})
155156

156157
export type HistoryItem = z.infer<typeof historyItemSchema>

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { memo } from "react"
22

33
import { vscode } from "@/utils/vscode"
44
import { formatLargeNumber, formatDate } from "@/utils/format"
5-
import { cn } from "@/lib/utils" // Added for cn utility
6-
import { useExtensionState } from "@/context/ExtensionStateContext" // Added for completion status
7-
import { ClineMessage } from "@roo/shared/ExtensionMessage" // Added for ClineMessage type
5+
import { useExtensionState } from "@/context/ExtensionStateContext"
86

97
import { CopyButton } from "./CopyButton"
108
import { useTaskSearch, HierarchicalHistoryItem } from "./useTaskSearch" // Updated import
@@ -13,29 +11,20 @@ import { Coins, ChevronRight } from "lucide-react" // Added ChevronRight for chi
1311

1412
const HistoryPreview = () => {
1513
const { tasks, showAllWorkspaces } = useTaskSearch()
16-
const { clineMessages, currentTaskItem } = useExtensionState()
14+
useExtensionState()
1715

1816
return (
1917
<>
2018
<div className="flex flex-col gap-3">
2119
{tasks.length !== 0 && (
2220
<>
2321
{tasks.slice(0, 3).map((item: HierarchicalHistoryItem) => {
24-
let isCompleted = false
25-
if (item.id === currentTaskItem?.id && clineMessages) {
26-
isCompleted = clineMessages.some(
27-
(msg: ClineMessage) =>
28-
(msg.type === "ask" && msg.ask === "completion_result") ||
29-
(msg.type === "say" && msg.say === "completion_result"),
30-
)
31-
}
22+
// Use the completed flag directly from the item
23+
const isTaskMarkedCompleted = item.completed ?? false
3224
return (
3325
<div
3426
key={item.id}
35-
className={cn(
36-
"bg-vscode-editor-background rounded relative overflow-hidden cursor-pointer border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60",
37-
{ "bg-green-100/10 dark:bg-green-900/20": isCompleted }, // Adjusted green styling for preview
38-
)}
27+
className="bg-vscode-editor-background rounded relative overflow-hidden cursor-pointer border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60"
3928
onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
4029
<div className="flex flex-col gap-2 p-3 pt-1">
4130
<div className="flex justify-between items-center">
@@ -50,8 +39,11 @@ const HistoryPreview = () => {
5039
<CopyButton itemTask={item.task} />
5140
</div>
5241
<div
53-
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap"
42+
className="overflow-hidden whitespace-pre-wrap"
5443
style={{
44+
color: isTaskMarkedCompleted
45+
? "var(--vscode-testing-iconPassed)"
46+
: "var(--vscode-foreground)",
5547
display: "-webkit-box",
5648
WebkitLineClamp: 2,
5749
WebkitBoxOrient: "vertical",

0 commit comments

Comments
 (0)