Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -218,6 +219,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
lastShownAnnouncementId: "jul-09-2025-3-23-0",

pinnedApiConfigs: {},
pinnedTasks: {},

autoApprovalEnabled: true,
alwaysAllowReadOnly: true,
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,7 @@ export class ClineProvider
currentApiConfigName,
listApiConfigMeta,
pinnedApiConfigs,
pinnedTasks,
mode,
customModePrompts,
customSupportPrompts,
Expand Down Expand Up @@ -1583,6 +1584,7 @@ export class ClineProvider
currentApiConfigName: currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
pinnedTasks: pinnedTasks ?? {},
mode: mode ?? defaultModeSlug,
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
Expand Down Expand Up @@ -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 ?? {},
Expand Down
15 changes: 15 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export interface ExtensionMessage {
| "maxReadFileLine"
| "fileSearchResults"
| "toggleApiConfigPin"
| "toggleTaskPin"
| "acceptInput"
| "setHistoryPreviewCollapsed"
| "commandExecutionStatus"
Expand Down Expand Up @@ -199,6 +200,7 @@ export type ExtensionState = Pick<
| "currentApiConfigName"
| "listApiConfigMeta"
| "pinnedApiConfigs"
| "pinnedTasks"
// | "lastShownAnnouncementId"
| "customInstructions"
// | "taskHistory" // Optional in GlobalSettings, required here.
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export interface WebviewMessage {
| "maxDiagnosticMessages"
| "searchFiles"
| "toggleApiConfigPin"
| "toggleTaskPin"
| "setHistoryPreviewCollapsed"
| "hasOpenedModeSelector"
| "accountButtonClicked"
Expand Down
13 changes: 12 additions & 1 deletion webview-ui/src/components/history/DeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 (
Expand Down
28 changes: 28 additions & 0 deletions webview-ui/src/components/history/TaskItemHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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", {
Expand All @@ -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>
)}
Expand Down
14 changes: 11 additions & 3 deletions webview-ui/src/components/history/useTaskSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) =>
Expand All @@ -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 })),
Expand Down
5 changes: 4 additions & 1 deletion webview-ui/src/i18n/locales/en/history.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Loading