Feat: Task Favorites ️ (#3392)

* Task Favorites

* Task management docs
This commit is contained in:
canvrno 2025-05-08 15:49:05 -07:00 committed by GitHub
parent 29f3cfa894
commit 7b416ccc70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1556 additions and 49 deletions

View File

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Task Favorites and several proto messages related to tasks

View File

@ -62,6 +62,7 @@
"getting-started/installing-dev-essentials",
"getting-started/model-selection-guide",
"getting-started/our-favorite-tech-stack",
"getting-started/task-management",
"getting-started/understanding-context-management",
"getting-started/what-is-cline"
]

View File

@ -0,0 +1,67 @@
---
title: "Task Management in Cline"
description: "Learn how to effectively manage your task history, use favorites, and organize your work in Cline."
---
# Task Management
As you use Cline, you'll accumulate many tasks over time. The task management system helps you organize, filter, search, and clean up your task history to keep your workspace efficient.
## Accessing Task History
You can access your task history by:
1. Clicking on the "History" button in the Cline sidebar
2. Using the command palette to search for "Cline: Show Task History"
## Task History Features
The task history view provides several powerful features:
### Searching and Filtering
- **Search Bar**: Use the fuzzy search at the top to quickly find tasks by content
- **Sort Options**: Sort tasks by:
- Newest (default)
- Oldest
- Most Expensive (highest API cost)
- Most Tokens (highest token usage)
- Most Relevant (when searching)
- **Favorites Filter**: Toggle to show only favorited tasks
### Task Actions
Each task in the history view has several actions available:
- **Open**: Click on a task to reopen it in the Cline chat
- **Favorite**: Click the star icon to mark a task as a favorite
- **Delete**: Remove individual tasks (favorites are protected from deletion)
- **Export**: Export a task's conversation to markdown
## ⭐ Task Favorites
The favorites feature allows you to mark important tasks that you want to preserve and find quickly.
### How Favorites Work
- **Marking Favorites**: Click the star icon next to any task to toggle its favorite status
- **Protection**: Favorited tasks are protected from individual and bulk deletion operations (can be overridden)
- **Filtering**: Use the favorites filter to quickly access your important tasks
## Batch Operations
The task history view supports several batch operations:
- **Select Multiple**: Use the checkboxes to select multiple tasks
- **Select All/None**: Quickly select or deselect all tasks
- **Delete Selected**: Remove all selected tasks
- **Delete All**: Remove all tasks from history (favorites are preserved unless you choose to include them)
## Best Practices
1. **Favorite Important Tasks**: Mark reference tasks or frequently accessed conversations as favorites
2. **Regular Cleanup**: Periodically remove old or unused tasks to improve performance
3. **Use Search**: Leverage the fuzzy search to quickly find specific conversations
4. **Export Valuable Tasks**: Export important tasks to markdown for external reference
Task management helps you maintain an organized workflow when using Cline, allowing you to quickly find past conversations, preserve important work, and keep your history clean and efficient.

View File

@ -16,9 +16,15 @@ service TaskService {
// Creates a new task with the given text and optional images
rpc newTask(NewTaskRequest) returns (Empty);
// Shows a task with the specified ID
rpc showTaskWithId(StringRequest) returns (Empty);
rpc showTaskWithId(StringRequest) returns (TaskResponse);
// Exports a task with the given ID to markdown
rpc exportTaskWithId(StringRequest) returns (Empty);
// Toggles the favorite status of a task
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
// Deletes all non-favorited tasks
rpc deleteNonFavoritedTasks(EmptyRequest) returns (DeleteNonFavoritedTasksResults);
// Gets filtered task history
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
}
// Request message for creating a new task
@ -27,3 +33,58 @@ message NewTaskRequest {
string text = 2;
repeated string images = 3;
}
// Request message for toggling task favorite status
message TaskFavoriteRequest {
Metadata metadata = 1;
string task_id = 2;
bool is_favorited = 3;
}
// Response for task details
message TaskResponse {
string id = 1;
string task = 2;
int64 ts = 3;
bool is_favorited = 4;
int64 size = 5;
double total_cost = 6;
int32 tokens_in = 7;
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
}
// Results returned when deleting non-favorited tasks
message DeleteNonFavoritedTasksResults {
int32 tasks_preserved = 1;
int32 tasks_deleted = 2;
}
// Request for getting task history with filtering
message GetTaskHistoryRequest {
Metadata metadata = 1;
bool favorites_only = 2;
string search_query = 3;
string sort_by = 4;
}
// Response for task history
message TaskHistoryArray {
repeated TaskItem tasks = 1;
int32 total_count = 2;
}
// Task item details for history list
message TaskItem {
string id = 1;
string task = 2;
int64 ts = 3;
bool is_favorited = 4;
int64 size = 5;
double total_cost = 6;
int32 tokens_in = 7;
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
}

View File

@ -597,11 +597,18 @@ export class Controller {
}
case "clearAllTaskHistory": {
const answer = await vscode.window.showWarningMessage(
"Are you sure you want to delete all history?",
"Delete",
"What would you like to delete?",
{ modal: true },
"Delete All Except Favorites",
"Delete Everything",
"Cancel",
)
if (answer === "Delete") {
if (answer === "Delete All Except Favorites") {
await this.deleteNonFavoriteTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
} else if (answer === "Delete Everything") {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
this.refreshTotalTasksSize()
@ -1538,6 +1545,43 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
// await this.postStateToWebview()
}
async deleteNonFavoriteTaskHistory() {
await this.clearTask()
const taskHistory = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[]) || []
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
// If user has no favorited tasks, show a warning message
if (favoritedTasks.length === 0) {
vscode.window.showWarningMessage("No favorited tasks found. Please favorite tasks before using this option.")
await this.postStateToWebview()
return
}
await updateGlobalState(this.context, "taskHistory", favoritedTasks)
// Delete non-favorited task directories
try {
const preserveTaskIds = favoritedTasks.map((task) => task.id)
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks")
if (await fileExistsAtPath(taskDirPath)) {
const taskDirs = await fs.readdir(taskDirPath)
for (const taskDir of taskDirs) {
if (!preserveTaskIds.includes(taskDir)) {
await fs.rm(path.join(taskDirPath, taskDir), { recursive: true, force: true })
}
}
}
} catch (error) {
vscode.window.showErrorMessage(
`Error deleting task history: ${error instanceof Error ? error.message : String(error)}`,
)
}
await this.postStateToWebview()
}
async refreshTotalTasksSize() {
getTotalTasksSize(this.context.globalStorageUri.fsPath)
.then((newTotalSize) => {

View File

@ -0,0 +1,88 @@
import path from "path"
import fs from "fs/promises"
import { Controller } from ".."
import { EmptyRequest } from "../../../shared/proto/common"
import { DeleteNonFavoritedTasksResults } from "../../../shared/proto/task"
import { getGlobalState, updateGlobalState } from "../../storage/state"
import { fileExistsAtPath } from "../../../utils/fs"
/**
* Deletes all non-favorited tasks, preserving only favorited ones
* @param controller The controller instance
* @param request Empty request
* @returns DeleteNonFavoritedTasksResults with counts of preserved and deleted tasks
*/
export async function deleteNonFavoritedTasks(
controller: Controller,
_request: EmptyRequest,
): Promise<DeleteNonFavoritedTasksResults> {
try {
// Clear current task first
await controller.clearTask()
// Get existing task history
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Filter out non-favorited tasks
const favoritedTasks = taskHistory.filter((task) => task.isFavorited === true)
const deletedCount = taskHistory.length - favoritedTasks.length
console.log(`[deleteNonFavoritedTasks] Found ${favoritedTasks.length} favorited tasks to preserve`)
// Update global state
if (favoritedTasks.length > 0) {
await updateGlobalState(controller.context, "taskHistory", favoritedTasks)
} else {
await updateGlobalState(controller.context, "taskHistory", undefined)
}
// Handle file system cleanup for deleted tasks
const preserveTaskIds = favoritedTasks.map((task) => task.id)
await cleanupTaskFiles(controller, preserveTaskIds)
// Update webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
return {
tasksPreserved: favoritedTasks.length,
tasksDeleted: deletedCount,
}
} catch (error) {
console.error("Error in deleteNonFavoritedTasks:", error)
throw error
}
}
/**
* Helper function to cleanup task files while preserving specified tasks
*/
async function cleanupTaskFiles(controller: Controller, preserveTaskIds: string[]) {
const taskDirPath = path.join(controller.context.globalStorageUri.fsPath, "tasks")
try {
if (await fileExistsAtPath(taskDirPath)) {
if (preserveTaskIds.length > 0) {
const taskDirs = await fs.readdir(taskDirPath)
console.debug(`[cleanupTaskFiles] Found ${taskDirs.length} task directories`)
// Delete only non-preserved task directories
for (const dir of taskDirs) {
if (!preserveTaskIds.includes(dir)) {
await fs.rm(path.join(taskDirPath, dir), { recursive: true, force: true })
}
}
} else {
// No tasks to preserve, delete everything
await fs.rm(taskDirPath, { recursive: true, force: true })
}
}
} catch (error) {
console.error("Error cleaning up task files:", error)
}
return true
}

View File

@ -0,0 +1,89 @@
import { Controller } from ".."
import { GetTaskHistoryRequest, TaskHistoryArray } from "../../../shared/proto/task"
import { getGlobalState } from "../../storage/state"
/**
* Gets filtered task history
* @param controller The controller instance
* @param request Filter parameters for task history
* @returns TaskHistoryArray with filtered task list
*/
export async function getTaskHistory(controller: Controller, request: GetTaskHistoryRequest): Promise<TaskHistoryArray> {
try {
const { favoritesOnly, searchQuery, sortBy } = request
// Get task history from global state
const taskHistory = ((await getGlobalState(controller.context, "taskHistory")) as any[]) || []
// Apply filters
let filteredTasks = taskHistory.filter((item) => {
// Basic filter: must have timestamp and task content
const hasRequiredFields = item.ts && item.task
// Apply favorites filter if requested
if (favoritesOnly && hasRequiredFields) {
return item.isFavorited === true
}
return hasRequiredFields
})
// Apply search if provided
if (searchQuery) {
// Simple search implementation
const query = searchQuery.toLowerCase()
filteredTasks = filteredTasks.filter((item) => item.task.toLowerCase().includes(query))
}
// Calculate total count before sorting
const totalCount = filteredTasks.length
// Apply sorting
if (sortBy) {
filteredTasks.sort((a, b) => {
switch (sortBy) {
case "oldest":
return a.ts - b.ts
case "mostExpensive":
return (b.totalCost || 0) - (a.totalCost || 0)
case "mostTokens":
return (
(b.tokensIn || 0) +
(b.tokensOut || 0) +
(b.cacheWrites || 0) +
(b.cacheReads || 0) -
((a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0))
)
case "newest":
default:
return b.ts - a.ts
}
})
} else {
// Default sort by newest
filteredTasks.sort((a, b) => b.ts - a.ts)
}
// Map to response format
const tasks = filteredTasks.map((item) => ({
id: item.id,
task: item.task,
ts: item.ts,
isFavorited: item.isFavorited || false,
size: item.size || 0,
totalCost: item.totalCost || 0,
tokensIn: item.tokensIn || 0,
tokensOut: item.tokensOut || 0,
cacheWrites: item.cacheWrites || 0,
cacheReads: item.cacheReads || 0,
}))
return {
tasks,
totalCount,
}
} catch (error) {
console.error("Error in getTaskHistory:", error)
throw error
}
}

View File

@ -5,18 +5,24 @@
import { registerMethod } from "./index"
import { cancelTask } from "./cancelTask"
import { clearTask } from "./clearTask"
import { deleteNonFavoritedTasks } from "./deleteNonFavoritedTasks"
import { deleteTasksWithIds } from "./deleteTasksWithIds"
import { exportTaskWithId } from "./exportTaskWithId"
import { getTaskHistory } from "./getTaskHistory"
import { newTask } from "./newTask"
import { showTaskWithId } from "./showTaskWithId"
import { toggleTaskFavorite } from "./toggleTaskFavorite"
// Register all task service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("cancelTask", cancelTask)
registerMethod("clearTask", clearTask)
registerMethod("deleteNonFavoritedTasks", deleteNonFavoritedTasks)
registerMethod("deleteTasksWithIds", deleteTasksWithIds)
registerMethod("exportTaskWithId", exportTaskWithId)
registerMethod("getTaskHistory", getTaskHistory)
registerMethod("newTask", newTask)
registerMethod("showTaskWithId", showTaskWithId)
registerMethod("toggleTaskFavorite", toggleTaskFavorite)
}

View File

@ -1,17 +1,73 @@
import { Controller } from ".."
import { Empty, StringRequest } from "../../../shared/proto/common"
import { StringRequest } from "../../../shared/proto/common"
import { TaskResponse } from "../../../shared/proto/task"
/**
* Shows a task with the specified ID
* @param controller The controller instance
* @param request The request containing the task ID
* @returns Empty response
* @returns TaskResponse with task details
*/
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<Empty> {
export async function showTaskWithId(controller: Controller, request: StringRequest): Promise<TaskResponse> {
try {
await controller.showTaskWithId(request.value)
return Empty.create()
const id = request.value
// First check if task exists in global state for faster access
const taskHistory = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
const historyItem = taskHistory.find((item) => item.id === id)
// We need to initialize the task before returning data
if (historyItem) {
// Always initialize the task with the history item
await controller.initTask(undefined, undefined, historyItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
// Return task data for gRPC response
return {
id: historyItem.id,
task: historyItem.task || "",
ts: historyItem.ts || 0,
isFavorited: historyItem.isFavorited || false,
size: historyItem.size || 0,
totalCost: historyItem.totalCost || 0,
tokensIn: historyItem.tokensIn || 0,
tokensOut: historyItem.tokensOut || 0,
cacheWrites: historyItem.cacheWrites || 0,
cacheReads: historyItem.cacheReads || 0,
}
}
// If not in global state, fetch from storage
const { historyItem: fetchedItem } = await controller.getTaskWithId(id)
// Initialize the task with the fetched item
await controller.initTask(undefined, undefined, fetchedItem)
// Send UI update to show the chat view
await controller.postMessageToWebview({
type: "action",
action: "chatButtonClicked",
})
return {
id: fetchedItem.id,
task: fetchedItem.task || "",
ts: fetchedItem.ts || 0,
isFavorited: fetchedItem.isFavorited || false,
size: fetchedItem.size || 0,
totalCost: fetchedItem.totalCost || 0,
tokensIn: fetchedItem.tokensIn || 0,
tokensOut: fetchedItem.tokensOut || 0,
cacheWrites: fetchedItem.cacheWrites || 0,
cacheReads: fetchedItem.cacheReads || 0,
}
} catch (error) {
console.error("Error in showTaskWithId:", error)
throw error
}
}

View File

@ -0,0 +1,51 @@
import { Controller } from "../"
import { Empty } from "../../../shared/proto/common"
import { TaskFavoriteRequest } from "../../../shared/proto/task"
export async function toggleTaskFavorite(controller: Controller, request: TaskFavoriteRequest): Promise<Empty> {
if (!request.taskId || request.isFavorited === undefined) {
const errorMsg = `[toggleTaskFavorite] Invalid request: taskId or isFavorited missing`
console.error(errorMsg)
return {}
}
try {
// Update in-memory state only
try {
const history = ((await controller.context.globalState.get("taskHistory")) as any[]) || []
const taskIndex = history.findIndex((item) => item.id === request.taskId)
if (taskIndex === -1) {
console.log(`[toggleTaskFavorite] Task not found in history array!`)
} else {
// Create a new array instead of modifying in place to ensure state change
const updatedHistory = [...history]
updatedHistory[taskIndex] = {
...updatedHistory[taskIndex],
isFavorited: request.isFavorited,
}
// Update global state and wait for it to complete
try {
await controller.context.globalState.update("taskHistory", updatedHistory)
} catch (stateErr) {
console.error("Error updating global state:", stateErr)
}
}
} catch (historyErr) {
console.error("Error processing task history:", historyErr)
}
// Post to webview
try {
await controller.postStateToWebview()
} catch (webviewErr) {
console.error("Error posting to webview:", webviewErr)
}
} catch (error) {
console.error("Error in toggleTaskFavorite:", error)
}
return {}
}

View File

@ -118,6 +118,7 @@ export class Task {
private cancelTask: () => Promise<void>
readonly taskId: string
private taskIsFavorited?: boolean
api: ApiHandler
private terminalManager: TerminalManager
private urlContentFetcher: UrlContentFetcher
@ -211,6 +212,7 @@ export class Task {
// Initialize taskId first
if (historyItem) {
this.taskId = historyItem.id
this.taskIsFavorited = historyItem.isFavorited
this.conversationHistoryDeletedRange = historyItem.conversationHistoryDeletedRange
} else if (task || images) {
this.taskId = Date.now().toString()
@ -314,6 +316,7 @@ export class Task {
size: taskDirSize,
shadowGitConfigWorkTree: await this.checkpointTracker?.getShadowGitConfigWorkTree(),
conversationHistoryDeletedRange: this.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
})
} catch (error) {
console.error("Failed to save cline messages:", error)

View File

@ -11,4 +11,5 @@ export type HistoryItem = {
size?: number
shadowGitConfigWorkTree?: string
conversationHistoryDeletedRange?: [number, number]
isFavorited?: boolean
}

View File

@ -17,6 +17,61 @@ export interface NewTaskRequest {
images: string[]
}
/** Request message for toggling task favorite status */
export interface TaskFavoriteRequest {
metadata?: Metadata | undefined
taskId: string
isFavorited: boolean
}
/** Response for task details */
export interface TaskResponse {
id: string
task: string
ts: number
isFavorited: boolean
size: number
totalCost: number
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
}
/** Results returned when deleting non-favorited tasks */
export interface DeleteNonFavoritedTasksResults {
tasksPreserved: number
tasksDeleted: number
}
/** Request for getting task history with filtering */
export interface GetTaskHistoryRequest {
metadata?: Metadata | undefined
favoritesOnly: boolean
searchQuery: string
sortBy: string
}
/** Response for task history */
export interface TaskHistoryArray {
tasks: TaskItem[]
totalCount: number
}
/** Task item details for history list */
export interface TaskItem {
id: string
task: string
ts: number
isFavorited: boolean
size: number
totalCost: number
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
}
function createBaseNewTaskRequest(): NewTaskRequest {
return { metadata: undefined, text: "", images: [] }
}
@ -110,6 +165,790 @@ export const NewTaskRequest: MessageFns<NewTaskRequest> = {
},
}
function createBaseTaskFavoriteRequest(): TaskFavoriteRequest {
return { metadata: undefined, taskId: "", isFavorited: false }
}
export const TaskFavoriteRequest: MessageFns<TaskFavoriteRequest> = {
encode(message: TaskFavoriteRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.taskId !== "") {
writer.uint32(18).string(message.taskId)
}
if (message.isFavorited !== false) {
writer.uint32(24).bool(message.isFavorited)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskFavoriteRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskFavoriteRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 18) {
break
}
message.taskId = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.isFavorited = reader.bool()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskFavoriteRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
taskId: isSet(object.taskId) ? globalThis.String(object.taskId) : "",
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
}
},
toJSON(message: TaskFavoriteRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.taskId !== "") {
obj.taskId = message.taskId
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
return obj
},
create<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(base?: I): TaskFavoriteRequest {
return TaskFavoriteRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskFavoriteRequest>, I>>(object: I): TaskFavoriteRequest {
const message = createBaseTaskFavoriteRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.taskId = object.taskId ?? ""
message.isFavorited = object.isFavorited ?? false
return message
},
}
function createBaseTaskResponse(): TaskResponse {
return {
id: "",
task: "",
ts: 0,
isFavorited: false,
size: 0,
totalCost: 0,
tokensIn: 0,
tokensOut: 0,
cacheWrites: 0,
cacheReads: 0,
}
}
export const TaskResponse: MessageFns<TaskResponse> = {
encode(message: TaskResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") {
writer.uint32(18).string(message.id)
}
if (message.task !== "") {
writer.uint32(26).string(message.task)
}
if (message.ts !== 0) {
writer.uint32(32).int64(message.ts)
}
if (message.isFavorited !== false) {
writer.uint32(40).bool(message.isFavorited)
}
if (message.size !== 0) {
writer.uint32(48).int64(message.size)
}
if (message.totalCost !== 0) {
writer.uint32(57).double(message.totalCost)
}
if (message.tokensIn !== 0) {
writer.uint32(64).int32(message.tokensIn)
}
if (message.tokensOut !== 0) {
writer.uint32(72).int32(message.tokensOut)
}
if (message.cacheWrites !== 0) {
writer.uint32(80).int32(message.cacheWrites)
}
if (message.cacheReads !== 0) {
writer.uint32(88).int32(message.cacheReads)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskResponse()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 2: {
if (tag !== 18) {
break
}
message.id = reader.string()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.task = reader.string()
continue
}
case 4: {
if (tag !== 32) {
break
}
message.ts = longToNumber(reader.int64())
continue
}
case 5: {
if (tag !== 40) {
break
}
message.isFavorited = reader.bool()
continue
}
case 6: {
if (tag !== 48) {
break
}
message.size = longToNumber(reader.int64())
continue
}
case 7: {
if (tag !== 57) {
break
}
message.totalCost = reader.double()
continue
}
case 8: {
if (tag !== 64) {
break
}
message.tokensIn = reader.int32()
continue
}
case 9: {
if (tag !== 72) {
break
}
message.tokensOut = reader.int32()
continue
}
case 10: {
if (tag !== 80) {
break
}
message.cacheWrites = reader.int32()
continue
}
case 11: {
if (tag !== 88) {
break
}
message.cacheReads = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskResponse {
return {
id: isSet(object.id) ? globalThis.String(object.id) : "",
task: isSet(object.task) ? globalThis.String(object.task) : "",
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
}
},
toJSON(message: TaskResponse): unknown {
const obj: any = {}
if (message.id !== "") {
obj.id = message.id
}
if (message.task !== "") {
obj.task = message.task
}
if (message.ts !== 0) {
obj.ts = Math.round(message.ts)
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
if (message.size !== 0) {
obj.size = Math.round(message.size)
}
if (message.totalCost !== 0) {
obj.totalCost = message.totalCost
}
if (message.tokensIn !== 0) {
obj.tokensIn = Math.round(message.tokensIn)
}
if (message.tokensOut !== 0) {
obj.tokensOut = Math.round(message.tokensOut)
}
if (message.cacheWrites !== 0) {
obj.cacheWrites = Math.round(message.cacheWrites)
}
if (message.cacheReads !== 0) {
obj.cacheReads = Math.round(message.cacheReads)
}
return obj
},
create<I extends Exact<DeepPartial<TaskResponse>, I>>(base?: I): TaskResponse {
return TaskResponse.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskResponse>, I>>(object: I): TaskResponse {
const message = createBaseTaskResponse()
message.id = object.id ?? ""
message.task = object.task ?? ""
message.ts = object.ts ?? 0
message.isFavorited = object.isFavorited ?? false
message.size = object.size ?? 0
message.totalCost = object.totalCost ?? 0
message.tokensIn = object.tokensIn ?? 0
message.tokensOut = object.tokensOut ?? 0
message.cacheWrites = object.cacheWrites ?? 0
message.cacheReads = object.cacheReads ?? 0
return message
},
}
function createBaseDeleteNonFavoritedTasksResults(): DeleteNonFavoritedTasksResults {
return { tasksPreserved: 0, tasksDeleted: 0 }
}
export const DeleteNonFavoritedTasksResults: MessageFns<DeleteNonFavoritedTasksResults> = {
encode(message: DeleteNonFavoritedTasksResults, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.tasksPreserved !== 0) {
writer.uint32(16).int32(message.tasksPreserved)
}
if (message.tasksDeleted !== 0) {
writer.uint32(24).int32(message.tasksDeleted)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): DeleteNonFavoritedTasksResults {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseDeleteNonFavoritedTasksResults()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 2: {
if (tag !== 16) {
break
}
message.tasksPreserved = reader.int32()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.tasksDeleted = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): DeleteNonFavoritedTasksResults {
return {
tasksPreserved: isSet(object.tasksPreserved) ? globalThis.Number(object.tasksPreserved) : 0,
tasksDeleted: isSet(object.tasksDeleted) ? globalThis.Number(object.tasksDeleted) : 0,
}
},
toJSON(message: DeleteNonFavoritedTasksResults): unknown {
const obj: any = {}
if (message.tasksPreserved !== 0) {
obj.tasksPreserved = Math.round(message.tasksPreserved)
}
if (message.tasksDeleted !== 0) {
obj.tasksDeleted = Math.round(message.tasksDeleted)
}
return obj
},
create<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(base?: I): DeleteNonFavoritedTasksResults {
return DeleteNonFavoritedTasksResults.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<DeleteNonFavoritedTasksResults>, I>>(object: I): DeleteNonFavoritedTasksResults {
const message = createBaseDeleteNonFavoritedTasksResults()
message.tasksPreserved = object.tasksPreserved ?? 0
message.tasksDeleted = object.tasksDeleted ?? 0
return message
},
}
function createBaseGetTaskHistoryRequest(): GetTaskHistoryRequest {
return { metadata: undefined, favoritesOnly: false, searchQuery: "", sortBy: "" }
}
export const GetTaskHistoryRequest: MessageFns<GetTaskHistoryRequest> = {
encode(message: GetTaskHistoryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.metadata !== undefined) {
Metadata.encode(message.metadata, writer.uint32(10).fork()).join()
}
if (message.favoritesOnly !== false) {
writer.uint32(16).bool(message.favoritesOnly)
}
if (message.searchQuery !== "") {
writer.uint32(26).string(message.searchQuery)
}
if (message.sortBy !== "") {
writer.uint32(34).string(message.sortBy)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): GetTaskHistoryRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseGetTaskHistoryRequest()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.metadata = Metadata.decode(reader, reader.uint32())
continue
}
case 2: {
if (tag !== 16) {
break
}
message.favoritesOnly = reader.bool()
continue
}
case 3: {
if (tag !== 26) {
break
}
message.searchQuery = reader.string()
continue
}
case 4: {
if (tag !== 34) {
break
}
message.sortBy = reader.string()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): GetTaskHistoryRequest {
return {
metadata: isSet(object.metadata) ? Metadata.fromJSON(object.metadata) : undefined,
favoritesOnly: isSet(object.favoritesOnly) ? globalThis.Boolean(object.favoritesOnly) : false,
searchQuery: isSet(object.searchQuery) ? globalThis.String(object.searchQuery) : "",
sortBy: isSet(object.sortBy) ? globalThis.String(object.sortBy) : "",
}
},
toJSON(message: GetTaskHistoryRequest): unknown {
const obj: any = {}
if (message.metadata !== undefined) {
obj.metadata = Metadata.toJSON(message.metadata)
}
if (message.favoritesOnly !== false) {
obj.favoritesOnly = message.favoritesOnly
}
if (message.searchQuery !== "") {
obj.searchQuery = message.searchQuery
}
if (message.sortBy !== "") {
obj.sortBy = message.sortBy
}
return obj
},
create<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(base?: I): GetTaskHistoryRequest {
return GetTaskHistoryRequest.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<GetTaskHistoryRequest>, I>>(object: I): GetTaskHistoryRequest {
const message = createBaseGetTaskHistoryRequest()
message.metadata =
object.metadata !== undefined && object.metadata !== null ? Metadata.fromPartial(object.metadata) : undefined
message.favoritesOnly = object.favoritesOnly ?? false
message.searchQuery = object.searchQuery ?? ""
message.sortBy = object.sortBy ?? ""
return message
},
}
function createBaseTaskHistoryArray(): TaskHistoryArray {
return { tasks: [], totalCount: 0 }
}
export const TaskHistoryArray: MessageFns<TaskHistoryArray> = {
encode(message: TaskHistoryArray, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.tasks) {
TaskItem.encode(v!, writer.uint32(18).fork()).join()
}
if (message.totalCount !== 0) {
writer.uint32(24).int32(message.totalCount)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskHistoryArray {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskHistoryArray()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 2: {
if (tag !== 18) {
break
}
message.tasks.push(TaskItem.decode(reader, reader.uint32()))
continue
}
case 3: {
if (tag !== 24) {
break
}
message.totalCount = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskHistoryArray {
return {
tasks: globalThis.Array.isArray(object?.tasks) ? object.tasks.map((e: any) => TaskItem.fromJSON(e)) : [],
totalCount: isSet(object.totalCount) ? globalThis.Number(object.totalCount) : 0,
}
},
toJSON(message: TaskHistoryArray): unknown {
const obj: any = {}
if (message.tasks?.length) {
obj.tasks = message.tasks.map((e) => TaskItem.toJSON(e))
}
if (message.totalCount !== 0) {
obj.totalCount = Math.round(message.totalCount)
}
return obj
},
create<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(base?: I): TaskHistoryArray {
return TaskHistoryArray.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskHistoryArray>, I>>(object: I): TaskHistoryArray {
const message = createBaseTaskHistoryArray()
message.tasks = object.tasks?.map((e) => TaskItem.fromPartial(e)) || []
message.totalCount = object.totalCount ?? 0
return message
},
}
function createBaseTaskItem(): TaskItem {
return {
id: "",
task: "",
ts: 0,
isFavorited: false,
size: 0,
totalCost: 0,
tokensIn: 0,
tokensOut: 0,
cacheWrites: 0,
cacheReads: 0,
}
}
export const TaskItem: MessageFns<TaskItem> = {
encode(message: TaskItem, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") {
writer.uint32(10).string(message.id)
}
if (message.task !== "") {
writer.uint32(18).string(message.task)
}
if (message.ts !== 0) {
writer.uint32(24).int64(message.ts)
}
if (message.isFavorited !== false) {
writer.uint32(32).bool(message.isFavorited)
}
if (message.size !== 0) {
writer.uint32(40).int64(message.size)
}
if (message.totalCost !== 0) {
writer.uint32(49).double(message.totalCost)
}
if (message.tokensIn !== 0) {
writer.uint32(56).int32(message.tokensIn)
}
if (message.tokensOut !== 0) {
writer.uint32(64).int32(message.tokensOut)
}
if (message.cacheWrites !== 0) {
writer.uint32(72).int32(message.cacheWrites)
}
if (message.cacheReads !== 0) {
writer.uint32(80).int32(message.cacheReads)
}
return writer
},
decode(input: BinaryReader | Uint8Array, length?: number): TaskItem {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input)
let end = length === undefined ? reader.len : reader.pos + length
const message = createBaseTaskItem()
while (reader.pos < end) {
const tag = reader.uint32()
switch (tag >>> 3) {
case 1: {
if (tag !== 10) {
break
}
message.id = reader.string()
continue
}
case 2: {
if (tag !== 18) {
break
}
message.task = reader.string()
continue
}
case 3: {
if (tag !== 24) {
break
}
message.ts = longToNumber(reader.int64())
continue
}
case 4: {
if (tag !== 32) {
break
}
message.isFavorited = reader.bool()
continue
}
case 5: {
if (tag !== 40) {
break
}
message.size = longToNumber(reader.int64())
continue
}
case 6: {
if (tag !== 49) {
break
}
message.totalCost = reader.double()
continue
}
case 7: {
if (tag !== 56) {
break
}
message.tokensIn = reader.int32()
continue
}
case 8: {
if (tag !== 64) {
break
}
message.tokensOut = reader.int32()
continue
}
case 9: {
if (tag !== 72) {
break
}
message.cacheWrites = reader.int32()
continue
}
case 10: {
if (tag !== 80) {
break
}
message.cacheReads = reader.int32()
continue
}
}
if ((tag & 7) === 4 || tag === 0) {
break
}
reader.skip(tag & 7)
}
return message
},
fromJSON(object: any): TaskItem {
return {
id: isSet(object.id) ? globalThis.String(object.id) : "",
task: isSet(object.task) ? globalThis.String(object.task) : "",
ts: isSet(object.ts) ? globalThis.Number(object.ts) : 0,
isFavorited: isSet(object.isFavorited) ? globalThis.Boolean(object.isFavorited) : false,
size: isSet(object.size) ? globalThis.Number(object.size) : 0,
totalCost: isSet(object.totalCost) ? globalThis.Number(object.totalCost) : 0,
tokensIn: isSet(object.tokensIn) ? globalThis.Number(object.tokensIn) : 0,
tokensOut: isSet(object.tokensOut) ? globalThis.Number(object.tokensOut) : 0,
cacheWrites: isSet(object.cacheWrites) ? globalThis.Number(object.cacheWrites) : 0,
cacheReads: isSet(object.cacheReads) ? globalThis.Number(object.cacheReads) : 0,
}
},
toJSON(message: TaskItem): unknown {
const obj: any = {}
if (message.id !== "") {
obj.id = message.id
}
if (message.task !== "") {
obj.task = message.task
}
if (message.ts !== 0) {
obj.ts = Math.round(message.ts)
}
if (message.isFavorited !== false) {
obj.isFavorited = message.isFavorited
}
if (message.size !== 0) {
obj.size = Math.round(message.size)
}
if (message.totalCost !== 0) {
obj.totalCost = message.totalCost
}
if (message.tokensIn !== 0) {
obj.tokensIn = Math.round(message.tokensIn)
}
if (message.tokensOut !== 0) {
obj.tokensOut = Math.round(message.tokensOut)
}
if (message.cacheWrites !== 0) {
obj.cacheWrites = Math.round(message.cacheWrites)
}
if (message.cacheReads !== 0) {
obj.cacheReads = Math.round(message.cacheReads)
}
return obj
},
create<I extends Exact<DeepPartial<TaskItem>, I>>(base?: I): TaskItem {
return TaskItem.fromPartial(base ?? ({} as any))
},
fromPartial<I extends Exact<DeepPartial<TaskItem>, I>>(object: I): TaskItem {
const message = createBaseTaskItem()
message.id = object.id ?? ""
message.task = object.task ?? ""
message.ts = object.ts ?? 0
message.isFavorited = object.isFavorited ?? false
message.size = object.size ?? 0
message.totalCost = object.totalCost ?? 0
message.tokensIn = object.tokensIn ?? 0
message.tokensOut = object.tokensOut ?? 0
message.cacheWrites = object.cacheWrites ?? 0
message.cacheReads = object.cacheReads ?? 0
return message
},
}
export type TaskServiceDefinition = typeof TaskServiceDefinition
export const TaskServiceDefinition = {
name: "TaskService",
@ -156,7 +995,7 @@ export const TaskServiceDefinition = {
name: "showTaskWithId",
requestType: StringRequest,
requestStream: false,
responseType: Empty,
responseType: TaskResponse,
responseStream: false,
options: {},
},
@ -169,6 +1008,33 @@ export const TaskServiceDefinition = {
responseStream: false,
options: {},
},
/** Toggles the favorite status of a task */
toggleTaskFavorite: {
name: "toggleTaskFavorite",
requestType: TaskFavoriteRequest,
requestStream: false,
responseType: Empty,
responseStream: false,
options: {},
},
/** Deletes all non-favorited tasks */
deleteNonFavoritedTasks: {
name: "deleteNonFavoritedTasks",
requestType: EmptyRequest,
requestStream: false,
responseType: DeleteNonFavoritedTasksResults,
responseStream: false,
options: {},
},
/** Gets filtered task history */
getTaskHistory: {
name: "getTaskHistory",
requestType: GetTaskHistoryRequest,
requestStream: false,
responseType: TaskHistoryArray,
responseStream: false,
options: {},
},
},
} as const
@ -189,6 +1055,17 @@ export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }
function longToNumber(int64: { toString(): string }): number {
const num = globalThis.Number(int64.toString())
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER")
}
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER")
}
return num
}
function isSet(value: any): boolean {
return value !== null && value !== undefined
}

View File

@ -92,6 +92,18 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
{formatDate(item.ts)}
</span>
</div>
{item.isFavorited && (
<div
style={{
position: "absolute",
top: "12px",
right: "12px",
color: "var(--vscode-button-background)",
}}>
<span className="codicon codicon-star-full" aria-label="Favorited" />
</div>
)}
<div
style={{
fontSize: "var(--vscode-font-size)",

View File

@ -24,6 +24,76 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
const [deleteAllDisabled, setDeleteAllDisabled] = useState(false)
const [selectedItems, setSelectedItems] = useState<string[]>([])
const [showFavoritesOnly, setShowFavoritesOnly] = useState(false)
// Keep track of pending favorite toggle operations
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
// Load filtered task history with gRPC
const [filteredTasks, setFilteredTasks] = useState<any[]>([])
// Load and refresh task history
const loadTaskHistory = useCallback(async () => {
try {
const response = await TaskServiceClient.getTaskHistory({
favoritesOnly: showFavoritesOnly,
searchQuery: searchQuery || undefined,
sortBy: sortOption,
})
setFilteredTasks(response.tasks || [])
} catch (error) {
console.error("Error loading task history:", error)
// Fallback to client-side filtering
setFilteredTasks(
taskHistory.filter((item) => {
const valid = item.ts && item.task
return valid && (!showFavoritesOnly || item.isFavorited)
}),
)
}
}, [showFavoritesOnly, searchQuery, sortOption, taskHistory])
// Load when filters change
useEffect(() => {
loadTaskHistory()
}, [loadTaskHistory])
const toggleFavorite = useCallback(
async (taskId: string, currentValue: boolean) => {
// Optimistic UI update
setPendingFavoriteToggles((prev) => ({ ...prev, [taskId]: !currentValue }))
try {
await TaskServiceClient.toggleTaskFavorite({
taskId,
isFavorited: !currentValue,
})
// Refresh if favorites filter is active
if (showFavoritesOnly) {
loadTaskHistory()
}
} catch (err) {
console.error(`[FAVORITE_TOGGLE_UI] Error for task ${taskId}:`, err)
// Revert optimistic update
setPendingFavoriteToggles((prev) => {
const updated = { ...prev }
delete updated[taskId]
return updated
})
} finally {
// Clean up pending state after 1 second
setTimeout(() => {
setPendingFavoriteToggles((prev) => {
const updated = { ...prev }
delete updated[taskId]
return updated
})
}, 1000)
}
},
[showFavoritesOnly, loadTaskHistory],
)
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
@ -87,9 +157,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
.toUpperCase()
}, [])
const presentableTasks = useMemo(() => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
const presentableTasks = useMemo(() => filteredTasks, [filteredTasks])
const fuse = useMemo(() => {
return new Fuse(presentableTasks, {
@ -252,7 +320,48 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<VSCodeRadio value="mostRelevant" disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }}>
Most Relevant
</VSCodeRadio>
<div
onClick={() => setShowFavoritesOnly(!showFavoritesOnly)}
style={{
display: "flex",
alignItems: "center",
marginLeft: "6px",
cursor: "pointer",
}}>
<div
style={{
width: "14px",
height: "14px",
borderRadius: "50%",
border: "1px solid var(--vscode-checkbox-border)",
backgroundColor: showFavoritesOnly ? "var(--vscode-checkbox-background)" : "transparent",
position: "relative",
display: "flex",
justifyContent: "center",
alignItems: "center",
marginRight: "6px",
}}>
{showFavoritesOnly && (
<div
style={{
width: "6px",
height: "6px",
borderRadius: "50%",
backgroundColor: "var(--vscode-checkbox-foreground)",
}}
/>
)}
</div>
<span style={{ display: "flex", alignItems: "center", gap: "6px", userSelect: "none" }}>
<div
className="codicon codicon-star-full"
style={{ color: "var(--vscode-button-background)", fontSize: "14px" }}
/>
Favorites
</span>
</div>
</VSCodeRadioGroup>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
<VSCodeButton
onClick={() => {
@ -337,6 +446,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}}>
{formatDate(item.ts)}
</span>
<div style={{ display: "flex", gap: "4px" }}>
{/* only show delete button if task not favorited */}
{!(pendingFavoriteToggles[item.id] ?? item.isFavorited) && (
<VSCodeButton
appearance="icon"
onClick={(e) => {
@ -351,13 +463,46 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
alignItems: "center",
gap: "3px",
fontSize: "11px",
// fontWeight: "bold",
}}>
<span className="codicon codicon-trash"></span>
{formatSize(item.size)}
</div>
</VSCodeButton>
)}
<VSCodeButton
appearance="icon"
onClick={(e) => {
e.stopPropagation()
toggleFavorite(item.id, item.isFavorited || false)
}}
style={{ padding: "0px" }}>
<div
className={`codicon ${
pendingFavoriteToggles[item.id] !== undefined
? pendingFavoriteToggles[item.id]
? "codicon-star-full"
: "codicon-star-empty"
: item.isFavorited
? "codicon-star-full"
: "codicon-star-empty"
}`}
style={{
color:
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
? "var(--vscode-button-background)"
: "inherit",
opacity: (pendingFavoriteToggles[item.id] ?? item.isFavorited) ? 1 : 0.7,
display:
(pendingFavoriteToggles[item.id] ?? item.isFavorited)
? "block"
: undefined,
}}
/>
</VSCodeButton>
</div>
</div>
<div style={{ marginBottom: "8px", position: "relative" }}>
<div
style={{
fontSize: "var(--vscode-font-size)",
@ -374,6 +519,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
__html: item.task,
}}
/>
</div>
<div
style={{
display: "flex",