Skip to content

Commit 21fed4c

Browse files
committed
Delete task confirmation enhancements
1 parent 8fae1ca commit 21fed4c

File tree

5 files changed

+148
-73
lines changed

5 files changed

+148
-73
lines changed

.changeset/chilly-bugs-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Delete task confirmation enhancements

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

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ import { useWindowSize } from "react-use"
33
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
44
import prettyBytes from "pretty-bytes"
55

6+
import { vscode } from "@/utils/vscode"
7+
import { formatLargeNumber } from "@/utils/format"
8+
import { Button } from "@/components/ui"
9+
610
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
11+
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
12+
import { HistoryItem } from "../../../../src/shared/HistoryItem"
13+
714
import { useExtensionState } from "../../context/ExtensionStateContext"
8-
import { vscode } from "../../utils/vscode"
915
import Thumbnails from "../common/Thumbnails"
10-
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
11-
import { formatLargeNumber } from "../../utils/format"
1216
import { normalizeApiConfiguration } from "../settings/ApiOptions"
13-
import { Button } from "../ui"
14-
import { HistoryItem } from "../../../../src/shared/HistoryItem"
17+
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
1518

1619
interface TaskHeaderProps {
1720
task: ClineMessage
@@ -46,7 +49,21 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
4649
const contextWindow = selectedModelInfo?.contextWindow || 1
4750

4851
/*
49-
When dealing with event listeners in React components that depend on state variables, we face a challenge. We want our listener to always use the most up-to-date version of a callback function that relies on current state, but we don't want to constantly add and remove event listeners as that function updates. This scenario often arises with resize listeners or other window events. Simply adding the listener in a useEffect with an empty dependency array risks using stale state, while including the callback in the dependencies can lead to unnecessary re-registrations of the listener. There are react hook libraries that provide a elegant solution to this problem by utilizing the useRef hook to maintain a reference to the latest callback function without triggering re-renders or effect re-runs. This approach ensures that our event listener always has access to the most current state while minimizing performance overhead and potential memory leaks from multiple listener registrations.
52+
When dealing with event listeners in React components that depend on state
53+
variables, we face a challenge. We want our listener to always use the most
54+
up-to-date version of a callback function that relies on current state, but
55+
we don't want to constantly add and remove event listeners as that function
56+
updates. This scenario often arises with resize listeners or other window
57+
events. Simply adding the listener in a useEffect with an empty dependency
58+
array risks using stale state, while including the callback in the
59+
dependencies can lead to unnecessary re-registrations of the listener. There
60+
are react hook libraries that provide a elegant solution to this problem by
61+
utilizing the useRef hook to maintain a reference to the latest callback
62+
function without triggering re-renders or effect re-runs. This approach
63+
ensures that our event listener always has access to the most current state
64+
while minimizing performance overhead and potential memory leaks from
65+
multiple listener registrations.
66+
5067
Sources
5168
- https://usehooks-ts.com/react-hook/use-event-listener
5269
- https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
@@ -350,27 +367,48 @@ export const highlightMentions = (text?: string, withShadow = true) => {
350367
})
351368
}
352369

353-
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
354-
<div className="flex flex-row gap-1">
355-
<Button
356-
variant="ghost"
357-
size="sm"
358-
title="Export task history"
359-
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
360-
<span className="codicon codicon-cloud-download" />
361-
</Button>
362-
{!!item?.size && item.size > 0 && (
370+
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
371+
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
372+
373+
return (
374+
<div className="flex flex-row gap-1">
363375
<Button
364376
variant="ghost"
365377
size="sm"
366-
title="Delete task from history"
367-
onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
368-
<span className="codicon codicon-trash" />
369-
{prettyBytes(item.size)}
378+
title="Export task history"
379+
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
380+
<span className="codicon codicon-cloud-download" />
370381
</Button>
371-
)}
372-
</div>
373-
)
382+
{!!item?.size && item.size > 0 && (
383+
<>
384+
<Button
385+
variant="ghost"
386+
size="sm"
387+
title="Delete Task (Shift + Click to skip confirmation)"
388+
onClick={(e) => {
389+
e.stopPropagation()
390+
391+
if (e.shiftKey) {
392+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
393+
} else {
394+
setDeleteTaskId(item.id)
395+
}
396+
}}>
397+
<span className="codicon codicon-trash" />
398+
{prettyBytes(item.size)}
399+
</Button>
400+
{deleteTaskId && (
401+
<DeleteTaskDialog
402+
taskId={deleteTaskId}
403+
onOpenChange={(open) => !open && setDeleteTaskId(null)}
404+
open
405+
/>
406+
)}
407+
</>
408+
)}
409+
</div>
410+
)
411+
}
374412

375413
const ContextWindowProgress = ({ contextWindow, contextTokens }: { contextWindow: number; contextTokens: number }) => (
376414
<>

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

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import React from "react"
1+
import { useCallback, useEffect } from "react"
2+
import { useKeyPress } from "react-use"
3+
import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
4+
25
import {
36
AlertDialog,
47
AlertDialogAction,
@@ -8,25 +11,36 @@ import {
811
AlertDialogFooter,
912
AlertDialogHeader,
1013
AlertDialogTitle,
11-
} from "@/components/ui/alert-dialog"
12-
import { Button } from "@/components/ui"
14+
Button,
15+
} from "@/components/ui"
16+
1317
import { vscode } from "@/utils/vscode"
1418

15-
interface DeleteTaskDialogProps {
19+
interface DeleteTaskDialogProps extends AlertDialogProps {
1620
taskId: string
17-
open: boolean
18-
onOpenChange: (open: boolean) => void
1921
}
2022

21-
export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialogProps) => {
22-
const handleDelete = () => {
23-
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
24-
onOpenChange(false)
25-
}
23+
export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
24+
const [isEnterPressed] = useKeyPress("Enter")
25+
26+
const { onOpenChange } = props
27+
28+
const onDelete = useCallback(() => {
29+
if (taskId) {
30+
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
31+
onOpenChange?.(false)
32+
}
33+
}, [taskId, onOpenChange])
34+
35+
useEffect(() => {
36+
if (taskId && isEnterPressed) {
37+
onDelete()
38+
}
39+
}, [taskId, isEnterPressed, onDelete])
2640

2741
return (
28-
<AlertDialog open={open} onOpenChange={onOpenChange}>
29-
<AlertDialogContent>
42+
<AlertDialog {...props}>
43+
<AlertDialogContent onEscapeKeyDown={() => onOpenChange?.(false)}>
3044
<AlertDialogHeader>
3145
<AlertDialogTitle>Delete Task</AlertDialogTitle>
3246
<AlertDialogDescription>
@@ -38,7 +52,7 @@ export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialo
3852
<Button variant="secondary">Cancel</Button>
3953
</AlertDialogCancel>
4054
<AlertDialogAction asChild>
41-
<Button variant="destructive" onClick={handleDelete}>
55+
<Button variant="destructive" onClick={onDelete}>
4256
Delete
4357
</Button>
4458
</AlertDialogAction>

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

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
3838
vscode.postMessage({ type: "showTaskWithId", text: id })
3939
}
4040

41-
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
42-
const [taskToDelete, setTaskToDelete] = useState<string | null>(null)
43-
44-
const handleDeleteHistoryItem = (id: string) => {
45-
setTaskToDelete(id)
46-
setDeleteDialogOpen(true)
47-
}
41+
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
4842

4943
const formatDate = (timestamp: number) => {
5044
const date = new Date(timestamp)
@@ -230,10 +224,15 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
230224
<Button
231225
variant="ghost"
232226
size="sm"
233-
title="Delete Task"
227+
title="Delete Task (Shift + Click to skip confirmation)"
234228
onClick={(e) => {
235229
e.stopPropagation()
236-
handleDeleteHistoryItem(item.id)
230+
231+
if (e.shiftKey) {
232+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
233+
} else {
234+
setDeleteTaskId(item.id)
235+
}
237236
}}>
238237
<span className="codicon codicon-trash" />
239238
{item.size && prettyBytes(item.size)}
@@ -403,17 +402,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
403402
)}
404403
/>
405404
</div>
406-
{taskToDelete && (
407-
<DeleteTaskDialog
408-
taskId={taskToDelete}
409-
open={deleteDialogOpen}
410-
onOpenChange={(open) => {
411-
setDeleteDialogOpen(open)
412-
if (!open) {
413-
setTaskToDelete(null)
414-
}
415-
}}
416-
/>
405+
{deleteTaskId && (
406+
<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
417407
)}
418408
</div>
419409
)

webview-ui/src/components/history/__tests__/HistoryView.test.tsx

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,26 +135,54 @@ describe("HistoryView", () => {
135135
})
136136
})
137137

138-
it("handles task deletion", async () => {
139-
const onDone = jest.fn()
140-
render(<HistoryView onDone={onDone} />)
138+
describe("task deletion", () => {
139+
it("shows confirmation dialog on regular click", () => {
140+
const onDone = jest.fn()
141+
render(<HistoryView onDone={onDone} />)
142+
143+
// Find and hover over first task
144+
const taskContainer = screen.getByTestId("virtuoso-item-1")
145+
fireEvent.mouseEnter(taskContainer)
146+
147+
// Click delete button to open confirmation dialog
148+
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
149+
fireEvent.click(deleteButton)
150+
151+
// Verify dialog is shown
152+
const dialog = screen.getByRole("alertdialog")
153+
expect(dialog).toBeInTheDocument()
154+
155+
// Find and click the confirm delete button in the dialog
156+
const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
157+
fireEvent.click(confirmDeleteButton)
158+
159+
// Verify vscode message was sent
160+
expect(vscode.postMessage).toHaveBeenCalledWith({
161+
type: "deleteTaskWithId",
162+
text: "1",
163+
})
164+
})
141165

142-
// Find and hover over first task
143-
const taskContainer = screen.getByTestId("virtuoso-item-1")
144-
fireEvent.mouseEnter(taskContainer)
166+
it("deletes immediately on shift-click without confirmation", () => {
167+
const onDone = jest.fn()
168+
render(<HistoryView onDone={onDone} />)
145169

146-
// Click delete button to open confirmation dialog
147-
const deleteButton = within(taskContainer).getByTitle("Delete Task")
148-
fireEvent.click(deleteButton)
170+
// Find and hover over first task
171+
const taskContainer = screen.getByTestId("virtuoso-item-1")
172+
fireEvent.mouseEnter(taskContainer)
149173

150-
// Find and click the confirm delete button in the dialog
151-
const confirmDeleteButton = screen.getByRole("button", { name: /delete/i })
152-
fireEvent.click(confirmDeleteButton)
174+
// Shift-click delete button
175+
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
176+
fireEvent.click(deleteButton, { shiftKey: true })
153177

154-
// Verify vscode message was sent
155-
expect(vscode.postMessage).toHaveBeenCalledWith({
156-
type: "deleteTaskWithId",
157-
text: "1",
178+
// Verify no dialog is shown
179+
expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
180+
181+
// Verify vscode message was sent
182+
expect(vscode.postMessage).toHaveBeenCalledWith({
183+
type: "deleteTaskWithId",
184+
text: "1",
185+
})
158186
})
159187
})
160188

0 commit comments

Comments
 (0)