mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add star feature for tasks to prevent accidental deletion
- Add starred field to HistoryItem type - Add star button UI component to task items - Implement toggle star functionality - Sort starred tasks to the top of the list - Prevent deletion of starred tasks with warning messages - Add batch delete protection for starred tasks - Add localization support for star-related messages Fixes #6244
This commit is contained in:
parent
62160758fc
commit
67b29f2731
9 changed files with 144 additions and 24 deletions
|
|
@ -16,6 +16,7 @@ export const historyItemSchema = z.object({
|
|||
totalCost: z.number(),
|
||||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
starred: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type HistoryItem = z.infer<typeof historyItemSchema>
|
||||
|
|
|
|||
|
|
@ -440,6 +440,17 @@ export const webviewMessageHandler = async (
|
|||
case "deleteTaskWithId":
|
||||
provider.deleteTaskWithId(message.text!)
|
||||
break
|
||||
case "toggleTaskStar":
|
||||
if (message.text) {
|
||||
const taskHistory = provider.getValue("taskHistory") || []
|
||||
const taskIndex = taskHistory.findIndex((task) => task.id === message.text)
|
||||
if (taskIndex !== -1) {
|
||||
taskHistory[taskIndex].starred = !taskHistory[taskIndex].starred
|
||||
await provider.setValue("taskHistory", taskHistory)
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
}
|
||||
break
|
||||
case "deleteMultipleTasksWithIds": {
|
||||
const ids = message.ids
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface WebviewMessage {
|
|||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "importSettings"
|
||||
| "toggleTaskStar"
|
||||
| "exportSettings"
|
||||
| "resetState"
|
||||
| "flushRouterModels"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from "react"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import {
|
||||
AlertDialog,
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "@/components/ui"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
interface BatchDeleteTaskDialogProps extends AlertDialogProps {
|
||||
taskIds: string[]
|
||||
|
|
@ -21,13 +22,31 @@ interface BatchDeleteTaskDialogProps extends AlertDialogProps {
|
|||
export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDialogProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const { onOpenChange } = props
|
||||
const { taskHistory } = useExtensionState()
|
||||
|
||||
// Filter out starred tasks
|
||||
const { deletableTaskIds, starredCount } = useMemo(() => {
|
||||
const deletable: string[] = []
|
||||
let starred = 0
|
||||
|
||||
taskIds.forEach((id) => {
|
||||
const task = taskHistory.find((item) => item.id === id)
|
||||
if (task?.starred) {
|
||||
starred++
|
||||
} else {
|
||||
deletable.push(id)
|
||||
}
|
||||
})
|
||||
|
||||
return { deletableTaskIds: deletable, starredCount: starred }
|
||||
}, [taskIds, taskHistory])
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
if (taskIds.length > 0) {
|
||||
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: taskIds })
|
||||
if (deletableTaskIds.length > 0) {
|
||||
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: deletableTaskIds })
|
||||
onOpenChange?.(false)
|
||||
}
|
||||
}, [taskIds, onOpenChange])
|
||||
}, [deletableTaskIds, onOpenChange])
|
||||
|
||||
return (
|
||||
<AlertDialog {...props}>
|
||||
|
|
@ -35,22 +54,41 @@ export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDial
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("history:deleteTasks")}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-vscode-foreground">
|
||||
<div className="mb-2">{t("history:confirmDeleteTasks", { count: taskIds.length })}</div>
|
||||
<div className="text-vscode-editor-foreground bg-vscode-editor-background p-2 rounded text-sm">
|
||||
{t("history:deleteTasksWarning")}
|
||||
</div>
|
||||
{starredCount > 0 ? (
|
||||
<>
|
||||
<div className="mb-2 text-vscode-notificationsWarningIcon-foreground">
|
||||
{t("history:starredTasksExcluded", { count: starredCount })}
|
||||
</div>
|
||||
{deletableTaskIds.length > 0 && (
|
||||
<div className="mb-2">
|
||||
{t("history:confirmDeleteTasks", { count: deletableTaskIds.length })}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mb-2">
|
||||
{t("history:confirmDeleteTasks", { count: deletableTaskIds.length })}
|
||||
</div>
|
||||
)}
|
||||
{deletableTaskIds.length > 0 && (
|
||||
<div className="text-vscode-editor-foreground bg-vscode-editor-background p-2 rounded text-sm">
|
||||
{t("history:deleteTasksWarning")}
|
||||
</div>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="secondary">{t("history:cancel")}</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
<span className="codicon codicon-trash mr-1"></span>
|
||||
{t("history:deleteItems", { count: taskIds.length })}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
{deletableTaskIds.length > 0 && (
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
<span className="codicon codicon-trash mr-1"></span>
|
||||
{t("history:deleteItems", { count: deletableTaskIds.length })}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Button,
|
||||
} from "@/components/ui"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
|
|
@ -24,9 +25,14 @@ interface DeleteTaskDialogProps extends AlertDialogProps {
|
|||
export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [isEnterPressed] = useKeyPress("Enter")
|
||||
const { taskHistory } = useExtensionState()
|
||||
|
||||
const { onOpenChange } = props
|
||||
|
||||
// Check if the task is starred
|
||||
const task = taskHistory.find((item) => item.id === taskId)
|
||||
const isStarred = task?.starred || false
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
if (taskId) {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
|
||||
|
|
@ -35,27 +41,33 @@ export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) =>
|
|||
}, [taskId, onOpenChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (taskId && isEnterPressed) {
|
||||
if (taskId && isEnterPressed && !isStarred) {
|
||||
onDelete()
|
||||
}
|
||||
}, [taskId, isEnterPressed, onDelete])
|
||||
}, [taskId, isEnterPressed, isStarred, onDelete])
|
||||
|
||||
return (
|
||||
<AlertDialog {...props}>
|
||||
<AlertDialogContent onEscapeKeyDown={() => onOpenChange?.(false)}>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("history:deleteTask")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{t("history:deleteTaskMessage")}</AlertDialogDescription>
|
||||
<AlertDialogTitle>
|
||||
{isStarred ? t("history:deleteStarredTask") : t("history:deleteTask")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{isStarred ? t("history:deleteStarredTaskMessage") : t("history:deleteTaskMessage")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel asChild>
|
||||
<Button variant="secondary">{t("history:cancel")}</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
{t("history:delete")}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
{!isStarred && (
|
||||
<AlertDialogAction asChild>
|
||||
<Button variant="destructive" onClick={onDelete}>
|
||||
{t("history:delete")}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
|
|
|||
40
webview-ui/src/components/history/StarButton.tsx
Normal file
40
webview-ui/src/components/history/StarButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
|
||||
interface StarButtonProps {
|
||||
itemId: string
|
||||
isStarred?: boolean
|
||||
onToggleStar: (itemId: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const StarButton: React.FC<StarButtonProps> = ({ itemId, isStarred, onToggleStar, className }) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onToggleStar(itemId)
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardTooltip content={isStarred ? t("history:unstarTask") : t("history:starTask")}>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors",
|
||||
"focus:outline-none focus:ring-1 focus:ring-vscode-focusBorder",
|
||||
className,
|
||||
)}
|
||||
aria-label={isStarred ? t("history:unstarTask") : t("history:starTask")}>
|
||||
<span
|
||||
className={cn("codicon", {
|
||||
"codicon-star-full text-vscode-notificationsWarningIcon-foreground": isStarred,
|
||||
"codicon-star-empty": !isStarred,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ import React from "react"
|
|||
import type { HistoryItem } from "@roo-code/types"
|
||||
import { formatDate } from "@/utils/format"
|
||||
import { DeleteButton } from "./DeleteButton"
|
||||
import { StarButton } from "./StarButton"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
export interface TaskItemHeaderProps {
|
||||
item: HistoryItem
|
||||
|
|
@ -11,12 +13,16 @@ export interface TaskItemHeaderProps {
|
|||
}
|
||||
|
||||
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
|
||||
const handleToggleStar = (itemId: string) => {
|
||||
vscode.postMessage({ type: "toggleTaskStar", text: itemId })
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex justify-between items-center", {
|
||||
// this is to balance out the margin when we don't have a delete button
|
||||
// because the delete button sorta pushes the date up due to its size
|
||||
"mb-1": !onDelete,
|
||||
"mb-1": !onDelete && !item.starred,
|
||||
})}>
|
||||
<div className="flex items-center flex-wrap gap-x-2 text-xs">
|
||||
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
|
||||
|
|
@ -27,6 +33,7 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
|
|||
{/* Action Buttons */}
|
||||
{!isSelectionMode && (
|
||||
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
|
||||
<StarButton itemId={item.id} isStarred={item.starred} onToggleStar={handleToggleStar} />
|
||||
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ export const useTaskSearch = () => {
|
|||
|
||||
// Then sort the results
|
||||
return [...results].sort((a, b) => {
|
||||
// Always sort starred items to the top
|
||||
if (a.starred && !b.starred) return -1
|
||||
if (!a.starred && b.starred) return 1
|
||||
|
||||
// If both are starred or both are not starred, apply the selected sort
|
||||
switch (sortOption) {
|
||||
case "oldest":
|
||||
return (a.ts || 0) - (b.ts || 0)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@
|
|||
"exportTask": "Export Task",
|
||||
"deleteTask": "Delete Task",
|
||||
"deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.",
|
||||
"deleteStarredTask": "Cannot Delete Starred Task",
|
||||
"deleteStarredTaskMessage": "This task is starred and cannot be deleted. Please unstar it first if you want to delete it.",
|
||||
"starTask": "Star task",
|
||||
"unstarTask": "Unstar task",
|
||||
"starredTasksExcluded": "{{count}} starred task(s) will not be deleted",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"exitSelection": "Exit Selection",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue