Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 63 additions & 27 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1751,43 +1751,79 @@ export class ClineProvider
await this.postMessageToWebview({ type: "condenseTaskContextResponse", text: taskId })
}

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

// remove task from stack if it's the current task
if (id === this.getCurrentTask()?.taskId) {
// Close the current task instance; delegation flows will be handled via metadata if applicable.
await this.removeClineFromStack()
// Collect all task IDs to delete (parent + all subtasks)
const allIdsToDelete: string[] = [id]

if (cascadeSubtasks) {
// Recursively collect all child IDs
const collectChildIds = async (taskId: string): Promise<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 all tasks from state in one batch
const taskHistory = this.getGlobalState("taskHistory") ?? []
const updatedTaskHistory = taskHistory.filter((task) => !allIdsToDelete.includes(task.id))
await this.updateGlobalState("taskHistory", updatedTaskHistory)
this.recentTasksCache = undefined

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

try {
await ShadowCheckpointService.deleteTask({ taskId: id, globalStorageDir, workspaceDir })
} catch (error) {
console.error(
`[deleteTaskWithId${id}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
)
}
for (const taskId of allIdsToDelete) {
try {
await ShadowCheckpointService.deleteTask({ taskId, globalStorageDir, workspaceDir })
} catch (error) {
console.error(
`[deleteTaskWithId${taskId}] failed to delete associated shadow repository or branch: ${error instanceof Error ? error.message : String(error)}`,
)
}

// delete the entire task directory including checkpoints and all content
try {
await fs.rm(taskDirPath, { recursive: true, force: true })
console.log(`[deleteTaskWithId${id}] removed task directory`)
} catch (error) {
console.error(
`[deleteTaskWithId${id}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
)
// Delete the task directory
try {
const dirPath = await getTaskDirectoryPath(globalStoragePath, taskId)
await fs.rm(dirPath, { recursive: true, force: true })
console.log(`[deleteTaskWithId${taskId}] removed task directory`)
} catch (error) {
console.error(
`[deleteTaskWithId${taskId}] failed to remove task directory: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

await this.postStateToWebview()
} catch (error) {
// If task is not found, just remove it from state
if (error instanceof Error && error.message === "Task not found") {
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions src/i18n/locales/pt-BR/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 63 additions & 87 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ import {
TerminalSquare,
MessageCircle,
Repeat2,
Split,
ArrowRight,
Check,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
Expand Down Expand Up @@ -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("")
Expand Down Expand Up @@ -385,6 +389,7 @@ export const ChatRowContent = ({
display: "flex",
alignItems: "center",
gap: "10px",
cursor: "default",
marginBottom: "10px",
wordBreak: "break-word",
}
Expand Down Expand Up @@ -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"
Expand All @@ -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>
</>
Expand All @@ -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>
</>
)
Expand Down Expand Up @@ -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":
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)!, {
Expand Down
Loading
Loading