diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dc5a9e6744..8d17bf6773 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -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, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a0739052b2..66a8351b6e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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), customModePrompts: stateValues.customModePrompts ?? {}, customSupportPrompts: stateValues.customSupportPrompts ?? {}, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 763e118125..d573058bce 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1307,6 +1307,21 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() } break + case "toggleTaskPin": + if (message.text) { + const currentPinnedTasks = getGlobalState("pinnedTasks") ?? {} + const updatedPinnedTasks: Record = { ...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() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5320190a7b..b14e38847a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -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. diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index a91d1af7ba..467ab7d649 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -169,6 +169,7 @@ export interface WebviewMessage { | "maxDiagnosticMessages" | "searchFiles" | "toggleApiConfigPin" + | "toggleTaskPin" | "setHistoryPreviewCollapsed" | "hasOpenedModeSelector" | "accountButtonClicked" diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx index 3e99027546..4317c2426d 100644 --- a/webview-ui/src/components/history/DeleteButton.tsx +++ b/webview-ui/src/components/history/DeleteButton.tsx @@ -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 ( diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx index bdddb090c8..68a0ade330 100644 --- a/webview-ui/src/components/history/TaskItemHeader.tsx +++ b/webview-ui/src/components/history/TaskItemHeader.tsx @@ -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 = ({ 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 (
= ({ item, isSelectionMode, {/* Action Buttons */} {!isSelectionMode && (
+ + + {onDelete && }
)} diff --git a/webview-ui/src/components/history/useTaskSearch.ts b/webview-ui/src/components/history/useTaskSearch.ts index 3969985b98..7674ce08b0 100644 --- a/webview-ui/src/components/history/useTaskSearch.ts +++ b/webview-ui/src/components/history/useTaskSearch.ts @@ -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("newest") const [lastNonRelevantSort, setLastNonRelevantSort] = useState("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, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index b156d0193b..719bee9e47 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -129,6 +129,9 @@ export interface ExtensionStateContextType extends ExtensionState { pinnedApiConfigs?: Record setPinnedApiConfigs: (value: Record) => void togglePinnedApiConfig: (configName: string) => void + pinnedTasks?: Record + setPinnedTasks: (value: Record) => 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 })), diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json index 8d00433170..4f879ff07b 100644 --- a/webview-ui/src/i18n/locales/en/history.json +++ b/webview-ui/src/i18n/locales/en/history.json @@ -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." }