Skip to content

Commit 67b29f2

Browse files
committed
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
1 parent 6216075 commit 67b29f2

File tree

9 files changed

+144
-24
lines changed

9 files changed

+144
-24
lines changed

packages/types/src/history.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const historyItemSchema = z.object({
1616
totalCost: z.number(),
1717
size: z.number().optional(),
1818
workspace: z.string().optional(),
19+
starred: z.boolean().optional(),
1920
})
2021

2122
export type HistoryItem = z.infer<typeof historyItemSchema>

src/core/webview/webviewMessageHandler.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,17 @@ export const webviewMessageHandler = async (
440440
case "deleteTaskWithId":
441441
provider.deleteTaskWithId(message.text!)
442442
break
443+
case "toggleTaskStar":
444+
if (message.text) {
445+
const taskHistory = provider.getValue("taskHistory") || []
446+
const taskIndex = taskHistory.findIndex((task) => task.id === message.text)
447+
if (taskIndex !== -1) {
448+
taskHistory[taskIndex].starred = !taskHistory[taskIndex].starred
449+
await provider.setValue("taskHistory", taskHistory)
450+
await provider.postStateToWebview()
451+
}
452+
}
453+
break
443454
case "deleteMultipleTasksWithIds": {
444455
const ids = message.ids
445456

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface WebviewMessage {
5959
| "deleteTaskWithId"
6060
| "exportTaskWithId"
6161
| "importSettings"
62+
| "toggleTaskStar"
6263
| "exportSettings"
6364
| "resetState"
6465
| "flushRouterModels"

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

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from "react"
1+
import { useCallback, useMemo } from "react"
22
import { useAppTranslation } from "@/i18n/TranslationContext"
33
import {
44
AlertDialog,
@@ -13,6 +13,7 @@ import {
1313
} from "@/components/ui"
1414
import { vscode } from "@/utils/vscode"
1515
import { AlertDialogProps } from "@radix-ui/react-alert-dialog"
16+
import { useExtensionState } from "@/context/ExtensionStateContext"
1617

1718
interface BatchDeleteTaskDialogProps extends AlertDialogProps {
1819
taskIds: string[]
@@ -21,36 +22,73 @@ interface BatchDeleteTaskDialogProps extends AlertDialogProps {
2122
export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDialogProps) => {
2223
const { t } = useAppTranslation()
2324
const { onOpenChange } = props
25+
const { taskHistory } = useExtensionState()
26+
27+
// Filter out starred tasks
28+
const { deletableTaskIds, starredCount } = useMemo(() => {
29+
const deletable: string[] = []
30+
let starred = 0
31+
32+
taskIds.forEach((id) => {
33+
const task = taskHistory.find((item) => item.id === id)
34+
if (task?.starred) {
35+
starred++
36+
} else {
37+
deletable.push(id)
38+
}
39+
})
40+
41+
return { deletableTaskIds: deletable, starredCount: starred }
42+
}, [taskIds, taskHistory])
2443

2544
const onDelete = useCallback(() => {
26-
if (taskIds.length > 0) {
27-
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: taskIds })
45+
if (deletableTaskIds.length > 0) {
46+
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: deletableTaskIds })
2847
onOpenChange?.(false)
2948
}
30-
}, [taskIds, onOpenChange])
49+
}, [deletableTaskIds, onOpenChange])
3150

3251
return (
3352
<AlertDialog {...props}>
3453
<AlertDialogContent className="max-w-md">
3554
<AlertDialogHeader>
3655
<AlertDialogTitle>{t("history:deleteTasks")}</AlertDialogTitle>
3756
<AlertDialogDescription className="text-vscode-foreground">
38-
<div className="mb-2">{t("history:confirmDeleteTasks", { count: taskIds.length })}</div>
39-
<div className="text-vscode-editor-foreground bg-vscode-editor-background p-2 rounded text-sm">
40-
{t("history:deleteTasksWarning")}
41-
</div>
57+
{starredCount > 0 ? (
58+
<>
59+
<div className="mb-2 text-vscode-notificationsWarningIcon-foreground">
60+
{t("history:starredTasksExcluded", { count: starredCount })}
61+
</div>
62+
{deletableTaskIds.length > 0 && (
63+
<div className="mb-2">
64+
{t("history:confirmDeleteTasks", { count: deletableTaskIds.length })}
65+
</div>
66+
)}
67+
</>
68+
) : (
69+
<div className="mb-2">
70+
{t("history:confirmDeleteTasks", { count: deletableTaskIds.length })}
71+
</div>
72+
)}
73+
{deletableTaskIds.length > 0 && (
74+
<div className="text-vscode-editor-foreground bg-vscode-editor-background p-2 rounded text-sm">
75+
{t("history:deleteTasksWarning")}
76+
</div>
77+
)}
4278
</AlertDialogDescription>
4379
</AlertDialogHeader>
4480
<AlertDialogFooter>
4581
<AlertDialogCancel asChild>
4682
<Button variant="secondary">{t("history:cancel")}</Button>
4783
</AlertDialogCancel>
48-
<AlertDialogAction asChild>
49-
<Button variant="destructive" onClick={onDelete}>
50-
<span className="codicon codicon-trash mr-1"></span>
51-
{t("history:deleteItems", { count: taskIds.length })}
52-
</Button>
53-
</AlertDialogAction>
84+
{deletableTaskIds.length > 0 && (
85+
<AlertDialogAction asChild>
86+
<Button variant="destructive" onClick={onDelete}>
87+
<span className="codicon codicon-trash mr-1"></span>
88+
{t("history:deleteItems", { count: deletableTaskIds.length })}
89+
</Button>
90+
</AlertDialogAction>
91+
)}
5492
</AlertDialogFooter>
5593
</AlertDialogContent>
5694
</AlertDialog>

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Button,
1515
} from "@/components/ui"
1616
import { useAppTranslation } from "@/i18n/TranslationContext"
17+
import { useExtensionState } from "@/context/ExtensionStateContext"
1718

1819
import { vscode } from "@/utils/vscode"
1920

@@ -24,9 +25,14 @@ interface DeleteTaskDialogProps extends AlertDialogProps {
2425
export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) => {
2526
const { t } = useAppTranslation()
2627
const [isEnterPressed] = useKeyPress("Enter")
28+
const { taskHistory } = useExtensionState()
2729

2830
const { onOpenChange } = props
2931

32+
// Check if the task is starred
33+
const task = taskHistory.find((item) => item.id === taskId)
34+
const isStarred = task?.starred || false
35+
3036
const onDelete = useCallback(() => {
3137
if (taskId) {
3238
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
@@ -35,27 +41,33 @@ export const DeleteTaskDialog = ({ taskId, ...props }: DeleteTaskDialogProps) =>
3541
}, [taskId, onOpenChange])
3642

3743
useEffect(() => {
38-
if (taskId && isEnterPressed) {
44+
if (taskId && isEnterPressed && !isStarred) {
3945
onDelete()
4046
}
41-
}, [taskId, isEnterPressed, onDelete])
47+
}, [taskId, isEnterPressed, isStarred, onDelete])
4248

4349
return (
4450
<AlertDialog {...props}>
4551
<AlertDialogContent onEscapeKeyDown={() => onOpenChange?.(false)}>
4652
<AlertDialogHeader>
47-
<AlertDialogTitle>{t("history:deleteTask")}</AlertDialogTitle>
48-
<AlertDialogDescription>{t("history:deleteTaskMessage")}</AlertDialogDescription>
53+
<AlertDialogTitle>
54+
{isStarred ? t("history:deleteStarredTask") : t("history:deleteTask")}
55+
</AlertDialogTitle>
56+
<AlertDialogDescription>
57+
{isStarred ? t("history:deleteStarredTaskMessage") : t("history:deleteTaskMessage")}
58+
</AlertDialogDescription>
4959
</AlertDialogHeader>
5060
<AlertDialogFooter>
5161
<AlertDialogCancel asChild>
5262
<Button variant="secondary">{t("history:cancel")}</Button>
5363
</AlertDialogCancel>
54-
<AlertDialogAction asChild>
55-
<Button variant="destructive" onClick={onDelete}>
56-
{t("history:delete")}
57-
</Button>
58-
</AlertDialogAction>
64+
{!isStarred && (
65+
<AlertDialogAction asChild>
66+
<Button variant="destructive" onClick={onDelete}>
67+
{t("history:delete")}
68+
</Button>
69+
</AlertDialogAction>
70+
)}
5971
</AlertDialogFooter>
6072
</AlertDialogContent>
6173
</AlertDialog>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from "react"
2+
import { cn } from "@/lib/utils"
3+
import { StandardTooltip } from "@/components/ui"
4+
import { useAppTranslation } from "@/i18n/TranslationContext"
5+
6+
interface StarButtonProps {
7+
itemId: string
8+
isStarred?: boolean
9+
onToggleStar: (itemId: string) => void
10+
className?: string
11+
}
12+
13+
export const StarButton: React.FC<StarButtonProps> = ({ itemId, isStarred, onToggleStar, className }) => {
14+
const { t } = useAppTranslation()
15+
16+
const handleClick = (e: React.MouseEvent) => {
17+
e.stopPropagation()
18+
onToggleStar(itemId)
19+
}
20+
21+
return (
22+
<StandardTooltip content={isStarred ? t("history:unstarTask") : t("history:starTask")}>
23+
<button
24+
onClick={handleClick}
25+
className={cn(
26+
"p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors",
27+
"focus:outline-none focus:ring-1 focus:ring-vscode-focusBorder",
28+
className,
29+
)}
30+
aria-label={isStarred ? t("history:unstarTask") : t("history:starTask")}>
31+
<span
32+
className={cn("codicon", {
33+
"codicon-star-full text-vscode-notificationsWarningIcon-foreground": isStarred,
34+
"codicon-star-empty": !isStarred,
35+
})}
36+
/>
37+
</button>
38+
</StandardTooltip>
39+
)
40+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import React from "react"
22
import type { HistoryItem } from "@roo-code/types"
33
import { formatDate } from "@/utils/format"
44
import { DeleteButton } from "./DeleteButton"
5+
import { StarButton } from "./StarButton"
56
import { cn } from "@/lib/utils"
7+
import { vscode } from "@/utils/vscode"
68

79
export interface TaskItemHeaderProps {
810
item: HistoryItem
@@ -11,12 +13,16 @@ export interface TaskItemHeaderProps {
1113
}
1214

1315
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
16+
const handleToggleStar = (itemId: string) => {
17+
vscode.postMessage({ type: "toggleTaskStar", text: itemId })
18+
}
19+
1420
return (
1521
<div
1622
className={cn("flex justify-between items-center", {
1723
// this is to balance out the margin when we don't have a delete button
1824
// because the delete button sorta pushes the date up due to its size
19-
"mb-1": !onDelete,
25+
"mb-1": !onDelete && !item.starred,
2026
})}>
2127
<div className="flex items-center flex-wrap gap-x-2 text-xs">
2228
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
@@ -27,6 +33,7 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
2733
{/* Action Buttons */}
2834
{!isSelectionMode && (
2935
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
36+
<StarButton itemId={item.id} isStarred={item.starred} onToggleStar={handleToggleStar} />
3037
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
3138
</div>
3239
)}

webview-ui/src/components/history/useTaskSearch.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const useTaskSearch = () => {
5959

6060
// Then sort the results
6161
return [...results].sort((a, b) => {
62+
// Always sort starred items to the top
63+
if (a.starred && !b.starred) return -1
64+
if (!a.starred && b.starred) return 1
65+
66+
// If both are starred or both are not starred, apply the selected sort
6267
switch (sortOption) {
6368
case "oldest":
6469
return (a.ts || 0) - (b.ts || 0)

webview-ui/src/i18n/locales/en/history.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"exportTask": "Export Task",
1616
"deleteTask": "Delete Task",
1717
"deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.",
18+
"deleteStarredTask": "Cannot Delete Starred Task",
19+
"deleteStarredTaskMessage": "This task is starred and cannot be deleted. Please unstar it first if you want to delete it.",
20+
"starTask": "Star task",
21+
"unstarTask": "Unstar task",
22+
"starredTasksExcluded": "{{count}} starred task(s) will not be deleted",
1823
"cancel": "Cancel",
1924
"delete": "Delete",
2025
"exitSelection": "Exit Selection",

0 commit comments

Comments
 (0)