Skip to content

Commit f677db3

Browse files
committed
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
1 parent 8f7ac57 commit f677db3

File tree

14 files changed

+572
-7
lines changed

14 files changed

+572
-7
lines changed

packages/types/src/history.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const historyItemSchema = z.object({
1717
size: z.number().optional(),
1818
workspace: z.string().optional(),
1919
mode: z.string().optional(),
20+
isFavorite: z.boolean().optional(),
21+
customName: z.string().optional(),
2022
})
2123

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

src/core/task-persistence/taskMetadata.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export async function taskMetadata({
9595
size: taskDirSize,
9696
workspace,
9797
mode,
98+
isFavorite: false, // Initialize as not favorited
99+
customName: undefined, // Initialize with no custom name
98100
}
99101

100102
return { historyItem, tokenUsage }

src/core/webview/webviewMessageHandler.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2558,5 +2558,57 @@ export const webviewMessageHandler = async (
25582558
}
25592559
break
25602560
}
2561+
case "toggleTaskFavorite": {
2562+
if (message.taskId) {
2563+
try {
2564+
// Get the task and update its favorite status
2565+
const { historyItem } = await provider.getTaskWithId(message.taskId)
2566+
if (historyItem) {
2567+
// Toggle the favorite status
2568+
const updatedHistoryItem = {
2569+
...historyItem,
2570+
isFavorite: !historyItem.isFavorite,
2571+
}
2572+
2573+
// Update the task metadata
2574+
await provider.updateTaskHistory(updatedHistoryItem)
2575+
2576+
// Refresh the webview state to reflect the change
2577+
await provider.postStateToWebview()
2578+
}
2579+
} catch (error) {
2580+
provider.log(
2581+
`Error toggling task favorite: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
2582+
)
2583+
vscode.window.showErrorMessage(t("common:errors.toggle_favorite_failed"))
2584+
}
2585+
}
2586+
break
2587+
}
2588+
case "renameTask": {
2589+
if (message.taskId && message.newName !== undefined) {
2590+
try {
2591+
// Get the task and update its custom name
2592+
const { historyItem } = await provider.getTaskWithId(message.taskId)
2593+
if (historyItem) {
2594+
// Update the custom name (empty string means remove custom name)
2595+
const updatedHistoryItem = {
2596+
...historyItem,
2597+
customName: message.newName.trim() || undefined,
2598+
}
2599+
2600+
// Update the task metadata
2601+
await provider.updateTaskHistory(updatedHistoryItem)
2602+
2603+
// Refresh the webview state to reflect the change
2604+
await provider.postStateToWebview()
2605+
}
2606+
} catch (error) {
2607+
provider.log(`Error renaming task: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
2608+
vscode.window.showErrorMessage(t("common:errors.rename_task_failed"))
2609+
}
2610+
}
2611+
break
2612+
}
25612613
}
25622614
}

