mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e261f2f47 | |||
| 5f050c5353 | |||
| 2516424020 |
@@ -28,6 +28,8 @@ service TaskService {
|
||||
rpc exportTaskWithId(StringRequest) returns (Empty);
|
||||
// Toggles the favorite status of a task
|
||||
rpc toggleTaskFavorite(TaskFavoriteRequest) returns (Empty);
|
||||
// Toggles the pin status of a task
|
||||
rpc toggleTaskPin(TaskPinRequest) returns (Empty);
|
||||
// Gets filtered task history
|
||||
rpc getTaskHistory(GetTaskHistoryRequest) returns (TaskHistoryArray);
|
||||
// Sends a response to a previous ask operation
|
||||
@@ -42,6 +44,8 @@ service TaskService {
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
// Explains changes with AI and adds inline comments to the diff view
|
||||
rpc explainChanges(ExplainChangesRequest) returns (Empty);
|
||||
// Updates the custom name for a task
|
||||
rpc updateTaskName(UpdateTaskNameRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -60,6 +64,21 @@ message TaskFavoriteRequest {
|
||||
bool is_favorited = 3;
|
||||
}
|
||||
|
||||
// Request message for toggling task pin status
|
||||
message TaskPinRequest {
|
||||
Metadata metadata = 1;
|
||||
string task_id = 2;
|
||||
bool is_pinned = 3;
|
||||
}
|
||||
|
||||
// Request message for updating task custom name
|
||||
message UpdateTaskNameRequest {
|
||||
Metadata metadata = 1;
|
||||
string task_id = 2;
|
||||
string custom_name = 3;
|
||||
string custom_name_color = 4;
|
||||
}
|
||||
|
||||
// Response for task details
|
||||
message TaskResponse {
|
||||
string id = 1;
|
||||
@@ -73,6 +92,7 @@ message TaskResponse {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
string model_id = 11;
|
||||
bool is_pinned = 12;
|
||||
}
|
||||
|
||||
// Request for getting task history with filtering
|
||||
@@ -103,6 +123,9 @@ message TaskItem {
|
||||
int32 cache_writes = 9;
|
||||
int32 cache_reads = 10;
|
||||
string model_id = 11;
|
||||
bool is_pinned = 12;
|
||||
string custom_name = 13;
|
||||
string custom_name_color = 14;
|
||||
}
|
||||
|
||||
// Request for ask response operation
|
||||
|
||||
@@ -1007,11 +1007,23 @@ export class Controller {
|
||||
const history = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||
if (existingItemIndex !== -1) {
|
||||
history[existingItemIndex] = item
|
||||
// Preserve custom fields from existing item when updating
|
||||
const existingItem = history[existingItemIndex]
|
||||
history[existingItemIndex] = {
|
||||
...item,
|
||||
// Only preserve these fields if they exist in the existing item and are not explicitly set in the new item
|
||||
customName: item.customName ?? existingItem.customName,
|
||||
customNameColor: item.customNameColor ?? existingItem.customNameColor,
|
||||
isPinned: item.isPinned ?? existingItem.isPinned,
|
||||
}
|
||||
} else {
|
||||
history.push(item)
|
||||
}
|
||||
this.stateManager.setGlobalState("taskHistory", history)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await this.stateManager.flushPendingState()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,15 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
// Calculate total count before sorting
|
||||
const totalCount = filteredTasks.length
|
||||
|
||||
// Apply sorting
|
||||
// Apply sorting - pinned tasks always come first
|
||||
if (sortBy) {
|
||||
filteredTasks.sort((a, b) => {
|
||||
// First, sort by pinned status
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
|
||||
// Then apply the selected sort
|
||||
switch (sortBy) {
|
||||
case "oldest":
|
||||
return a.ts - b.ts
|
||||
@@ -88,8 +94,13 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Default sort by newest
|
||||
filteredTasks.sort((a, b) => b.ts - a.ts)
|
||||
// Default sort by newest, but pinned first
|
||||
filteredTasks.sort((a, b) => {
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
return b.ts - a.ts
|
||||
})
|
||||
}
|
||||
|
||||
// Map to response format
|
||||
@@ -98,6 +109,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
task: item.task,
|
||||
ts: item.ts,
|
||||
isFavorited: item.isFavorited || false,
|
||||
isPinned: item.isPinned || false,
|
||||
size: item.size || 0,
|
||||
totalCost: item.totalCost || 0,
|
||||
tokensIn: item.tokensIn || 0,
|
||||
@@ -105,6 +117,8 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
|
||||
cacheWrites: item.cacheWrites || 0,
|
||||
cacheReads: item.cacheReads || 0,
|
||||
modelId: item.modelId || "",
|
||||
customName: item.customName || "",
|
||||
customNameColor: item.customNameColor || "",
|
||||
}))
|
||||
|
||||
return TaskHistoryArray.create({
|
||||
|
||||
@@ -30,6 +30,9 @@ export async function toggleTaskFavorite(controller: Controller, request: TaskFa
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { TaskPinRequest } from "@shared/proto/cline/task"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../"
|
||||
|
||||
export async function toggleTaskPin(controller: Controller, request: TaskPinRequest): Promise<Empty> {
|
||||
if (!request.taskId || request.isPinned === undefined) {
|
||||
const errorMsg = `[toggleTaskPin] Invalid request: taskId or isPinned missing`
|
||||
Logger.error(errorMsg)
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
try {
|
||||
// Update in-memory state only
|
||||
try {
|
||||
const history = controller.stateManager.getGlobalStateKey("taskHistory")
|
||||
|
||||
const taskIndex = history.findIndex((item) => item.id === request.taskId)
|
||||
|
||||
if (taskIndex === -1) {
|
||||
Logger.log(`[toggleTaskPin] 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],
|
||||
isPinned: request.isPinned,
|
||||
}
|
||||
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
}
|
||||
} catch (historyErr) {
|
||||
Logger.error("Error processing task history:", historyErr)
|
||||
}
|
||||
|
||||
// Post to webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
Logger.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error in toggleTaskPin:", error)
|
||||
}
|
||||
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UpdateTaskNameRequest } from "@shared/proto/cline/task"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Controller } from "../"
|
||||
|
||||
export async function updateTaskName(controller: Controller, request: UpdateTaskNameRequest): Promise<Empty> {
|
||||
if (!request.taskId) {
|
||||
const errorMsg = `[updateTaskName] Invalid request: taskId missing`
|
||||
Logger.error(errorMsg)
|
||||
return Empty.create({})
|
||||
}
|
||||
|
||||
Logger.log(`[updateTaskName] Received request for task ${request.taskId}:`, {
|
||||
customName: request.customName,
|
||||
customNameColor: request.customNameColor,
|
||||
})
|
||||
|
||||
try {
|
||||
// Update in-memory state only
|
||||
try {
|
||||
const history = controller.stateManager.getGlobalStateKey("taskHistory")
|
||||
|
||||
const taskIndex = history.findIndex((item) => item.id === request.taskId)
|
||||
|
||||
if (taskIndex === -1) {
|
||||
Logger.log(`[updateTaskName] Task not found in history array!`)
|
||||
} else {
|
||||
const oldTask = history[taskIndex]
|
||||
Logger.log(`[updateTaskName] Current task state:`, {
|
||||
customName: oldTask.customName,
|
||||
customNameColor: oldTask.customNameColor,
|
||||
})
|
||||
|
||||
// Create a new array instead of modifying in place to ensure state change
|
||||
const updatedHistory = [...history]
|
||||
updatedHistory[taskIndex] = {
|
||||
...updatedHistory[taskIndex],
|
||||
customName: request.customName || undefined,
|
||||
customNameColor: request.customNameColor || undefined,
|
||||
}
|
||||
|
||||
Logger.log(`[updateTaskName] Updated task state:`, {
|
||||
customName: updatedHistory[taskIndex].customName,
|
||||
customNameColor: updatedHistory[taskIndex].customNameColor,
|
||||
})
|
||||
|
||||
// Update global state and wait for it to complete
|
||||
try {
|
||||
controller.stateManager.setGlobalState("taskHistory", updatedHistory)
|
||||
Logger.log(`[updateTaskName] Successfully saved to global state`)
|
||||
|
||||
// Force immediate write to disk to survive hot reloads
|
||||
await controller.stateManager.flushPendingState()
|
||||
Logger.log(`[updateTaskName] Successfully flushed to disk`)
|
||||
} catch (stateErr) {
|
||||
Logger.error("Error updating global state:", stateErr)
|
||||
}
|
||||
}
|
||||
} catch (historyErr) {
|
||||
Logger.error("Error processing task history:", historyErr)
|
||||
}
|
||||
|
||||
// Post to webview
|
||||
try {
|
||||
await controller.postStateToWebview()
|
||||
} catch (webviewErr) {
|
||||
Logger.error("Error posting to webview:", webviewErr)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error in updateTaskName:", error)
|
||||
}
|
||||
|
||||
return Empty.create({})
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export class MessageStateHandler {
|
||||
* This is used by methods that already hold the stateMutex lock
|
||||
* Should NOT be called directly - use saveClineMessagesAndUpdateHistory() instead
|
||||
*/
|
||||
private async saveClineMessagesAndUpdateHistoryInternal(): Promise<void> {
|
||||
private async saveClineMessagesAndUpdateHistoryInternal(existingHistoryItem?: HistoryItem): Promise<void> {
|
||||
try {
|
||||
await saveClineMessages(this.taskId, this.clineMessages)
|
||||
|
||||
@@ -122,6 +122,10 @@ export class MessageStateHandler {
|
||||
isFavorited: this.taskIsFavorited,
|
||||
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
|
||||
modelId: lastModelInfo?.modelInfo?.modelId,
|
||||
// Preserve custom fields from existing history item
|
||||
customName: existingHistoryItem?.customName,
|
||||
customNameColor: existingHistoryItem?.customNameColor,
|
||||
isPinned: existingHistoryItem?.isPinned,
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error("Failed to save cline messages:", error)
|
||||
|
||||
@@ -14,7 +14,10 @@ export type HistoryItem = {
|
||||
cwdOnTaskInitialization?: string
|
||||
conversationHistoryDeletedRange?: [number, number]
|
||||
isFavorited?: boolean
|
||||
isPinned?: boolean
|
||||
checkpointManagerErrorMessage?: string
|
||||
|
||||
modelId?: string
|
||||
customName?: string
|
||||
customNameColor?: string
|
||||
}
|
||||
|
||||
@@ -161,7 +161,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
<div className="flex items-center select-none grow min-w-0 gap-1 justify-between">
|
||||
{!isTaskExpanded && (
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
|
||||
<span className="ph-no-capture text-base">{highlightedText}</span>
|
||||
{currentTaskItem?.customName ? (
|
||||
<span className="ph-no-capture text-base font-medium">{currentTaskItem.customName}</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-base">{highlightedText}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,9 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.history-preview-item.pinned {
|
||||
border-bottom: 3px solid var(--vscode-button-background);
|
||||
}
|
||||
.history-preview-item:hover {
|
||||
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
|
||||
pointer-events: auto;
|
||||
@@ -100,6 +103,23 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
.history-view-all-btn:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.history-pin-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 8px;
|
||||
background-color: var(--vscode-button-background);
|
||||
border-radius: 2px 2px 0 0;
|
||||
padding: 2px 1px 0px 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
.history-pin-badge .codicon {
|
||||
color: var(--vscode-button-foreground);
|
||||
font-size: 12px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
@@ -145,27 +165,82 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
|
||||
{taskHistory.filter((item) => item.ts && item.task).length > 0 ? (
|
||||
taskHistory
|
||||
.filter((item) => item.ts && item.task)
|
||||
.slice(0, 3)
|
||||
.sort((a, b) => {
|
||||
// Pinned tasks first
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
// Then by timestamp (newest first)
|
||||
return b.ts - a.ts
|
||||
})
|
||||
.slice(0, 5)
|
||||
.map((item) => (
|
||||
<div className="history-preview-item" key={item.id} onClick={() => handleHistorySelect(item.id)}>
|
||||
<div
|
||||
className={`history-preview-item ${item.isPinned ? "pinned" : ""}`}
|
||||
key={item.id}
|
||||
onClick={() => handleHistorySelect(item.id)}>
|
||||
{item.isPinned && (
|
||||
<div
|
||||
aria-label="Pinned"
|
||||
className="history-pin-badge"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
// Toggle pin status
|
||||
try {
|
||||
const { TaskPinRequest } = await import("@shared/proto/cline/task")
|
||||
await TaskServiceClient.toggleTaskPin(
|
||||
TaskPinRequest.create({
|
||||
taskId: item.id,
|
||||
isPinned: false,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error unpinning task:", error)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<span className="codicon codicon-pin" />
|
||||
</div>
|
||||
)}
|
||||
<div className="history-task-content">
|
||||
{item.isFavorited && (
|
||||
<span
|
||||
aria-label="Favorited"
|
||||
className="codicon codicon-star-full"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
// Toggle favorite status
|
||||
try {
|
||||
const { TaskFavoriteRequest } = await import("@shared/proto/cline/task")
|
||||
await TaskServiceClient.toggleTaskFavorite(
|
||||
TaskFavoriteRequest.create({
|
||||
taskId: item.id,
|
||||
isFavorited: false,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error unfavoriting task:", error)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
color: "var(--vscode-button-background)",
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
marginLeft: "-4px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="history-task-description ph-no-capture">{item.task}</div>
|
||||
<div className="history-task-description ph-no-capture">
|
||||
{item.customName || item.task}
|
||||
</div>
|
||||
</div>
|
||||
<div className="history-meta-stack">
|
||||
<span className="history-date">{formatDate(item.ts)}</span>
|
||||
{item.totalCost != null && (
|
||||
<span className="history-cost-chip">${item.totalCost.toFixed(2)}</span>
|
||||
)}
|
||||
<span className="history-date">{formatDate(item.ts)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -49,6 +49,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
// Keep track of pending favorite toggle operations
|
||||
const [pendingFavoriteToggles, setPendingFavoriteToggles] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Keep track of pending pin toggle operations
|
||||
const [pendingPinToggles, setPendingPinToggles] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Load filtered task history with gRPC
|
||||
const [tasks, setTasks] = useState<any[]>([])
|
||||
|
||||
@@ -67,7 +70,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
} catch (error) {
|
||||
console.error("Error loading task history:", error)
|
||||
}
|
||||
}, [showFavoritesOnly, showCurrentWorkspaceOnly, searchQuery, sortOption, taskHistory])
|
||||
}, [showFavoritesOnly, showCurrentWorkspaceOnly, searchQuery, sortOption])
|
||||
|
||||
// Load when filters change
|
||||
useEffect(() => {
|
||||
@@ -92,10 +95,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}),
|
||||
)
|
||||
|
||||
// Refresh if either filter is active to ensure proper combined filtering
|
||||
if (showFavoritesOnly || showCurrentWorkspaceOnly) {
|
||||
loadTaskHistory()
|
||||
}
|
||||
// Always refresh to show the updated favorite state immediately
|
||||
loadTaskHistory()
|
||||
} catch (err) {
|
||||
console.error(`[FAVORITE_TOGGLE_UI] Error for task ${taskId}:`, err)
|
||||
// Revert optimistic update
|
||||
@@ -115,7 +116,47 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
[showFavoritesOnly, loadTaskHistory],
|
||||
[loadTaskHistory],
|
||||
)
|
||||
|
||||
const togglePin = useCallback(
|
||||
async (taskId: string, currentValue: boolean) => {
|
||||
// Optimistic UI update
|
||||
setPendingPinToggles((prev) => ({ ...prev, [taskId]: !currentValue }))
|
||||
|
||||
try {
|
||||
// Import TaskPinRequest locally to avoid formatter issues
|
||||
const { TaskPinRequest } = await import("@shared/proto/cline/task")
|
||||
|
||||
await TaskServiceClient.toggleTaskPin(
|
||||
TaskPinRequest.create({
|
||||
taskId,
|
||||
isPinned: !currentValue,
|
||||
}),
|
||||
)
|
||||
|
||||
// Always refresh to ensure proper sorting with pinned items
|
||||
loadTaskHistory()
|
||||
} catch (err) {
|
||||
console.error(`[PIN_TOGGLE_UI] Error for task ${taskId}:`, err)
|
||||
// Revert optimistic update
|
||||
setPendingPinToggles((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[taskId]
|
||||
return updated
|
||||
})
|
||||
} finally {
|
||||
// Clean up pending state after 1 second
|
||||
setTimeout(() => {
|
||||
setPendingPinToggles((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[taskId]
|
||||
return updated
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
[loadTaskHistory],
|
||||
)
|
||||
|
||||
// Use the onRelinquishControl hook instead of message event
|
||||
@@ -254,6 +295,21 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Sort pinned tasks to top of each group
|
||||
const sortWithPinnedFirst = (tasks: any[]) => {
|
||||
return tasks.sort((a, b) => {
|
||||
// Pinned tasks first
|
||||
if (a.isPinned !== b.isPinned) {
|
||||
return a.isPinned ? -1 : 1
|
||||
}
|
||||
// Then by timestamp (already sorted by taskHistorySearchResults)
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
sortWithPinnedFirst(todayTasks)
|
||||
sortWithPinnedFirst(olderTasks)
|
||||
|
||||
const groups: { tasks: any[]; label: string }[] = []
|
||||
if (todayTasks.length > 0) {
|
||||
groups.push({ tasks: todayTasks, label: "Today" })
|
||||
@@ -417,9 +473,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
handleHistorySelect={handleHistorySelect}
|
||||
index={index}
|
||||
item={item}
|
||||
onTaskUpdated={loadTaskHistory}
|
||||
pendingFavoriteToggles={pendingFavoriteToggles}
|
||||
pendingPinToggles={pendingPinToggles}
|
||||
selectedItems={selectedItems}
|
||||
toggleFavorite={toggleFavorite}
|
||||
togglePin={togglePin}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
@@ -450,7 +509,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
) : (
|
||||
<Button
|
||||
aria-label="Delete all history"
|
||||
className="w-full"
|
||||
className="w-full border border-transparent hover:!border-red-500"
|
||||
disabled={deleteAllDisabled || taskHistory.length === 0}
|
||||
onClick={() => {
|
||||
setDeleteAllDisabled(true)
|
||||
@@ -459,7 +518,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
||||
.catch((error) => console.error("Error deleting task history:", error))
|
||||
.finally(() => setDeleteAllDisabled(false))
|
||||
}}
|
||||
variant="danger">
|
||||
variant="secondary">
|
||||
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { HistoryItem } from "@shared/HistoryItem"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { UpdateTaskNameRequest } from "@shared/proto/cline/task"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
ChevronsDownUpIcon,
|
||||
ChevronsUpDownIcon,
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
Pin,
|
||||
StarIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useCallback, useMemo, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -23,33 +25,147 @@ type HistoryViewItemProps = {
|
||||
index: number
|
||||
selectedItems: string[]
|
||||
pendingFavoriteToggles: Record<string, boolean>
|
||||
pendingPinToggles: Record<string, boolean>
|
||||
handleDeleteHistoryItem: (id: string) => void
|
||||
toggleFavorite: (id: string, isCurrentlyFavorited: boolean) => void
|
||||
togglePin: (id: string, isCurrentlyPinned: boolean) => void
|
||||
handleHistorySelect: (itemId: string, checked: boolean) => void
|
||||
onTaskUpdated: () => void
|
||||
}
|
||||
|
||||
const HistoryViewItem = ({
|
||||
item,
|
||||
pendingFavoriteToggles,
|
||||
pendingPinToggles,
|
||||
handleDeleteHistoryItem,
|
||||
toggleFavorite,
|
||||
togglePin,
|
||||
handleHistorySelect,
|
||||
selectedItems,
|
||||
onTaskUpdated,
|
||||
}: HistoryViewItemProps) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [isEditingName, setIsEditingName] = useState(false)
|
||||
const [editNameValue, setEditNameValue] = useState(item.customName || "")
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showColorPicker, setShowColorPicker] = useState(false)
|
||||
|
||||
const presetColors = [
|
||||
{ name: "Yellow", value: "#f0c674" },
|
||||
{ name: "Blue", value: "#81a2be" },
|
||||
{ name: "Green", value: "#b5bd68" },
|
||||
{ name: "Orange", value: "#de935f" },
|
||||
{ name: "Purple", value: "#b294bb" },
|
||||
{ name: "Red", value: "#cc6666" },
|
||||
]
|
||||
|
||||
// Debug logging to see what color the item actually has
|
||||
console.log(`[HistoryViewItem] Rendering item ${item.id}:`, {
|
||||
customName: item.customName,
|
||||
customNameColor: item.customNameColor,
|
||||
})
|
||||
|
||||
const isFavoritedItem = useMemo(
|
||||
() => pendingFavoriteToggles[item.id] ?? item.isFavorited,
|
||||
[item.id, item.isFavorited, pendingFavoriteToggles],
|
||||
)
|
||||
|
||||
const isPinnedItem = useMemo(() => pendingPinToggles[item.id] ?? item.isPinned, [item.id, item.isPinned, pendingPinToggles])
|
||||
|
||||
const handleSaveCustomName = useCallback(
|
||||
async (colorOverride?: string) => {
|
||||
const trimmedValue = editNameValue.trim()
|
||||
if (trimmedValue === item.customName || (trimmedValue === "" && !item.customName)) {
|
||||
setIsEditingName(false)
|
||||
setShowColorPicker(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await TaskServiceClient.updateTaskName(
|
||||
UpdateTaskNameRequest.create({
|
||||
taskId: item.id,
|
||||
customName: trimmedValue,
|
||||
customNameColor: colorOverride !== undefined ? colorOverride : item.customNameColor,
|
||||
}),
|
||||
)
|
||||
setIsEditingName(false)
|
||||
setShowColorPicker(false)
|
||||
// Reload the history view to show the updated name
|
||||
onTaskUpdated()
|
||||
} catch (error) {
|
||||
console.error("Error updating task name:", error)
|
||||
}
|
||||
},
|
||||
[editNameValue, item.id, item.customName, item.customNameColor, onTaskUpdated],
|
||||
)
|
||||
|
||||
const handleColorSelect = useCallback(
|
||||
async (color: string) => {
|
||||
try {
|
||||
// Get the current name being edited, or use the existing custom name
|
||||
const nameToSave = editNameValue.trim() || item.customName || ""
|
||||
|
||||
console.log(`[HistoryViewItem] Sending color update:`, {
|
||||
taskId: item.id,
|
||||
customName: nameToSave,
|
||||
customNameColor: color,
|
||||
currentItemColor: item.customNameColor,
|
||||
})
|
||||
|
||||
await TaskServiceClient.updateTaskName(
|
||||
UpdateTaskNameRequest.create({
|
||||
taskId: item.id,
|
||||
customName: nameToSave,
|
||||
customNameColor: color,
|
||||
}),
|
||||
)
|
||||
|
||||
console.log(`[HistoryViewItem] Color update sent successfully`)
|
||||
// Reload the history view to show the updated color
|
||||
onTaskUpdated()
|
||||
// Don't close the picker - let user continue editing
|
||||
} catch (error) {
|
||||
console.error("Error updating task color:", error)
|
||||
}
|
||||
},
|
||||
[item.id, item.customName, editNameValue, onTaskUpdated],
|
||||
)
|
||||
|
||||
const handleCancelEdit = useCallback(() => {
|
||||
setEditNameValue(item.customName || "")
|
||||
setIsEditingName(false)
|
||||
}, [item.customName])
|
||||
|
||||
const handleCopyTask = useCallback(() => {
|
||||
navigator.clipboard.writeText(item.task).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}, [item.task])
|
||||
|
||||
const handleShowTaskWithId = useCallback((id: string) => {
|
||||
TaskServiceClient.showTaskWithId(StringRequest.create({ value: id })).catch((error) =>
|
||||
console.error("Error showing task:", error),
|
||||
)
|
||||
}, [])
|
||||
|
||||
const formatDate = useCallback((timestamp: number) => {
|
||||
const formatDateShort = useCallback((timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return {
|
||||
date: date.toLocaleString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}),
|
||||
time: date.toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}),
|
||||
}
|
||||
}, [])
|
||||
|
||||
const formatDateLong = useCallback((timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
const today = new Date()
|
||||
const isToday = today.toDateString() === date.toDateString()
|
||||
@@ -76,151 +192,304 @@ const HistoryViewItem = ({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="history-item cursor-pointer flex group mb-1 hover:bg-list-hover border-b border-accent/10" key={item.id}>
|
||||
<VSCodeCheckbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
className="pl-3 pr-1 py-auto self-start mt-3"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const checked = (e.target as HTMLInputElement).checked
|
||||
handleHistorySelect(item.id, checked)
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="group mb-2 rounded mx-3"
|
||||
key={item.id}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
"color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent)"
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor =
|
||||
"color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent)"
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent)",
|
||||
transition: "background-color 0.2s",
|
||||
}}>
|
||||
{/* Header bar with icons */}
|
||||
<div
|
||||
className="flex flex-col gap-2 py-2 pl-2 pr-3 relative flex-grow w-full"
|
||||
className="flex items-center justify-between gap-2 px-3 pt-0 pb-0 rounded-t"
|
||||
style={{
|
||||
backgroundColor: "#212121",
|
||||
}}>
|
||||
{/* Left side: Custom name field */}
|
||||
<div className="flex-1 min-w-0 py-1">
|
||||
{isEditingName ? (
|
||||
<div className="relative">
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full bg-transparent border-none outline-none text-[10px] text-[var(--vscode-input-foreground)] px-1"
|
||||
maxLength={75}
|
||||
onBlur={() => {
|
||||
// Save on blur (clicking outside, tabbing away, etc.)
|
||||
handleSaveCustomName()
|
||||
setShowColorPicker(false)
|
||||
}}
|
||||
onChange={(e) => setEditNameValue(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onFocus={() => setShowColorPicker(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSaveCustomName()
|
||||
setShowColorPicker(false)
|
||||
} else if (e.key === "Escape") {
|
||||
handleCancelEdit()
|
||||
setShowColorPicker(false)
|
||||
}
|
||||
e.stopPropagation()
|
||||
}}
|
||||
placeholder="Click to name..."
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-input-background)",
|
||||
borderBottom: "1px solid var(--vscode-input-border)",
|
||||
}}
|
||||
type="text"
|
||||
value={editNameValue}
|
||||
/>
|
||||
{showColorPicker && (
|
||||
<div
|
||||
className="absolute top-full left-0 mt-1 p-2 rounded shadow-lg z-50"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-dropdown-background)",
|
||||
border: "1px solid var(--vscode-dropdown-border)",
|
||||
}}>
|
||||
<div className="flex gap-1">
|
||||
{presetColors.map((color) => (
|
||||
<button
|
||||
aria-label={`${color.name} color`}
|
||||
className="w-6 h-6 rounded cursor-pointer border-2 hover:scale-110 transition-transform"
|
||||
key={color.value}
|
||||
onClick={() => handleColorSelect(color.value)}
|
||||
style={{
|
||||
backgroundColor: color.value,
|
||||
borderColor:
|
||||
item.customNameColor === color.value
|
||||
? "var(--vscode-focusBorder)"
|
||||
: "transparent",
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="w-full text-left text-[10px] px-1 py-0.5 rounded hover:bg-[var(--vscode-input-background)] transition-colors truncate"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsEditingName(true)
|
||||
setEditNameValue(item.customName || "")
|
||||
setShowColorPicker(true)
|
||||
}}
|
||||
style={{
|
||||
color: item.customName
|
||||
? item.customNameColor || "#f0c674"
|
||||
: "var(--vscode-descriptionForeground)",
|
||||
fontStyle: item.customName ? "normal" : "italic",
|
||||
opacity: item.customName ? 1 : 0.7,
|
||||
}}
|
||||
type="button">
|
||||
{item.customName || "Click to name..."}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
aria-label={isPinnedItem ? "Unpin task" : "Pin task"}
|
||||
className={cn("p-0 transition-opacity", {
|
||||
"opacity-0 group-hover:opacity-100": !isPinnedItem,
|
||||
})}
|
||||
disabled={pendingPinToggles[item.id] !== undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePin(item.id, isPinnedItem)
|
||||
}}
|
||||
variant="icon">
|
||||
<Pin
|
||||
className={cn("opacity-70", {
|
||||
"text-button-background fill-button-background opacity-100": isPinnedItem,
|
||||
})}
|
||||
size={16}
|
||||
style={{ transform: "rotate(45deg) scale(0.7)" }}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={isFavoritedItem ? "Remove from favorites" : "Add to favorites"}
|
||||
className={cn("p-0 transition-opacity", {
|
||||
"opacity-0 group-hover:opacity-100": !isFavoritedItem && !isPinnedItem,
|
||||
})}
|
||||
disabled={pendingFavoriteToggles[item.id] !== undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFavorite(item.id, isFavoritedItem)
|
||||
}}
|
||||
variant="icon">
|
||||
<StarIcon
|
||||
className={cn("opacity-70", {
|
||||
"text-button-background fill-button-background opacity-100": isFavoritedItem,
|
||||
})}
|
||||
size={16}
|
||||
style={{ transform: "scale(0.7)" }}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Copy task text"
|
||||
className="p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleCopyTask()
|
||||
}}
|
||||
variant="icon">
|
||||
{copied ? (
|
||||
<CheckIcon className="opacity-70" size={16} style={{ transform: "scale(0.7)" }} />
|
||||
) : (
|
||||
<CopyIcon className="opacity-70" size={16} style={{ transform: "scale(0.7)" }} />
|
||||
)}
|
||||
</Button>
|
||||
<VSCodeCheckbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const checked = (e.target as HTMLInputElement).checked
|
||||
handleHistorySelect(item.id, checked)
|
||||
}}
|
||||
style={{ transform: "scale(0.7)" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div
|
||||
className="cursor-pointer px-3 pb-1 pt-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleShowTaskWithId(item.id)
|
||||
}}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="line-clamp-1 overflow-hidden break-words whitespace-pre-wrap">
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
aria-label="Delete"
|
||||
className="p-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
disabled={isFavoritedItem}
|
||||
<div className="flex justify-between gap-3 min-h-[60px]">
|
||||
{/* Left side: Task title and details link */}
|
||||
<div className="flex flex-col justify-between flex-1 min-w-0">
|
||||
<div className="text-sm font-normal mb-1 line-clamp-2 break-words" style={{ lineHeight: "1.2" }}>
|
||||
<span className="ph-no-capture">{item.task}</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-[10px] text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)] flex items-center gap-1 p-0 bg-transparent border-none cursor-pointer self-start mb-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
setExpanded(!expanded)
|
||||
}}
|
||||
variant="ghost">
|
||||
<span className="flex items-center gap-1 text-xs">
|
||||
<TrashIcon className="stroke-1" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={isFavoritedItem ? "Remove from favorites" : "Add to favorites"}
|
||||
className="p-0"
|
||||
disabled={pendingFavoriteToggles[item.id] !== undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleFavorite(item.id, isFavoritedItem)
|
||||
}}
|
||||
variant="icon">
|
||||
<StarIcon
|
||||
className={cn("opacity-70", {
|
||||
"text-button-background fill-button-background opacity-100": isFavoritedItem,
|
||||
})}
|
||||
type="button">
|
||||
Task Details
|
||||
<ChevronRightIcon
|
||||
className="transition-transform"
|
||||
size={10}
|
||||
style={{
|
||||
transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right side: Cost and date */}
|
||||
<div className="flex flex-col items-center gap-2 flex-shrink-0">
|
||||
{/* Cost badge */}
|
||||
{item.totalCost != null && (
|
||||
<div
|
||||
className="px-3 py-1 rounded-full text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-badge-background)",
|
||||
color: "var(--vscode-badge-foreground)",
|
||||
}}>
|
||||
${item.totalCost.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<div className="text-xs text-[var(--vscode-descriptionForeground)] mt-[2px]">
|
||||
{formatDateShort(item.ts).date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setExpanded(!expanded)
|
||||
}}
|
||||
variant="icon">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="text-description text-xs uppercase">{formatDate(item.ts)}</div>
|
||||
<div className="self-end flex items-center text-xs">
|
||||
<span className="text-description">${item.totalCost?.toFixed(4) ?? 0}</span>
|
||||
{expanded ? (
|
||||
<ChevronsDownUpIcon className="text-description" />
|
||||
) : (
|
||||
<ChevronsUpDownIcon className="text-description hidden opacity-0 group-hover:opacity-100 transition-opacity group-hover:block" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
{expanded && (
|
||||
<Button
|
||||
className="m-0 text-xs cursor-pointer p-2 bg-accent/10 w-full rounded-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setExpanded(!expanded)
|
||||
}}
|
||||
variant="text">
|
||||
<div className="flex flex-col gap-1 w-full text-xs">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-1 flex-wrap w-full">
|
||||
<div className="flex justify-between items-center w-full gap-1 text-xs">
|
||||
<span className="font-medium text-description">Tokens:</span>
|
||||
<div className="flex items-center gap-1 text-description text-xs">
|
||||
<span className="flex items-center gap-1 text-description">
|
||||
<ArrowUpIcon className="text-description !size-1" />
|
||||
{formatLargeNumber(item.tokensIn || 0)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 text-description">
|
||||
<ArrowDownIcon className="text-description !size-1" />
|
||||
{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{item.cacheWrites
|
||||
? item.cacheWrites > 0 && (
|
||||
<span className="flex items-center gap-1 text-description">
|
||||
<ArrowRightIcon className="text-description !size-1" />
|
||||
{formatLargeNumber(item.cacheWrites)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
{item.cacheReads
|
||||
? item.cacheReads > 0 && (
|
||||
<span className="flex items-center gap-1 text-description">
|
||||
<ArrowLeftIcon className="text-description !size-1" />
|
||||
{formatLargeNumber(item.cacheReads)}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
{/* Expanded details */}
|
||||
{expanded && (
|
||||
<div className="px-3 pb-3">
|
||||
<div
|
||||
className="p-3 rounded text-[10px]"
|
||||
style={{
|
||||
backgroundColor: "#212121",
|
||||
}}>
|
||||
<div className="flex flex-col gap-1 text-[10px]">
|
||||
<div className="flex justify-between items-center text-[10px]">
|
||||
<span className="font-medium text-[var(--vscode-descriptionForeground)] text-[10px]">Date:</span>
|
||||
<span className="text-[var(--vscode-descriptionForeground)] text-[10px]">
|
||||
{formatDateLong(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{item.modelId && (
|
||||
<div className="flex justify-between items-center w-full gap-1 text-xs">
|
||||
<span className="font-medium text-description">Model:</span>
|
||||
<span className="text-description">{item.modelId}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center w-full gap-1 text-xs">
|
||||
<span className="font-medium text-description">Size:</span>
|
||||
<span className="items-center gap-2 flex text-description">
|
||||
{formatSize(item.size)}
|
||||
<Button
|
||||
aria-label="Export"
|
||||
className="m-0 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
TaskServiceClient.exportTaskWithId(
|
||||
StringRequest.create({ value: item.id }),
|
||||
).catch((err) => console.error("Failed to export task:", err))
|
||||
}}
|
||||
variant="ghost">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
<div className="flex justify-between items-center text-[10px]">
|
||||
<span className="font-medium text-[var(--vscode-descriptionForeground)] text-[10px]">
|
||||
Tokens:
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-[var(--vscode-descriptionForeground)] text-[10px]">
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowUpIcon size={12} />
|
||||
{formatLargeNumber(item.tokensIn || 0)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowDownIcon size={12} />
|
||||
{formatLargeNumber(item.tokensOut || 0)}
|
||||
</span>
|
||||
{item.cacheWrites && item.cacheWrites > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowRightIcon size={12} />
|
||||
{formatLargeNumber(item.cacheWrites)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{item.cacheReads && item.cacheReads > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowLeftIcon size={12} />
|
||||
{formatLargeNumber(item.cacheReads)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.modelId && (
|
||||
<div className="flex justify-between items-center text-[10px]">
|
||||
<span className="font-medium text-[var(--vscode-descriptionForeground)] text-[10px]">
|
||||
Model:
|
||||
</span>
|
||||
<span className="text-[var(--vscode-descriptionForeground)] text-[10px]">{item.modelId}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center text-[10px]">
|
||||
<span className="font-medium text-[var(--vscode-descriptionForeground)] text-[10px]">Size:</span>
|
||||
<span className="flex items-center gap-2 text-[var(--vscode-descriptionForeground)] text-[10px]">
|
||||
{formatSize(item.size)}
|
||||
<Button
|
||||
aria-label="Export"
|
||||
className="m-0 p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
TaskServiceClient.exportTaskWithId(StringRequest.create({ value: item.id })).catch(
|
||||
(err) => console.error("Failed to export task:", err),
|
||||
)
|
||||
}}
|
||||
variant="ghost">
|
||||
<DownloadIcon size={14} />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user