From 29e1c3991aa1e6be3a27e821d049204af12978df Mon Sep 17 00:00:00 2001 From: Marian Popeanga Date: Tue, 20 Oct 2026 13:20:07 -0700 Subject: [PATCH 1/3] Feature #2112 Rename tasks in history --- packages/types/src/__tests__/history.test.ts | 20 ++++ packages/types/src/history.ts | 1 + packages/types/src/vscode-extension-host.ts | 1 + src/core/webview/ClineProvider.ts | 15 +++ .../ClineProvider.taskHistory.spec.ts | 50 +++++++++ src/core/webview/webviewMessageHandler.ts | 5 + .../src/components/chat/TaskActions.tsx | 21 +++- webview-ui/src/components/chat/TaskHeader.tsx | 26 ++++- .../chat/__tests__/TaskActions.spec.tsx | 15 +++ .../chat/__tests__/TaskHeader.spec.tsx | 40 ++++++- .../src/components/history/RenameButton.tsx | 33 ++++++ .../src/components/history/SubtaskRow.tsx | 4 +- .../src/components/history/TaskItem.tsx | 20 +++- .../src/components/history/TaskItemFooter.tsx | 4 + .../components/history/TaskTitleEditor.tsx | 66 ++++++++++++ .../history/__tests__/RenameButton.spec.tsx | 26 +++++ .../history/__tests__/TaskItem.spec.tsx | 37 +++++++ .../history/__tests__/TaskItemFooter.spec.tsx | 16 +++ .../__tests__/TaskTitleEditor.spec.tsx | 102 ++++++++++++++++++ .../history/__tests__/useTaskSearch.spec.tsx | 16 +++ webview-ui/src/components/history/types.ts | 7 ++ .../src/components/history/useTaskSearch.ts | 9 +- webview-ui/src/i18n/locales/ca/history.json | 1 + webview-ui/src/i18n/locales/de/history.json | 1 + webview-ui/src/i18n/locales/en/history.json | 1 + webview-ui/src/i18n/locales/es/history.json | 1 + webview-ui/src/i18n/locales/fr/history.json | 1 + webview-ui/src/i18n/locales/hi/history.json | 1 + webview-ui/src/i18n/locales/id/history.json | 1 + webview-ui/src/i18n/locales/it/history.json | 1 + webview-ui/src/i18n/locales/ja/history.json | 1 + webview-ui/src/i18n/locales/ko/history.json | 1 + webview-ui/src/i18n/locales/nl/history.json | 1 + webview-ui/src/i18n/locales/pl/history.json | 1 + .../src/i18n/locales/pt-BR/history.json | 1 + webview-ui/src/i18n/locales/ru/history.json | 1 + webview-ui/src/i18n/locales/tr/history.json | 1 + webview-ui/src/i18n/locales/vi/history.json | 1 + .../src/i18n/locales/zh-CN/history.json | 1 + .../src/i18n/locales/zh-TW/history.json | 1 + 40 files changed, 536 insertions(+), 16 deletions(-) create mode 100644 webview-ui/src/components/history/RenameButton.tsx create mode 100644 webview-ui/src/components/history/TaskTitleEditor.tsx create mode 100644 webview-ui/src/components/history/__tests__/RenameButton.spec.tsx create mode 100644 webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx diff --git a/packages/types/src/__tests__/history.test.ts b/packages/types/src/__tests__/history.test.ts index 75fcf1d86c..883bb138d2 100644 --- a/packages/types/src/__tests__/history.test.ts +++ b/packages/types/src/__tests__/history.test.ts @@ -44,3 +44,23 @@ describe("pendingTaskActionSchema", () => { expect(parsed.pendingAction?.actionId).toBe("finish-1") }) }) + +describe("historyItemSchema", () => { + const baseItem = { + id: "task-1", + number: 1, + ts: 1, + task: "Original prompt", + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + } + + it("keeps a user-assigned title", () => { + expect(historyItemSchema.parse({ ...baseItem, title: "Renamed" }).title).toBe("Renamed") + }) + + it("leaves title unset when absent", () => { + expect(historyItemSchema.parse(baseItem)).not.toHaveProperty("title") + }) +}) diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 5d9671842e..131194212e 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -33,6 +33,7 @@ export const historyItemSchema = z.object({ number: z.number(), ts: z.number(), task: z.string(), + title: z.string().optional(), // User-assigned display name; `task` keeps the original prompt tokensIn: z.number(), tokensOut: z.number(), cacheWrites: z.number().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index a5393ddc66..dc407325d9 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -502,6 +502,7 @@ export interface WebviewMessage { | "deleteTaskWithId" | "abandonSubtaskWithId" | "exportTaskWithId" + | "renameTaskWithId" | "importSettings" | "exportSettings" | "resetState" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ff18465f09..ca00782cb8 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2450,6 +2450,21 @@ export class ClineProvider } } + /* + * Sets a user-assigned display title for a history item. A blank title + * clears it so the history falls back to the original prompt. + */ + async renameTaskWithId(id: string, title: string | undefined) { + const historyItem = this.taskHistoryStore.get(id) + if (!historyItem) { + this.log(`[renameTaskWithId] task ${id} not found in history`) + return + } + + const trimmed = title?.trim() + await this.updateTaskHistory({ ...historyItem, title: trimmed ? trimmed : undefined }) + } + /* Condenses a task's message history to use fewer tokens. */ async condenseTaskContext(taskId: string) { const task = this.taskRegistry.getById(taskId) diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index bec8a31040..923aaf2d53 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -597,6 +597,56 @@ describe("ClineProvider Task History Synchronization", () => { }) }) + describe("renameTaskWithId", () => { + const getStored = (id: string) => provider.taskHistoryStore.get(id) + + it("stores a trimmed title and broadcasts the updated item", async () => { + await provider.resolveWebviewView(mockWebviewView) + provider.isViewLaunched = true + await provider.updateTaskHistory(createHistoryItem({ id: "task-rename", task: "Original prompt" })) + mockPostMessage.mockClear() + + await provider.renameTaskWithId("task-rename", " Renamed ") + + expect(getStored("task-rename")).toMatchObject({ title: "Renamed", task: "Original prompt" }) + const updates = findCallsByType(mockPostMessage.mock.calls, "taskHistoryItemUpdated") + expect(updates.at(-1)?.[0].taskHistoryItem).toMatchObject({ id: "task-rename", title: "Renamed" }) + }) + + it.each([ + ["an empty string", ""], + ["whitespace", " "], + ["undefined", undefined], + ])("clears the title when given %s", async (_label, title) => { + await provider.updateTaskHistory( + createHistoryItem({ id: "task-clear", task: "Original prompt", title: "Old title" }), + { broadcast: false }, + ) + + await provider.renameTaskWithId("task-clear", title) + + expect(getStored("task-clear")?.title).toBeUndefined() + expect(getStored("task-clear")?.task).toBe("Original prompt") + }) + + it("keeps the title when the task later saves metadata without one", async () => { + const item = createHistoryItem({ id: "task-keep", task: "Original prompt", status: "active" }) + await provider.updateTaskHistory(item, { broadcast: false }) + await provider.renameTaskWithId("task-keep", "Renamed") + + // Mirrors Task.saveClineMessages(), whose taskMetadata() result never carries a title. + await provider.updateTaskHistory({ ...item, tokensIn: 500 }, { broadcast: false }) + + expect(getStored("task-keep")).toMatchObject({ title: "Renamed", tokensIn: 500 }) + }) + + it("does nothing for an unknown task", async () => { + await provider.renameTaskWithId("missing-task", "Renamed") + + expect(getStored("missing-task")).toBeUndefined() + }) + }) + describe("broadcastTaskHistoryUpdate", () => { it("sends taskHistoryUpdated message with sorted history", async () => { await provider.resolveWebviewView(mockWebviewView) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5596b90e67..bd7c589958 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -981,6 +981,11 @@ export const webviewMessageHandler = async ( case "exportTaskWithId": await provider.exportTaskWithId(message.text!) break + case "renameTaskWithId": + if (message.taskId) { + await provider.renameTaskWithId(message.taskId, message.text) + } + break case "getTaskWithAggregatedCosts": { try { const taskId = message.text diff --git a/webview-ui/src/components/chat/TaskActions.tsx b/webview-ui/src/components/chat/TaskActions.tsx index 283d8d0b67..f3e4b29cc0 100644 --- a/webview-ui/src/components/chat/TaskActions.tsx +++ b/webview-ui/src/components/chat/TaskActions.tsx @@ -8,15 +8,24 @@ import { useCopyToClipboard } from "@/utils/clipboard" import { useExtensionState } from "@/context/ExtensionStateContext" import { DeleteTaskDialog } from "../history/DeleteTaskDialog" -import { CopyIcon, CheckIcon, DownloadIcon, Trash2Icon, FileJsonIcon, MessageSquareCodeIcon } from "lucide-react" +import { + CopyIcon, + CheckIcon, + DownloadIcon, + Trash2Icon, + FileJsonIcon, + MessageSquareCodeIcon, + PencilIcon, +} from "lucide-react" import { LucideIconButton } from "./LucideIconButton" interface TaskActionsProps { item?: HistoryItem buttonsDisabled: boolean + onRename?: () => void } -export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { +export const TaskActions = ({ item, buttonsDisabled, onRename }: TaskActionsProps) => { const [deleteTaskId, setDeleteTaskId] = useState(null) const { t } = useTranslation() const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard() @@ -24,6 +33,14 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { return (
+ {item?.id && onRename && ( + + )} setIsRenaming(true), []) + const stopRename = useCallback(() => setIsRenaming(false), []) + const customTitle = currentTaskItem?.title const textContainerRef = useRef(null) const textRef = useRef(null) @@ -179,10 +184,19 @@ const TaskHeader = ({
- {isTaskExpanded && {t("chat:task.title")}} + {isTaskExpanded && + (isRenaming && currentTaskItem ? ( + + ) : ( + {customTitle || t("chat:task.title")} + ))} {!isTaskExpanded && (
- + {customTitle ? ( + {customTitle} + ) : ( + + )}
)}
@@ -336,7 +350,11 @@ const TaskHeader = ({ {task.images && task.images.length > 0 && }
e.stopPropagation()}> - +
diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index 4b096817db..e18590f483 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -83,6 +83,21 @@ describe("TaskActions", () => { expect(screen.queryByTestId("share-button")).not.toBeInTheDocument() }) + it("calls onRename when the rename button is clicked", () => { + const onRename = vi.fn() + render() + + fireEvent.click(screen.getByTestId("rename-current-task-button")) + + expect(onRename).toHaveBeenCalledTimes(1) + }) + + it("does not render the rename button without onRename", () => { + render() + + expect(screen.queryByTestId("rename-current-task-button")).not.toBeInTheDocument() + }) + it("renders export button", () => { render() diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 022186f40e..0e826dd5f1 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -38,7 +38,7 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ // Create a variable to hold the mock state const mockExtensionState: { apiConfiguration: ProviderSettings - currentTaskItem: { id: string } | null + currentTaskItem: { id: string; task?: string; title?: string } | null clineMessages: any[] taskHistory: any[] } = { @@ -272,6 +272,44 @@ describe("TaskHeader", () => { }) }) + describe("Rename", () => { + beforeEach(() => { + mockPostMessage.mockClear() + mockExtensionState.currentTaskItem = { id: "test-task-id", task: "Test task" } + }) + + afterEach(() => { + mockExtensionState.currentTaskItem = { id: "test-task-id" } + }) + + it("shows the custom title instead of the first message when the task is renamed", () => { + mockExtensionState.currentTaskItem = { id: "test-task-id", task: "Test task", title: "My title" } + renderTaskHeader() + + expect(screen.getByText("My title")).toBeInTheDocument() + expect(screen.queryByText("Test task")).not.toBeInTheDocument() + }) + + it("renames from the expanded header without collapsing it", () => { + renderTaskHeader() + fireEvent.click(screen.getByText("Test task")) + + fireEvent.click(screen.getByTestId("rename-current-task-button")) + const input = screen.getByTestId("task-title-input") + fireEvent.click(input) + fireEvent.change(input, { target: { value: "Renamed" } }) + fireEvent.keyDown(input, { key: "Enter" }) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "renameTaskWithId", + taskId: "test-task-id", + text: "Renamed", + }) + expect(screen.queryByTestId("task-title-input")).not.toBeInTheDocument() + expect(screen.getByTestId("rename-current-task-button")).toBeInTheDocument() + }) + }) + describe("Context window percentage calculation", () => { // The percentage should be calculated as: // contextTokens / (contextWindow - reservedForOutput) * 100 diff --git a/webview-ui/src/components/history/RenameButton.tsx b/webview-ui/src/components/history/RenameButton.tsx new file mode 100644 index 0000000000..ecaca37559 --- /dev/null +++ b/webview-ui/src/components/history/RenameButton.tsx @@ -0,0 +1,33 @@ +import { useCallback } from "react" + +import { Button, StandardTooltip } from "@/components/ui" +import { useAppTranslation } from "@/i18n/TranslationContext" + +type RenameButtonProps = { + onRename: () => void +} + +export const RenameButton = ({ onRename }: RenameButtonProps) => { + const { t } = useAppTranslation() + + const handleRenameClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + onRename() + }, + [onRename], + ) + + return ( + + + + ) +} diff --git a/webview-ui/src/components/history/SubtaskRow.tsx b/webview-ui/src/components/history/SubtaskRow.tsx index c0aa88489a..1e110ed195 100644 --- a/webview-ui/src/components/history/SubtaskRow.tsx +++ b/webview-ui/src/components/history/SubtaskRow.tsx @@ -3,7 +3,7 @@ import { ArrowRight } from "lucide-react" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import type { SubtaskTreeNode } from "./types" -import { countAllSubtasks } from "./types" +import { countAllSubtasks, getTaskDisplayTitle } from "./types" import { StandardTooltip } from "../ui" import SubtaskCollapsibleRow from "./SubtaskCollapsibleRow" import { TaskStatusBadge } from "./TaskStatusBadge" @@ -51,7 +51,7 @@ const SubtaskRow = ({ node, depth, onToggleExpand, className }: SubtaskRowProps) } }}> - {item.task} + {getTaskDisplayTitle(item)} {(item.status === "delegated" || item.status === "interrupted") && ( diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index eba5e59ac9..85c2c0c6c0 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -1,12 +1,13 @@ -import { memo } from "react" +import { memo, useCallback, useState } from "react" import { ArrowRight, Folder } from "lucide-react" -import type { DisplayHistoryItem } from "./types" +import { getTaskDisplayTitle, type DisplayHistoryItem } from "./types" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import { Checkbox } from "@/components/ui/checkbox" import TaskItemFooter from "./TaskItemFooter" +import { TaskTitleEditor } from "./TaskTitleEditor" import { StandardTooltip } from "../ui" interface TaskItemProps { @@ -32,7 +33,15 @@ const TaskItem = ({ onDelete, className, }: TaskItemProps) => { + const [isRenaming, setIsRenaming] = useState(false) + const startRename = useCallback(() => setIsRenaming(true), []) + const stopRename = useCallback(() => setIsRenaming(false), []) + const handleClick = () => { + if (isRenaming) { + return + } + if (isSelectionMode && onToggleSelection) { onToggleSelection(item.id, !isSelected) } else { @@ -71,7 +80,9 @@ const TaskItem = ({
- {item.highlight ? ( + {isRenaming ? ( + + ) : item.highlight ? (
- {item.task} + {getTaskDisplayTitle(item)}
)} @@ -115,6 +126,7 @@ const TaskItem = ({ isSelectionMode={isSelectionMode} isSubtask={item.isSubtask} onDelete={onDelete} + onRename={isRenaming ? undefined : startRename} />
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index 72c6b64420..5d75822f4c 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -4,6 +4,7 @@ import { formatTimeAgo } from "@/utils/format" import { CopyButton } from "./CopyButton" import { ExportButton } from "./ExportButton" import { DeleteButton } from "./DeleteButton" +import { RenameButton } from "./RenameButton" import { StandardTooltip } from "../ui/standard-tooltip" import { useAppTranslation } from "@/i18n/TranslationContext" import { Split } from "lucide-react" @@ -15,6 +16,7 @@ export interface TaskItemFooterProps { isSelectionMode?: boolean isSubtask?: boolean onDelete?: (taskId: string) => void + onRename?: () => void } const TaskItemFooter: React.FC = ({ @@ -23,6 +25,7 @@ const TaskItemFooter: React.FC = ({ isSelectionMode = false, isSubtask = false, onDelete, + onRename, }) => { const { t } = useAppTranslation() @@ -63,6 +66,7 @@ const TaskItemFooter: React.FC = ({ {/* Action Buttons for non-compact view */} {!isSelectionMode && (
+ {variant === "full" && onRename && } {variant === "full" && } {onDelete && } diff --git a/webview-ui/src/components/history/TaskTitleEditor.tsx b/webview-ui/src/components/history/TaskTitleEditor.tsx new file mode 100644 index 0000000000..9b7e1c2130 --- /dev/null +++ b/webview-ui/src/components/history/TaskTitleEditor.tsx @@ -0,0 +1,66 @@ +import { useCallback, useState } from "react" +import type { HistoryItem } from "@roo-code/types" + +import { Input } from "@/components/ui" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@/lib/utils" +import { vscode } from "@/utils/vscode" + +import { getTaskDisplayTitle } from "./types" + +type TaskTitleEditorProps = { + item: Pick + onDone: () => void + className?: string +} + +/** + * Inline input for renaming a history item. Enter saves, Escape or blur cancels. + * Saving a blank value, or the original prompt, clears the custom title. + */ +export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProps) => { + const { t } = useAppTranslation() + const [value, setValue] = useState(() => getTaskDisplayTitle(item)) + + const save = useCallback(() => { + const trimmed = value.trim() + const nextTitle = trimmed && trimmed !== item.task.trim() ? trimmed : undefined + + if (nextTitle !== item.title) { + vscode.postMessage({ type: "renameTaskWithId", taskId: item.id, text: nextTitle ?? "" }) + } + + onDone() + }, [value, item, onDone]) + + // Let keydown keep bubbling: VS Code's webview host listens on `window` to run + // clipboard and undo shortcuts, so stopping propagation breaks paste (Cmd/Ctrl+V). + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() + save() + } else if (e.key === "Escape") { + e.preventDefault() + onDone() + } + }, + [save, onDone], + ) + + return ( + e.currentTarget.select()} + onChange={(e) => setValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={onDone} + onClick={(e) => e.stopPropagation()} + className={cn("flex-1 min-w-0 text-base rounded-md select-text", className)} + /> + ) +} diff --git a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx new file mode 100644 index 0000000000..aa21db13fc --- /dev/null +++ b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx @@ -0,0 +1,26 @@ +import { render, screen, fireEvent } from "@/utils/test-utils" + +import { RenameButton } from "../RenameButton" + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +describe("RenameButton", () => { + it("calls onRename without letting the click reach the task row", () => { + const onRename = vi.fn() + const onRowClick = vi.fn() + render( +
+ +
, + ) + + fireEvent.click(screen.getByTestId("rename-task-button")) + + expect(onRename).toHaveBeenCalledTimes(1) + expect(onRowClick).not.toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index df8fc742d3..4fb4a82d72 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -1,5 +1,7 @@ import { render, screen, fireEvent } from "@/utils/test-utils" +import { vscode } from "@src/utils/vscode" + import TaskItem from "../TaskItem" vi.mock("@src/utils/vscode") @@ -109,4 +111,39 @@ describe("TaskItem", () => { const taskItem = screen.getByTestId("task-item-1") expect(taskItem).toHaveClass("hover:text-vscode-foreground") }) + + it("shows the custom title instead of the prompt when renamed", () => { + render() + + expect(screen.getByText("My title")).toBeInTheDocument() + expect(screen.queryByText("Test task")).not.toBeInTheDocument() + }) + + it("opens an inline editor from the rename button without opening the task", () => { + render() + + fireEvent.click(screen.getByTestId("rename-task-button")) + + expect(screen.getByTestId("task-title-input")).toHaveValue("Test task") + fireEvent.click(screen.getByTestId("task-title-input")) + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + + it("saves the new title on Enter and closes the editor", () => { + render() + + fireEvent.click(screen.getByTestId("rename-task-button")) + const input = screen.getByTestId("task-title-input") + fireEvent.change(input, { target: { value: " Renamed task " } }) + fireEvent.keyDown(input, { key: "Enter" }) + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "renameTaskWithId", taskId: "1", text: "Renamed task" }) + expect(screen.queryByTestId("task-title-input")).not.toBeInTheDocument() + }) + + it("does not show the rename button in the compact variant", () => { + render() + + expect(screen.queryByTestId("rename-task-button")).not.toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index fc8d7edcca..83047dc0b4 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -114,4 +114,20 @@ describe("TaskItemFooter", () => { expect(screen.queryByTestId(/task-status-badge-/)).not.toBeInTheDocument() }) + + it("shows rename button in full variant when onRename is provided", () => { + render() + + expect(screen.getByTestId("rename-task-button")).toBeInTheDocument() + }) + + it.each([ + ["compact variant", { variant: "compact" as const, onRename: vi.fn() }], + ["selection mode", { variant: "full" as const, onRename: vi.fn(), isSelectionMode: true }], + ["no onRename", { variant: "full" as const }], + ])("hides rename button in %s", (_label, props) => { + render() + + expect(screen.queryByTestId("rename-task-button")).not.toBeInTheDocument() + }) }) diff --git a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx new file mode 100644 index 0000000000..aba0460968 --- /dev/null +++ b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx @@ -0,0 +1,102 @@ +import { render, screen, fireEvent } from "@/utils/test-utils" + +import { vscode } from "@src/utils/vscode" + +import { TaskTitleEditor } from "../TaskTitleEditor" + +vi.mock("@src/utils/vscode") + +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +const renderEditor = (title?: string) => { + const onDone = vi.fn() + render() + return { input: screen.getByTestId("task-title-input"), onDone } +} + +const submit = (input: HTMLElement, value: string) => { + fireEvent.change(input, { target: { value } }) + fireEvent.keyDown(input, { key: "Enter" }) +} + +describe("TaskTitleEditor", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it.each([ + ["the original prompt when there is no title", undefined, "Original prompt"], + ["the current title when renamed", "Custom", "Custom"], + ])("starts with %s", (_label, title, expected) => { + const { input } = renderEditor(title) + + expect(input).toHaveValue(expected) + }) + + it("sends the trimmed title on Enter", () => { + const { input, onDone } = renderEditor() + + submit(input, " New name ") + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "renameTaskWithId", + taskId: "task-1", + text: "New name", + }) + expect(onDone).toHaveBeenCalled() + }) + + it.each([ + ["a blank value", " "], + ["the original prompt", "Original prompt"], + ])("clears an existing title when saving %s", (_label, value) => { + const { input } = renderEditor("Custom") + + submit(input, value) + + expect(vscode.postMessage).toHaveBeenCalledWith({ type: "renameTaskWithId", taskId: "task-1", text: "" }) + }) + + it.each([ + ["the title is unchanged", "Custom", "Custom"], + ["an untitled task is saved blank", undefined, ""], + ])("does not send a message when %s", (_label, title, value) => { + const { input, onDone } = renderEditor(title) + + submit(input, value) + + expect(vscode.postMessage).not.toHaveBeenCalled() + expect(onDone).toHaveBeenCalled() + }) + + it("lets keydown reach window so VS Code can run clipboard shortcuts", () => { + const onWindowKeyDown = vi.fn() + window.addEventListener("keydown", onWindowKeyDown) + try { + const { input } = renderEditor() + + fireEvent.keyDown(input, { key: "v", metaKey: true }) + + expect(onWindowKeyDown).toHaveBeenCalledTimes(1) + } finally { + window.removeEventListener("keydown", onWindowKeyDown) + } + }) + + it.each([ + ["Escape", (input: HTMLElement) => fireEvent.keyDown(input, { key: "Escape" })], + ["blur", (input: HTMLElement) => fireEvent.blur(input)], + ])("cancels on %s without saving", (_label, cancel) => { + const { input, onDone } = renderEditor() + + fireEvent.change(input, { target: { value: "Discarded" } }) + cancel(input) + + expect(vscode.postMessage).not.toHaveBeenCalled() + expect(onDone).toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx index bea79814fa..f4b0ea37c8 100644 --- a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx +++ b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx @@ -154,6 +154,22 @@ describe("useTaskSearch", () => { expect((result.current.tasks[0] as any).highlight).toBe("Create a React component") }) + it("searches and highlights the custom title of a renamed task", () => { + mockUseExtensionState.mockReturnValue({ + taskHistory: [{ ...mockTaskHistory[1], title: "Coverage sprint" }], + cwd: "/workspace/project1", + }) + const { result } = renderHook(() => useTaskSearch()) + + act(() => { + result.current.setSearchQuery("Coverage") + }) + + expect(result.current.tasks).toEqual([ + expect.objectContaining({ id: "task-2", highlight: "Coverage sprint" }), + ]) + }) + it("automatically switches to mostRelevant when searching", () => { const { result } = renderHook(() => useTaskSearch()) diff --git a/webview-ui/src/components/history/types.ts b/webview-ui/src/components/history/types.ts index 0de5e43081..8eca94abcb 100644 --- a/webview-ui/src/components/history/types.ts +++ b/webview-ui/src/components/history/types.ts @@ -10,6 +10,13 @@ export interface DisplayHistoryItem extends HistoryItem { isSubtask?: boolean } +/** + * The text shown for a task in history: the user-assigned title when set, otherwise the original prompt. + */ +export function getTaskDisplayTitle(item: Pick): string { + return item.title || item.task +} + /** * A node in the subtask tree, representing a task and its recursively nested children. */ diff --git a/webview-ui/src/components/history/useTaskSearch.ts b/webview-ui/src/components/history/useTaskSearch.ts index 3969985b98..9ac00a591e 100644 --- a/webview-ui/src/components/history/useTaskSearch.ts +++ b/webview-ui/src/components/history/useTaskSearch.ts @@ -4,6 +4,8 @@ import { Fzf } from "fzf" import { highlightFzfMatch } from "@/utils/highlight" import { useExtensionState } from "@/context/ExtensionStateContext" +import { getTaskDisplayTitle } from "./types" + type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant" export const useTaskSearch = () => { @@ -33,7 +35,7 @@ export const useTaskSearch = () => { const fzf = useMemo(() => { return new Fzf(presentableTasks, { - selector: (item) => item.task, + selector: getTaskDisplayTitle, }) }, [presentableTasks]) @@ -44,12 +46,13 @@ export const useTaskSearch = () => { const searchResults = fzf.find(searchQuery) results = searchResults.map((result) => { const positions = Array.from(result.positions) - const taskEndIndex = result.item.task.length + const displayTitle = getTaskDisplayTitle(result.item) + const taskEndIndex = displayTitle.length return { ...result.item, highlight: highlightFzfMatch( - result.item.task, + displayTitle, positions.filter((p) => p < taskEndIndex), ), workspace: result.item.workspace, diff --git a/webview-ui/src/i18n/locales/ca/history.json b/webview-ui/src/i18n/locales/ca/history.json index a872651d23..9277538d30 100644 --- a/webview-ui/src/i18n/locales/ca/history.json +++ b/webview-ui/src/i18n/locales/ca/history.json @@ -20,6 +20,7 @@ "apiCostLabel": "Cost d'API:", "copyPrompt": "Copiar prompt", "exportTask": "Exportar tasca", + "renameTask": "Canvia el nom de la tasca", "deleteTask": "Eliminar tasca", "deleteTaskMessage": "Estàs segur que vols eliminar aquesta tasca? Aquesta acció no es pot desfer.", "cancel": "Cancel·lar", diff --git a/webview-ui/src/i18n/locales/de/history.json b/webview-ui/src/i18n/locales/de/history.json index b10fcd445e..89917ce251 100644 --- a/webview-ui/src/i18n/locales/de/history.json +++ b/webview-ui/src/i18n/locales/de/history.json @@ -20,6 +20,7 @@ "apiCostLabel": "API-Kosten:", "copyPrompt": "Prompt kopieren", "exportTask": "Aufgabe exportieren", + "renameTask": "Aufgabe umbenennen", "deleteTask": "Aufgabe löschen", "deleteTaskMessage": "Bist du sicher, dass du diese Aufgabe löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "cancel": "Abbrechen", diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json index 6d53cd3663..0eb7ea53ec 100644 --- a/webview-ui/src/i18n/locales/en/history.json +++ b/webview-ui/src/i18n/locales/en/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Delete Task (Shift + Click to skip confirmation)", "copyPrompt": "Copy Prompt", "exportTask": "Export Task", + "renameTask": "Rename Task", "deleteTask": "Delete Task", "deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.", "cancel": "Cancel", diff --git a/webview-ui/src/i18n/locales/es/history.json b/webview-ui/src/i18n/locales/es/history.json index 91d28abb99..a555ca5e73 100644 --- a/webview-ui/src/i18n/locales/es/history.json +++ b/webview-ui/src/i18n/locales/es/history.json @@ -20,6 +20,7 @@ "apiCostLabel": "Costo de API:", "copyPrompt": "Copiar prompt", "exportTask": "Exportar tarea", + "renameTask": "Renombrar tarea", "deleteTask": "Eliminar tarea", "deleteTaskMessage": "¿Estás seguro de que quieres eliminar esta tarea? Esta acción no se puede deshacer.", "cancel": "Cancelar", diff --git a/webview-ui/src/i18n/locales/fr/history.json b/webview-ui/src/i18n/locales/fr/history.json index 443bb0eb3e..a240602dba 100644 --- a/webview-ui/src/i18n/locales/fr/history.json +++ b/webview-ui/src/i18n/locales/fr/history.json @@ -20,6 +20,7 @@ "apiCostLabel": "Coût API:", "copyPrompt": "Copier le prompt", "exportTask": "Exporter la tâche", + "renameTask": "Renommer la tâche", "deleteTask": "Supprimer la tâche", "deleteTaskMessage": "Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action ne peut pas être annulée.", "cancel": "Annuler", diff --git a/webview-ui/src/i18n/locales/hi/history.json b/webview-ui/src/i18n/locales/hi/history.json index 3dd7cca9a9..c3a6dc7526 100644 --- a/webview-ui/src/i18n/locales/hi/history.json +++ b/webview-ui/src/i18n/locales/hi/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "कार्य हटाएं (Shift + क्लिक पुष्टि छोड़ने के लिए)", "copyPrompt": "प्रॉम्प्ट कॉपी करें", "exportTask": "कार्य निर्यात करें", + "renameTask": "कार्य का नाम बदलें", "deleteTask": "कार्य हटाएं", "deleteTaskMessage": "क्या आप वाकई इस कार्य को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती है।", "cancel": "रद्द करें", diff --git a/webview-ui/src/i18n/locales/id/history.json b/webview-ui/src/i18n/locales/id/history.json index 772ca25384..4cc3503970 100644 --- a/webview-ui/src/i18n/locales/id/history.json +++ b/webview-ui/src/i18n/locales/id/history.json @@ -20,6 +20,7 @@ "apiCostLabel": "Biaya API:", "copyPrompt": "Salin Prompt", "exportTask": "Ekspor Tugas", + "renameTask": "Ganti Nama Tugas", "deleteTask": "Hapus Tugas", "deleteTaskMessage": "Apakah kamu yakin ingin menghapus tugas ini? Aksi ini tidak dapat dibatalkan.", "cancel": "Batal", diff --git a/webview-ui/src/i18n/locales/it/history.json b/webview-ui/src/i18n/locales/it/history.json index 4097d43ce2..5719dc3cb9 100644 --- a/webview-ui/src/i18n/locales/it/history.json +++ b/webview-ui/src/i18n/locales/it/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Elimina attività (Shift + Clic per saltare conferma)", "copyPrompt": "Copia prompt", "exportTask": "Esporta attività", + "renameTask": "Rinomina attività", "deleteTask": "Elimina attività", "deleteTaskMessage": "Sei sicuro di voler eliminare questa attività? Questa azione non può essere annullata.", "cancel": "Annulla", diff --git a/webview-ui/src/i18n/locales/ja/history.json b/webview-ui/src/i18n/locales/ja/history.json index be4897ba34..1f635dfb54 100644 --- a/webview-ui/src/i18n/locales/ja/history.json +++ b/webview-ui/src/i18n/locales/ja/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "タスクを削除(Shift + クリックで確認をスキップ)", "copyPrompt": "プロンプトをコピー", "exportTask": "タスクをエクスポート", + "renameTask": "タスク名を変更", "deleteTask": "タスクを削除", "deleteTaskMessage": "このタスクを削除してもよろしいですか?この操作は元に戻せません。", "cancel": "キャンセル", diff --git a/webview-ui/src/i18n/locales/ko/history.json b/webview-ui/src/i18n/locales/ko/history.json index 8a13ff12cd..b5a204b7bf 100644 --- a/webview-ui/src/i18n/locales/ko/history.json +++ b/webview-ui/src/i18n/locales/ko/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "작업 삭제 (Shift + 클릭으로 확인 생략)", "copyPrompt": "프롬프트 복사", "exportTask": "작업 내보내기", + "renameTask": "작업 이름 바꾸기", "deleteTask": "작업 삭제", "deleteTaskMessage": "이 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "cancel": "취소", diff --git a/webview-ui/src/i18n/locales/nl/history.json b/webview-ui/src/i18n/locales/nl/history.json index db1515bfe5..2a72b0bfe8 100644 --- a/webview-ui/src/i18n/locales/nl/history.json +++ b/webview-ui/src/i18n/locales/nl/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Taak verwijderen (Shift + Klik om bevestiging over te slaan)", "copyPrompt": "Prompt kopiëren", "exportTask": "Taak exporteren", + "renameTask": "Taak hernoemen", "deleteTask": "Taak verwijderen", "deleteTaskMessage": "Weet je zeker dat je deze taak wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", "cancel": "Annuleren", diff --git a/webview-ui/src/i18n/locales/pl/history.json b/webview-ui/src/i18n/locales/pl/history.json index 2924d4710e..229f28030f 100644 --- a/webview-ui/src/i18n/locales/pl/history.json +++ b/webview-ui/src/i18n/locales/pl/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Usuń zadanie (Shift + Klik, aby pominąć potwierdzenie)", "copyPrompt": "Kopiuj prompt", "exportTask": "Eksportuj zadanie", + "renameTask": "Zmień nazwę zadania", "deleteTask": "Usuń zadanie", "deleteTaskMessage": "Czy na pewno chcesz usunąć to zadanie? Tej akcji nie można cofnąć.", "cancel": "Anuluj", diff --git a/webview-ui/src/i18n/locales/pt-BR/history.json b/webview-ui/src/i18n/locales/pt-BR/history.json index 79c84b70ef..c9b2848ee6 100644 --- a/webview-ui/src/i18n/locales/pt-BR/history.json +++ b/webview-ui/src/i18n/locales/pt-BR/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Excluir tarefa (Shift + Clique para pular confirmação)", "copyPrompt": "Copiar prompt", "exportTask": "Exportar tarefa", + "renameTask": "Renomear tarefa", "deleteTask": "Excluir tarefa", "deleteTaskMessage": "Tem certeza que deseja excluir esta tarefa? Esta ação não pode ser desfeita.", "cancel": "Cancelar", diff --git a/webview-ui/src/i18n/locales/ru/history.json b/webview-ui/src/i18n/locales/ru/history.json index 3035ec59d9..482b8453ad 100644 --- a/webview-ui/src/i18n/locales/ru/history.json +++ b/webview-ui/src/i18n/locales/ru/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Удалить задачу (Shift + клик для пропуска подтверждения)", "copyPrompt": "Скопировать запрос", "exportTask": "Экспортировать задачу", + "renameTask": "Переименовать задачу", "deleteTask": "Удалить задачу", "deleteTaskMessage": "Вы уверены, что хотите удалить эту задачу? Это действие нельзя отменить.", "cancel": "Отмена", diff --git a/webview-ui/src/i18n/locales/tr/history.json b/webview-ui/src/i18n/locales/tr/history.json index 2ebc1a0154..10b7e82362 100644 --- a/webview-ui/src/i18n/locales/tr/history.json +++ b/webview-ui/src/i18n/locales/tr/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Görevi Sil (Onayı atlamak için Shift + Tıkla)", "copyPrompt": "Promptu Kopyala", "exportTask": "Görevi Dışa Aktar", + "renameTask": "Görevi Yeniden Adlandır", "deleteTask": "Görevi Sil", "deleteTaskMessage": "Bu görevi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "cancel": "İptal", diff --git a/webview-ui/src/i18n/locales/vi/history.json b/webview-ui/src/i18n/locales/vi/history.json index a6efa0671e..48ec3a00a4 100644 --- a/webview-ui/src/i18n/locales/vi/history.json +++ b/webview-ui/src/i18n/locales/vi/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "Xóa nhiệm vụ (Shift + Click để bỏ qua xác nhận)", "copyPrompt": "Sao chép lời nhắc", "exportTask": "Xuất nhiệm vụ", + "renameTask": "Đổi tên nhiệm vụ", "deleteTask": "Xóa nhiệm vụ", "deleteTaskMessage": "Bạn có chắc chắn muốn xóa nhiệm vụ này không? Hành động này không thể hoàn tác.", "cancel": "Hủy", diff --git a/webview-ui/src/i18n/locales/zh-CN/history.json b/webview-ui/src/i18n/locales/zh-CN/history.json index 6b6bd03300..2c97a23067 100644 --- a/webview-ui/src/i18n/locales/zh-CN/history.json +++ b/webview-ui/src/i18n/locales/zh-CN/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "删除任务(Shift + 点击跳过确认)", "copyPrompt": "复制提示词", "exportTask": "导出任务", + "renameTask": "重命名任务", "deleteTask": "删除任务", "deleteTaskMessage": "确认删除该任务?此操作不可逆。", "cancel": "取消", diff --git a/webview-ui/src/i18n/locales/zh-TW/history.json b/webview-ui/src/i18n/locales/zh-TW/history.json index 5fb3230c80..01bb30014b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/history.json +++ b/webview-ui/src/i18n/locales/zh-TW/history.json @@ -13,6 +13,7 @@ "deleteTaskTitle": "刪除工作(按住 Shift 並點選可跳過確認)", "copyPrompt": "複製提示詞", "exportTask": "匯出工作", + "renameTask": "重新命名工作", "deleteTask": "刪除工作", "deleteTaskMessage": "確定要刪除此工作嗎?此操作無法復原。", "cancel": "取消", From a364abeace4b5706fcaa8ce4f53beba747e1e081 Mon Sep 17 00:00:00 2001 From: Marian Popeanga Date: Wed, 21 Oct 2026 21:33:14 -0700 Subject: [PATCH 2/3] addressed review feedback for task rename --- src/core/webview/ClineProvider.ts | 18 ++++++-- .../ClineProvider.taskHistory.spec.ts | 15 +++++++ .../__tests__/webviewMessageHandler.spec.ts | 17 ++++++++ webview-ui/playwright/gallery/stories.tsx | 37 ++++++++++++++++ webview-ui/src/components/chat/TaskHeader.tsx | 12 +++--- .../chat/__tests__/TaskActions.spec.tsx | 2 +- .../chat/__tests__/TaskHeader.spec.tsx | 24 +++++++++++ .../src/components/history/RenameButton.tsx | 19 +++------ .../src/components/history/TaskItem.tsx | 8 ++-- .../components/history/TaskTitleEditor.tsx | 33 +++++++++++---- .../history/__tests__/HistoryView.visual.tsx | 23 ++++++++++ .../history/__tests__/RenameButton.spec.tsx | 2 +- .../__tests__/TaskTitleEditor.spec.tsx | 40 ++++++++++++++++++ .../history-rename-editor-dark.png | Bin 0 -> 18446 bytes ...tory-rename-editor-high-contrast-light.png | Bin 0 -> 18263 bytes .../history-rename-editor-high-contrast.png | Bin 0 -> 18434 bytes .../history-rename-editor-light.png | Bin 0 -> 18536 bytes 17 files changed, 214 insertions(+), 36 deletions(-) create mode 100644 webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png create mode 100644 webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png create mode 100644 webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png create mode 100644 webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ca00782cb8..67422a9b54 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2455,14 +2455,26 @@ export class ClineProvider * clears it so the history falls back to the original prompt. */ async renameTaskWithId(id: string, title: string | undefined) { - const historyItem = this.taskHistoryStore.get(id) - if (!historyItem) { + if (!this.taskHistoryStore.get(id)) { this.log(`[renameTaskWithId] task ${id} not found in history`) return } + // Read and write under the store lock so a queued task save or delegation + // transition cannot be overwritten by a stale snapshot of the item. const trimmed = title?.trim() - await this.updateTaskHistory({ ...historyItem, title: trimmed ? trimmed : undefined }) + await this.taskHistoryStore.atomicReadAndUpdate(id, (historyItem) => ({ + ...historyItem, + title: trimmed ? trimmed : undefined, + })) + this.recentTasksCache = undefined + + if (this.isViewLaunched) { + const updatedItem = this.taskHistoryStore.get(id) + if (updatedItem) { + await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedItem }) + } + } } /* Condenses a task's message history to use fewer tokens. */ diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index 923aaf2d53..7aaa0740ce 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -640,10 +640,25 @@ describe("ClineProvider Task History Synchronization", () => { expect(getStored("task-keep")).toMatchObject({ title: "Renamed", tokensIn: 500 }) }) + it("keeps a concurrent task save that is queued before the rename", async () => { + const item = createHistoryItem({ id: "task-race", task: "Original prompt", status: "active" }) + await provider.updateTaskHistory(item, { broadcast: false }) + + // Start the save without awaiting it, so the rename runs while the save is queued. + const save = provider.updateTaskHistory({ ...item, tokensIn: 500 }, { broadcast: false }) + const rename = provider.renameTaskWithId("task-race", "Renamed") + await Promise.all([save, rename]) + + expect(getStored("task-race")).toMatchObject({ title: "Renamed", tokensIn: 500 }) + }) + it("does nothing for an unknown task", async () => { + const logSpy = vi.spyOn(provider, "log") + await provider.renameTaskWithId("missing-task", "Renamed") expect(getStored("missing-task")).toBeUndefined() + expect(logSpy).toHaveBeenCalledWith("[renameTaskWithId] task missing-task not found in history") }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index d5c72de742..685a7b11c1 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -132,6 +132,7 @@ const mockClineProvider = { getCurrentTask: vi.fn(), getTaskWithId: vi.fn(), createTaskWithHistoryItem: vi.fn(), + renameTaskWithId: vi.fn(), getSkillsManager: vi.fn(), cwd: "/mock/workspace", } as unknown as ClineProvider @@ -3036,3 +3037,19 @@ describe("updateProfileModel", () => { expect(updateProfileModel).not.toHaveBeenCalled() }) }) + +describe("webviewMessageHandler - renameTaskWithId", () => { + beforeEach(() => { + vi.mocked(mockClineProvider.renameTaskWithId).mockClear() + }) + + it.each([ + ["forwards taskId and text", { taskId: "task-1", text: "New name" }, [["task-1", "New name"]]], + ["forwards an empty title so the provider can clear it", { taskId: "task-1", text: "" }, [["task-1", ""]]], + ["skips the call when taskId is missing", { text: "New name" }, []], + ])("%s", async (_label, payload, expectedCalls) => { + await webviewMessageHandler(mockClineProvider, { type: "renameTaskWithId", ...payload }) + + expect(vi.mocked(mockClineProvider.renameTaskWithId).mock.calls).toEqual(expectedCalls) + }) +}) diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index 90be332890..1d7e2be387 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -168,6 +168,43 @@ export const stories: Record = { ) }, + "history-rename": async () => { + const [{ AppProviders }, { default: HistoryView }] = await Promise.all([ + import("../AppProviders"), + import("@/components/history/HistoryView"), + ]) + // The visual test freezes the clock at this time so "time ago" labels stay stable. + const now = new Date("2026-06-01T12:00:00Z").getTime() + const workspace = "/workspace/demo" + const baseItem = { tokensIn: 1200, tokensOut: 800, totalCost: 0.12, workspace } + return ( + +
+ undefined} /> +
+
+ ) + }, "ignored-disabled-tools-warning": async () => { const [{ AppProviders }, { ChatRowContent }] = await Promise.all([ import("../AppProviders"), diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 985da5970c..c3d0594b99 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useRef, useState, useMemo } from "react" +import { memo, useRef, useState, useMemo } from "react" import { useTranslation } from "react-i18next" import { ChevronUp, @@ -68,8 +68,6 @@ const TaskHeader = ({ const { id: modelId, info: model } = useSelectedModel(apiConfiguration) const [isTaskExpanded, setIsTaskExpanded] = useState(false) const [isRenaming, setIsRenaming] = useState(false) - const startRename = useCallback(() => setIsRenaming(true), []) - const stopRename = useCallback(() => setIsRenaming(false), []) const customTitle = currentTaskItem?.title const textContainerRef = useRef(null) @@ -186,7 +184,11 @@ const TaskHeader = ({
{isTaskExpanded && (isRenaming && currentTaskItem ? ( - + setIsRenaming(false)} + className="text-sm" + /> ) : ( {customTitle || t("chat:task.title")} ))} @@ -353,7 +355,7 @@ const TaskHeader = ({ setIsRenaming(true)} />
diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index e18590f483..2e4be8d561 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -87,7 +87,7 @@ describe("TaskActions", () => { const onRename = vi.fn() render() - fireEvent.click(screen.getByTestId("rename-current-task-button")) + fireEvent.click(screen.getByLabelText("history:renameTask")) expect(onRename).toHaveBeenCalledTimes(1) }) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 0e826dd5f1..363a2b6aae 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -290,6 +290,30 @@ describe("TaskHeader", () => { expect(screen.queryByText("Test task")).not.toBeInTheDocument() }) + it.each([ + ["the custom title", "My title", "My title"], + ["the default label without a custom title", undefined, "chat:task.title"], + ])("shows %s as the expanded heading", (_label, title, expected) => { + mockExtensionState.currentTaskItem = { id: "test-task-id", task: "Test task", title } + renderTaskHeader() + fireEvent.click(screen.getByText(title ?? "Test task")) + + expect(screen.getByText(expected)).toHaveClass("font-bold") + expect(screen.queryByTestId("task-title-input")).not.toBeInTheDocument() + }) + + it("closes the header editor on Escape without saving", () => { + renderTaskHeader() + fireEvent.click(screen.getByText("Test task")) + fireEvent.click(screen.getByTestId("rename-current-task-button")) + + fireEvent.keyDown(screen.getByTestId("task-title-input"), { key: "Escape" }) + + expect(screen.queryByTestId("task-title-input")).not.toBeInTheDocument() + expect(screen.getByText("chat:task.title")).toBeInTheDocument() + expect(mockPostMessage).not.toHaveBeenCalled() + }) + it("renames from the expanded header without collapsing it", () => { renderTaskHeader() fireEvent.click(screen.getByText("Test task")) diff --git a/webview-ui/src/components/history/RenameButton.tsx b/webview-ui/src/components/history/RenameButton.tsx index ecaca37559..bd834525d2 100644 --- a/webview-ui/src/components/history/RenameButton.tsx +++ b/webview-ui/src/components/history/RenameButton.tsx @@ -1,5 +1,3 @@ -import { useCallback } from "react" - import { Button, StandardTooltip } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" @@ -9,23 +7,20 @@ type RenameButtonProps = { export const RenameButton = ({ onRename }: RenameButtonProps) => { const { t } = useAppTranslation() - - const handleRenameClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - onRename() - }, - [onRename], - ) + const label = t("history:renameTask") return ( - + diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index 85c2c0c6c0..f9cb85f360 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useState } from "react" +import { memo, useState } from "react" import { ArrowRight, Folder } from "lucide-react" import { getTaskDisplayTitle, type DisplayHistoryItem } from "./types" @@ -34,8 +34,6 @@ const TaskItem = ({ className, }: TaskItemProps) => { const [isRenaming, setIsRenaming] = useState(false) - const startRename = useCallback(() => setIsRenaming(true), []) - const stopRename = useCallback(() => setIsRenaming(false), []) const handleClick = () => { if (isRenaming) { @@ -81,7 +79,7 @@ const TaskItem = ({
{isRenaming ? ( - + setIsRenaming(false)} /> ) : item.highlight ? (
setIsRenaming(true)} />
diff --git a/webview-ui/src/components/history/TaskTitleEditor.tsx b/webview-ui/src/components/history/TaskTitleEditor.tsx index 9b7e1c2130..c02e044311 100644 --- a/webview-ui/src/components/history/TaskTitleEditor.tsx +++ b/webview-ui/src/components/history/TaskTitleEditor.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from "react" import type { HistoryItem } from "@roo-code/types" -import { Input } from "@/components/ui" +import { AutosizeTextarea } from "@/components/ui" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@/lib/utils" import { vscode } from "@/utils/vscode" @@ -14,17 +14,22 @@ type TaskTitleEditorProps = { className?: string } +// Titles are a single line: line breaks and repeated whitespace collapse to one space. +const normalizeTitle = (text: string) => text.replace(/\s+/g, " ").trim() + /** - * Inline input for renaming a history item. Enter saves, Escape or blur cancels. + * Inline editor for renaming a history item. Enter saves, Escape or blur cancels. * Saving a blank value, or the original prompt, clears the custom title. + * + * It wraps instead of scrolling sideways so a long title stays readable at narrow widths. */ export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProps) => { const { t } = useAppTranslation() - const [value, setValue] = useState(() => getTaskDisplayTitle(item)) + const [value, setValue] = useState(() => normalizeTitle(getTaskDisplayTitle(item))) const save = useCallback(() => { - const trimmed = value.trim() - const nextTitle = trimmed && trimmed !== item.task.trim() ? trimmed : undefined + const normalized = normalizeTitle(value) + const nextTitle = normalized && normalized !== normalizeTitle(item.task) ? normalized : undefined if (nextTitle !== item.title) { vscode.postMessage({ type: "renameTaskWithId", taskId: item.id, text: nextTitle ?? "" }) @@ -36,8 +41,12 @@ export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProp // Let keydown keep bubbling: VS Code's webview host listens on `window` to run // clipboard and undo shortcuts, so stopping propagation breaks paste (Cmd/Ctrl+V). const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { + (e: React.KeyboardEvent) => { if (e.key === "Enter") { + // IME users press Enter to confirm a conversion candidate, not to save. + if (e.nativeEvent.isComposing || e.keyCode === 229) { + return + } e.preventDefault() save() } else if (e.key === "Escape") { @@ -49,18 +58,24 @@ export const TaskTitleEditor = ({ item, onDone, className }: TaskTitleEditorProp ) return ( - e.currentTarget.select()} onChange={(e) => setValue(e.target.value)} onKeyDown={handleKeyDown} onBlur={onDone} onClick={(e) => e.stopPropagation()} - className={cn("flex-1 min-w-0 text-base rounded-md select-text", className)} + className={cn( + "flex-1 min-w-0 resize-none border rounded-md px-2 py-1 text-base select-text break-words", + className, + )} /> ) } diff --git a/webview-ui/src/components/history/__tests__/HistoryView.visual.tsx b/webview-ui/src/components/history/__tests__/HistoryView.visual.tsx index 878c39f1d6..4c325eaa3a 100644 --- a/webview-ui/src/components/history/__tests__/HistoryView.visual.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryView.visual.tsx @@ -21,4 +21,27 @@ for (const theme of visualThemes) { focusedControl: selectionButton, }) }) + + test(`renders task rename states in the VS Code ${theme.name} theme`, async ({ mount, page }) => { + // Matches the "history-rename" story's reference time so "time ago" labels stay stable. + await page.clock.setFixedTime(new Date("2026-06-01T12:00:00Z")) + const component = mountedStory(await mount("history-rename")) + await applyVisualTheme(page, theme) + + // A renamed task shows its custom title. Hovering a row reveals the rename action. + const renamedTask = component.getByTestId("task-item-task-renamed") + await expect(renamedTask.getByText("Auth refactor")).toBeVisible() + const originalTask = component.getByTestId("task-item-task-original") + await originalTask.getByText("Write unit tests for the billing service").hover() + const renameButton = originalTask.getByTestId("rename-task-button") + await expect(renameButton).toBeVisible() + + // The rename action swaps the title for an inline editor. + await renameButton.click() + const input = originalTask.getByTestId("task-title-input") + await expect(input).toBeFocused() + await expect(input).toHaveValue("Write unit tests for the billing service") + await expect(component).toHaveScreenshot(`history-rename-editor-${theme.name}.png`) + await expectBoundedLayout(page, component, { focusedControl: input }) + }) } diff --git a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx index aa21db13fc..2bc837ee12 100644 --- a/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx +++ b/webview-ui/src/components/history/__tests__/RenameButton.spec.tsx @@ -18,7 +18,7 @@ describe("RenameButton", () => {
, ) - fireEvent.click(screen.getByTestId("rename-task-button")) + fireEvent.click(screen.getByRole("button", { name: "history:renameTask" })) expect(onRename).toHaveBeenCalledTimes(1) expect(onRowClick).not.toHaveBeenCalled() diff --git a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx index aba0460968..951dc85ac8 100644 --- a/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskTitleEditor.spec.tsx @@ -50,6 +50,32 @@ describe("TaskTitleEditor", () => { expect(onDone).toHaveBeenCalled() }) + it("collapses line breaks and repeated spaces into one line", () => { + const { input } = renderEditor() + + submit(input, " First line\n\nsecond line ") + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "renameTaskWithId", + taskId: "task-1", + text: "First line second line", + }) + }) + + it("starts with a multi-line prompt shown on one line", () => { + render() + + expect(screen.getByTestId("task-title-input")).toHaveValue("Line one Line two") + }) + + it("does not send a message when a multi-line prompt is saved unchanged", () => { + render() + + fireEvent.keyDown(screen.getByTestId("task-title-input"), { key: "Enter" }) + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + it.each([ ["a blank value", " "], ["the original prompt", "Original prompt"], @@ -73,6 +99,20 @@ describe("TaskTitleEditor", () => { expect(onDone).toHaveBeenCalled() }) + it.each([ + ["isComposing is set", { isComposing: true }], + ["the IME keyCode 229 is reported", { keyCode: 229 }], + ])("ignores Enter while an IME composition is active (%s)", (_label, imeState) => { + const { input, onDone } = renderEditor() + + fireEvent.change(input, { target: { value: "にほん" } }) + fireEvent.keyDown(input, { key: "Enter", ...imeState }) + + expect(vscode.postMessage).not.toHaveBeenCalled() + expect(onDone).not.toHaveBeenCalled() + expect(input).toBeInTheDocument() + }) + it("lets keydown reach window so VS Code can run clipboard shortcuts", () => { const onWindowKeyDown = vi.fn() window.addEventListener("keydown", onWindowKeyDown) diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2bff5fbfafc180d5f2540e8a2becd186ab958445 GIT binary patch literal 18446 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 bObyB2;h5=9<;w@JK-84Am5QGH6a0Sx+Ou|I literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast-light.png new file mode 100644 index 0000000000000000000000000000000000000000..1174b66e5b34c1d2015c937aa9826c3fbca88dff GIT binary patch literal 18263 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 fA4Ifc%q4;K!>D-b;$!y7oQ`_KLxk4g>j literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-high-contrast.png new file mode 100644 index 0000000000000000000000000000000000000000..75cbf522227cd3739c82b02fee56c6907bd335ce GIT binary patch literal 18434 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 literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png b/webview-ui/src/components/history/__tests__/__screenshots__/history-rename-editor-light.png new file mode 100644 index 0000000000000000000000000000000000000000..7190f6b6c8d6a337d5f66e91caf729254b722e48 GIT binary patch literal 18536 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 literal 0 HcmV?d00001 From aef968e54308099c65bda84dc6161ab928c62cc9 Mon Sep 17 00:00:00 2001 From: Marian Popeanga Date: Fri, 23 Oct 2026 11:39:15 -0700 Subject: [PATCH 3/3] #2122 keep task titles safe from stale history writes --- src/core/webview/ClineProvider.ts | 12 +++++- .../ClineProvider.taskHistory.spec.ts | 37 +++++++++++++++++-- .../history/__tests__/SubtaskRow.spec.tsx | 9 +++++ .../history/__tests__/useTaskSearch.spec.tsx | 25 +++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 67422a9b54..eafd902eb2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -3274,6 +3274,10 @@ export class ClineProvider * Updates a task in the task history and optionally broadcasts the updated history to the webview. * Now delegates to TaskHistoryStore for per-task file persistence. * + * This writer never owns the `title` field: `title` is written only by the rename path + * (`renameTaskWithId` via `taskHistoryStore.atomicReadAndUpdate`). Callers here build items + * from snapshots read before taking the store lock, so any title they carry can be stale. + * * @param item The history item to update or add * @param options.broadcast Whether to broadcast the updated history to the webview (default: true) * @returns The updated task history array @@ -3281,13 +3285,17 @@ export class ClineProvider async updateTaskHistory(item: HistoryItem, options: { broadcast?: boolean } = {}): Promise { const { broadcast = true } = options - const history = await this.taskHistoryStore.upsert(item) + // Drop the snapshot's title so upsertCore preserves the value held under the store lock + // instead of treating a stale title as a changed field and reverting a concurrent rename. + const { title: _title, ...itemWithoutTitle } = item + + const history = await this.taskHistoryStore.upsert(itemWithoutTitle) this.recentTasksCache = undefined // Broadcast the updated history to the webview if requested. // Prefer per-item updates to avoid repeatedly cloning/sending the full history. if (broadcast && this.isViewLaunched) { - const updatedItem = this.taskHistoryStore.get(item.id) ?? item + const updatedItem = this.taskHistoryStore.get(item.id) ?? itemWithoutTitle await this.postMessageToWebview({ type: "taskHistoryItemUpdated", taskHistoryItem: updatedItem }) } diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index 7aaa0740ce..30d635df4b 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -618,10 +618,12 @@ describe("ClineProvider Task History Synchronization", () => { ["whitespace", " "], ["undefined", undefined], ])("clears the title when given %s", async (_label, title) => { - await provider.updateTaskHistory( - createHistoryItem({ id: "task-clear", task: "Original prompt", title: "Old title" }), - { broadcast: false }, - ) + // Set the starting title through the rename path: updateTaskHistory drops `title`. + await provider.updateTaskHistory(createHistoryItem({ id: "task-clear", task: "Original prompt" }), { + broadcast: false, + }) + await provider.renameTaskWithId("task-clear", "Old title") + expect(getStored("task-clear")?.title).toBe("Old title") await provider.renameTaskWithId("task-clear", title) @@ -652,6 +654,33 @@ describe("ClineProvider Task History Synchronization", () => { expect(getStored("task-race")).toMatchObject({ title: "Renamed", tokensIn: 500 }) }) + it("does not write a title through a full-item update", async () => { + await provider.updateTaskHistory( + createHistoryItem({ id: "task-title-owner", task: "Original prompt", title: "Stale title" }), + { broadcast: false }, + ) + + expect(getStored("task-title-owner")).toMatchObject({ id: "task-title-owner", task: "Original prompt" }) + expect(getStored("task-title-owner")?.title).toBeUndefined() + }) + + it("does not revert a rename when a full-item update from an older snapshot lands", async () => { + const item = createHistoryItem({ id: "task-stale-write", task: "Original prompt", status: "active" }) + await provider.updateTaskHistory(item, { broadcast: false }) + + // A prior rename leaves a title on the record; a caller reading the item now + // (e.g. onTaskCompleted, mode or profile switch) captures it in a pre-lock snapshot. + await provider.renameTaskWithId("task-stale-write", "Old title") + const staleSnapshot = { ...item, title: "Old title" } + + // A queued rename completes after the snapshot was taken. + await provider.renameTaskWithId("task-stale-write", "Renamed") + + await provider.updateTaskHistory({ ...staleSnapshot, status: "completed" }, { broadcast: false }) + + expect(getStored("task-stale-write")).toMatchObject({ title: "Renamed", status: "completed" }) + }) + it("does nothing for an unknown task", async () => { const logSpy = vi.spyOn(provider, "log") diff --git a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx index 6337b9f1fa..4fb609081f 100644 --- a/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx +++ b/webview-ui/src/components/history/__tests__/SubtaskRow.spec.tsx @@ -55,6 +55,15 @@ describe("SubtaskRow", () => { expect(screen.getByText("Leaf task content")).toBeInTheDocument() }) + it("shows the custom title instead of the original prompt when both are set", () => { + const node = createMockNode({ id: "leaf-2", task: "Original prompt", title: "Custom title" }) + + render() + + expect(screen.getByText("Custom title")).toBeInTheDocument() + expect(screen.queryByText("Original prompt")).not.toBeInTheDocument() + }) + it("renders with correct depth indentation", () => { const node = createMockNode({ id: "leaf-1", task: "Indented task" }) diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx index f4b0ea37c8..179fa9d3de 100644 --- a/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx +++ b/webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx @@ -13,8 +13,10 @@ vi.mock("@/utils/highlight", () => ({ })) import { useExtensionState } from "@/context/ExtensionStateContext" +import { highlightFzfMatch } from "@/utils/highlight" const mockUseExtensionState = useExtensionState as ReturnType +const mockHighlightFzfMatch = highlightFzfMatch as ReturnType const mockTaskHistory: HistoryItem[] = [ { @@ -170,6 +172,29 @@ describe("useTaskSearch", () => { ]) }) + it("keeps match positions from a custom title that is longer than the task", () => { + // "Write unit tests" (16 chars) vs "Coverage sprint with deep tests" (31 chars): + // with the old task.length bound, every position of "deep" (21-24) would be filtered out. + mockUseExtensionState.mockReturnValue({ + taskHistory: [{ ...mockTaskHistory[1], title: "Coverage sprint with deep tests" }], + cwd: "/workspace/project1", + }) + const { result } = renderHook(() => useTaskSearch()) + + act(() => { + result.current.setSearchQuery("deep") + }) + + expect(result.current.tasks).toEqual([ + expect.objectContaining({ id: "task-2", highlight: "Coverage sprint with deep tests" }), + ]) + // fzf reports positions in match order; normalize before comparing so this pins the + // position set (21-24), not fzf's internal ordering. + const lastCall = mockHighlightFzfMatch.mock.calls.at(-1) + expect(lastCall?.[0]).toBe("Coverage sprint with deep tests") + expect([...(lastCall?.[1] ?? [])].sort((a, b) => a - b)).toEqual([21, 22, 23, 24]) + }) + it("automatically switches to mostRelevant when searching", () => { const { result } = renderHook(() => useTaskSearch())