Compare commits

...

34 Commits

Author SHA1 Message Date
abeatrix 9fdb862654 filter 2025-09-20 00:59:30 -07:00
abeatrix fe69e22067 feat: add model id to task and show in History
Add model_id field to TaskResponse and TaskItem protobuf messages and implement tracking throughout the task lifecycle. This enables better task history management by recording which model was used for each task, supporting future features like model-specific filtering and analytics.

- Add model_id field to proto definitions
- Track model ID in task execution and message state handling
- Include model ID in task history responses
- Display model information in history view UI
2025-09-19 20:04:21 -07:00
abeatrix 1ebb7eb694 clean up 2025-09-19 16:31:36 -07:00
abeatrix 230e94b5f3 Merge branch 'main' into bee/taskheader-ui 2025-09-19 16:15:10 -07:00
abeatrix 7368984343 Apply styling feedback 2025-09-18 23:54:34 -07:00
abeatrix 87884775fa Update storybook 2025-09-16 18:54:18 -07:00
abeatrix 80968508d4 clean up 2025-09-16 18:41:21 -07:00
abeatrix 45fea9247d Merge branch 'main' into bee/taskheader-ui 2025-09-16 18:35:25 -07:00
abeatrix 306941aedd Merge branch 'main' into bee/taskheader-ui 2025-09-16 17:03:46 -07:00
abeatrix 6f420028d2 Update Task Action Button Text 2025-09-16 13:43:21 -07:00
abeatrix 01568f7d18 add changeset 2025-09-16 13:20:00 -07:00
Jose R. Perez 2fc2b7d5e7 feat: small vertical positioning adjustment 2025-09-16 15:58:48 -04:00
abeatrix c434fc0815 add keyboard navigation for auto condense threshold slider
Add arrow key controls to adjust auto condense threshold with 5% steps (10% with Shift). Include focus management, accessibility attributes, and click-outside handling for improved UX.
2025-09-16 11:16:36 -07:00
abeatrix 6cb6af2892 debounce 2025-09-16 10:24:16 -07:00
abeatrix 49d9a58e7b clean up & persist expand state 2025-09-16 09:46:56 -07:00
abeatrix 899ddda189 price tag and warning positioning 2025-09-15 20:49:44 -07:00
abeatrix ef1b32eaf5 fix(ui): fix useEffect cleanup and add key prop to InfoRow
- Return cleanup function from useEffect instead of nested setTimeout
- Add key prop to InfoRow component to ensure proper re-rendering
- Improves component lifecycle management and prevents memory leaks
2025-09-15 20:24:59 -07:00
abeatrix 4e549ae782 migrate inline styes to tailwind for FocusChain 2025-09-15 20:03:05 -07:00
abeatrix 94a17964e0 migrate to tailwind 2025-09-15 19:44:39 -07:00
abeatrix 5b183982dc Merge branch 'main' into bee/taskheader-ui 2025-09-15 14:09:53 -07:00
abeatrix bab60a9df9 Extract TaskHeader styles to CSS module
Move inline styles from TaskHeader component to external CSS module file for better maintainability and separation of concerns.
2025-09-15 12:55:52 -07:00
Jose R. Perez b6abc9ee99 feat: enhance task header UX with expanded progress bar hitbox smart boundary detection timeline block hover dimming effects optimized spacing and standard confirmation dialog styling 2025-09-15 11:49:27 -04:00
abeatrix 026136f24e Update styles 2025-09-12 13:10:25 -07:00
abeatrix b57a1c4862 Dynamic marker & remove HeroTooltip 2025-09-10 12:40:07 -07:00
abeatrix 005c16118f Merge branch 'main' into bee/taskheader-ui 2025-09-10 00:56:46 -07:00
abeatrix 909e0d15ce v2 style 2025-09-10 00:55:09 -07:00
abeatrix c120d0dd66 Add configurable auto-condense threshold for context window management
- Add autoCondenseThreshold setting to control when context window compaction occurs
- Update ContextManager to accept threshold percentage parameter (0-100%)
- Add ContextWindowDetails component to display context usage in task header
- Extend protobuf schema and state management for new threshold setting
- Default threshold set to 75% when auto-condense is enabled
2025-09-09 12:55:15 -07:00
abeatrix 126968bc83 Add CheckpointError component and clean up task header UI
- Add new CheckpointError component for error handling
- Remove commented code from FocusChainContainer
- Add rounded corners to focus chain container
- Refactor formatLargeNumber function with default parameter handling
- Clean up token display formatting in TaskHeader
2025-09-09 00:04:56 -07:00
abeatrix 57777ee7e0 Clean up 2025-09-08 21:24:47 -07:00
abeatrix 9530601c46 Prevent event bubbling in task header button clicks
Add preventDefault and stopPropagation to all task header button click handlers to prevent unintended parent element interactions. Also standardize styling by replacing inline styles with Tailwind classes and update text color class for consistency.
2025-09-08 20:54:32 -07:00
abeatrix 22b0088219 Add interactive auto-compact marker to context window bar
- Add state management for auto-compact marker position (default 75%)
- Make context window bar clickable to reposition marker
- Wrap entire bar in tooltip instead of just marker
- Add click handler to calculate percentage from mouse position
- Fix cost display conditional and remove redundant CSS property
2025-09-08 20:36:43 -07:00
abeatrix b35eb4dcfd Remove edit thread btn 2025-09-08 14:32:24 -07:00
abeatrix 6a7d89aecf use tailwind 2025-09-06 01:41:31 -07:00
abeatrix 324be60127 Update TaskHeader UI
- Create new FocusChainContainer component for todo list management
- Add onSendMessage prop to TaskHeader and TaskSection components
- Refactor TaskHeader to use new FocusChainContainer
- Update tooltip styling and Tailwind configuration
- Improve task progress visualization and interaction handling
2025-09-06 01:36:33 -07:00
39 changed files with 15837 additions and 15741 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Refactor Task Header UI with interactive context window management
+1
View File
@@ -140,6 +140,7 @@ message UpdateSettingsRequest {
optional BrowserSettingsUpdate browser_settings = 20;
optional string default_terminal_profile = 21;
optional bool yolo_mode_toggled = 22;
optional int32 auto_condense_threshold = 23;
}
// Complete API Configuration message
+2
View File
@@ -63,6 +63,7 @@ message TaskResponse {
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
string model_id = 11;
}
// Request for getting task history with filtering
@@ -92,6 +93,7 @@ message TaskItem {
int32 tokens_out = 8;
int32 cache_writes = 9;
int32 cache_reads = 10;
string model_id = 11;
}
// Request for ask response operation
+1
View File
@@ -58,6 +58,7 @@ export interface ApiProviderInfo {
providerId: string
model: ApiHandlerModel
customPrompt?: string // "compact"
autoCondenseThreshold?: number // 0-1 range
}
export interface SingleCompletionHandler {
@@ -108,15 +108,21 @@ export class ContextManager {
/**
* Determine whether we should compact context window, based on token counts
*/
shouldCompactContextWindow(clineMessages: ClineMessage[], api: ApiHandler, previousApiReqIndex: number): boolean {
shouldCompactContextWindow(
clineMessages: ClineMessage[],
api: ApiHandler,
previousApiReqIndex: number,
thresholdPercentage?: number,
): boolean {
if (previousApiReqIndex >= 0) {
const previousRequest = clineMessages[previousApiReqIndex]
if (previousRequest && previousRequest.text) {
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
const { maxAllowedSize } = getContextWindowInfo(api)
return totalTokens >= maxAllowedSize
const { contextWindow, maxAllowedSize } = getContextWindowInfo(api)
const thresholdTokens = thresholdPercentage ? Math.floor(contextWindow * thresholdPercentage) : maxAllowedSize
return totalTokens >= thresholdTokens
}
}
return false
+2
View File
@@ -749,6 +749,7 @@ export class Controller {
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
const autoCondenseThreshold = this.stateManager.getGlobalSettingsKey("autoCondenseThreshold")
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
const clineMessages = this.task?.messageStateHandler.getClineMessages() || []
@@ -805,6 +806,7 @@ export class Controller {
taskHistory: processedTaskHistory,
platform,
shouldShowAnnouncement,
autoCondenseThreshold,
favoritedModelIds,
// NEW: Add workspace information
workspaceRoots: this.workspaceManager?.getRoots() ?? [],
@@ -288,6 +288,11 @@ export async function updateSettings(controller: Controller, request: UpdateSett
}
}
if (request.autoCondenseThreshold !== undefined) {
const threshold = Math.min(1, Math.max(0, request.autoCondenseThreshold)) // Clamp to 0-1 range
controller.stateManager.setGlobalState("autoCondenseThreshold", threshold)
}
// Post updated state to webview
await controller.postStateToWebview()
@@ -103,6 +103,7 @@ export async function getTaskHistory(controller: Controller, request: GetTaskHis
tokensOut: item.tokensOut || 0,
cacheWrites: item.cacheWrites || 0,
cacheReads: item.cacheReads || 0,
modelId: item.modelId || "",
}))
return TaskHistoryArray.create({
+2 -3
View File
@@ -992,10 +992,9 @@ export class StateManager {
planModeVercelAiGatewayModelInfo:
this.taskStateCache["planModeVercelAiGatewayModelInfo"] ||
this.globalStateCache["planModeVercelAiGatewayModelInfo"],
planModeOcaModelId: this.globalStateCache["planModeOcaModelId"],
planModeOcaModelId: this.globalStateCache["planModeOcaModelId"],
planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"],
// Act mode configurations
actModeApiProvider: this.taskStateCache["actModeApiProvider"] || this.globalStateCache["actModeApiProvider"],
actModeApiModelId: this.taskStateCache["actModeApiModelId"] || this.globalStateCache["actModeApiModelId"],
@@ -1055,7 +1054,7 @@ export class StateManager {
actModeVercelAiGatewayModelInfo:
this.taskStateCache["actModeVercelAiGatewayModelInfo"] ||
this.globalStateCache["actModeVercelAiGatewayModelInfo"],
actModeOcaModelId: this.globalStateCache["actModeOcaModelId"],
actModeOcaModelId: this.globalStateCache["actModeOcaModelId"],
actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"],
}
}
+1
View File
@@ -97,6 +97,7 @@ export interface Settings {
focusChainSettings: FocusChainSettings
customPrompt: "compact" | undefined
difyBaseUrl: string | undefined
autoCondenseThreshold: number | undefined // number from 0 to 1
ocaBaseUrl: string | undefined
// Plan mode configurations
+4
View File
@@ -235,6 +235,9 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
context.globalState.get<GlobalStateAndSettings["mcpMarketplaceCatalog"]>("mcpMarketplaceCatalog")
const qwenCodeOauthPath = context.globalState.get<GlobalStateAndSettings["qwenCodeOauthPath"]>("qwenCodeOauthPath")
const customPrompt = context.globalState.get<GlobalStateAndSettings["customPrompt"]>("customPrompt")
const autoCondenseThreshold = context.globalState.get<GlobalStateAndSettings["autoCondenseThreshold"]>(
"autoCondenseThreshold",
) as number | undefined // number from 0 to 1
// Get mode-related configurations
const mode = context.globalState.get<GlobalStateAndSettings["mode"]>("mode")
@@ -551,6 +554,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
mcpMarketplaceCatalog,
qwenCodeOauthPath,
customPrompt,
autoCondenseThreshold: autoCondenseThreshold || 75,
// Multi-root workspace support
workspaceRoots,
primaryRootIndex: primaryRootIndex ?? 0,
+8 -4
View File
@@ -1847,9 +1847,9 @@ export class Task {
// checkpointManagerErrorMessage is already set and will be part of the state.
// No explicit UI message here, error message will be in ExtensionState.
}
const modelId = this.api.getModel().id
// Separate logic when using the auto-condense context management vs the original context management methods
if (this.useAutoCondense && isNextGenModelFamily(this.api.getModel().id)) {
if (this.useAutoCondense && isNextGenModelFamily(modelId)) {
// when we initially trigger the context cleanup, we will be increasing the context window size, so we need some state `currentlySummarizing`
// to store whether we have already started the context summarization flow, so we don't attempt to summarize again. additionally, immediately
// post summarizing we need to increment the conversationHistoryDeletedRange to mask out the summarization-trigger user & assistant response messaages
@@ -1869,10 +1869,14 @@ export class Task {
}
}
} else {
const autoCondenseThreshold = this.stateManager.getGlobalSettingsKey("autoCondenseThreshold") as
| number
| undefined
shouldCompact = this.contextManager.shouldCompactContextWindow(
this.messageStateHandler.getClineMessages(),
this.api,
previousApiReqIndex,
autoCondenseThreshold,
)
// There is an edge case where the summarize_task tool call completes but the user cancels the next request before it finishes
@@ -2026,9 +2030,9 @@ export class Task {
cancelReason,
streamingFailedMessage,
})
await this.messageStateHandler.saveClineMessagesAndUpdateHistory()
await this.messageStateHandler.saveClineMessagesAndUpdateHistory(modelId)
telemetryService.captureConversationTurnEvent(this.ulid, providerId, this.api.getModel().id, "assistant", {
telemetryService.captureConversationTurnEvent(this.ulid, providerId, modelId, "assistant", {
tokensIn: inputTokens,
tokensOut: outputTokens,
cacheWriteTokens,
+2 -1
View File
@@ -64,7 +64,7 @@ export class MessageStateHandler {
this.clineMessages = newMessages
}
async saveClineMessagesAndUpdateHistory(): Promise<void> {
async saveClineMessagesAndUpdateHistory(modelId?: string): Promise<void> {
try {
await saveClineMessages(this.context, this.taskId, this.clineMessages)
@@ -104,6 +104,7 @@ export class MessageStateHandler {
conversationHistoryDeletedRange: this.taskState.conversationHistoryDeletedRange,
isFavorited: this.taskIsFavorited,
checkpointManagerErrorMessage: this.taskState.checkpointManagerErrorMessage,
modelId,
})
} catch (error) {
console.error("Failed to save cline messages:", error)
+1
View File
@@ -71,6 +71,7 @@ export interface ExtensionState {
useAutoCondense?: boolean
focusChainSettings: FocusChainSettings
customPrompt?: string
autoCondenseThreshold?: number
favoritedModelIds: string[]
// NEW: Add workspace information
workspaceRoots: WorkspaceRoot[]
+3
View File
@@ -15,4 +15,7 @@ export type HistoryItem = {
conversationHistoryDeletedRange?: [number, number]
isFavorited?: boolean
checkpointManagerErrorMessage?: string
// The last model ID used for this task
modelId?: string
}
+15
View File
@@ -28,6 +28,14 @@ const mockVSCodeDarkTheme = {
"--vscode-activityWarningBadge-background": "#F9C20B",
"--vscode-badge-background": "#007ACC",
"--vscode-badge-foreground": "#FFFFFF",
"--vscode-charts-green": "#73C991",
"--vscode-charts-yellow": "#F9C20B",
"--vscode-charts-red": "#F14C4C",
"--vscode-menu-background": "#252526",
"--vscode-menu-border": "#454545",
"--vscode-menu-foreground": "#CCCCCC",
"--vscode-menu-selectionBackground": "#062F4A",
"--vscode-menu-selectionForeground": "#FFFFFF",
}
const mockVSCodeLightTheme = {
@@ -60,6 +68,13 @@ const mockVSCodeLightTheme = {
"--vscode-activityWarningBadge-background": "#F9C20B",
"--vscode-badge-background": "#007ACC",
"--vscode-badge-foreground": "#FFFFFF",
"--vscode-charts-green": "#73C991",
"--vscode-charts-yellow": "#F9C20B",
"--vscode-menu-background": "#F3F3F3",
"--vscode-menu-border": "#D4D4D4",
"--vscode-menu-foreground": "#6F6F6F",
"--vscode-menu-selectionBackground": "#007ACC",
"--vscode-menu-selectionForeground": "#FFFFFF",
}
// Mock VSCode theme variables for Storybook
+14372 -14372
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -153,8 +153,8 @@ const createApiReqMessage = (minutesAgo: number, request: string, metrics: any =
"api_req_started",
JSON.stringify({
request,
tokensIn: 850,
tokensOut: 420,
tokensIn: 19500,
tokensOut: 4220,
cacheWrites: 120,
cacheReads: 60,
size: 12345,
@@ -173,7 +173,7 @@ const mockActiveMessages: ClineMessage[] = [
"I'll help you create a responsive navigation component for your React application. Let me start by examining your current project structure and then create a modern, accessible navigation component.",
),
createMessage(4.3, "say", "tool", JSON.stringify({ tool: "listFilesTopLevel", path: "src/components" })),
createApiReqMessage(4.2, "Component creation request", { tokensIn: 1200, tokensOut: 680, cost: 0.042 }),
createApiReqMessage(4.2, "Component creation request", { tokensIn: 12020, tokensOut: 6180, cost: 0.042 }),
createMessage(
4,
"say",
@@ -190,7 +190,7 @@ const mockActiveMessages: ClineMessage[] = [
content: "// Navigation component code...",
}),
),
createApiReqMessage(3.5, "Final response request", { tokensIn: 450, tokensOut: 320, cost: 0.018 }),
createApiReqMessage(3.5, "Final response request", { tokensIn: 41550, tokensOut: 3320, cost: 0.018 }),
createMessage(
3.3,
"say",
@@ -213,6 +213,8 @@ const mockStreamingMessages: ClineMessage[] = [
// Reusable state and decorator factories
const createMockState = (overrides: any = {}) => ({
...useExtensionState(),
useAutoCondense: true,
autoCondenseThreshold: 0.5,
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: mockActiveMessages,
@@ -414,14 +416,14 @@ export const AutoApprovalEnabled: Story = {
const createPlanModeMessages = () => [
createMessage(5, "say", "task", "Help me refactor my React application to use TypeScript and improve performance"),
createApiReqMessage(4.9, "Planning analysis request", { tokensIn: 1800, tokensOut: 950, cost: 0.065 }),
createApiReqMessage(4.9, "Planning analysis request", { tokensIn: 20000, tokensOut: 19500, cost: 0.065 }),
createMessage(
4.7,
"say",
"text",
"I'll help you refactor your React application to use TypeScript and improve performance. Let me create a detailed plan for this migration.",
),
createApiReqMessage(4.5, "Detailed planning request", { tokensIn: 2200, tokensOut: 1400, cost: 0.095 }),
createApiReqMessage(4.5, "Detailed planning request", { tokensIn: 20002, tokensOut: 12500, cost: 0.095 }),
createAskMessage(
"plan_mode_respond",
"Here's my comprehensive plan for refactoring your React application with TypeScript migration and performance optimization phases.",
@@ -5,7 +5,7 @@ import DynamicTextArea from "react-textarea-autosize"
import Thumbnails from "@/components/common/Thumbnails"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { highlightText } from "./task-header/TaskHeader"
import { highlightText } from "./task-header/Highlights"
interface UserMessageProps {
text?: string
@@ -44,6 +44,7 @@ export const TaskSection: React.FC<TaskSectionProps> = ({
lastProgressMessageText={lastProgressMessageText}
onClose={messageHandlers.handleTaskCloseButtonClick}
onScrollToMessage={scrollBehavior.scrollToMessage}
onSendMessage={messageHandlers.handleSendMessage}
task={task}
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
@@ -0,0 +1,42 @@
import React, { useMemo } from "react"
export const AutoCondenseMarker: React.FC<{ threshold: number; usage: number; isContextWindowHoverOpen?: boolean }> = ({
threshold,
usage,
isContextWindowHoverOpen,
}) => {
// The marker position is calculated based on the threshold percentage
// It goes over the progress bar to indicate where the auto-condense will trigger
// and it should highlight from what the current percentage (usage) is
// to the threshold percentage
const marker = useMemo(() => {
const _threshold = threshold * 100
return {
start: _threshold + "%",
label: _threshold.toFixed(0),
end: usage >= threshold * 100 ? usage - _threshold + "%" : undefined,
}
}, [threshold, usage, isContextWindowHoverOpen])
if (!threshold) {
return null
}
return (
<div className="flex-1" id="auto-condense-threshold-marker">
<div
className="absolute top-0 bottom-0 h-full cursor-pointer pointer-events-none z-10 bg-button-background shadow-lg outline-button-background/80 outline-0.5 w-1.5"
style={{ left: marker.start }}>
{isContextWindowHoverOpen && (
<div className="absolute -top-4 -left-1 text-button-background/80">{marker.label}%</div>
)}
{marker.end !== undefined && (
<div
className="fixed top-0 bottom-0 h-full z-20 bg-black/45"
style={{ left: marker.start, width: marker.end }}></div>
)}
</div>
</div>
)
}
AutoCondenseMarker.displayName = "AutoCondenseMarker"
@@ -0,0 +1,66 @@
import { Alert } from "@heroui/react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { XIcon } from "lucide-react"
import { useMemo, useState } from "react"
interface CheckpointErrorProps {
checkpointManagerErrorMessage?: string
handleCheckpointSettingsClick: () => void
}
export const CheckpointError: React.FC<CheckpointErrorProps> = ({
checkpointManagerErrorMessage,
handleCheckpointSettingsClick,
}) => {
const [dismissed, setDismissed] = useState(false)
const messages = useMemo(() => {
const message = checkpointManagerErrorMessage?.replace(/disabling checkpoints\.$/, "")
const showDisableButton = checkpointManagerErrorMessage?.endsWith("disabling checkpoints.")
const showGitInstructions = checkpointManagerErrorMessage?.includes("Git must be installed to use checkpoints.")
return { message, showDisableButton, showGitInstructions }
}, [checkpointManagerErrorMessage])
if (!checkpointManagerErrorMessage || dismissed) {
return null
}
return (
<div className="flex items-center justify-center w-full opacity-80 hover:opacity-100 transition-opacity duration-200">
<Alert
className="rounded-sm border-0 bg-[var(--vscode-inputValidation-errorBackground)] text-[var(--vscode-inputValidation-errorForeground)] p-1.5"
color="warning"
description={
<div className="flex gap-2">
{messages.showDisableButton && (
<button
className="underline cursor-pointer bg-transparent border-0 p-0 text-inherit"
onClick={handleCheckpointSettingsClick}>
Disable Checkpoints
</button>
)}
{messages.showGitInstructions && (
<a
className="text-link underline"
href="https://github.com/cline/cline/wiki/Installing-Git-for-Checkpoints">
See instructions
</a>
)}
</div>
}
endContent={
<VSCodeButton
appearance="icon"
aria-label="Dismiss"
className="inline-flex opacity-100 hover:bg-transparent hover:opacity-60 p-0"
onClick={() => setDismissed(true)}
title="Dismiss Checkpoint Error">
<XIcon size={12} />
</VSCodeButton>
}
hideIconWrapper={true}
isVisible={!dismissed}
title={messages.message}
variant="faded"
/>
</div>
)
}
@@ -0,0 +1,266 @@
import { cn, Progress, Tooltip } from "@heroui/react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import debounce from "lodash/debounce"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import { updateSetting } from "@/components/settings/utils/settingsHandlers"
import { formatLargeNumber as formatTokenNumber } from "@/utils/format"
import { AutoCondenseMarker } from "./AutoCondenseMarker"
import CompactTaskButton from "./buttons/CompactTaskButton"
import { ContextWindowSummary } from "./ContextWindowSummary"
// Type definitions
interface ContextWindowInfoProps {
tokensIn?: number
tokensOut?: number
cacheWrites?: number
cacheReads?: number
size?: number
}
interface ContextWindowProgressProps extends ContextWindowInfoProps {
useAutoCondense: boolean
lastApiReqTotalTokens?: number
contextWindow?: number
autoCondenseThreshold?: number
onSendMessage?: (command: string, files: string[], images: string[]) => void
}
const ConfirmationDialog = memo<{
onConfirm: (e: React.MouseEvent) => void
onCancel: (e: React.MouseEvent) => void
}>(({ onConfirm, onCancel }) => (
<div className="text-xs my-2 flex items-center gap-0 justify-between">
<span className="font-semibold text-sm">Compact the current task?</span>
<span className="flex gap-1">
<VSCodeButton
appearance="secondary"
className="text-xs"
onClick={onCancel}
title="No, keep the task as is"
type="button">
Cancel
</VSCodeButton>
<VSCodeButton
appearance="primary"
autoFocus={true}
className="text-xs"
onClick={onConfirm}
title="Yes, compact the task"
type="button">
Yes
</VSCodeButton>
</span>
</div>
))
ConfirmationDialog.displayName = "ConfirmationDialog"
const ContextWindow: React.FC<ContextWindowProgressProps> = ({
contextWindow = 0,
lastApiReqTotalTokens = 0,
autoCondenseThreshold = 0.75,
onSendMessage,
useAutoCondense,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
}) => {
const [isOpened, setIsOpened] = useState(false)
const [threshold, setThreshold] = useState(useAutoCondense ? autoCondenseThreshold : 0)
const [confirmationNeeded, setConfirmationNeeded] = useState(false)
const progressBarRef = useRef<HTMLDivElement>(null)
const handleContextWindowBarClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect()
const clickX = event.clientX - rect.left
const percentage = Math.max(0, Math.min(1, clickX / rect.width))
const newThreshold = Math.round(percentage * 100) / 100
setConfirmationNeeded(false)
setThreshold(newThreshold)
updateSetting("autoCondenseThreshold", newThreshold)
}, [])
const handleCompactClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setConfirmationNeeded(!confirmationNeeded)
},
[confirmationNeeded],
)
const handleConfirm = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
onSendMessage?.("/compact", [], [])
setConfirmationNeeded(false)
},
[onSendMessage],
)
const handleCancel = useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setConfirmationNeeded(false)
}, [])
const tokenData = useMemo(() => {
if (!contextWindow) {
return null
}
return {
percentage: (lastApiReqTotalTokens / contextWindow) * 100,
max: formatTokenNumber(contextWindow),
used: formatTokenNumber(lastApiReqTotalTokens),
}
}, [contextWindow, lastApiReqTotalTokens])
const debounceCloseHover = useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
const showHover = debounce((open: boolean) => setIsOpened(open), 100)
return showHover(false)
}, [])
// Keyboard event handlers
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!useAutoCondense) {
return
}
const step = event.shiftKey ? 0.1 : 0.05 // Larger step with Shift
let newThreshold = threshold
switch (event.key) {
case "ArrowLeft":
case "ArrowDown":
event.preventDefault()
event.stopPropagation()
setIsOpened(true) // Keep tooltip open on interaction
newThreshold = Math.max(0, threshold - step)
break
case "ArrowRight":
case "ArrowUp":
event.preventDefault()
event.stopPropagation()
setIsOpened(true) // Keep tooltip open on interaction
newThreshold = Math.min(1, threshold + step)
break
default:
return
}
if (newThreshold !== threshold) {
setThreshold(newThreshold)
updateSetting("autoCondenseThreshold", newThreshold)
}
},
[threshold, useAutoCondense, setIsOpened],
)
const handleFocus = useCallback(() => {
setIsOpened(true)
}, [])
const handleBlur = useCallback(() => {
setIsOpened(false)
}, [])
// Close tooltip when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (progressBarRef.current && !progressBarRef.current.contains(event.target as Node)) {
setIsOpened(false)
}
}
if (isOpened) {
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}
}, [isOpened])
if (!tokenData) {
return null
}
return (
<div className="flex flex-col my-1.5" onMouseLeave={debounceCloseHover}>
<div className="flex gap-1 flex-row @max-xs:flex-col @max-xs:items-start items-center text-sm">
<div className="flex items-center gap-1.5 flex-1 whitespace-nowrap text-xs">
<span className="cursor-pointer" title="Current tokens used in this request">
{tokenData.used}
</span>
<div className="flex relative items-center gap-1 flex-1 w-full h-full" onMouseEnter={() => setIsOpened(true)}>
<Tooltip
closeDelay={2000}
content={
<ContextWindowSummary
autoCompactThreshold={threshold}
cacheReads={cacheReads}
cacheWrites={cacheWrites}
contextWindow={tokenData.max}
percentage={tokenData.percentage}
tokensIn={tokensIn}
tokensOut={tokensOut}
tokenUsed={tokenData.used}
/>
}
isOpen={isOpened}
offset={-2}
placement="bottom"
showArrow={true}>
<div
aria-label="Auto condense threshold"
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={Math.round(threshold * 100)}
aria-valuetext={`${Math.round(threshold * 100)}% threshold`}
className="relative w-full text-badge-foreground context-window-progress brightness-100"
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
ref={progressBarRef}
role="slider"
tabIndex={useAutoCondense ? 0 : -1}>
<Progress
aria-label="Context window usage progress"
classNames={{
base: "drop-shadow-md w-full cursor-pointer",
track: cn("rounded max-h-2 h-3 bg-foreground/10"),
indicator: "bg-foreground rounded-r",
label: "tracking-wider font-medium text-foreground/80",
value: "text-description",
}}
color="success"
onClick={handleContextWindowBarClick}
size="md"
value={tokenData.percentage}
/>
{useAutoCondense && (
<AutoCondenseMarker
isContextWindowHoverOpen={isOpened}
key={threshold}
threshold={threshold}
usage={tokenData.percentage}
/>
)}
{isOpened}
</div>
</Tooltip>
</div>
<span className="cursor-pointer" title="Maximum context window size for this model">
{tokenData.max}
</span>
</div>
<CompactTaskButton onClick={handleCompactClick} />
</div>
{confirmationNeeded && <ConfirmationDialog onCancel={handleCancel} onConfirm={handleConfirm} />}
</div>
)
}
export default memo(ContextWindow)
@@ -0,0 +1,157 @@
import { cn } from "@heroui/react"
import React, { memo, useEffect, useMemo, useState } from "react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { formatLargeNumber as formatTokenNumber } from "@/utils/format"
interface TokenUsageInfoProps {
tokensIn?: number
tokensOut?: number
cacheWrites?: number
cacheReads?: number
}
interface TokenDetail {
title: string
value?: number
icon: string
}
interface TaskContextWindowButtonsProps extends TokenUsageInfoProps {
percentage: number
tokenUsed: string
contextWindow: string
autoCompactThreshold?: number
isThresholdChanged?: boolean
isThresholdFadingOut?: boolean
}
const InfoRow = memo<{
label: string
value: React.ReactNode
tooltip?: string
labelTooltip?: string
placement: "top" | "bottom" | "left" | "right"
}>(({ label, value, tooltip, labelTooltip, placement }) => (
<div className="flex justify-between gap-3">
{labelTooltip ? (
<HeroTooltip content={labelTooltip} placement={placement}>
<div className="font-semibold">{label}</div>
</HeroTooltip>
) : (
<div className="font-semibold">{label}</div>
)}
<div className="text-muted-foreground cursor-auto">
{tooltip ? (
<HeroTooltip content={tooltip} placement={placement}>
<span>{value}</span>
</HeroTooltip>
) : (
<span>{value}</span>
)}
</div>
</div>
))
InfoRow.displayName = "InfoRow"
// Constants
const TOKEN_DETAILS_CONFIG: Omit<TokenDetail, "value">[] = [
{ title: "Prompt Tokens", icon: "codicon-arrow-up" },
{ title: "Completion Tokens", icon: "codicon-arrow-down" },
{ title: "Cache Writes", icon: "codicon-arrow-left" },
{ title: "Cache Reads", icon: "codicon-arrow-right" },
]
const TokenUsageInfo = memo<TokenUsageInfoProps>(({ tokensIn, tokensOut, cacheWrites, cacheReads }) => {
const contextTokenDetails = useMemo(() => {
const values = [tokensIn, tokensOut, cacheWrites || 0, cacheReads || 0]
return TOKEN_DETAILS_CONFIG.map((config, index) => ({ ...config, value: values[index] })).filter((item) => item.value)
}, [tokensIn, tokensOut, cacheWrites, cacheReads])
const TokenDetailItem = memo<TokenDetail>(({ title, value, icon }) => (
<HeroTooltip content={title} key={`${icon}-${value}`} placement="bottom">
<span className="flex items-center gap-0.5 text-muted-foreground">
<i className={`codicon ${icon} font-semibold `} />
{value ? formatTokenNumber(value) : "--"}
</span>
</HeroTooltip>
))
TokenDetailItem.displayName = "TokenDetailItem"
if (!tokensIn) {
return null
}
return (
<div className="flex items-center justify-between flex-wrap">
<div className="font-semibold mr-1">Token Usage</div>
<div className="flex items-center justify-between flex-wrap gap-1 opacity-80">
{contextTokenDetails.map((item) => (
<TokenDetailItem key={item.icon} {...item} />
))}
</div>
</div>
)
})
TokenUsageInfo.displayName = "TokenUsageInfo"
export const ContextWindowSummary: React.FC<TaskContextWindowButtonsProps> = ({
contextWindow,
tokenUsed,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
percentage,
autoCompactThreshold = 0,
}) => {
const [thresholdDisplay, setThresholdDisplay] = useState(autoCompactThreshold)
const [isThresholdChanged, setIsThresholdChanged] = useState<"up" | "down" | undefined>(undefined)
const [isThresholdFadingOut, setIsThresholdFadingOut] = useState(false)
useEffect(() => {
if (autoCompactThreshold !== thresholdDisplay) {
const type = autoCompactThreshold > thresholdDisplay ? "up" : "down"
setIsThresholdChanged(type)
setThresholdDisplay(autoCompactThreshold)
return () => {
setTimeout(() => {
setIsThresholdFadingOut(true)
setTimeout(() => {
setIsThresholdChanged(undefined)
setIsThresholdFadingOut(false)
}, 1000) // Duration of fade-out effect
}, 2000) // Duration to show the changed value before starting fade-out
}
}
}, [autoCompactThreshold, thresholdDisplay])
return (
<div className="flex flex-col gap-2.5 bg-menu rounded shadow-sm border border-menu-border z-100 max-w-xs p-4">
{thresholdDisplay > 0 && (
<InfoRow
key={thresholdDisplay}
label="Auto Condense Threshold"
labelTooltip="When the context window usage exceeds current threshold, the task will be automatically condensed."
placement="right"
tooltip="Click on the context window bar to set a new auto condense threshold."
value={
<span
className={cn({
"transition-all": !isThresholdChanged && !isThresholdFadingOut,
"text-success/50 transition-discrete": isThresholdChanged === "up" && !isThresholdFadingOut,
"text-error/50 transition-discrete": isThresholdChanged === "down" && !isThresholdFadingOut,
"text-muted-foreground transition-all": isThresholdFadingOut,
})}>{`${(thresholdDisplay * 100).toFixed(0)}%`}</span>
}
/>
)}
<InfoRow
label="Context Window"
placement="bottom"
tooltip={`${tokenUsed} of ${contextWindow}`}
value={percentage ? `${percentage.toFixed(2)}% used` : contextWindow}
/>
<TokenUsageInfo cacheReads={cacheReads} cacheWrites={cacheWrites} tokensIn={tokensIn} tokensOut={tokensOut} />
</div>
)
}
@@ -0,0 +1,215 @@
import { cn } from "@heroui/react"
import { isCompletedFocusChainItem, isFocusChainItem } from "@shared/focus-chain-utils"
import { StringRequest } from "@shared/proto/cline/common"
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import React, { memo, useCallback, useMemo, useState } from "react"
import ChecklistRenderer from "@/components/common/ChecklistRenderer"
import { FileServiceClient } from "@/services/grpc-client"
// Optimized interface with readonly properties to prevent accidental mutations
interface TodoInfo {
readonly currentTodo: { text: string; completed: boolean; index: number } | null
readonly currentIndex: number
readonly completedCount: number
readonly totalCount: number
readonly progressPercentage: number
}
interface FocusChainProps {
readonly lastProgressMessageText?: string
readonly currentTaskItemId?: string
}
// Static strings to avoid recreating them
const COMPLETED_MESSAGE = "All tasks have been completed!"
const TODO_LIST_LABEL = "To-Do list"
const NEW_STEPS_MESSAGE = "New steps will be generated if you continue the task"
const CLICK_TO_EDIT_TITLE = "Click to edit to-do list in file"
// Optimized header component with minimal re-renders
const ToDoListHeader = memo<{
todoInfo: TodoInfo
isExpanded: boolean
}>(({ todoInfo, isExpanded }) => {
const { currentTodo, currentIndex, totalCount, completedCount, progressPercentage } = todoInfo
const isCompleted = completedCount === totalCount
// Pre-compute display text
const displayText = isCompleted ? COMPLETED_MESSAGE : currentTodo?.text || TODO_LIST_LABEL
return (
<div
className={cn("relative w-full h-full", {
"text-success": isCompleted,
})}>
<div
className={cn(
"absolute bottom-0 left-0 transition-[width] duration-300 ease-in-out pointer-events-none z-1 h-1 bg-success",
{
"opacity-0": progressPercentage === 0 || progressPercentage === 100,
},
)}
style={{
width: `${100 - progressPercentage}%`,
}}
/>
<div className="flex items-center justify-between gap-2 z-10 py-2.5 px-1.5">
<div className="flex items-center gap-1.5 flex-1 min-w-0">
<span
className={cn(
"rounded-lg px-2 py-0.25 text-xs inline-block shrink-0 bg-badge-foreground/20 text-badge-foreground",
{
"bg-success text-black": isCompleted,
},
)}>
{currentIndex}/{totalCount}
</span>
<span className="header-text text-xs font-medium break-words overflow-hidden text-ellipsis whitespace-nowrap max-w-[calc(100%-60px)]">
{displayText}
</span>
</div>
<div className="flex items-center justify-between text-foreground">
{isExpanded ? <ChevronDownIcon className="ml-0.25" size="16" /> : <ChevronRightIcon size="16" />}
</div>
</div>
</div>
)
})
ToDoListHeader.displayName = "ToDoListHeader"
// Cache for parsed todo info to avoid re-parsing identical text
const todoInfoCache = new Map<string, TodoInfo | null>()
const MAX_CACHE_SIZE = 100
// Highly optimized parsing with minimal allocations
const parseCurrentTodoInfo = (text: string): TodoInfo | null => {
if (!text) {
return null
}
// Check cache first
const cached = todoInfoCache.get(text)
if (cached !== undefined) {
return cached
}
let completedCount = 0
let totalCount = 0
let firstIncompleteIndex = -1
let firstIncompleteText: string | null = null
// Process text line by line without creating intermediate arrays
let lineStart = 0
let lineEnd = text.indexOf("\n")
while (lineStart < text.length) {
const line = lineEnd === -1 ? text.substring(lineStart).trim() : text.substring(lineStart, lineEnd).trim()
if (isFocusChainItem(line)) {
const isCompleted = isCompletedFocusChainItem(line)
if (isCompleted) {
completedCount++
} else if (firstIncompleteIndex === -1) {
firstIncompleteIndex = totalCount
// Extract text only for the first incomplete item
firstIncompleteText = line.substring(5).trim()
}
totalCount++
}
if (lineEnd === -1) {
break
}
lineStart = lineEnd + 1
lineEnd = text.indexOf("\n", lineStart)
}
if (totalCount === 0) {
todoInfoCache.set(text, null)
return null
}
const currentTodo = firstIncompleteText ? { text: firstIncompleteText, completed: false, index: firstIncompleteIndex } : null
const result: TodoInfo = {
currentTodo,
currentIndex: firstIncompleteIndex >= 0 ? firstIncompleteIndex + 1 : totalCount,
completedCount,
totalCount,
progressPercentage: (completedCount / totalCount) * 100,
}
// Cache the result with size management
if (todoInfoCache.size >= MAX_CACHE_SIZE) {
// Remove oldest entry (first key)
const firstKey = todoInfoCache.keys().next().value
if (firstKey) {
todoInfoCache.delete(firstKey)
}
}
todoInfoCache.set(text, result)
return result
}
// Main component with aggressive optimization
export const FocusChain: React.FC<FocusChainProps> = memo(
({ currentTaskItemId, lastProgressMessageText }) => {
const [isExpanded, setIsExpanded] = useState(false)
// Parse todo info with caching
const todoInfo = useMemo(
() => (lastProgressMessageText ? parseCurrentTodoInfo(lastProgressMessageText) : null),
[lastProgressMessageText],
)
// Static callbacks that don't change
const handleToggle = useCallback(() => setIsExpanded((prev) => !prev), [])
const handleEditClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (currentTaskItemId) {
FileServiceClient.openFocusChainFile(StringRequest.create({ value: currentTaskItemId }))
}
},
[currentTaskItemId],
)
// Early return for no content
if (!todoInfo) {
return null
}
const isCompleted = todoInfo.completedCount === todoInfo.totalCount
return (
<div
className="relative rounded-sm bg-toolbar-hover/65 flex flex-col gap-1.5 select-none hover:bg-toolbar-hover overflow-hidden opacity-80 hover:opacity-100 transition-all duration-200 cursor-pointer"
onClick={handleToggle}
title={CLICK_TO_EDIT_TITLE}>
<ToDoListHeader isExpanded={isExpanded} todoInfo={todoInfo} />
{isExpanded && (
<div className="mx-1 pb-2 px-1 relative" onClick={handleEditClick}>
<ChecklistRenderer text={lastProgressMessageText!} />
{isCompleted && (
<div className="mt-2 text-xs font-semibold text-muted-foreground">{NEW_STEPS_MESSAGE}</div>
)}
</div>
)}
</div>
)
},
(prevProps, nextProps) => {
// Custom comparison for better performance
return (
prevProps.lastProgressMessageText === nextProps.lastProgressMessageText &&
prevProps.currentTaskItemId === nextProps.currentTaskItemId
)
},
)
FocusChain.displayName = "FocusChain"
@@ -0,0 +1,76 @@
import { mentionRegexGlobal } from "@shared/context-mentions"
import { StringRequest } from "@shared/proto/cline/common"
import { FileServiceClient } from "@/services/grpc-client"
import { validateSlashCommand } from "@/utils/slash-commands"
// Optimized highlighting functions
const highlightSlashCommands = (text: string, withShadow = true) => {
const match = text.match(/^\s*\/([a-zA-Z0-9_-]+)(\s*|$)/)
if (!match || validateSlashCommand(match[1]) !== "full") {
return text
}
const commandName = match[1]
const commandEndIndex = match[0].length
const beforeCommand = text.substring(0, text.indexOf("/"))
const afterCommand = match[2] + text.substring(commandEndIndex)
return [
beforeCommand,
<span className={withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} key="slashCommand">
/{commandName}
</span>,
afterCommand,
]
}
export const highlightMentions = (text: string, withShadow = true) => {
if (!mentionRegexGlobal.test(text)) {
return text
}
const parts = text.split(mentionRegexGlobal)
const result: (string | JSX.Element)[] = []
for (let i = 0; i < parts.length; i++) {
if (i % 2 === 0) {
if (parts[i]) {
result.push(parts[i])
}
} else {
result.push(
<span
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} cursor-pointer`}
key={`mention-${Math.floor(i / 2)}`}
onClick={() => FileServiceClient.openMention(StringRequest.create({ value: parts[i] }))}>
@{parts[i]}
</span>,
)
}
}
return result.length === 1 ? result[0] : result
}
export const highlightText = (text?: string, withShadow = true) => {
if (!text) {
return text
}
const slashResult = highlightSlashCommands(text, withShadow)
if (slashResult === text) {
return highlightMentions(text, withShadow)
}
if (Array.isArray(slashResult) && slashResult.length === 3) {
const [beforeCommand, commandElement, afterCommand] = slashResult as [string, JSX.Element, string]
const mentionResult = highlightMentions(afterCommand, withShadow)
return Array.isArray(mentionResult)
? [beforeCommand, commandElement, ...mentionResult]
: [beforeCommand, commandElement, mentionResult]
}
return slashResult
}
File diff suppressed because it is too large Load Diff
@@ -8,9 +8,9 @@ import TaskTimelineTooltip from "./TaskTimelineTooltip"
import { getColor } from "./util"
// Timeline dimensions and spacing
const TIMELINE_HEIGHT = "13px"
const TIMELINE_HEIGHT = "12px"
const BLOCK_WIDTH = "13px"
const BLOCK_GAP = "3px"
const BLOCK_GAP = "4px"
const _TOOLTIP_MARGIN = 32 // 32px margin on each side
interface TaskTimelineProps {
@@ -21,6 +21,7 @@ interface TaskTimelineProps {
const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) => {
const containerRef = useRef<HTMLDivElement>(null)
const scrollableRef = useRef<HTMLDivElement>(null)
const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null)
const { taskTimelinePropsMessages, messageIndexMap } = useMemo(() => {
if (messages.length <= 1) {
@@ -103,10 +104,22 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
}
}
const handleMouseEnter = () => {
setHoveredIndex(index)
}
const handleMouseLeave = () => {
setHoveredIndex(null)
}
const isHovered = hoveredIndex === index
return (
<TaskTimelineTooltip message={message}>
<div
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
width: BLOCK_WIDTH,
height: "100%",
@@ -114,12 +127,14 @@ const TaskTimeline: React.FC<TaskTimelineProps> = ({ messages, onBlockClick }) =
flexShrink: 0,
cursor: "pointer",
marginRight: BLOCK_GAP,
opacity: isHovered ? 0.7 : 1,
transition: "opacity 0.2s ease",
}}
/>
</TaskTimelineTooltip>
)
},
[taskTimelinePropsMessages, messageIndexMap, onBlockClick],
[taskTimelinePropsMessages, messageIndexMap, onBlockClick, hoveredIndex],
)
// Scroll to the end when messages change
@@ -0,0 +1,36 @@
import { cn, Tooltip } from "@heroui/react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { FoldVerticalIcon } from "lucide-react"
const CompactTaskButton: React.FC<{
className?: string
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}> = ({ onClick, className }) => {
return (
<Tooltip
content={
<div className="flex flex-col gap-1.5 bg-menu rounded shadow-sm border border-menu-border z-100 max-w-xs p-4">
<div className="text-sm font-medium">Compact Task</div>
<div className="text-sm text-muted-foreground">
Reduces the number of tokens used by summarizing the task. To enable automatic condensing, turn on{" "}
<kbd>Auto Condense</kbd> in the settings and set the auto-condense threshold by clicking on the context
window usage bar.
</div>
</div>
}
placement="bottom">
<VSCodeButton
appearance="icon"
className={cn(
"text-badge-foreground flex items-center text-sm font-bold hover:bg-transparent hover:opacity-80",
className,
)}
onClick={onClick}
type="button">
<FoldVerticalIcon size={12} />
</VSCodeButton>
</Tooltip>
)
}
export default CompactTaskButton
@@ -1,13 +1,16 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { Button } from "@heroui/button"
import { cn } from "@heroui/react"
import { CheckIcon, CopyIcon } from "lucide-react"
import { useCallback, useState } from "react"
import HeroTooltip from "@/components/common/HeroTooltip"
const CopyTaskButton: React.FC<{
taskText?: string
}> = ({ taskText }) => {
className?: string
}> = ({ taskText, className }) => {
const [copied, setCopied] = useState(false)
const handleCopy = () => {
const handleCopy = useCallback(() => {
if (!taskText) {
return
}
@@ -16,20 +19,19 @@ const CopyTaskButton: React.FC<{
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}
}, [taskText])
return (
<HeroTooltip content="Copy Task">
<VSCodeButton
appearance="icon"
aria-label="Copy Task"
className="p-0"
onClick={handleCopy}
style={{ padding: "0px 0px" }}>
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
<i className={`codicon codicon-${copied ? "check" : "copy"}`} />
</div>
</VSCodeButton>
<HeroTooltip content="Copy Text" placement="right">
<Button
aria-label="Copy"
className={cn("bg-transparent hover:opacity-100", className)}
isIconOnly={true}
onPress={() => handleCopy()}
radius="sm"
size="sm">
{copied ? <CheckIcon size="14" /> : <CopyIcon size="14" />}
</Button>
</HeroTooltip>
)
}
@@ -1,32 +1,29 @@
import { Button, cn } from "@heroui/react"
import { StringArrayRequest } from "@shared/proto/cline/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { TrashIcon } from "lucide-react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { TaskServiceClient } from "@/services/grpc-client"
import { formatSize } from "@/utils/format"
const DeleteTaskButton: React.FC<{
taskSize: string
taskId?: string
}> = ({ taskSize, taskId }) => (
<HeroTooltip content="Delete Task">
<VSCodeButton
appearance="icon"
aria-label="Delete task"
onClick={() => taskId && TaskServiceClient.deleteTasksWithIds(StringArrayRequest.create({ value: [taskId] }))}
style={{ padding: "0px 0px" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "3px",
fontSize: "10px",
fontWeight: "bold",
opacity: 0.6,
}}>
<i className={`codicon codicon-trash`} />
{taskSize}
</div>
</VSCodeButton>
taskSize?: number
className?: string
}> = ({ taskId, className, taskSize }) => (
<HeroTooltip content={`Delete Task (size: ${taskSize ? formatSize(taskSize) : "--"})`} placement="right">
<Button
aria-label="Delete Task"
className={cn("flex items-center border-0 text-sm font-bold bg-transparent hover:opacity-100 p-0", className)}
isIconOnly={true}
onPress={() => {
taskId && TaskServiceClient.deleteTasksWithIds(StringArrayRequest.create({ value: [taskId] }))
}}
radius="sm"
size="sm">
<TrashIcon size="14" />
</Button>
</HeroTooltip>
)
DeleteTaskButton.displayName = "DeleteTaskButton"
export default DeleteTaskButton
@@ -0,0 +1,36 @@
import { Button, cn } from "@heroui/react"
import { NewTaskRequest } from "@shared/proto/cline/task"
import { GitCompareIcon } from "lucide-react"
import { TaskServiceClient } from "@/services/grpc-client"
const ForkTaskButton: React.FC<{
text?: string
images?: string[]
files?: string[]
className?: string
}> = ({ text, images = [], files = [], className }) => {
return (
<Button
aria-label="Fork Task"
className={cn("flex items-center border-0 text-sm font-bold bg-transparent hover:opacity-100", className)}
disabled={!text?.trim()}
isIconOnly={true}
onPress={() =>
text &&
TaskServiceClient.newTask(
NewTaskRequest.create({
text: text.trim(),
images,
files,
}),
)
}
radius="sm"
size="sm"
title="Fork Task">
<GitCompareIcon size="14" />
</Button>
)
}
export default ForkTaskButton
@@ -0,0 +1,30 @@
import { cn } from "@heroui/react"
import { XIcon } from "lucide-react"
import HeroTooltip from "@/components/common/HeroTooltip"
const NewTaskButton: React.FC<{
onClick: () => void
className?: string
}> = ({ className, onClick }) => {
return (
<HeroTooltip content="Close current task to start new one" delay={1000} placement="bottom">
<button
aria-label="Start a New Task"
className={cn(
"flex items-center border-0 text-sm font-bold bg-transparent opacity-70 hover:opacity-100",
className,
"hover:bg-muted/10 px-0 cursor-pointer",
)}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onClick()
}}
type="button">
<XIcon size="14" />
</button>
</HeroTooltip>
)
}
export default NewTaskButton
@@ -1,11 +1,12 @@
import { Button, cn } from "@heroui/react"
import { StringRequest } from "@shared/proto/cline/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { ArrowDownToLineIcon } from "lucide-react"
import { FileServiceClient } from "@/services/grpc-client"
const OpenDiskTaskHistoryButton: React.FC<{
taskId?: string
}> = ({ taskId }) => {
className?: string
}> = ({ taskId, className }) => {
const handleOpenDiskTaskHistory = () => {
if (!taskId) {
return
@@ -17,18 +18,16 @@ const OpenDiskTaskHistoryButton: React.FC<{
}
return (
<HeroTooltip content="Open Disk Task History">
<VSCodeButton
appearance="icon"
aria-label="Open Disk Task History"
className="p-0"
onClick={handleOpenDiskTaskHistory}
style={{ padding: "0px 0px" }}>
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
<i className={`codicon codicon-folder`} />
</div>
</VSCodeButton>
</HeroTooltip>
<Button
aria-label="Open Disk Task History"
className={cn("flex items-center border-0 text-sm font-bold bg-transparent hover:opacity-100", className)}
isIconOnly={true}
onPress={() => handleOpenDiskTaskHistory()}
radius="sm"
size="sm"
title="Export Task">
<ArrowDownToLineIcon size="14" />
</Button>
)
}
@@ -1,4 +1,6 @@
import { cn } from "@heroui/react"
import { parseFocusChainItem } from "@shared/focus-chain-utils"
import { CheckIcon, CircleIcon } from "lucide-react"
import React, { useCallback, useEffect, useRef, useState } from "react"
interface ChecklistRendererProps {
@@ -102,31 +104,18 @@ const ChecklistRenderer: React.FC<ChecklistRendererProps> = ({ text }) => {
overflowY: items.length >= 10 ? "auto" : "visible",
}}>
{items.map((item, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "flex-start",
gap: "6px",
padding: "1px 0",
}}>
<span
style={{
fontSize: "11px",
color: item.checked ? "var(--vscode-charts-green)" : "var(--vscode-descriptionForeground)",
flexShrink: 0,
marginTop: "1px",
}}>
{item.checked ? "✓" : "○"}
// biome-ignore lint/suspicious/noArrayIndexKey: Using index as key for checklist items
<div className="flex *:items-start gap-1.5 p-0.5" key={`checklist-item-${index}`}>
<span className={cn("text-xs shrink-0", item.checked ? "text-success" : "text-badge-foreground")}>
{item.checked ? <CheckIcon size={10} /> : <CircleIcon size={10} />}
</span>
<span
className={cn(
"text-xs shrink-0 break-words",
item.checked ? "text-description" : "text-badge-foreground",
)}
style={{
color: item.checked ? "var(--vscode-descriptionForeground)" : "inherit",
textDecoration: item.checked ? "line-through" : "none",
opacity: item.checked ? 0.7 : 1,
fontSize: "12px",
wordBreak: "break-word",
overflowWrap: "anywhere",
lineHeight: "1.3",
}}>
{item.text}
@@ -1,5 +1,5 @@
import { Tooltip } from "@heroui/react"
import React from "react"
import { cn, Tooltip } from "@heroui/react"
import React, { useMemo } from "react"
interface HeroTooltipProps {
content: React.ReactNode
@@ -8,6 +8,8 @@ interface HeroTooltipProps {
delay?: number
closeDelay?: number
placement?: "top" | "bottom" | "left" | "right"
showArrow?: boolean
disabled?: boolean
}
/**
@@ -18,39 +20,41 @@ const HeroTooltip: React.FC<HeroTooltipProps> = ({
content,
children,
className,
showArrow = false,
delay = 0,
closeDelay = 500,
placement = "top",
disabled = false,
}) => {
// If content is a simple string, wrap it in the tailwind styled divs
const formattedContent =
typeof content === "string" ? (
const formattedContent = useMemo(() => {
return typeof content === "string" ? (
<div
className={`bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]
border border-[var(--vscode-widget-border)] rounded p-2 w-full shadow-md text-xs max-w-[250px] ${className}`}>
<div
className="whitespace-pre-wrap break-words max-h-[150px] overflow-y-auto text-[11px]
font-[var(--vscode-editor-font-family)] p-1 rounded">
{content}
</div>
className={cn(
"bg-code-background text-code-foreground border-1 rounded shadow-md max-w-[250px] text-sm",
className,
"p-2",
)}>
<span className="whitespace-pre-wrap break-words overflow-y-auto">{content}</span>
</div>
) : (
// If content is already a React node, assume it's pre-formatted
content
)
}, [content, className])
return (
<Tooltip
classNames={{
content: "hero-tooltip-content pointer-events-none", // Prevent hovering over tooltip
}}
closeDelay={0}
closeDelay={closeDelay}
content={formattedContent} // Immediate close when cursor moves away
delay={delay}
disableAnimation={true}
isDisabled={false}
isDisabled={disabled}
placement={placement} // Disable animation for immediate appearance/disappearance
showArrow={false}>
showArrow={showArrow}>
{children}
</Tooltip>
)
+233 -430
View File
@@ -1,7 +1,19 @@
import { cn } from "@heroui/react"
import { BooleanRequest, EmptyRequest, StringArrayRequest, StringRequest } from "@shared/proto/cline/common"
import { GetTaskHistoryRequest, TaskFavoriteRequest } from "@shared/proto/cline/task"
import { VSCodeButton, VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse, { FuseResult } from "fuse.js"
import {
ArrowDownIcon,
ArrowDownToLineIcon,
ArrowLeftIcon,
ArrowRightIcon,
ArrowUpIcon,
BrainIcon,
FilterIcon,
HardDriveIcon,
Trash2Icon,
} from "lucide-react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { Virtuoso } from "react-virtuoso"
import DangerButton from "@/components/common/DangerButton"
@@ -275,466 +287,257 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
[taskHistorySearchResults],
)
return (
<>
<style>
{`
.history-item:hover {
background-color: var(--vscode-list-hoverBackground);
}
.delete-button, .export-button {
opacity: 0;
pointer-events: none;
}
.history-item:hover .delete-button,
.history-item:hover .export-button {
opacity: 1;
pointer-events: auto;
}
.history-item-highlight {
background-color: var(--vscode-editor-findMatchHighlightBackground);
color: inherit;
}
`}
</style>
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "10px 17px 10px 20px",
}}>
<h3
style={{
color: "var(--vscode-foreground)",
margin: 0,
}}>
History
</h3>
<VSCodeButton onClick={() => onDone()}>Done</VSCodeButton>
</div>
<div style={{ padding: "5px 17px 6px 17px" }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "6px",
}}>
<VSCodeTextField
onInput={(e) => {
const newValue = (e.target as HTMLInputElement)?.value
setSearchQuery(newValue)
if (newValue && !searchQuery && sortOption !== "mostRelevant") {
setLastNonRelevantSort(sortOption)
setSortOption("mostRelevant")
}
}}
placeholder="Fuzzy search history..."
style={{ width: "100%" }}
value={searchQuery}>
<div
className="codicon codicon-search"
slot="start"
style={{
fontSize: 13,
marginTop: 2.5,
opacity: 0.8,
}}></div>
{searchQuery && (
<div
aria-label="Clear search"
className="input-icon-button codicon codicon-close"
onClick={() => setSearchQuery("")}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
<VSCodeRadioGroup
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}
style={{ display: "flex", flexWrap: "wrap" }}
value={sortOption}>
<VSCodeRadio value="newest">Newest</VSCodeRadio>
<VSCodeRadio value="oldest">Oldest</VSCodeRadio>
<VSCodeRadio value="mostExpensive">Most Expensive</VSCodeRadio>
<VSCodeRadio value="mostTokens">Most Tokens</VSCodeRadio>
<VSCodeRadio disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }} value="mostRelevant">
Most Relevant
</VSCodeRadio>
<CustomFilterRadio
checked={showCurrentWorkspaceOnly}
icon="workspace"
label="Workspace"
onChange={() => setShowCurrentWorkspaceOnly(!showCurrentWorkspaceOnly)}
/>
<CustomFilterRadio
checked={showFavoritesOnly}
icon="star-full"
label="Favorites"
onChange={() => setShowFavoritesOnly(!showFavoritesOnly)}
/>
</VSCodeRadioGroup>
// const overview = useMemo(() => {
// return {
// totalTasks: tasks.length,
// totalTokens: tasks.reduce((sum, task) => sum + (task.tokensIn || 0) + (task.tokensOut || 0), 0),
// totalCost: tasks.reduce((sum, task) => sum + (task.totalCost || 0), 0),
// totalTasksSize: totalTasksSize || 0,
// totalFavorites: tasks.filter((task) => task.isFavorited).length,
// }
// }, [tasks.length, totalTasksSize])
<div className="flex justify-end gap-2.5">
<VSCodeButton onClick={() => handleBatchHistorySelect(true)}>Select All</VSCodeButton>
<VSCodeButton onClick={() => handleBatchHistorySelect(false)}>Select None</VSCodeButton>
</div>
return (
<div className="flex flex-col overflow-hidden w-full h-full gap-3">
<div className="flex justify-between items-center px-5 mt-3">
<h3 className="text-foreground m-0">History</h3>
<VSCodeButton onClick={() => onDone()}>Close</VSCodeButton>
</div>
<div className="px-5">
<div className="flex flex-col gap-1.5">
<VSCodeTextField
onInput={(e) => {
const newValue = (e.target as HTMLInputElement)?.value
setSearchQuery(newValue)
if (newValue && !searchQuery && sortOption !== "mostRelevant") {
setLastNonRelevantSort(sortOption)
setSortOption("mostRelevant")
}
}}
placeholder="Fuzzy search history..."
style={{ width: "100%" }}
value={searchQuery}>
<div className="codicon codicon-search opacity-80" slot="start" />
{searchQuery && (
<div
aria-label="Clear search"
className="input-icon-button codicon codicon-close"
onClick={() => setSearchQuery("")}
slot="end"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
/>
)}
</VSCodeTextField>
<div className="flex gap-2 items-center">
<FilterIcon size={14} />
<VSCodeDropdown
className="flex-1/2"
name="sort-by"
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}
value={sortOption}>
<VSCodeOption value="newest">Newest</VSCodeOption>
<VSCodeOption value="oldest">Oldest</VSCodeOption>
<VSCodeOption value="mostExpensive">Most Expensive</VSCodeOption>
<VSCodeOption value="mostTokens">Most Tokens</VSCodeOption>
<VSCodeOption disabled={!searchQuery} style={{ opacity: searchQuery ? 1 : 0.5 }} value="mostRelevant">
Most Relevant
</VSCodeOption>
</VSCodeDropdown>
<CustomFilterRadio
checked={showCurrentWorkspaceOnly}
icon="workspace"
label="Workspace"
onChange={() => setShowCurrentWorkspaceOnly(!showCurrentWorkspaceOnly)}
/>
<CustomFilterRadio
checked={showFavoritesOnly}
icon="star-full"
label="Favorites"
onChange={() => setShowFavoritesOnly(!showFavoritesOnly)}
/>
</div>
</div>
<div style={{ flexGrow: 1, overflowY: "auto", margin: 0 }}>
<Virtuoso
data={taskHistorySearchResults}
itemContent={(index, item) => (
</div>
<div className="flex-grow">
<Virtuoso
data={taskHistorySearchResults}
itemContent={(_, item) => (
<div
className="w-full flex shrink-0 cursor-pointer border-b border-muted-foreground/30 *:last:border-0 hover:bg-muted/40"
key={item.id}>
<div
className="history-item"
key={item.id}
style={{
cursor: "pointer",
borderBottom:
index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
display: "flex",
}}>
<VSCodeCheckbox
checked={selectedItems.includes(item.id)}
className="pl-3 pr-1 py-auto"
onClick={(e) => {
const checked = (e.target as HTMLInputElement).checked
handleHistorySelect(item.id, checked)
e.stopPropagation()
}}
/>
<div
onClick={() => handleShowTaskWithId(item.id)}
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
padding: "12px 20px",
paddingLeft: "16px",
position: "relative",
flexGrow: 1,
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
className="flex flex-col gap-2 relative flex-grow mx-3 my-1 py-2 px-1"
onClick={() => handleShowTaskWithId(item.id)}>
<div className="flex justify-between items-center">
<div className="inline-flex items-center gap-2">
<VSCodeCheckbox
checked={selectedItems.includes(item.id)}
className="text-xs"
onClick={(e) => {
const checked = (e.target as HTMLInputElement).checked
handleHistorySelect(item.id, checked)
e.stopPropagation()
}}
/>
<span className="text-description text-xs font-medium capitalize">
{formatDate(item.ts)}
</span>
<div style={{ display: "flex", gap: "4px" }}>
{/* only show delete button if task not favorited */}
{!(pendingFavoriteToggles[item.id] ?? item.isFavorited) && (
<VSCodeButton
appearance="icon"
aria-label="Delete"
className="delete-button"
onClick={(e) => {
e.stopPropagation()
handleDeleteHistoryItem(item.id)
}}
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"
aria-label={item.isFavorited ? "Remove from favorites" : "Add to favorites"}
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",
<div className="flex gap-0.5 items-center">
<VSCodeButton
appearance="icon"
aria-label="Delete"
className="text-description text-xs "
disabled={pendingFavoriteToggles[item.id] ?? item.isFavorited}
onClick={(e) => {
e.stopPropagation()
handleDeleteHistoryItem(item.id)
}}>
<span
className="ph-no-capture"
dangerouslySetInnerHTML={{
__html: item.task,
}}
<Trash2Icon size={12} />
</VSCodeButton>
<VSCodeButton
appearance="icon"
aria-label="Export"
className="text-description text-xs "
onClick={(e) => {
e.stopPropagation()
TaskServiceClient.exportTaskWithId(
StringRequest.create({ value: item.id }),
).catch((err) => console.error("Failed to export task:", err))
}}>
<ArrowDownToLineIcon size={12} />
</VSCodeButton>
<VSCodeButton
appearance="icon"
aria-label={item.isFavorited ? "Remove from favorites" : "Add to favorites"}
className="text-xs "
onClick={(e) => {
e.stopPropagation()
toggleFavorite(item.id, !!item.isFavorited)
}}>
<div
className={cn({
"codicon codicon-star-full text-button-background block":
pendingFavoriteToggles[item.id] ?? item.isFavorited,
"codicon codicon-star-empty": !(
pendingFavoriteToggles[item.id] ?? item.isFavorited
),
})}
/>
</div>
</VSCodeButton>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
Tokens:
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-up"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(item.tokensIn || 0)}
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-down"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-2px",
}}
/>
{formatLargeNumber(item.tokensOut || 0)}
</span>
</div>
{!item.totalCost && <ExportButton itemId={item.id} />}
</div>
<div className="relative text-sm text-foreground overflow-hidden whitespace-pre-wrap wrap-anywhere max-h-13 py-1">
<span
className="text-xs text-foreground"
dangerouslySetInnerHTML={{
__html: item.task,
}}
/>
</div>
{item.modelId && (
<div className="flex gap-1 items-center text-xs text-description">
<BrainIcon size={12} />
{item.modelId}
</div>
)}
<div className="flex gap-2 text-description flex-wrap justify-between">
<div className="flex gap-2 text-description flex-wrap text-xs">
{item.totalCost > 0 && (
<div className="flex gap-1 items-center text-xs">${item.totalCost?.toFixed(4)}</div>
)}
<div className="flex gap-0.5 items-center">
<span>Tokens</span>
<span className="inline-flex items-center">
<ArrowUpIcon size={12} />
{formatLargeNumber(item.tokensIn || 0)}
</span>
<span className="inline-flex items-center">
<ArrowDownIcon size={12} />
{formatLargeNumber(item.tokensOut || 0)}
</span>
</div>
{!!(item.cacheWrites || item.cacheReads) && (
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
flexWrap: "wrap",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
Cache:
</span>
{item.cacheWrites + item.cacheReads > 0 && (
<div className="flex gap-0.5 items-center">
<span>Cache</span>
{item.cacheWrites > 0 && (
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-right"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: "-1px",
}}
/>
<span className="inline-flex items-center">
<ArrowRightIcon size={12} />
{formatLargeNumber(item.cacheWrites)}
</span>
)}
{item.cacheReads > 0 && (
<span
style={{
display: "flex",
alignItems: "center",
gap: "3px",
color: "var(--vscode-descriptionForeground)",
}}>
<i
className="codicon codicon-arrow-left"
style={{
fontSize: "12px",
fontWeight: "bold",
marginBottom: 0,
}}
/>
<span className="inline-flex items-center">
<ArrowLeftIcon size={12} />
{formatLargeNumber(item.cacheReads)}
</span>
)}
</div>
)}
{!!item.totalCost && (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: -2,
}}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
}}>
<span
style={{
fontWeight: 500,
color: "var(--vscode-descriptionForeground)",
}}>
API Cost:
</span>
<span
style={{
color: "var(--vscode-descriptionForeground)",
}}>
${item.totalCost?.toFixed(4)}
</span>
</div>
<ExportButton itemId={item.id} />
</div>
)}
</div>
<div className="flex items-center text-xs text-description">
<HardDriveIcon className="px-0.5" size={12} />
{formatSize(item.size)}
</div>
</div>
</div>
)}
style={{
flexGrow: 1,
overflowY: "scroll",
}}
/>
</div>
<div
style={{
padding: "10px 10px",
borderTop: "1px solid var(--vscode-panel-border)",
}}>
{selectedItems.length > 0 ? (
<DangerButton
aria-label="Delete selected items"
onClick={() => {
handleDeleteSelectedHistoryItems(selectedItems)
}}
style={{ width: "100%" }}>
Delete {selectedItems.length > 1 ? selectedItems.length : ""} Selected
{selectedItemsSize > 0 ? ` (${formatSize(selectedItemsSize)})` : ""}
</DangerButton>
) : (
<DangerButton
aria-label="Delete all history"
disabled={deleteAllDisabled || taskHistory.length === 0}
onClick={() => {
setDeleteAllDisabled(true)
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({}))
.then(() => fetchTotalTasksSize())
.catch((error) => console.error("Error deleting task history:", error))
.finally(() => setDeleteAllDisabled(false))
}}
style={{ width: "100%" }}>
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
</DangerButton>
</div>
)}
</div>
style={{
flexGrow: 1,
overflowY: "scroll",
}}
/>
</div>
</>
<div className="flex p-1 gap-2 border-t border-muted-foreground/20">
<VSCodeButton
className="flex-1/2"
onClick={() => handleBatchHistorySelect(selectedItems.length !== taskHistorySearchResults.length)}>
{selectedItems.length === taskHistorySearchResults.length ? "Deselect All" : "Select All"}
</VSCodeButton>
{selectedItems.length > 0 ? (
<DangerButton
aria-label="Delete selected items"
className="flex-1/2"
onClick={() => {
handleDeleteSelectedHistoryItems(selectedItems)
}}>
Delete {selectedItems.length > 1 ? selectedItems.length : ""} Selected
{selectedItemsSize > 0 ? ` (${formatSize(selectedItemsSize)})` : ""}
</DangerButton>
) : (
<DangerButton
aria-label="Delete all history"
className="flex-1/2"
disabled={deleteAllDisabled || taskHistory.length === 0}
onClick={() => {
setDeleteAllDisabled(true)
TaskServiceClient.deleteAllTaskHistory(BooleanRequest.create({}))
.then(() => fetchTotalTasksSize())
.catch((error) => console.error("Error deleting task history:", error))
.finally(() => setDeleteAllDisabled(false))
}}>
Delete All History{totalTasksSize !== null ? ` (${formatSize(totalTasksSize)})` : ""}
</DangerButton>
)}
</div>
</div>
)
}
const ExportButton = ({ itemId }: { itemId: string }) => (
<VSCodeButton
appearance="icon"
aria-label="Export"
className="export-button"
onClick={(e) => {
e.stopPropagation()
TaskServiceClient.exportTaskWithId(StringRequest.create({ value: itemId })).catch((err) =>
console.error("Failed to export task:", err),
)
}}>
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>EXPORT</div>
</VSCodeButton>
)
// https://gist.github.com/evenfrost/1ba123656ded32fb7a0cd4651efd4db0
export const highlight = (fuseSearchResult: FuseResult<any>[], highlightClassName: string = "history-item-highlight") => {
const set = (obj: Record<string, any>, path: string, value: any) => {
@@ -53,6 +53,7 @@ export interface ExtensionStateContextType extends ExtensionState {
showAccount: boolean
showAnnouncement: boolean
showChatModelSelector: boolean
expandTaskHeader: boolean
// Setters
setShowAnnouncement: (value: boolean) => void
@@ -72,6 +73,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
setMcpMarketplaceCatalog: (value: McpMarketplaceCatalog) => void
setTotalTasksSize: (value: number | null) => void
setExpandTaskHeader: (value: boolean) => void
// Refresh functions
refreshOpenRouterModels: () => void
@@ -206,6 +208,7 @@ export const ExtensionStateContextProvider: React.FC<{
yoloModeToggled: false,
customPrompt: undefined,
useAutoCondense: false,
autoCondenseThreshold: undefined,
favoritedModelIds: [],
// NEW: Add workspace information with defaults
@@ -213,6 +216,7 @@ export const ExtensionStateContextProvider: React.FC<{
primaryRootIndex: 0,
isMultiRootWorkspace: false,
})
const [expandTaskHeader, setExpandTaskHeader] = useState(true)
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
@@ -717,6 +721,8 @@ export const ExtensionStateContextProvider: React.FC<{
refreshOpenRouterModels,
onRelinquishControl,
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
expandTaskHeader,
setExpandTaskHeader,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
+8
View File
@@ -82,14 +82,22 @@ export default {
foreground: "var(--vscode-banner-foreground)",
icon: "var(--vscode-banner-iconForeground)",
},
toolbar: {
DEFAULT: "var(--vscode-toolbar-background)",
hover: "var(--vscode-toolbar-hoverBackground)",
},
error: "var(--vscode-errorForeground)",
description: "var(--vscode-descriptionForeground)",
success: "var(--vscode-charts-green)",
warning: "var(--vscode-charts-yellow)",
},
fontSize: {
xl: "calc(2 * var(--vscode-font-size))",
lg: "calc(1.5 * var(--vscode-font-size))",
md: "calc(1.25 * var(--vscode-font-size))",
sm: "var(--vscode-font-size)",
xs: "calc(0.85 * var(--vscode-font-size))",
xxs: "calc(0.75 * var(--vscode-font-size))",
},
},
},