mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: implement enhanced chat history management with favorites and custom naming
- Add isFavorite and customName fields to HistoryItem type with backward compatibility - Create FavoriteButton component with star icon and toggle functionality - Create RenameButton component with inline editing capability - Update TaskItemHeader to include favorite and rename buttons - Add favorites filtering toggle to HistoryView - Enhance useTaskSearch hook with favorites filtering and custom name search - Update webview message handlers for toggleTaskFavorite and renameTask operations - Add comprehensive test coverage for new components and functionality - Maintain backward compatibility with existing history data Resolves #6410
This commit is contained in:
parent
8f7ac57e47
commit
f677db3501
14 changed files with 572 additions and 7 deletions
|
|
@ -17,6 +17,8 @@ export const historyItemSchema = z.object({
|
|||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
isFavorite: z.boolean().optional(),
|
||||
customName: z.string().optional(),
|
||||
})
|
||||
|
||||
export type HistoryItem = z.infer<typeof historyItemSchema>
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ export async function taskMetadata({
|
|||
size: taskDirSize,
|
||||
workspace,
|
||||
mode,
|
||||
isFavorite: false, // Initialize as not favorited
|
||||
customName: undefined, // Initialize with no custom name
|
||||
}
|
||||
|
||||
return { historyItem, tokenUsage }
|
||||
|
|
|
|||
|
|
@ -2558,5 +2558,57 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "toggleTaskFavorite": {
|
||||
if (message.taskId) {
|
||||
try {
|
||||
// Get the task and update its favorite status
|
||||
const { historyItem } = await provider.getTaskWithId(message.taskId)
|
||||
if (historyItem) {
|
||||
// Toggle the favorite status
|
||||
const updatedHistoryItem = {
|
||||
...historyItem,
|
||||
isFavorite: !historyItem.isFavorite,
|
||||
}
|
||||
|
||||
// Update the task metadata
|
||||
await provider.updateTaskHistory(updatedHistoryItem)
|
||||
|
||||
// Refresh the webview state to reflect the change
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error toggling task favorite: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(t("common:errors.toggle_favorite_failed"))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "renameTask": {
|
||||
if (message.taskId && message.newName !== undefined) {
|
||||
try {
|
||||
// Get the task and update its custom name
|
||||
const { historyItem } = await provider.getTaskWithId(message.taskId)
|
||||
if (historyItem) {
|
||||
// Update the custom name (empty string means remove custom name)
|
||||
const updatedHistoryItem = {
|
||||
...historyItem,
|
||||
customName: message.newName.trim() || undefined,
|
||||
}
|
||||
|
||||
// Update the task metadata
|
||||
await provider.updateTaskHistory(updatedHistoryItem)
|
||||
|
||||
// Refresh the webview state to reflect the change
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
} catch (error) {
|
||||
provider.log(`Error renaming task: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
|
||||
vscode.window.showErrorMessage(t("common:errors.rename_task_failed"))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@
|
|||
"command_already_exists": "Command \"{{commandName}}\" already exists",
|
||||
"create_command_failed": "Failed to create command",
|
||||
"command_template_content": "---\ndescription: \"Brief description of what this command does\"\n---\n\nThis is a new slash command. Edit this file to customize the command behavior.",
|
||||
"toggle_favorite_failed": "Failed to toggle task favorite status",
|
||||
"rename_task_failed": "Failed to rename task",
|
||||
"claudeCode": {
|
||||
"processExited": "Claude Code process exited with code {{exitCode}}.",
|
||||
"errorOutput": "Error output: {{output}}",
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@ export interface WebviewMessage {
|
|||
| "deleteCommand"
|
||||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "toggleTaskFavorite"
|
||||
| "renameTask"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
@ -270,6 +272,8 @@ export interface WebviewMessage {
|
|||
codebaseIndexGeminiApiKey?: string
|
||||
codebaseIndexMistralApiKey?: string
|
||||
}
|
||||
taskId?: string
|
||||
newName?: string
|
||||
}
|
||||
|
||||
export const checkoutDiffPayloadSchema = z.object({
|
||||
|
|
|
|||
28
webview-ui/src/components/history/FavoriteButton.tsx
Normal file
28
webview-ui/src/components/history/FavoriteButton.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
|
||||
interface FavoriteButtonProps {
|
||||
isFavorite: boolean
|
||||
onToggleFavorite: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const FavoriteButton: React.FC<FavoriteButtonProps> = ({ isFavorite, onToggleFavorite, className = "" }) => {
|
||||
return (
|
||||
<StandardTooltip content={isFavorite ? "Remove from favorites" : "Add to favorites"}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggleFavorite()
|
||||
}}
|
||||
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${className}`}
|
||||
data-testid="favorite-button">
|
||||
<span
|
||||
className={`codicon ${
|
||||
isFavorite ? "codicon-star-full" : "codicon-star-empty"
|
||||
} text-sm ${isFavorite ? "text-yellow-400" : "text-vscode-descriptionForeground"}`}
|
||||
/>
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -37,6 +37,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
setLastNonRelevantSort,
|
||||
showAllWorkspaces,
|
||||
setShowAllWorkspaces,
|
||||
showFavoritesOnly,
|
||||
setShowFavoritesOnly,
|
||||
handleToggleFavorite,
|
||||
handleRename,
|
||||
} = useTaskSearch()
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
|
|
@ -152,6 +156,27 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={showFavoritesOnly ? "favorites" : "all"}
|
||||
onValueChange={(value) => setShowFavoritesOnly(value === "favorites")}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue>{showFavoritesOnly ? "Favorites Only" : "All Tasks"}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="codicon codicon-list-unordered" />
|
||||
All Tasks
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="favorites">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="codicon codicon-star-full text-yellow-400" />
|
||||
Favorites Only
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue>
|
||||
|
|
@ -243,6 +268,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
isSelected={selectedTaskIds.includes(item.id)}
|
||||
onToggleSelection={toggleTaskSelection}
|
||||
onDelete={setDeleteTaskId}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onRename={handleRename}
|
||||
className="m-2 mr-0"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
80
webview-ui/src/components/history/RenameButton.tsx
Normal file
80
webview-ui/src/components/history/RenameButton.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useState } from "react"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface RenameButtonProps {
|
||||
currentName: string
|
||||
onRename: (newName: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const RenameButton: React.FC<RenameButtonProps> = ({ currentName, onRename, className = "" }) => {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState(currentName)
|
||||
|
||||
const handleStartEdit = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setEditValue(currentName)
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmedValue = editValue.trim()
|
||||
if (trimmedValue !== currentName) {
|
||||
onRename(trimmedValue)
|
||||
}
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditValue(currentName)
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSave()
|
||||
} else if (e.key === "Escape") {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<VSCodeTextField
|
||||
value={editValue}
|
||||
onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="text-xs"
|
||||
style={{ minWidth: "120px" }}
|
||||
autoFocus
|
||||
data-testid="rename-input"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors"
|
||||
data-testid="rename-save">
|
||||
<span className="codicon codicon-check text-xs text-green-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors"
|
||||
data-testid="rename-cancel">
|
||||
<span className="codicon codicon-close text-xs text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardTooltip content="Rename task">
|
||||
<button
|
||||
onClick={handleStartEdit}
|
||||
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${className}`}
|
||||
data-testid="rename-button">
|
||||
<span className="codicon codicon-edit text-sm text-vscode-descriptionForeground" />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ interface TaskItemProps {
|
|||
isSelected?: boolean
|
||||
onToggleSelection?: (taskId: string, isSelected: boolean) => void
|
||||
onDelete?: (taskId: string) => void
|
||||
onToggleFavorite?: (taskId: string) => void
|
||||
onRename?: (taskId: string, newName: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +33,8 @@ const TaskItem = ({
|
|||
isSelected = false,
|
||||
onToggleSelection,
|
||||
onDelete,
|
||||
onToggleFavorite,
|
||||
onRename,
|
||||
className,
|
||||
}: TaskItemProps) => {
|
||||
const handleClick = () => {
|
||||
|
|
@ -70,7 +74,13 @@ const TaskItem = ({
|
|||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Header with metadata */}
|
||||
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />
|
||||
<TaskItemHeader
|
||||
item={item}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onDelete={onDelete}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onRename={onRename}
|
||||
/>
|
||||
|
||||
{/* Task content */}
|
||||
<div
|
||||
|
|
@ -80,7 +90,7 @@ const TaskItem = ({
|
|||
})}
|
||||
data-testid="task-content"
|
||||
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
|
||||
{item.highlight ? undefined : item.task}
|
||||
{item.highlight ? undefined : item.customName || item.task}
|
||||
</div>
|
||||
|
||||
{/* Task Item Footer */}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,27 @@ import React from "react"
|
|||
import type { HistoryItem } from "@roo-code/types"
|
||||
import { formatDate } from "@/utils/format"
|
||||
import { DeleteButton } from "./DeleteButton"
|
||||
import { FavoriteButton } from "./FavoriteButton"
|
||||
import { RenameButton } from "./RenameButton"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TaskItemHeaderProps {
|
||||
item: HistoryItem
|
||||
isSelectionMode: boolean
|
||||
onDelete?: (taskId: string) => void
|
||||
onToggleFavorite?: (taskId: string) => void
|
||||
onRename?: (taskId: string, newName: string) => void
|
||||
}
|
||||
|
||||
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
|
||||
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({
|
||||
item,
|
||||
isSelectionMode,
|
||||
onDelete,
|
||||
onToggleFavorite,
|
||||
onRename,
|
||||
}) => {
|
||||
const displayName = item.customName || item.task
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex justify-between items-center", {
|
||||
|
|
@ -22,11 +34,23 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
|
|||
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
{item.isFavorite && (
|
||||
<span className="codicon codicon-star-full text-yellow-400 text-xs" title="Favorited" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
{!isSelectionMode && (
|
||||
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
|
||||
{onToggleFavorite && (
|
||||
<FavoriteButton
|
||||
isFavorite={item.isFavorite || false}
|
||||
onToggleFavorite={() => onToggleFavorite(item.id)}
|
||||
/>
|
||||
)}
|
||||
{onRename && (
|
||||
<RenameButton currentName={displayName} onRename={(newName) => onRename(item.id, newName)} />
|
||||
)}
|
||||
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { FavoriteButton } from "../FavoriteButton"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
const renderWithTooltipProvider = (component: React.ReactElement) => {
|
||||
return render(<TooltipProvider>{component}</TooltipProvider>)
|
||||
}
|
||||
|
||||
describe("FavoriteButton", () => {
|
||||
it("renders unfavorited state by default", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
renderWithTooltipProvider(<FavoriteButton isFavorite={false} onToggleFavorite={mockOnToggle} />)
|
||||
|
||||
const button = screen.getByTestId("favorite-button")
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
const icon = button.querySelector("span")
|
||||
expect(icon).toHaveClass("text-vscode-descriptionForeground")
|
||||
expect(icon).not.toHaveClass("text-yellow-400")
|
||||
})
|
||||
|
||||
it("renders favorited state correctly", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
renderWithTooltipProvider(<FavoriteButton isFavorite={true} onToggleFavorite={mockOnToggle} />)
|
||||
|
||||
const button = screen.getByTestId("favorite-button")
|
||||
expect(button).toBeInTheDocument()
|
||||
|
||||
const icon = button.querySelector("span")
|
||||
expect(icon).toHaveClass("text-yellow-400")
|
||||
expect(icon).not.toHaveClass("text-vscode-descriptionForeground")
|
||||
})
|
||||
|
||||
it("calls onToggleFavorite when clicked", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
renderWithTooltipProvider(<FavoriteButton isFavorite={false} onToggleFavorite={mockOnToggle} />)
|
||||
|
||||
const button = screen.getByTestId("favorite-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(mockOnToggle).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("uses correct icon for unfavorited state", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
renderWithTooltipProvider(<FavoriteButton isFavorite={false} onToggleFavorite={mockOnToggle} />)
|
||||
|
||||
const icon = screen.getByTestId("favorite-button").querySelector("span")
|
||||
expect(icon).toHaveClass("codicon-star-empty")
|
||||
expect(icon).not.toHaveClass("codicon-star-full")
|
||||
})
|
||||
|
||||
it("uses correct icon for favorited state", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
renderWithTooltipProvider(<FavoriteButton isFavorite={true} onToggleFavorite={mockOnToggle} />)
|
||||
|
||||
const icon = screen.getByTestId("favorite-button").querySelector("span")
|
||||
expect(icon).toHaveClass("codicon-star-full")
|
||||
expect(icon).not.toHaveClass("codicon-star-empty")
|
||||
})
|
||||
|
||||
it("stops event propagation when clicked", () => {
|
||||
const mockOnToggle = vi.fn()
|
||||
const mockParentClick = vi.fn()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<div onClick={mockParentClick}>
|
||||
<FavoriteButton isFavorite={false} onToggleFavorite={mockOnToggle} />
|
||||
</div>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const button = screen.getByTestId("favorite-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(mockOnToggle).toHaveBeenCalledTimes(1)
|
||||
expect(mockParentClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
import { RenameButton } from "../RenameButton"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
const renderWithTooltipProvider = (component: React.ReactElement) => {
|
||||
return render(<TooltipProvider>{component}</TooltipProvider>)
|
||||
}
|
||||
|
||||
describe("RenameButton", () => {
|
||||
it("renders rename button by default", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows input field when edit mode is activated", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByTestId("rename-input")
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue("Test Task")
|
||||
})
|
||||
|
||||
it("shows save and cancel buttons in edit mode", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const saveButton = screen.getByTestId("rename-save")
|
||||
const cancelButton = screen.getByTestId("rename-cancel")
|
||||
|
||||
expect(saveButton).toBeInTheDocument()
|
||||
expect(cancelButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("cancels edit mode when cancel button is clicked", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const cancelButton = screen.getByTestId("rename-cancel")
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(mockOnRename).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId("rename-button")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("does not call onRename when value is unchanged", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByTestId("rename-input")
|
||||
fireEvent.keyDown(input, { key: "Enter" })
|
||||
|
||||
expect(mockOnRename).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("stops event propagation when editing", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
const mockParentClick = vi.fn()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<div onClick={mockParentClick}>
|
||||
<RenameButton currentName="Test Task" onRename={mockOnRename} />
|
||||
</div>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const editContainer = screen.getByTestId("rename-input").parentElement
|
||||
fireEvent.click(editContainer!)
|
||||
|
||||
expect(mockParentClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("cancels edit mode when Escape key is pressed", () => {
|
||||
const mockOnRename = vi.fn()
|
||||
renderWithTooltipProvider(<RenameButton currentName="Test Task" onRename={mockOnRename} />)
|
||||
|
||||
const button = screen.getByTestId("rename-button")
|
||||
fireEvent.click(button)
|
||||
|
||||
const input = screen.getByTestId("rename-input")
|
||||
fireEvent.keyDown(input, { key: "Escape" })
|
||||
|
||||
expect(mockOnRename).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId("rename-button")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -12,7 +12,14 @@ vi.mock("@/utils/highlight", () => ({
|
|||
highlightFzfMatch: vi.fn((text) => `<mark>${text}</mark>`),
|
||||
}))
|
||||
|
||||
vi.mock("@/utils/vscode", () => ({
|
||||
vscode: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
const mockUseExtensionState = useExtensionState as ReturnType<typeof vi.fn>
|
||||
|
||||
|
|
@ -26,6 +33,8 @@ const mockTaskHistory: HistoryItem[] = [
|
|||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/workspace/project1",
|
||||
isFavorite: false,
|
||||
customName: undefined,
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
|
|
@ -38,6 +47,8 @@ const mockTaskHistory: HistoryItem[] = [
|
|||
cacheWrites: 25,
|
||||
cacheReads: 10,
|
||||
workspace: "/workspace/project1",
|
||||
isFavorite: true,
|
||||
customName: "My Custom Test Task",
|
||||
},
|
||||
{
|
||||
id: "task-3",
|
||||
|
|
@ -48,6 +59,8 @@ const mockTaskHistory: HistoryItem[] = [
|
|||
tokensOut: 75,
|
||||
totalCost: 0.05,
|
||||
workspace: "/workspace/project2",
|
||||
isFavorite: true,
|
||||
customName: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -284,4 +297,113 @@ describe("useTaskSearch", () => {
|
|||
// When not searching, it should fall back to newest
|
||||
expect(result.current.sortOption).toBe("mostRelevant")
|
||||
})
|
||||
|
||||
describe("favorites functionality", () => {
|
||||
it("filters to show only favorites when showFavoritesOnly is true", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowAllWorkspaces(true)
|
||||
result.current.setShowFavoritesOnly(true)
|
||||
})
|
||||
|
||||
expect(result.current.tasks).toHaveLength(2)
|
||||
expect(result.current.tasks.every((task) => task.isFavorite)).toBe(true)
|
||||
expect(result.current.tasks[0].id).toBe("task-2")
|
||||
expect(result.current.tasks[1].id).toBe("task-3")
|
||||
})
|
||||
|
||||
it("shows all tasks when showFavoritesOnly is false", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowAllWorkspaces(true)
|
||||
result.current.setShowFavoritesOnly(false)
|
||||
})
|
||||
|
||||
expect(result.current.tasks).toHaveLength(3)
|
||||
expect(result.current.showFavoritesOnly).toBe(false)
|
||||
})
|
||||
|
||||
it("handles toggle favorite functionality", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleFavorite("task-1")
|
||||
})
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "toggleTaskFavorite",
|
||||
taskId: "task-1",
|
||||
})
|
||||
})
|
||||
|
||||
it("handles rename functionality", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.handleRename("task-1", "New Task Name")
|
||||
})
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "renameTask",
|
||||
taskId: "task-1",
|
||||
newName: "New Task Name",
|
||||
})
|
||||
})
|
||||
|
||||
it("searches in custom names when available", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowAllWorkspaces(true)
|
||||
result.current.setSearchQuery("Custom")
|
||||
})
|
||||
|
||||
expect(result.current.tasks).toHaveLength(1)
|
||||
expect(result.current.tasks[0].id).toBe("task-2")
|
||||
expect((result.current.tasks[0] as any).highlight).toBe("<mark>My Custom Test Task</mark>")
|
||||
})
|
||||
|
||||
it("falls back to original task name when custom name is not available", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowAllWorkspaces(true)
|
||||
result.current.setSearchQuery("React")
|
||||
})
|
||||
|
||||
expect(result.current.tasks).toHaveLength(1)
|
||||
expect(result.current.tasks[0].id).toBe("task-1")
|
||||
expect((result.current.tasks[0] as any).highlight).toBe("<mark>Create a React component</mark>")
|
||||
})
|
||||
|
||||
it("combines favorites filter with search", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowAllWorkspaces(true)
|
||||
result.current.setShowFavoritesOnly(true)
|
||||
result.current.setSearchQuery("authentication")
|
||||
})
|
||||
|
||||
expect(result.current.tasks).toHaveLength(1)
|
||||
expect(result.current.tasks[0].id).toBe("task-3")
|
||||
expect(result.current.tasks[0].isFavorite).toBe(true)
|
||||
})
|
||||
|
||||
it("combines favorites filter with workspace filter", () => {
|
||||
const { result } = renderHook(() => useTaskSearch())
|
||||
|
||||
act(() => {
|
||||
result.current.setShowFavoritesOnly(true)
|
||||
})
|
||||
|
||||
// Should only show favorites from current workspace
|
||||
expect(result.current.tasks).toHaveLength(1)
|
||||
expect(result.current.tasks[0].id).toBe("task-2")
|
||||
expect(result.current.tasks[0].workspace).toBe("/workspace/project1")
|
||||
expect(result.current.tasks[0].isFavorite).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Fzf } from "fzf"
|
|||
|
||||
import { highlightFzfMatch } from "@/utils/highlight"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { vscode } from "@/utils/vscode"
|
||||
|
||||
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ export const useTaskSearch = () => {
|
|||
const [sortOption, setSortOption] = useState<SortOption>("newest")
|
||||
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
|
||||
const [showAllWorkspaces, setShowAllWorkspaces] = useState(false)
|
||||
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
|
||||
|
|
@ -28,12 +30,15 @@ export const useTaskSearch = () => {
|
|||
if (!showAllWorkspaces) {
|
||||
tasks = tasks.filter((item) => item.workspace === cwd)
|
||||
}
|
||||
if (showFavoritesOnly) {
|
||||
tasks = tasks.filter((item) => item.isFavorite === true)
|
||||
}
|
||||
return tasks
|
||||
}, [taskHistory, showAllWorkspaces, cwd])
|
||||
}, [taskHistory, showAllWorkspaces, showFavoritesOnly, cwd])
|
||||
|
||||
const fzf = useMemo(() => {
|
||||
return new Fzf(presentableTasks, {
|
||||
selector: (item) => item.task,
|
||||
selector: (item) => item.customName || item.task,
|
||||
})
|
||||
}, [presentableTasks])
|
||||
|
||||
|
|
@ -44,12 +49,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 searchText = result.item.customName || result.item.task
|
||||
const taskEndIndex = searchText.length
|
||||
|
||||
return {
|
||||
...result.item,
|
||||
highlight: highlightFzfMatch(
|
||||
result.item.task,
|
||||
searchText,
|
||||
positions.filter((p) => p < taskEndIndex),
|
||||
),
|
||||
workspace: result.item.workspace,
|
||||
|
|
@ -78,6 +84,21 @@ export const useTaskSearch = () => {
|
|||
})
|
||||
}, [presentableTasks, searchQuery, fzf, sortOption])
|
||||
|
||||
const handleToggleFavorite = (taskId: string) => {
|
||||
vscode.postMessage({
|
||||
type: "toggleTaskFavorite",
|
||||
taskId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRename = (taskId: string, newName: string) => {
|
||||
vscode.postMessage({
|
||||
type: "renameTask",
|
||||
taskId,
|
||||
newName,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
searchQuery,
|
||||
|
|
@ -88,5 +109,9 @@ export const useTaskSearch = () => {
|
|||
setLastNonRelevantSort,
|
||||
showAllWorkspaces,
|
||||
setShowAllWorkspaces,
|
||||
showFavoritesOnly,
|
||||
setShowFavoritesOnly,
|
||||
handleToggleFavorite,
|
||||
handleRename,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue