mirror of
https://github.com/cline/cline.git
synced 2025-06-03 03:59:07 +00:00
Feat: Task Favorites ⭐️ (#3392)
* Task Favorites * Task management docs
This commit is contained in:
parent
29f3cfa894
commit
7b416ccc70
5
.changeset/selfish-garlics-lay.md
Normal file
5
.changeset/selfish-garlics-lay.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Task Favorites and several proto messages related to tasks
|
@ -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"
|
||||
]
|
||||
|
67
docs/getting-started/task-management.mdx
Normal file
67
docs/getting-started/task-management.mdx
Normal 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.
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
88
src/core/controller/task/deleteNonFavoritedTasks.ts
Normal file
88
src/core/controller/task/deleteNonFavoritedTasks.ts
Normal 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
|
||||
}
|
89
src/core/controller/task/getTaskHistory.ts
Normal file
89
src/core/controller/task/getTaskHistory.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
51
src/core/controller/task/toggleTaskFavorite.ts
Normal file
51
src/core/controller/task/toggleTaskFavorite.ts
Normal 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 {}
|
||||
}
|
@ -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)
|
||||
|
@ -11,4 +11,5 @@ export type HistoryItem = {
|
||||
size?: number
|
||||
shadowGitConfigWorkTree?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)",
|
||||
|
@ -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,43 +446,80 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "11px",
|
||||
// fontWeight: "bold",
|
||||
}}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
{formatSize(item.size)}
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
<div style={{ display: "flex", gap: "4px" }}>
|
||||
{/* only show delete button if task not favorited */}
|
||||
{!(pendingFavoriteToggles[item.id] ?? item.isFavorited) && (
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button"
|
||||
style={{ padding: "0px 0px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
fontSize: "11px",
|
||||
}}>
|
||||
<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)",
|
||||
color: "var(--vscode-foreground)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-foreground)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.task,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
Loading…
Reference in New Issue
Block a user