Skip to content

Commit a8e9b8f

Browse files
committed
feat: Add task pinning functionality
- Add pinnedTasks field to global settings schema - Implement toggleTaskPin message handler for backend state management - Update ExtensionStateContext to include pinned tasks state - Add pin/unpin button to TaskItemHeader with consistent codicon styling - Update task sorting logic to prioritize pinned tasks first - Prevent deletion of pinned tasks with user-friendly alerts - Add translation keys for pin/unpin functionality - Integrate pinned tasks state throughout the application Resolves #6244
1 parent b117c0f commit a8e9b8f

File tree

10 files changed

+98
-5
lines changed

10 files changed

+98
-5
lines changed

packages/types/src/global-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const globalSettingsSchema = z.object({
3737
currentApiConfigName: z.string().optional(),
3838
listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(),
3939
pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
40+
pinnedTasks: z.record(z.string(), z.boolean()).optional(),
4041

4142
lastShownAnnouncementId: z.string().optional(),
4243
customInstructions: z.string().optional(),
@@ -218,6 +219,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
218219
lastShownAnnouncementId: "jul-09-2025-3-23-0",
219220

220221
pinnedApiConfigs: {},
222+
pinnedTasks: {},
221223

222224
autoApprovalEnabled: true,
223225
alwaysAllowReadOnly: true,

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,6 +1479,7 @@ export class ClineProvider
14791479
currentApiConfigName,
14801480
listApiConfigMeta,
14811481
pinnedApiConfigs,
1482+
pinnedTasks,
14821483
mode,
14831484
customModePrompts,
14841485
customSupportPrompts,
@@ -1583,6 +1584,7 @@ export class ClineProvider
15831584
currentApiConfigName: currentApiConfigName ?? "default",
15841585
listApiConfigMeta: listApiConfigMeta ?? [],
15851586
pinnedApiConfigs: pinnedApiConfigs ?? {},
1587+
pinnedTasks: pinnedTasks ?? {},
15861588
mode: mode ?? defaultModeSlug,
15871589
customModePrompts: customModePrompts ?? {},
15881590
customSupportPrompts: customSupportPrompts ?? {},
@@ -1760,6 +1762,7 @@ export class ClineProvider
17601762
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
17611763
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
17621764
pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
1765+
pinnedTasks: stateValues.pinnedTasks ?? {},
17631766
modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
17641767
customModePrompts: stateValues.customModePrompts ?? {},
17651768
customSupportPrompts: stateValues.customSupportPrompts ?? {},

src/core/webview/webviewMessageHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,21 @@ export const webviewMessageHandler = async (
13071307
await provider.postStateToWebview()
13081308
}
13091309
break
1310+
case "toggleTaskPin":
1311+
if (message.text) {
1312+
const currentPinnedTasks = getGlobalState("pinnedTasks") ?? {}
1313+
const updatedPinnedTasks: Record<string, boolean> = { ...currentPinnedTasks }
1314+
1315+
if (currentPinnedTasks[message.text]) {
1316+
delete updatedPinnedTasks[message.text]
1317+
} else {
1318+
updatedPinnedTasks[message.text] = true
1319+
}
1320+
1321+
await updateGlobalState("pinnedTasks", updatedPinnedTasks)
1322+
await provider.postStateToWebview()
1323+
}
1324+
break
13101325
case "enhancementApiConfigId":
13111326
await updateGlobalState("enhancementApiConfigId", message.text)
13121327
await provider.postStateToWebview()

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export interface ExtensionMessage {
9999
| "maxReadFileLine"
100100
| "fileSearchResults"
101101
| "toggleApiConfigPin"
102+
| "toggleTaskPin"
102103
| "acceptInput"
103104
| "setHistoryPreviewCollapsed"
104105
| "commandExecutionStatus"
@@ -199,6 +200,7 @@ export type ExtensionState = Pick<
199200
| "currentApiConfigName"
200201
| "listApiConfigMeta"
201202
| "pinnedApiConfigs"
203+
| "pinnedTasks"
202204
// | "lastShownAnnouncementId"
203205
| "customInstructions"
204206
// | "taskHistory" // Optional in GlobalSettings, required here.

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export interface WebviewMessage {
169169
| "maxDiagnosticMessages"
170170
| "searchFiles"
171171
| "toggleApiConfigPin"
172+
| "toggleTaskPin"
172173
| "setHistoryPreviewCollapsed"
173174
| "hasOpenedModeSelector"
174175
| "accountButtonClicked"

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback } from "react"
22

33
import { Button, StandardTooltip } from "@/components/ui"
44
import { useAppTranslation } from "@/i18n/TranslationContext"
5+
import { useExtensionState } from "@/context/ExtensionStateContext"
56
import { vscode } from "@/utils/vscode"
67

78
type DeleteButtonProps = {
@@ -11,17 +12,27 @@ type DeleteButtonProps = {
1112

1213
export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
1314
const { t } = useAppTranslation()
15+
const { pinnedTasks } = useExtensionState()
16+
const isPinned = pinnedTasks?.[itemId] || false
1417

1518
const handleDeleteClick = useCallback(
1619
(e: React.MouseEvent) => {
1720
e.stopPropagation()
21+
22+
// Prevent deletion of pinned tasks
23+
if (isPinned) {
24+
// Show a simple alert for now - we can improve this later
25+
alert(t("history:pinnedTaskCannotDelete"))
26+
return
27+
}
28+
1829
if (e.shiftKey) {
1930
vscode.postMessage({ type: "deleteTaskWithId", text: itemId })
2031
} else if (onDelete) {
2132
onDelete(itemId)
2233
}
2334
},
24-
[itemId, onDelete],
35+
[itemId, onDelete, isPinned, t],
2536
)
2637

2738
return (

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import type { HistoryItem } from "@roo-code/types"
33
import { formatDate } from "@/utils/format"
44
import { DeleteButton } from "./DeleteButton"
55
import { cn } from "@/lib/utils"
6+
import { Button } from "@/components/ui/button"
7+
import { StandardTooltip } from "@/components/ui"
8+
import { useExtensionState } from "@/context/ExtensionStateContext"
9+
import { vscode } from "@/utils/vscode"
10+
import { useAppTranslation } from "@/i18n/TranslationContext"
611

712
export interface TaskItemHeaderProps {
813
item: HistoryItem
@@ -11,6 +16,18 @@ export interface TaskItemHeaderProps {
1116
}
1217

1318
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
19+
const { pinnedTasks, togglePinnedTask } = useExtensionState()
20+
const { t } = useAppTranslation()
21+
const isPinned = pinnedTasks?.[item.id] || false
22+
23+
const handlePinToggle = (e: React.MouseEvent) => {
24+
e.stopPropagation()
25+
togglePinnedTask(item.id)
26+
vscode.postMessage({
27+
type: "toggleTaskPin",
28+
text: item.id,
29+
})
30+
}
1431
return (
1532
<div
1633
className={cn("flex justify-between items-center", {
@@ -27,6 +44,17 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
2744
{/* Action Buttons */}
2845
{!isSelectionMode && (
2946
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
47+
<StandardTooltip content={isPinned ? t("history:unpin") : t("history:pin")}>
48+
<Button
49+
variant="ghost"
50+
size="sm"
51+
onClick={handlePinToggle}
52+
className={cn("size-5 flex items-center justify-center", {
53+
"opacity-100 bg-accent": isPinned,
54+
})}>
55+
<span className="codicon codicon-pin text-xs opacity-50" />
56+
</Button>
57+
</StandardTooltip>
3058
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
3159
</div>
3260
)}

webview-ui/src/components/history/useTaskSearch.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
77
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
88

99
export const useTaskSearch = () => {
10-
const { taskHistory, cwd } = useExtensionState()
10+
const { taskHistory, cwd, pinnedTasks } = useExtensionState()
1111
const [searchQuery, setSearchQuery] = useState("")
1212
const [sortOption, setSortOption] = useState<SortOption>("newest")
1313
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@@ -57,8 +57,16 @@ export const useTaskSearch = () => {
5757
})
5858
}
5959

60-
// Then sort the results
60+
// Then sort the results with pinned tasks first
6161
return [...results].sort((a, b) => {
62+
const aPinned = pinnedTasks?.[a.id] || false
63+
const bPinned = pinnedTasks?.[b.id] || false
64+
65+
// Pinned tasks always come first
66+
if (aPinned && !bPinned) return -1
67+
if (!aPinned && bPinned) return 1
68+
69+
// If both are pinned or both are unpinned, sort by the selected option
6270
switch (sortOption) {
6371
case "oldest":
6472
return (a.ts || 0) - (b.ts || 0)
@@ -76,7 +84,7 @@ export const useTaskSearch = () => {
7684
return (b.ts || 0) - (a.ts || 0)
7785
}
7886
})
79-
}, [presentableTasks, searchQuery, fzf, sortOption])
87+
}, [presentableTasks, searchQuery, fzf, sortOption, pinnedTasks])
8088

8189
return {
8290
tasks,

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ export interface ExtensionStateContextType extends ExtensionState {
129129
pinnedApiConfigs?: Record<string, boolean>
130130
setPinnedApiConfigs: (value: Record<string, boolean>) => void
131131
togglePinnedApiConfig: (configName: string) => void
132+
pinnedTasks?: Record<string, boolean>
133+
setPinnedTasks: (value: Record<string, boolean>) => void
134+
togglePinnedTask: (taskId: string) => void
132135
terminalCompressProgressBar?: boolean
133136
setTerminalCompressProgressBar: (value: boolean) => void
134137
setHistoryPreviewCollapsed: (value: boolean) => void
@@ -216,6 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
216219
maxImageFileSize: 5, // Default max image file size in MB
217220
maxTotalImageSize: 20, // Default max total image size in MB
218221
pinnedApiConfigs: {}, // Empty object for pinned API configs
222+
pinnedTasks: {}, // Empty object for pinned tasks
219223
terminalZshOhMy: false, // Default Oh My Zsh integration setting
220224
maxConcurrentFileReads: 5, // Default concurrent file reads
221225
terminalZshP10k: false, // Default Powerlevel10k integration setting
@@ -464,6 +468,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
464468
setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })),
465469
setMaxTotalImageSize: (value) => setState((prevState) => ({ ...prevState, maxTotalImageSize: value })),
466470
setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })),
471+
setPinnedTasks: (value) => setState((prevState) => ({ ...prevState, pinnedTasks: value })),
467472
setTerminalCompressProgressBar: (value) =>
468473
setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })),
469474
togglePinnedApiConfig: (configId) =>
@@ -481,6 +486,21 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
481486

482487
return { ...prevState, pinnedApiConfigs: newPinned }
483488
}),
489+
togglePinnedTask: (taskId) =>
490+
setState((prevState) => {
491+
const currentPinned = prevState.pinnedTasks || {}
492+
const newPinned = {
493+
...currentPinned,
494+
[taskId]: !currentPinned[taskId],
495+
}
496+
497+
// If the task is now unpinned, remove it from the object
498+
if (!newPinned[taskId]) {
499+
delete newPinned[taskId]
500+
}
501+
502+
return { ...prevState, pinnedTasks: newPinned }
503+
}),
484504
setHistoryPreviewCollapsed: (value) =>
485505
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
486506
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),

webview-ui/src/i18n/locales/en/history.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,8 @@
4141
"mostTokens": "Most Tokens",
4242
"mostRelevant": "Most Relevant"
4343
},
44-
"viewAllHistory": "View all tasks"
44+
"viewAllHistory": "View all tasks",
45+
"pin": "Pin task",
46+
"unpin": "Unpin task",
47+
"pinnedTaskCannotDelete": "Cannot delete pinned task. Unpin it first to delete."
4548
}

0 commit comments

Comments
 (0)