mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Delete task confirmation enhancements
This commit is contained in:
parent
8fae1ca975
commit
21fed4cb79
5 changed files with 149 additions and 74 deletions
5
.changeset/chilly-bugs-pay.md
Normal file
5
.changeset/chilly-bugs-pay.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Delete task confirmation enhancements
|
||||
|
|
@ -3,16 +3,19 @@ import { useWindowSize } from "react-use"
|
|||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { Button } from "@/components/ui"
|
||||
|
||||
import { ClineMessage } from "../../../../src/shared/ExtensionMessage"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
||||
import { formatLargeNumber } from "../../utils/format"
|
||||
import { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import { Button } from "../ui"
|
||||
import { HistoryItem } from "../../../../src/shared/HistoryItem"
|
||||
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import { normalizeApiConfiguration } from "../settings/ApiOptions"
|
||||
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
|
||||
|
||||
interface TaskHeaderProps {
|
||||
task: ClineMessage
|
||||
tokensIn: number
|
||||
|
|
@ -46,7 +49,21 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
const contextWindow = selectedModelInfo?.contextWindow || 1
|
||||
|
||||
/*
|
||||
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.
|
||||
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.
|
||||
|
||||
Sources
|
||||
- https://usehooks-ts.com/react-hook/use-event-listener
|
||||
- https://streamich.github.io/react-use/?path=/story/sensors-useevent--docs
|
||||
|
|
@ -350,27 +367,48 @@ export const highlightMentions = (text?: string, withShadow = true) => {
|
|||
})
|
||||
}
|
||||
|
||||
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
|
||||
<div className="flex flex-row gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Export task history"
|
||||
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
|
||||
<span className="codicon codicon-cloud-download" />
|
||||
</Button>
|
||||
{!!item?.size && item.size > 0 && (
|
||||
const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
|
||||
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Delete task from history"
|
||||
onClick={() => vscode.postMessage({ type: "deleteTaskWithId", text: item.id })}>
|
||||
<span className="codicon codicon-trash" />
|
||||
{prettyBytes(item.size)}
|
||||
title="Export task history"
|
||||
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
|
||||
<span className="codicon codicon-cloud-download" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
{!!item?.size && item.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Delete Task (Shift + Click to skip confirmation)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.shiftKey) {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
|
||||
} else {
|
||||
setDeleteTaskId(item.id)
|
||||
}
|
||||
}}>
|
||||
<span className="codicon codicon-trash" />
|
||||
{prettyBytes(item.size)}
|
||||
</Button>
|
||||
{deleteTaskId && (
|
||||
<DeleteTaskDialog
|
||||
taskId={deleteTaskId}
|
||||
onOpenChange={(open) => !open && setDeleteTaskId(null)}
|
||||
open
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ContextWindowProgress = ({ contextWindow, contextTokens }: { contextWindow: number; contextTokens: number }) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import React from "react"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { useKeyPress } from "react-use"
|
||||
import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -8,25 +11,36 @@ import {
|
|||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button } from "@/components/ui"
|
||||
Button,
|
||||
} from "@/components/ui"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
interface DeleteTaskDialogProps {
|
||||
interface DeleteTaskDialogProps extends AlertDialogProps {
|
||||
taskId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialogProps) => {
|
||||
const handleDelete = () => {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
|
||||
onOpenChange(false)
|
||||
}
|
||||
export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
|
||||
const [isEnterPressed] = useKeyPress("Enter")
|
||||
|
||||
const { onOpenChange } = props
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
if (taskId) {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
|
||||
onOpenChange?.(false)
|
||||
}
|
||||
}, [taskId, onOpenChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (taskId && isEnterPressed) {
|
||||
onDelete()
|
||||
}
|
||||
}, [taskId, isEnterPressed, onDelete])
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialog {...props}>
|
||||
<AlertDialogContent onEscapeKeyDown={() => onOpenChange?.(false)}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Task</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
|
|
@ -38,7 +52,7 @@ export const DeleteTaskDialog = ({ taskId, open, onOpenChange }: DeleteTaskDialo
|
|||
<Button variant="secondary">Cancel</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
|
|
|
|||
|
|
@ -38,13 +38,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
vscode.postMessage({ type: "showTaskWithId", text: id })
|
||||
}
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [taskToDelete, setTaskToDelete] = useState<string | null>(null)
|
||||
|
||||
const handleDeleteHistoryItem = (id: string) => {
|
||||
setTaskToDelete(id)
|
||||
setDeleteDialogOpen(true)
|
||||
}
|
||||
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
|
|
@ -230,10 +224,15 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
title="Delete Task"
|
||||
title="Delete Task (Shift + Click to skip confirmation)"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
|
||||
if (e.shiftKey) {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
|
||||
} else {
|
||||
setDeleteTaskId(item.id)
|
||||
}
|
||||
}}>
|
||||
<span className="codicon codicon-trash" />
|
||||
{item.size && prettyBytes(item.size)}
|
||||
|
|
@ -403,17 +402,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{taskToDelete && (
|
||||
<DeleteTaskDialog
|
||||
taskId={taskToDelete}
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setDeleteDialogOpen(open)
|
||||
if (!open) {
|
||||
setTaskToDelete(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{deleteTaskId && (
|
||||
<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -135,26 +135,54 @@ describe("HistoryView", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("handles task deletion", async () => {
|
||||
const onDone = jest.fn()
|
||||
render(<HistoryView onDone={onDone} />)
|
||||
describe("task deletion", () => {
|
||||
it("shows confirmation dialog on regular click", () => {
|
||||
const onDone = jest.fn()
|
||||
render(<HistoryView onDone={onDone} />)
|
||||
|
||||
// Find and hover over first task
|
||||
const taskContainer = screen.getByTestId("virtuoso-item-1")
|
||||
fireEvent.mouseEnter(taskContainer)
|
||||
// Find and hover over first task
|
||||
const taskContainer = screen.getByTestId("virtuoso-item-1")
|
||||
fireEvent.mouseEnter(taskContainer)
|
||||
|
||||
// Click delete button to open confirmation dialog
|
||||
const deleteButton = within(taskContainer).getByTitle("Delete Task")
|
||||
fireEvent.click(deleteButton)
|
||||
// Click delete button to open confirmation dialog
|
||||
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
// Find and click the confirm delete button in the dialog
|
||||
const confirmDeleteButton = screen.getByRole("button", { name: /delete/i })
|
||||
fireEvent.click(confirmDeleteButton)
|
||||
// Verify dialog is shown
|
||||
const dialog = screen.getByRole("alertdialog")
|
||||
expect(dialog).toBeInTheDocument()
|
||||
|
||||
// Verify vscode message was sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteTaskWithId",
|
||||
text: "1",
|
||||
// Find and click the confirm delete button in the dialog
|
||||
const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
|
||||
fireEvent.click(confirmDeleteButton)
|
||||
|
||||
// Verify vscode message was sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteTaskWithId",
|
||||
text: "1",
|
||||
})
|
||||
})
|
||||
|
||||
it("deletes immediately on shift-click without confirmation", () => {
|
||||
const onDone = jest.fn()
|
||||
render(<HistoryView onDone={onDone} />)
|
||||
|
||||
// Find and hover over first task
|
||||
const taskContainer = screen.getByTestId("virtuoso-item-1")
|
||||
fireEvent.mouseEnter(taskContainer)
|
||||
|
||||
// Shift-click delete button
|
||||
const deleteButton = within(taskContainer).getByTitle("Delete Task (Shift + Click to skip confirmation)")
|
||||
fireEvent.click(deleteButton, { shiftKey: true })
|
||||
|
||||
// Verify no dialog is shown
|
||||
expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
|
||||
|
||||
// Verify vscode message was sent
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "deleteTaskWithId",
|
||||
text: "1",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue