Skip to content

Commit 5848db6

Browse files
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>
1 parent 3b703b8 commit 5848db6

66 files changed

Lines changed: 2350 additions & 371 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/core/webview/ClineProvider.ts

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,43 +1747,79 @@ export class ClineProvider
17471747
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
17481748
}
17491749

1750-
// this function deletes a task from task hidtory, and deletes it's checkpoints and delete the task folder
1751-
async deleteTaskWithId(id: string) {
1750+
// this function deletes a task from task history, and deletes its checkpoints and delete the task folder
1751+
// If the task has subtasks (childIds), they will also be deleted recursively
1752+
async deleteTaskWithId(id: string, cascadeSubtasks: boolean = true) {
17521753
try {
1753-
// get the task directory full path
1754-
const { taskDirPath } = await this.getTaskWithId(id)
1754+
// get the task directory full path and history item
1755+
const { taskDirPath, historyItem } = await this.getTaskWithId(id)
17551756

1756-
// remove task from stack if it's the current task
1757-
if (id === this.getCurrentTask()?.taskId) {
1758-
// Close the current task instance; delegation flows will be handled via metadata if applicable.
1759-
await this.removeClineFromStack()
1757+
// Collect all task IDs to delete (parent + all subtasks)
1758+
const allIdsToDelete: string[] = [id]
1759+
1760+
if (cascadeSubtasks) {
1761+
// Recursively collect all child IDs
1762+
const collectChildIds = async (taskId: string): Promise<void> => {
1763+
try {
1764+
const { historyItem: item } = await this.getTaskWithId(taskId)
1765+
if (item.childIds && item.childIds.length > 0) {
1766+
for (const childId of item.childIds) {
1767+
allIdsToDelete.push(childId)
1768+
await collectChildIds(childId)
1769+
}
1770+
}
1771+
} catch (error) {
1772+
// Child task may already be deleted or not found, continue
1773+
console.log(`[deleteTaskWithId] child task ${taskId} not found, skipping`)
1774+
}
1775+
}
1776+
1777+
await collectChildIds(id)
17601778
}
17611779

1762-
// delete task from the task history state
1763-
await this.deleteTaskFromState(id)
1780+
// Remove from stack if any of the tasks to delete are in the current task stack
1781+
for (const taskId of allIdsToDelete) {
1782+
if (taskId === this.getCurrentTask()?.taskId) {
1783+
// Close the current task instance; delegation flows will be handled via metadata if applicable.
1784+
await this.removeClineFromStack()
1785+
break
1786+
}
1787+
}
1788+
1789+
// Delete all tasks from state in one batch
1790+
const taskHistory = this.getGlobalState("taskHistory") ?? []
1791+
const updatedTaskHistory = taskHistory.filter((task) => !allIdsToDelete.includes(task.id))
1792+
await this.updateGlobalState("taskHistory", updatedTaskHistory)
1793+
this.recentTasksCache = undefined
17641794

1765-
// Delete associated shadow repository or branch.
1766-
// TODO: Store `workspaceDir` in the `HistoryItem` object.
1795+
// Delete associated shadow repositories or branches and task directories
17671796
const globalStorageDir = this.contextProxy.globalStorageUri.fsPath
17681797
const workspaceDir = this.cwd
1798+
const { getTaskDirectoryPath } = await import("../../utils/storage")
1799+
const globalStoragePath = this.contextProxy.globalStorageUri.fsPath
17691800

1770-
try {
1771-
await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
1772-
} catch (error) {
1773-
console.error(
1774-
`[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
1775-
)
1776-
}
1801+
for (const taskId of allIdsToDelete) {
1802+
try {
1803+
await ShadowCheckpointService.deleteTask({ taskId, globalStorageDir, workspaceDir })
1804+
} catch (error) {
1805+
console.error(
1806+
`[deleteTaskWithId${taskId}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
1807+
)
1808+
}
17771809

1778-
// delete the entire task directory including checkpoints and all content
1779-
try {
1780-
await fs.rm(taskDirPath, { recursive: true, force: true })
1781-
console.log(`[deleteTaskWithId${id}] removed task directory`)
1782-
} catch (error) {
1783-
console.error(
1784-
`[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
1785-
)
1810+
// Delete the task directory
1811+
try {
1812+
const dirPath = await getTaskDirectoryPath(globalStoragePath, taskId)
1813+
await fs.rm(dirPath, { recursive: true, force: true })
1814+
console.log(`[deleteTaskWithId${taskId}] removed task directory`)
1815+
} catch (error) {
1816+
console.error(
1817+
`[deleteTaskWithId${taskId}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
1818+
)
1819+
}
17861820
}
1821+
1822+
await this.postStateToWebview()
17871823
} catch (error) {
17881824
// If task is not found, just remove it from state
17891825
if (error instanceof Error && error.message === "Task not found") {

src/i18n/locales/en/common.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
},
66
"number_format": {
77
"thousand_suffix": "k",
8-
"million_suffix": "m",
9-
"billion_suffix": "b"
8+
"million_suffix": "M",
9+
"billion_suffix": "B"
1010
},
1111
"welcome": "Welcome, {{name}}! You have {{count}} notifications.",
1212
"items": {

src/i18n/locales/pt-BR/common.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 63 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ import {
6868
TerminalSquare,
6969
MessageCircle,
7070
Repeat2,
71+
Split,
72+
ArrowRight,
73+
Check,
7174
} from "lucide-react"
7275
import { cn } from "@/lib/utils"
7376
import { PathTooltip } from "../ui/PathTooltip"
@@ -176,7 +179,8 @@ export const ChatRowContent = ({
176179
}: ChatRowContentProps) => {
177180
const { t, i18n } = useTranslation()
178181

179-
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState()
182+
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages, currentTaskItem } =
183+
useExtensionState()
180184
const { info: model } = useSelectedModel(apiConfiguration)
181185
const [isEditing, setIsEditing] = useState(false)
182186
const [editedContent, setEditedContent] = useState("")
@@ -385,6 +389,7 @@ export const ChatRowContent = ({
385389
display: "flex",
386390
alignItems: "center",
387391
gap: "10px",
392+
cursor: "default",
388393
marginBottom: "10px",
389394
wordBreak: "break-word",
390395
}
@@ -815,10 +820,34 @@ export const ChatRowContent = ({
815820
</>
816821
)
817822
case "newTask":
823+
// Find all newTask messages to determine which child task ID corresponds to this message
824+
const newTaskMessages = clineMessages.filter((msg) => {
825+
if (msg.type === "ask" && msg.ask === "tool") {
826+
const t = safeJsonParse<ClineSayTool>(msg.text)
827+
return t?.tool === "newTask"
828+
}
829+
return false
830+
})
831+
const thisNewTaskIndex = newTaskMessages.findIndex((msg) => msg.ts === message.ts)
832+
const childIds = currentTaskItem?.childIds || []
833+
834+
// Only get the child task ID if this newTask has been approved (has a corresponding entry in childIds)
835+
// This prevents showing a link to a previous task when the current newTask is still awaiting approval
836+
// Note: We don't use delegatedToId here because it persists after child tasks complete and would
837+
// incorrectly point to the previous task when a new newTask is awaiting approval
838+
const childTaskId =
839+
thisNewTaskIndex >= 0 && thisNewTaskIndex < childIds.length ? childIds[thisNewTaskIndex] : undefined
840+
841+
// Check if the next message is a subtask_result - if so, don't show the button
842+
// since the result is displayed right after this message
843+
const currentMessageIndex = clineMessages.findIndex((msg) => msg.ts === message.ts)
844+
const nextMessage = currentMessageIndex >= 0 ? clineMessages[currentMessageIndex + 1] : undefined
845+
const isFollowedBySubtaskResult = nextMessage?.type === "say" && nextMessage?.say === "subtask_result"
846+
818847
return (
819848
<>
820849
<div style={headerStyle}>
821-
{toolIcon("tasklist")}
850+
<Split className="size-4" />
822851
<span style={{ fontWeight: "bold" }}>
823852
<Trans
824853
i18nKey="chat:subtasks.wantsToCreate"
@@ -827,32 +856,19 @@ export const ChatRowContent = ({
827856
/>
828857
</span>
829858
</div>
830-
<div
831-
style={{
832-
marginTop: "4px",
833-
backgroundColor: "var(--vscode-badge-background)",
834-
border: "1px solid var(--vscode-badge-background)",
835-
borderRadius: "4px 4px 0 0",
836-
overflow: "hidden",
837-
marginBottom: "2px",
838-
}}>
839-
<div
840-
style={{
841-
padding: "9px 10px 9px 14px",
842-
backgroundColor: "var(--vscode-badge-background)",
843-
borderBottom: "1px solid var(--vscode-editorGroup-border)",
844-
fontWeight: "bold",
845-
fontSize: "var(--vscode-font-size)",
846-
color: "var(--vscode-badge-foreground)",
847-
display: "flex",
848-
alignItems: "center",
849-
gap: "6px",
850-
}}>
851-
<span className="codicon codicon-arrow-right"></span>
852-
{t("chat:subtasks.newTaskContent")}
853-
</div>
854-
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
855-
<MarkdownBlock markdown={tool.content} />
859+
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pb-1">
860+
<MarkdownBlock markdown={tool.content} />
861+
<div>
862+
{childTaskId && !isFollowedBySubtaskResult && (
863+
<button
864+
className="cursor-pointer flex gap-1 items-center mt-2 text-vscode-descriptionForeground hover:text-vscode-descriptionForeground hover:underline font-normal"
865+
onClick={() =>
866+
vscode.postMessage({ type: "showTaskWithId", text: childTaskId })
867+
}>
868+
{t("chat:subtasks.goToSubtask")}
869+
<ArrowRight className="size-3" />
870+
</button>
871+
)}
856872
</div>
857873
</div>
858874
</>
@@ -864,33 +880,8 @@ export const ChatRowContent = ({
864880
{toolIcon("check-all")}
865881
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.wantsToFinish")}</span>
866882
</div>
867-
<div
868-
style={{
869-
marginTop: "4px",
870-
backgroundColor: "var(--vscode-editor-background)",
871-
border: "1px solid var(--vscode-badge-background)",
872-
borderRadius: "4px",
873-
overflow: "hidden",
874-
marginBottom: "8px",
875-
}}>
876-
<div
877-
style={{
878-
padding: "9px 10px 9px 14px",
879-
backgroundColor: "var(--vscode-badge-background)",
880-
borderBottom: "1px solid var(--vscode-editorGroup-border)",
881-
fontWeight: "bold",
882-
fontSize: "var(--vscode-font-size)",
883-
color: "var(--vscode-badge-foreground)",
884-
display: "flex",
885-
alignItems: "center",
886-
gap: "6px",
887-
}}>
888-
<span className="codicon codicon-check"></span>
889-
{t("chat:subtasks.completionContent")}
890-
</div>
891-
<div style={{ padding: "12px 16px", backgroundColor: "var(--vscode-editor-background)" }}>
892-
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
893-
</div>
883+
<div className="text-muted-foreground pl-6">
884+
<MarkdownBlock markdown={t("chat:subtasks.completionInstructions")} />
894885
</div>
895886
</>
896887
)
@@ -1019,40 +1010,25 @@ export const ChatRowContent = ({
10191010
/>
10201011
)
10211012
case "subtask_result":
1013+
// Get the child task ID that produced this result
1014+
const completedChildTaskId = currentTaskItem?.completedByChildId
10221015
return (
1023-
<div>
1024-
<div
1025-
style={{
1026-
marginTop: "0px",
1027-
backgroundColor: "var(--vscode-badge-background)",
1028-
border: "1px solid var(--vscode-badge-background)",
1029-
borderRadius: "0 0 4px 4px",
1030-
overflow: "hidden",
1031-
marginBottom: "8px",
1032-
}}>
1033-
<div
1034-
style={{
1035-
padding: "9px 10px 9px 14px",
1036-
backgroundColor: "var(--vscode-badge-background)",
1037-
borderBottom: "1px solid var(--vscode-editorGroup-border)",
1038-
fontWeight: "bold",
1039-
fontSize: "var(--vscode-font-size)",
1040-
color: "var(--vscode-badge-foreground)",
1041-
display: "flex",
1042-
alignItems: "center",
1043-
gap: "6px",
1044-
}}>
1045-
<span className="codicon codicon-arrow-left"></span>
1046-
{t("chat:subtasks.resultContent")}
1047-
</div>
1048-
<div
1049-
style={{
1050-
padding: "12px 16px",
1051-
backgroundColor: "var(--vscode-editor-background)",
1052-
}}>
1053-
<MarkdownBlock markdown={message.text} />
1054-
</div>
1016+
<div className="border-l border-muted-foreground/80 ml-2 pl-4 pt-2 pb-1 -mt-5">
1017+
<div style={headerStyle}>
1018+
<span style={{ fontWeight: "bold" }}>{t("chat:subtasks.resultContent")}</span>
1019+
<Check className="size-3" />
10551020
</div>
1021+
<MarkdownBlock markdown={message.text} />
1022+
{completedChildTaskId && (
1023+
<button
1024+
className="cursor-pointer flex gap-1 items-center mt-2 text-vscode-descriptionForeground hover:text-vscode-descriptionForeground hover:underline font-normal"
1025+
onClick={() =>
1026+
vscode.postMessage({ type: "showTaskWithId", text: completedChildTaskId })
1027+
}>
1028+
{t("chat:subtasks.goToSubtask")}
1029+
<ArrowRight className="size-3" />
1030+
</button>
1031+
)}
10561032
</div>
10571033
)
10581034
case "reasoning":

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
14821482
aggregatedCostsMap.get(currentTaskItem.id)!.childrenCost > 0
14831483
)
14841484
}
1485+
parentTaskId={currentTaskItem?.parentTaskId}
14851486
costBreakdown={
14861487
currentTaskItem?.id && aggregatedCostsMap.has(currentTaskItem.id)
14871488
? getCostBreakdownIfNeeded(aggregatedCostsMap.get(currentTaskItem.id)!, {

0 commit comments

Comments
 (0)