mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
b117c0fe52
commit
a8e9b8f76e
10 changed files with 98 additions and 5 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ?? {},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ export interface WebviewMessage {
|
|||
| "maxDiagnosticMessages"
|
||||
| "searchFiles"
|
||||
| "toggleApiConfigPin"
|
||||
| "toggleTaskPin"
|
||||
| "setHistoryPreviewCollapsed"
|
||||
| "hasOpenedModeSelector"
|
||||
| "accountButtonClicked"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Reference in a new issue