Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const globalSettingsSchema = z.object({
lastShownAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
taskHistory: z.array(historyItemSchema).optional(),
starredTaskIds: z.array(z.string()).optional(),

condensingApiConfigId: z.string().optional(),
customCondensingPrompt: z.string().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const historyItemSchema = z.object({
totalCost: z.number(),
size: z.number().optional(),
workspace: z.string().optional(),
isStarred: z.boolean().optional(),
})

export type HistoryItem = z.infer<typeof historyItemSchema>
6 changes: 5 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,11 @@ export class ClineProvider
clineMessages: this.getCurrentCline()?.clineMessages || [],
taskHistory: (taskHistory || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts)
.map((item: HistoryItem) => ({
...item,
isStarred: (this.getGlobalState("starredTaskIds") || []).includes(item.id),
})),
soundEnabled: soundEnabled ?? false,
ttsEnabled: ttsEnabled ?? false,
ttsSpeed: ttsSpeed ?? 1.0,
Expand Down
32 changes: 32 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,38 @@ export const webviewMessageHandler = async (
break
}

case "toggleTaskStar": {
if (message.text) {
const taskId = message.text
const starredTaskIds = getGlobalState("starredTaskIds") || []
const isCurrentlyStarred = starredTaskIds.includes(taskId)

let updatedStarredTaskIds: string[]
if (isCurrentlyStarred) {
// Unstar the task
updatedStarredTaskIds = starredTaskIds.filter((id) => id !== taskId)
} else {
// Star the task
updatedStarredTaskIds = [...starredTaskIds, taskId]
}

await updateGlobalState("starredTaskIds", updatedStarredTaskIds)

// Update the task history to reflect the starred status
const taskHistory = getGlobalState("taskHistory") || []
const updatedTaskHistory = taskHistory.map((task) => {
if (task.id === taskId) {
return { ...task, isStarred: !isCurrentlyStarred }
}
return task
})
await updateGlobalState("taskHistory", updatedTaskHistory)

await provider.postStateToWebview()
}
break
}

case "switchTab": {
if (message.tab) {
// Capture tab shown event for all switchTab messages (which are user-initiated)
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface WebviewMessage {
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
| "toggleTaskStar"
| "importSettings"
| "exportSettings"
| "resetState"
Expand Down
65 changes: 51 additions & 14 deletions webview-ui/src/components/history/BatchDeleteTaskDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from "react"
import { useCallback, useMemo } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import {
AlertDialog,
Expand All @@ -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[]
Expand All @@ -21,36 +22,72 @@ interface BatchDeleteTaskDialogProps extends AlertDialogProps {
export const BatchDeleteTaskDialog = ({ taskIds, ...props }: BatchDeleteTaskDialogProps) => {
const { t } = useAppTranslation()
const { onOpenChange } = props
const { taskHistory } = useExtensionState()

// Check if any of the selected tasks are starred
const starredTaskIds = useMemo(() => {
return taskIds.filter((id) => {
const task = taskHistory.find((t) => t.id === id)
return task?.isStarred || false
})
}, [taskIds, taskHistory])

const hasStarredTasks = starredTaskIds.length > 0
const unstarredTaskIds = taskIds.filter((id) => !starredTaskIds.includes(id))

const onDelete = useCallback(() => {
if (taskIds.length > 0) {
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: taskIds })
if (unstarredTaskIds.length > 0) {
vscode.postMessage({ type: "deleteMultipleTasksWithIds", ids: unstarredTaskIds })
onOpenChange?.(false)
}
}, [taskIds, onOpenChange])
}, [unstarredTaskIds, onOpenChange])

return (
<AlertDialog {...props}>
<AlertDialogContent className="max-w-md">
<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>
{hasStarredTasks ? (
<>
<div className="mb-2 text-vscode-errorForeground">
{starredTaskIds.length === taskIds.length
? "All selected tasks are starred. Please unstar them before deleting."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline string for starred tasks (e.g. 'All selected tasks are starred. Please unstar them before deleting.') is hardcoded. For proper internationalization, use the translation function (t) with a translation key.

Suggested change
? "All selected tasks are starred. Please unstar them before deleting."
? t("history:allTasksStarredUnstarBeforeDelete")

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

: `${starredTaskIds.length} of ${taskIds.length} selected tasks are starred and will not be deleted.`}
</div>
{unstarredTaskIds.length > 0 && (
<>
<div className="mb-2">
{t("history:confirmDeleteTasks", { count: unstarredTaskIds.length })}
</div>
<div className="text-vscode-editor-foreground bg-vscode-editor-background p-2 rounded text-sm">
{t("history:deleteTasksWarning")}
</div>
</>
)}
</>
) : (
<>
<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>
</>
)}
</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>
{unstarredTaskIds.length > 0 && (
<AlertDialogAction asChild>
<Button variant="destructive" onClick={onDelete}>
<span className="codicon codicon-trash mr-1"></span>
{t("history:deleteItems", { count: unstarredTaskIds.length })}
</Button>
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Expand Down
32 changes: 22 additions & 10 deletions webview-ui/src/components/history/DeleteTaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Button,
} from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useExtensionState } from "@/context/ExtensionStateContext"

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

Expand All @@ -24,38 +25,49 @@ 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((t) => t.id === taskId)
const isStarred = task?.isStarred || false

const onDelete = useCallback(() => {
if (taskId) {
if (taskId && !isStarred) {
vscode.postMessage({ type: "deleteTaskWithId", text: taskId })
onOpenChange?.(false)
}
}, [taskId, onOpenChange])
}, [taskId, isStarred, 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>
<AlertDialogDescription>
{isStarred
? "This task is starred. Please unstar it before deleting."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message for starred tasks ('This task is starred. Please unstar it before deleting.') is hardcoded. It should be translated using t() for consistency with i18n requirements.

Suggested change
? "This task is starred. Please unstar it before deleting."
? t("history:starredTaskDeleteMessage")

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

: 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>
Expand Down
19 changes: 18 additions & 1 deletion webview-ui/src/components/history/TaskItemHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { HistoryItem } from "@roo-code/types"
import { formatDate } from "@/utils/format"
import { DeleteButton } from "./DeleteButton"
import { cn } from "@/lib/utils"
import { vscode } from "@/utils/vscode"

export interface TaskItemHeaderProps {
item: HistoryItem
Expand All @@ -11,22 +12,38 @@ export interface TaskItemHeaderProps {
}

const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
const handleStarClick = (e: React.MouseEvent) => {
e.stopPropagation()
vscode.postMessage({ type: "toggleTaskStar", text: item.id })
}

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.isStarred,
})}>
<div className="flex items-center flex-wrap gap-x-2 text-xs">
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
{formatDate(item.ts)}
</span>
{item.isStarred && (
<span className="text-vscode-textPreformat-foreground" title="Starred task">
<i className="codicon codicon-star-full" />
</span>
)}
</div>

{/* Action Buttons */}
{!isSelectionMode && (
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
<button
onClick={handleStarClick}
className="p-1 hover:bg-vscode-toolbar-hoverBackground rounded"
title={item.isStarred ? "Unstar task" : "Star task"}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title attribute on the star button uses inline text ('Star task'/'Unstar task'). Replace these with t()-based translation keys for proper internationalization.

Suggested change
title={item.isStarred ? "Unstar task" : "Star task"}>
title={item.isStarred ? t('unstarTask') : t('starTask')}>

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

<i className={cn("codicon", item.isStarred ? "codicon-star-full" : "codicon-star-empty")} />
</button>
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ vi.mock("@/i18n/TranslationContext", () => ({
}),
}))

vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
taskHistory: [
{ id: "task-1", isStarred: false },
{ id: "task-2", isStarred: false },
{ id: "task-3", isStarred: false },
],
}),
}))

describe("BatchDeleteTaskDialog", () => {
const mockTaskIds = ["task-1", "task-2", "task-3"]
const mockOnOpenChange = vi.fn()
Expand Down Expand Up @@ -65,8 +75,13 @@ describe("BatchDeleteTaskDialog", () => {
it("does not call vscode.postMessage when taskIds is empty", () => {
render(<BatchDeleteTaskDialog taskIds={[]} open={true} onOpenChange={mockOnOpenChange} />)

const deleteButton = screen.getByText("Delete 0 items")
fireEvent.click(deleteButton)
// When there are no tasks, there should be no delete button
const deleteButton = screen.queryByText("Delete 0 items")
expect(deleteButton).not.toBeInTheDocument()

// Cancel button should still work
const cancelButton = screen.getByText("Cancel")
fireEvent.click(cancelButton)

expect(vscode.postMessage).not.toHaveBeenCalled()
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ vi.mock("react-use", () => ({
useKeyPress: vi.fn(),
}))

vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
taskHistory: [
{ id: "test-task-id", isStarred: false },
{ id: "starred-task-id", isStarred: true },
],
}),
}))

import { useKeyPress } from "react-use"

const mockUseKeyPress = useKeyPress as any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ describe("TaskItemHeader", () => {
it("shows delete button when not in selection mode", () => {
render(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={vi.fn()} />)

expect(screen.getByRole("button")).toBeInTheDocument()
expect(screen.getByTestId("delete-task-button")).toBeInTheDocument()
})

it("shows star button when not in selection mode", () => {
render(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={vi.fn()} />)

const starButton = screen.getByTitle("Star task")
expect(starButton).toBeInTheDocument()
})
})
Loading