src/i18n/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
"command_already_exists": "Command \"{{commandName}}\" already exists",
7979
"create_command_failed": "Failed to create command",
8080
"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.",
81+
"toggle_favorite_failed": "Failed to toggle task favorite status",
82+
"rename_task_failed": "Failed to rename task",
8183
"claudeCode": {
8284
"processExited": "Claude Code process exited with code {{exitCode}}.",
8385
"errorOutput": "Error output: {{output}}",

src/shared/WebviewMessage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ export interface WebviewMessage {
208208
| "deleteCommand"
209209
| "createCommand"
210210
| "insertTextIntoTextarea"
211+
| "toggleTaskFavorite"
212+
| "renameTask"
211213
text?: string
212214
editedMessageContent?: string
213215
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
@@ -270,6 +272,8 @@ export interface WebviewMessage {
270272
codebaseIndexGeminiApiKey?: string
271273
codebaseIndexMistralApiKey?: string
272274
}
275+
taskId?: string
276+
newName?: string
273277
}
274278

275279
export const checkoutDiffPayloadSchema = z.object({
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from "react"
2+
import { StandardTooltip } from "@/components/ui"
3+
4+
interface FavoriteButtonProps {
5+
isFavorite: boolean
6+
onToggleFavorite: () => void
7+
className?: string
8+
}
9+
10+
export const FavoriteButton: React.FC<FavoriteButtonProps> = ({ isFavorite, onToggleFavorite, className = "" }) => {
11+
return (
12+
<StandardTooltip content={isFavorite ? "Remove from favorites" : "Add to favorites"}>
13+
<button
14+
onClick={(e) => {
15+
e.stopPropagation()
16+
onToggleFavorite()
17+
}}
18+
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${className}`}
19+
data-testid="favorite-button">
20+
<span
21+
className={`codicon ${
22+
isFavorite ? "codicon-star-full" : "codicon-star-empty"
23+
} text-sm ${isFavorite ? "text-yellow-400" : "text-vscode-descriptionForeground"}`}
24+
/>
25+
</button>
26+
</StandardTooltip>
27+
)
28+
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
3737
setLastNonRelevantSort,
3838
showAllWorkspaces,
3939
setShowAllWorkspaces,
40+
showFavoritesOnly,
41+
setShowFavoritesOnly,
42+
handleToggleFavorite,
43+
handleRename,
4044
} = useTaskSearch()
4145
const { t } = useAppTranslation()
4246

@@ -152,6 +156,27 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
152156
</SelectItem>
153157
</SelectContent>
154158
</Select>
159+
<Select
160+
value={showFavoritesOnly ? "favorites" : "all"}
161+
onValueChange={(value) => setShowFavoritesOnly(value === "favorites")}>
162+
<SelectTrigger className="flex-1">
163+
<SelectValue>{showFavoritesOnly ? "Favorites Only" : "All Tasks"}</SelectValue>
164+
</SelectTrigger>
165+
<SelectContent>
166+
<SelectItem value="all">
167+
<div className="flex items-center gap-2">
168+
<span className="codicon codicon-list-unordered" />
169+
All Tasks
170+
</div>
171+
</SelectItem>
172+
<SelectItem value="favorites">
173+
<div className="flex items-center gap-2">
174+
<span className="codicon codicon-star-full text-yellow-400" />
175+
Favorites Only
176+
</div>
177+
</SelectItem>
178+
</SelectContent>
179+
</Select>
155180
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
156181
<SelectTrigger className="flex-1">
157182
<SelectValue>
@@ -243,6 +268,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
243268
isSelected={selectedTaskIds.includes(item.id)}
244269
onToggleSelection={toggleTaskSelection}
245270
onDelete={setDeleteTaskId}
271+
onToggleFavorite={handleToggleFavorite}
272+
onRename={handleRename}
246273
className="m-2 mr-0"
247274
/>
248275
)}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useState } from "react"
2+
import { StandardTooltip } from "@/components/ui"
3+
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
4+
5+
interface RenameButtonProps {
6+
currentName: string
7+
onRename: (newName: string) => void
8+
className?: string
9+
}
10+
11+
export const RenameButton: React.FC<RenameButtonProps> = ({ currentName, onRename, className = "" }) => {
12+
const [isEditing, setIsEditing] = useState(false)
13+
const [editValue, setEditValue] = useState(currentName)
14+
15+
const handleStartEdit = (e: React.MouseEvent) => {
16+
e.stopPropagation()
17+
setEditValue(currentName)
18+
setIsEditing(true)
19+
}
20+
21+
const handleSave = () => {
22+
const trimmedValue = editValue.trim()
23+
if (trimmedValue !== currentName) {
24+
onRename(trimmedValue)
25+
}
26+
setIsEditing(false)
27+
}
28+
29+
const handleCancel = () => {
30+
setEditValue(currentName)
31+
setIsEditing(false)
32+
}
33+
34+
const handleKeyDown = (e: React.KeyboardEvent) => {
35+
if (e.key === "Enter") {
36+
handleSave()
37+
} else if (e.key === "Escape") {
38+
handleCancel()
39+
}
40+
}
41+
42+
if (isEditing) {
43+
return (
44+
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
45+
<VSCodeTextField
46+
value={editValue}
47+
onInput={(e) => setEditValue((e.target as HTMLInputElement).value)}
48+
onKeyDown={handleKeyDown}
49+
className="text-xs"
50+
style={{ minWidth: "120px" }}
51+
autoFocus
52+
data-testid="rename-input"
53+
/>
54+
<button
55+
onClick={handleSave}
56+
className="p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors"
57+
data-testid="rename-save">
58+
<span className="codicon codicon-check text-xs text-green-400" />
59+
</button>
60+
<button
61+
onClick={handleCancel}
62+
className="p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors"
63+
data-testid="rename-cancel">
64+
<span className="codicon codicon-close text-xs text-red-400" />
65+
</button>
66+
</div>
67+
)
68+
}
69+
70+
return (
71+
<StandardTooltip content="Rename task">
72+
<button
73+
onClick={handleStartEdit}
74+
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${className}`}
75+
data-testid="rename-button">
76+
<span className="codicon codicon-edit text-sm text-vscode-descriptionForeground" />
77+
</button>
78+
</StandardTooltip>
79+
)
80+
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ interface TaskItemProps {
2020
isSelected?: boolean
2121
onToggleSelection?: (taskId: string, isSelected: boolean) => void
2222
onDelete?: (taskId: string) => void
23+
onToggleFavorite?: (taskId: string) => void
24+
onRename?: (taskId: string, newName: string) => void
2325
className?: string
2426
}
2527

@@ -31,6 +33,8 @@ const TaskItem = ({
3133
isSelected = false,
3234
onToggleSelection,
3335
onDelete,
36+
onToggleFavorite,
37+
onRename,
3438
className,
3539
}: TaskItemProps) => {
3640
const handleClick = () => {
@@ -70,7 +74,13 @@ const TaskItem = ({
7074

7175
<div className="flex-1 min-w-0">
7276
{/* Header with metadata */}
73-
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />
77+
<TaskItemHeader
78+
item={item}
79+
isSelectionMode={isSelectionMode}
80+
onDelete={onDelete}
81+
onToggleFavorite={onToggleFavorite}
82+
onRename={onRename}
83+
/>
7484

7585
{/* Task content */}
7686
<div
@@ -80,7 +90,7 @@ const TaskItem = ({
8090
})}
8191
data-testid="task-content"
8292
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
83-
{item.highlight ? undefined : item.task}
93+
{item.highlight ? undefined : item.customName || item.task}
8494
</div>
8595

8696
{/* Task Item Footer */}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@ import React from "react"
22
import type { HistoryItem } from "@roo-code/types"
33
import { formatDate } from "@/utils/format"
44
import { DeleteButton } from "./DeleteButton"
5+
import { FavoriteButton } from "./FavoriteButton"
6+
import { RenameButton } from "./RenameButton"
57
import { cn } from "@/lib/utils"
68

79
export interface TaskItemHeaderProps {
810
item: HistoryItem
911
isSelectionMode: boolean
1012
onDelete?: (taskId: string) => void
13+
onToggleFavorite?: (taskId: string) => void
14+
onRename?: (taskId: string, newName: string) => void
1115
}
1216

13-
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
17+
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({
18+
item,
19+
isSelectionMode,
20+
onDelete,
21+
onToggleFavorite,
22+
onRename,
23+
}) => {
24+
const displayName = item.customName || item.task
25+
1426
return (
1527
<div
1628
className={cn("flex justify-between items-center", {
@@ -22,11 +34,23 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
2234
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
2335
{formatDate(item.ts)}
2436
</span>
37+
{item.isFavorite && (
38+
<span className="codicon codicon-star-full text-yellow-400 text-xs" title="Favorited" />
39+
)}
2540
</div>
2641

2742
{/* Action Buttons */}
2843
{!isSelectionMode && (
2944
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
45+
{onToggleFavorite && (
46+
<FavoriteButton
47+
isFavorite={item.isFavorite || false}
48+
onToggleFavorite={() => onToggleFavorite(item.id)}
49+
/>
50+
)}
51+
{onRename && (
52+
<RenameButton currentName={displayName} onRename={(newName) => onRename(item.id, newName)} />
53+
)}
3054
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
3155
</div>
3256
)}

0 commit comments

Comments
 (0)