diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 8c75024879c..a11a3f21614 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -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 diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 82c780adf47..29979b163d9 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e2765222..88e7a7a4fcf 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -59,6 +59,7 @@ export interface WebviewMessage { | "deleteTaskWithId" | "exportTaskWithId" | "importSettings" + | "toggleTaskStar" | "exportSettings" | "resetState" | "flushRouterModels" diff --git a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx index decc905315a..68aa9922b59 100644 --- a/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx +++ b/webview-ui/src/components/history/BatchDeleteTaskDialog.tsx @@ -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 ( @@ -35,22 +54,41 @@ export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDial {t("history:deleteTasks")} -
{t("history:confirmDeleteTasks", { count: taskIds.length })}
-
- {t("history:deleteTasksWarning")} -
+ {starredCount > 0 ? ( + <> +
+ {t("history:starredTasksExcluded", { count: starredCount })} +
+ {deletableTaskIds.length > 0 && ( +
+ {t("history:confirmDeleteTasks", { count: deletableTaskIds.length })} +
+ )} + + ) : ( +
+ {t("history:confirmDeleteTasks", { count: deletableTaskIds.length })} +
+ )} + {deletableTaskIds.length > 0 && ( +
+ {t("history:deleteTasksWarning")} +
+ )}
- - - + {deletableTaskIds.length > 0 && ( + + + + )}
diff --git a/webview-ui/src/components/history/DeleteTaskDialog.tsx b/webview-ui/src/components/history/DeleteTaskDialog.tsx index d0e3ab16a4d..21964a68ef1 100644 --- a/webview-ui/src/components/history/DeleteTaskDialog.tsx +++ b/webview-ui/src/components/history/DeleteTaskDialog.tsx @@ -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 ( onOpenChange?.(false)}> - {t("history:deleteTask")} - {t("history:deleteTaskMessage")} + + {isStarred ? t("history:deleteStarredTask") : t("history:deleteTask")} + + + {isStarred ? t("history:deleteStarredTaskMessage") : t("history:deleteTaskMessage")} + - - - + {!isStarred && ( + + + + )} diff --git a/webview-ui/src/components/history/StarButton.tsx b/webview-ui/src/components/history/StarButton.tsx new file mode 100644 index 00000000000..016cdd7ff2f --- /dev/null +++ b/webview-ui/src/components/history/StarButton.tsx @@ -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 = ({ itemId, isStarred, onToggleStar, className }) => { + const { t } = useAppTranslation() + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation() + onToggleStar(itemId) + } + + return ( + + + + ) +} diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx index bdddb090c86..f3bed921bd9 100644 --- a/webview-ui/src/components/history/TaskItemHeader.tsx +++ b/webview-ui/src/components/history/TaskItemHeader.tsx @@ -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 = ({ item, isSelectionMode, onDelete }) => { + const handleToggleStar = (itemId: string) => { + vscode.postMessage({ type: "toggleTaskStar", text: itemId }) + } + return (
@@ -27,6 +33,7 @@ const TaskItemHeader: React.FC = ({ item, isSelectionMode, {/* Action Buttons */} {!isSelectionMode && (
+ {onDelete && }
)} diff --git a/webview-ui/src/components/history/useTaskSearch.ts b/webview-ui/src/components/history/useTaskSearch.ts index 3969985b98a..8819197dea4 100644 --- a/webview-ui/src/components/history/useTaskSearch.ts +++ b/webview-ui/src/components/history/useTaskSearch.ts @@ -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) diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json index 8d004331709..db38fee557e 100644 --- a/webview-ui/src/i18n/locales/en/history.json +++ b/webview-ui/src/i18n/locales/en/history.json @@ -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",