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
This commit is contained in:
Roo Code 2025-07-29 16:09:20 +00:00
parent b117c0fe52
commit a8e9b8f76e
10 changed files with 98 additions and 5 deletions

View file

@ -37,6 +37,7 @@ export const globalSettingsSchema = z.object({
currentApiConfigName: z.string().optional(),
listApiConfigMeta: z.array(providerSettingsEntrySchema).optional(),
pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
pinnedTasks: z.record(z.string(), z.boolean()).optional(),
lastShownAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
@ -218,6 +219,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
lastShownAnnouncementId: "jul-09-2025-3-23-0",
pinnedApiConfigs: {},
pinnedTasks: {},
autoApprovalEnabled: true,
alwaysAllowReadOnly: true,

View file

@ -1479,6 +1479,7 @@ export class ClineProvider
currentApiConfigName,
listApiConfigMeta,
pinnedApiConfigs,
pinnedTasks,
mode,
customModePrompts,
customSupportPrompts,
@ -1583,6 +1584,7 @@ export class ClineProvider
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
pinnedTasks: pinnedTasks ?? {},
mode: mode ?? defaultModeSlug,
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
@ -1760,6 +1762,7 @@ export class ClineProvider
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
pinnedTasks: stateValues.pinnedTasks ?? {},
modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
customModePrompts: stateValues.customModePrompts ?? {},
customSupportPrompts: stateValues.customSupportPrompts ?? {},

View file

@ -1307,6 +1307,21 @@ export const webviewMessageHandler = async (
await provider.postStateToWebview()
}
break
case "toggleTaskPin":
if (message.text) {
const currentPinnedTasks = getGlobalState("pinnedTasks") ?? {}
const updatedPinnedTasks: Record<string, boolean> = { ...currentPinnedTasks }
if (currentPinnedTasks[message.text]) {
delete updatedPinnedTasks[message.text]
} else {
updatedPinnedTasks[message.text] = true
}
await updateGlobalState("pinnedTasks", updatedPinnedTasks)
await provider.postStateToWebview()
}
break
case "enhancementApiConfigId":
await updateGlobalState("enhancementApiConfigId", message.text)
await provider.postStateToWebview()

View file

@ -99,6 +99,7 @@ export interface ExtensionMessage {
| "maxReadFileLine"
| "fileSearchResults"
| "toggleApiConfigPin"
| "toggleTaskPin"
| "acceptInput"
| "setHistoryPreviewCollapsed"
| "commandExecutionStatus"
@ -199,6 +200,7 @@ export type ExtensionState = Pick<
| "currentApiConfigName"
| "listApiConfigMeta"
| "pinnedApiConfigs"
| "pinnedTasks"
// | "lastShownAnnouncementId"
| "customInstructions"
// | "taskHistory" // Optional in GlobalSettings, required here.

View file

@ -169,6 +169,7 @@ export interface WebviewMessage {
| "maxDiagnosticMessages"
| "searchFiles"
| "toggleApiConfigPin"
| "toggleTaskPin"
| "setHistoryPreviewCollapsed"
| "hasOpenedModeSelector"
| "accountButtonClicked"

View file

@ -2,6 +2,7 @@ import { useCallback } from "react"
import { Button, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
type DeleteButtonProps = {
@ -11,17 +12,27 @@ type DeleteButtonProps = {
export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
const { t } = useAppTranslation()
const { pinnedTasks } = useExtensionState()
const isPinned = pinnedTasks?.[itemId] || false
const handleDeleteClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
// Prevent deletion of pinned tasks
if (isPinned) {
// Show a simple alert for now - we can improve this later
alert(t("history:pinnedTaskCannotDelete"))
return
}
if (e.shiftKey) {
vscode.postMessage({ type: "deleteTaskWithId", text: itemId })
} else if (onDelete) {
onDelete(itemId)
}
},
[itemId, onDelete],
[itemId, onDelete, isPinned, t],
)
return (

View file

@ -3,6 +3,11 @@ import type { HistoryItem } from "@roo-code/types"
import { formatDate } from "@/utils/format"
import { DeleteButton } from "./DeleteButton"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { StandardTooltip } from "@/components/ui"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { vscode } from "@/utils/vscode"
import { useAppTranslation } from "@/i18n/TranslationContext"
export interface TaskItemHeaderProps {
item: HistoryItem
@ -11,6 +16,18 @@ export interface TaskItemHeaderProps {
}
const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
const { pinnedTasks, togglePinnedTask } = useExtensionState()
const { t } = useAppTranslation()
const isPinned = pinnedTasks?.[item.id] || false
const handlePinToggle = (e: React.MouseEvent) => {
e.stopPropagation()
togglePinnedTask(item.id)
vscode.postMessage({
type: "toggleTaskPin",
text: item.id,
})
}
return (
<div
className={cn("flex justify-between items-center", {
@ -27,6 +44,17 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, isSelectionMode,
{/* Action Buttons */}
{!isSelectionMode && (
<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
<StandardTooltip content={isPinned ? t("history:unpin") : t("history:pin")}>
<Button
variant="ghost"
size="sm"
onClick={handlePinToggle}
className={cn("size-5 flex items-center justify-center", {
"opacity-100 bg-accent": isPinned,
})}>
<span className="codicon codicon-pin text-xs opacity-50" />
</Button>
</StandardTooltip>
{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
</div>
)}

View file

@ -7,7 +7,7 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
export const useTaskSearch = () => {
const { taskHistory, cwd } = useExtensionState()
const { taskHistory, cwd, pinnedTasks } = useExtensionState()
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@ -57,8 +57,16 @@ export const useTaskSearch = () => {
})
}
// Then sort the results
// Then sort the results with pinned tasks first
return [...results].sort((a, b) => {
const aPinned = pinnedTasks?.[a.id] || false
const bPinned = pinnedTasks?.[b.id] || false
// Pinned tasks always come first
if (aPinned && !bPinned) return -1
if (!aPinned && bPinned) return 1
// If both are pinned or both are unpinned, sort by the selected option
switch (sortOption) {
case "oldest":
return (a.ts || 0) - (b.ts || 0)
@ -76,7 +84,7 @@ export const useTaskSearch = () => {
return (b.ts || 0) - (a.ts || 0)
}
})
}, [presentableTasks, searchQuery, fzf, sortOption])
}, [presentableTasks, searchQuery, fzf, sortOption, pinnedTasks])
return {
tasks,

View file

@ -129,6 +129,9 @@ export interface ExtensionStateContextType extends ExtensionState {
pinnedApiConfigs?: Record<string, boolean>
setPinnedApiConfigs: (value: Record<string, boolean>) => void
togglePinnedApiConfig: (configName: string) => void
pinnedTasks?: Record<string, boolean>
setPinnedTasks: (value: Record<string, boolean>) => void
togglePinnedTask: (taskId: string) => void
terminalCompressProgressBar?: boolean
setTerminalCompressProgressBar: (value: boolean) => void
setHistoryPreviewCollapsed: (value: boolean) => void
@ -216,6 +219,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
maxImageFileSize: 5, // Default max image file size in MB
maxTotalImageSize: 20, // Default max total image size in MB
pinnedApiConfigs: {}, // Empty object for pinned API configs
pinnedTasks: {}, // Empty object for pinned tasks
terminalZshOhMy: false, // Default Oh My Zsh integration setting
maxConcurrentFileReads: 5, // Default concurrent file reads
terminalZshP10k: false, // Default Powerlevel10k integration setting
@ -464,6 +468,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })),
setMaxTotalImageSize: (value) => setState((prevState) => ({ ...prevState, maxTotalImageSize: value })),
setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })),
setPinnedTasks: (value) => setState((prevState) => ({ ...prevState, pinnedTasks: value })),
setTerminalCompressProgressBar: (value) =>
setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })),
togglePinnedApiConfig: (configId) =>
@ -481,6 +486,21 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
return { ...prevState, pinnedApiConfigs: newPinned }
}),
togglePinnedTask: (taskId) =>
setState((prevState) => {
const currentPinned = prevState.pinnedTasks || {}
const newPinned = {
...currentPinned,
[taskId]: !currentPinned[taskId],
}
// If the task is now unpinned, remove it from the object
if (!newPinned[taskId]) {
delete newPinned[taskId]
}
return { ...prevState, pinnedTasks: newPinned }
}),
setHistoryPreviewCollapsed: (value) =>
setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })),
setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })),

View file

@ -41,5 +41,8 @@
"mostTokens": "Most Tokens",
"mostRelevant": "Most Relevant"
},
"viewAllHistory": "View all tasks"
"viewAllHistory": "View all tasks",
"pin": "Pin task",
"unpin": "Unpin task",
"pinnedTaskCannotDelete": "Cannot delete pinned task. Unpin it first to delete."
}