feat(webview): add edit toggle for todo updates

Extract SubtaskDetail into a shared type and remove the unused SubtaskCostList component.
This commit is contained in:
Toray Altas 2026-01-18 03:20:07 -05:00
parent 188a1c6e8e
commit 788a21a192
7 changed files with 39 additions and 139 deletions

View file

@ -48,19 +48,7 @@ import { QueuedMessages } from "./QueuedMessages"
import DismissibleUpsell from "../common/DismissibleUpsell"
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
import { Cloud } from "lucide-react"
/**
* Detailed information about a subtask for UI display.
* Matches the SubtaskDetail interface from backend aggregateTaskCosts.ts
*/
interface SubtaskDetail {
id: string // Task ID
name: string // First 50 chars of task description
tokens: number // tokensIn + tokensOut
cost: number // Aggregated total cost
status: "active" | "completed" | "delegated"
hasNestedChildren: boolean // Has its own subtasks
}
import type { SubtaskDetail } from "@src/types/subtasks"
export interface ChatViewProps {
isHidden: boolean

View file

@ -1,123 +0,0 @@
import { memo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ChevronRight, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatLargeNumber } from "@/utils/format"
export interface SubtaskDetail {
id: string
name: string
tokens: number
cost: number
status: "active" | "completed" | "delegated"
hasNestedChildren: boolean
}
export interface SubtaskCostListProps {
subtasks: SubtaskDetail[]
onSubtaskClick?: (subtaskId: string) => void
}
interface SubtaskRowProps {
subtask: SubtaskDetail
isLast: boolean
onClick?: () => void
t: (key: string, options?: Record<string, unknown>) => string
}
const statusColors: Record<SubtaskDetail["status"], string> = {
active: "bg-vscode-testing-iconQueued",
completed: "bg-vscode-testing-iconPassed",
delegated: "bg-vscode-testing-iconSkipped",
}
const SubtaskRow = memo(({ subtask, isLast, onClick, t }: SubtaskRowProps) => {
return (
<button
type="button"
onClick={onClick}
className={cn(
"flex items-center gap-2 w-full text-left py-1 px-2 rounded",
"text-xs text-vscode-descriptionForeground",
"hover:bg-vscode-list-hoverBackground transition-colors",
)}>
{/* Tree indicator */}
<span className="text-vscode-descriptionForeground opacity-50 font-mono">{isLast ? "└─" : "├─"}</span>
{/* Status indicator */}
<span
className={cn("size-2 rounded-full shrink-0", statusColors[subtask.status])}
title={t(`subtasks.status.${subtask.status}`)}
/>
{/* Subtask name */}
<span className="flex-1 truncate" title={subtask.name}>
&quot;{subtask.name}&quot;
</span>
{/* Nested indicator */}
{subtask.hasNestedChildren && (
<span className="text-vscode-descriptionForeground opacity-50" title={t("subtasks.hasNestedChildren")}>
+
</span>
)}
{/* Token count */}
<span className="text-vscode-descriptionForeground opacity-70 tabular-nums">
{formatLargeNumber(subtask.tokens)}
</span>
{/* Cost */}
<span className="min-w-[60px] text-right tabular-nums">${subtask.cost.toFixed(2)}</span>
</button>
)
})
SubtaskRow.displayName = "SubtaskRow"
export const SubtaskCostList = memo(({ subtasks, onSubtaskClick }: SubtaskCostListProps) => {
const { t } = useTranslation("chat")
const [isExpanded, setIsExpanded] = useState(false)
if (!subtasks || subtasks.length === 0) {
return null
}
return (
<div className="mt-2 border-t border-vscode-sideBar-background pt-2">
{/* Collapsible Header */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className={cn(
"flex items-center gap-1 w-full text-left",
"text-sm text-vscode-descriptionForeground",
"hover:text-vscode-foreground transition-colors",
)}
aria-expanded={isExpanded}>
{isExpanded ? <ChevronDown className="size-4" /> : <ChevronRight className="size-4" />}
<span>{t("subtasks.count", { count: subtasks.length })}</span>
</button>
{/* Expanded Subtask List */}
{isExpanded && (
<div className="mt-2 ml-2 space-y-1">
{subtasks.map((subtask, index) => (
<SubtaskRow
key={subtask.id}
subtask={subtask}
isLast={index === subtasks.length - 1}
onClick={() => onSubtaskClick?.(subtask.id)}
t={t}
/>
))}
</div>
)}
</div>
)
})
SubtaskCostList.displayName = "SubtaskCostList"
export default SubtaskCostList

View file

@ -35,7 +35,7 @@ import { ContextWindowProgress } from "./ContextWindowProgress"
import { Mention } from "./Mention"
import { TodoListDisplay } from "./TodoListDisplay"
import { LucideIconButton } from "./LucideIconButton"
import type { SubtaskDetail } from "./SubtaskCostList"
import type { SubtaskDetail } from "@src/types/subtasks"
export interface TaskHeaderProps {
task: ClineMessage

View file

@ -5,7 +5,7 @@ import { useState, useRef, useMemo, useEffect } from "react"
import { formatLargeNumber } from "@src/utils/format"
import type { SubtaskDetail } from "./SubtaskCostList"
import type { SubtaskDetail } from "@src/types/subtasks"
type TodoStatus = "completed" | "in_progress" | "pending"

View file

@ -179,6 +179,28 @@ const UpdateTodoListToolBlock: React.FC<UpdateTodoListToolBlockProps> = ({
Todo List Updated
</span>
<div className="flex-grow" />
{editable && (
<button
onClick={() => setIsEditing(!isEditing)}
style={{
border: isEditing
? "1px solid var(--vscode-button-border)"
: "1px solid var(--vscode-button-secondaryBorder)",
background: isEditing
? "var(--vscode-button-background)"
: "var(--vscode-button-secondaryBackground)",
color: isEditing
? "var(--vscode-button-foreground)"
: "var(--vscode-button-secondaryForeground)",
borderRadius: 4,
padding: "2px 8px",
cursor: "pointer",
fontSize: 13,
marginLeft: 8,
}}>
{isEditing ? "Done" : "Edit"}
</button>
)}
</div>
</ToolUseBlockHeader>
<div className="overflow-x-auto max-w-full" style={{ padding: "6px 0 2px 0" }}>

View file

@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest"
import { render, screen, fireEvent } from "@testing-library/react"
import { TodoListDisplay } from "../TodoListDisplay"
import type { SubtaskDetail } from "../SubtaskCostList"
import type { SubtaskDetail } from "@src/types/subtasks"
// Mock i18next
vi.mock("i18next", () => ({

View file

@ -0,0 +1,13 @@
export type SubtaskDetail = {
/** Task ID */
id: string
/** First 50 chars of task description */
name: string
/** tokensIn + tokensOut */
tokens: number
/** Aggregated total cost */
cost: number
status: "active" | "completed" | "delegated"
/** Has its own subtasks */
hasNestedChildren: boolean
}