Compare commits

...
Author SHA1 Message Date
abeatrix 104a566dbf Revert "feat: new Task Header UI with configurable auto condense threshold (#6049)"
This reverts commit b3aee3857c.
2025-09-23 12:10:53 -07:00
23 changed files with 932 additions and 1291 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"claude-dev": patch
---
Refactor Task Header UI with interactive context window management
+3 -2
View File
@@ -238,8 +238,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") // number from 0 to 1
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")
-15
View File
@@ -28,14 +28,6 @@ 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 = {
@@ -68,13 +60,6 @@ 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
+6 -8
View File
@@ -153,8 +153,8 @@ const createApiReqMessage = (minutesAgo: number, request: string, metrics: any =
"api_req_started",
JSON.stringify({
request,
tokensIn: 19500,
tokensOut: 4220,
tokensIn: 850,
tokensOut: 420,
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: 12020, tokensOut: 6180, cost: 0.042 }),
createApiReqMessage(4.2, "Component creation request", { tokensIn: 1200, tokensOut: 680, cost: 0.042 }),
createMessage(
4,
"say",
@@ -190,7 +190,7 @@ const mockActiveMessages: ClineMessage[] = [
content: "// Navigation component code...",
}),
),
createApiReqMessage(3.5, "Final response request", { tokensIn: 41550, tokensOut: 3320, cost: 0.018 }),
createApiReqMessage(3.5, "Final response request", { tokensIn: 450, tokensOut: 320, cost: 0.018 }),
createMessage(
3.3,
"say",
@@ -213,8 +213,6 @@ const mockStreamingMessages: ClineMessage[] = [
// Reusable state and decorator factories
const createMockState = (overrides: any = {}) => ({
...useExtensionState(),
useAutoCondense: true,
autoCondenseThreshold: 0.5,
welcomeViewCompleted: true,
showWelcome: false,
clineMessages: mockActiveMessages,
@@ -416,14 +414,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: 20000, tokensOut: 19500, cost: 0.065 }),
createApiReqMessage(4.9, "Planning analysis request", { tokensIn: 1800, tokensOut: 950, 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: 20002, tokensOut: 12500, cost: 0.095 }),
createApiReqMessage(4.5, "Detailed planning request", { tokensIn: 2200, tokensOut: 1400, 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/Highlights"
import { highlightText } from "./task-header/TaskHeader"
interface UserMessageProps {
text?: string
@@ -44,7 +44,6 @@ 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}
@@ -1,132 +0,0 @@
import { cn } from "@heroui/react"
import React, { useEffect, useMemo, useRef, useState } from "react"
export const AutoCondenseMarker: React.FC<{
threshold: number
usage: number
isContextWindowHoverOpen?: boolean
shouldAnimate?: boolean
}> = ({ threshold, usage, isContextWindowHoverOpen, shouldAnimate = false }) => {
const [isAnimating, setIsAnimating] = useState(false)
const [animatedPosition, setAnimatedPosition] = useState(0)
const [showPercentageAfterAnimation, setShowPercentageAfterAnimation] = useState(false)
const [isFadingOut, setIsFadingOut] = useState(false)
// Refs to store animation frame and timeout IDs for cleanup
const animationFrameRef = useRef<number | null>(null)
const fadeOutTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// Animation effect when shouldAnimate prop changes (initial load)
useEffect(() => {
// Cleanup function to cancel any pending animations or timeouts
const cleanup = () => {
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current)
animationFrameRef.current = null
}
if (fadeOutTimeoutRef.current !== null) {
clearTimeout(fadeOutTimeoutRef.current)
fadeOutTimeoutRef.current = null
}
if (hideTimeoutRef.current !== null) {
clearTimeout(hideTimeoutRef.current)
hideTimeoutRef.current = null
}
}
if (shouldAnimate && threshold > 0) {
// Clean up any existing animations before starting new one
cleanup()
setIsAnimating(true)
const targetPosition = threshold * 100
const duration = 1200 // ms - slowed down from 800ms
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
// Ease-out animation curve
const easeOut = 1 - (1 - progress) ** 3
const currentPosition = easeOut * targetPosition
setAnimatedPosition(currentPosition)
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame(animate)
} else {
animationFrameRef.current = null
setIsAnimating(false)
setShowPercentageAfterAnimation(true)
// Start fade out after 1 second
fadeOutTimeoutRef.current = setTimeout(() => {
setIsFadingOut(true)
// Completely hide after fade transition
hideTimeoutRef.current = setTimeout(() => {
setShowPercentageAfterAnimation(false)
setIsFadingOut(false)
hideTimeoutRef.current = null
setAnimatedPosition(threshold * 100) // Ensure it ends exactly at threshold
}, 300) // 300ms fade duration
fadeOutTimeoutRef.current = null
}, 1000)
}
}
animationFrameRef.current = requestAnimationFrame(animate)
}
// Cleanup on unmount or when dependencies change
return cleanup
}, [shouldAnimate, threshold])
// 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
// Always use the current threshold for position and label - animation only affects visual movement
const position = _threshold
const startingPosition = isAnimating ? animatedPosition : position
return {
start: startingPosition + "%",
label: startingPosition.toFixed(0),
end: usage > startingPosition ? usage - startingPosition + "%" : 0,
}
}, [threshold, usage, isAnimating, animatedPosition])
if (!threshold) {
return null
}
return (
<div className="flex-1" id="auto-condense-threshold-marker">
<div
className={cn(
"absolute top-0 bottom-0 h-full cursor-pointer pointer-events-none z-10 bg-button-background shadow-lg w-1",
{
"transition-all duration-75": !isAnimating,
},
)}
style={{
left: marker.start,
transform: isAnimating ? `translateX(${animatedPosition - threshold * 100}%)` : "translateX(0)",
}}>
{(isContextWindowHoverOpen || isAnimating || showPercentageAfterAnimation) && (
<div
className={cn(
"absolute -top-4 -left-1 text-button-background font-mono text-xs transition-opacity duration-300",
{
"opacity-0": isFadingOut,
},
)}>
{marker.label}%
</div>
)}
</div>
</div>
)
}
AutoCondenseMarker.displayName = "AutoCondenseMarker"
@@ -1,66 +0,0 @@
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>
)
}
@@ -1,284 +0,0 @@
import { cn, Progress, Tooltip } from "@heroui/react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import debounce from "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 [shouldAnimateMarker, setShouldAnimateMarker] = useState(false)
// Trigger marker animation when component first mounts (TaskHeader expands)
useEffect(() => {
if (useAutoCondense && threshold > 0) {
setShouldAnimateMarker(true)
// Reset animation flag after animation completes
const timer = setTimeout(() => {
setShouldAnimateMarker(false)
}, 1400) // Slightly longer than animation duration (1200ms + buffer)
return () => clearTimeout(timer)
}
}, []) // Empty dependency array means this only runs on mount
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)
}, [])
// Close tooltip when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element
const isInsideProgressBar = progressBarRef.current && progressBarRef.current.contains(target as Node)
// Check if click is inside any tooltip content by looking for our custom class
const isInsideTooltipContent = target.closest(".context-window-tooltip-content") !== null
if (!isInsideProgressBar && !isInsideTooltipContent) {
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={0}
content={
<ContextWindowSummary
autoCompactThreshold={useAutoCondense ? threshold : undefined}
cacheReads={cacheReads}
cacheWrites={cacheWrites}
contextWindow={tokenData.max}
percentage={tokenData.percentage}
tokensIn={tokensIn}
tokensOut={tokensOut}
tokenUsed={tokenData.used}
/>
}
disableAnimation={true}
isOpen={isOpened}
offset={-2}
placement="bottom"
shouldCloseOnBlur={false}
shouldCloseOnInteractOutside={() => false}
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"
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"
disableAnimation={true}
onClick={handleContextWindowBarClick}
size="md"
value={tokenData.percentage}
/>
{useAutoCondense && (
<AutoCondenseMarker
isContextWindowHoverOpen={isOpened}
shouldAnimate={shouldAnimateMarker}
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)
@@ -1,184 +0,0 @@
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
import React, { memo, useCallback, useMemo, useState } from "react"
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
}
// New accordion item component
const AccordionItem = memo<{
title: string
value: React.ReactNode
isExpanded: boolean
onToggle: (event?: React.MouseEvent) => void
children?: React.ReactNode
}>(({ title, value, isExpanded, onToggle, children }) => {
const handleClick = useCallback(
(event: React.MouseEvent) => {
event.preventDefault()
event.stopPropagation()
onToggle(event)
},
[onToggle],
)
return (
<div className="flex flex-col">
<div
className="flex justify-between items-center gap-3 cursor-pointer hover:bg-foreground/5 rounded px-1 py-0.5 transition-colors"
onClick={handleClick}>
<div className="flex items-center gap-1">
{isExpanded ? <ChevronDownIcon size={12} /> : <ChevronRightIcon size={12} />}
<div className="font-semibold text-sm">{title}</div>
</div>
<div className="text-muted-foreground text-sm">{value}</div>
</div>
{isExpanded && children && <div className="ml-4 mt-2 mb-1 text-xs text-muted-foreground">{children}</div>}
</div>
)
})
AccordionItem.displayName = "AccordionItem"
// 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 TokenUsageDetails = 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])
if (!tokensIn) {
return <div>No token usage data available</div>
}
return (
<div className="space-y-2">
{contextTokenDetails.map((item) => (
<div className="flex items-center justify-between" key={item.icon}>
<div className="flex items-center gap-1">
<i className={`codicon ${item.icon} text-xs`} />
<span>{item.title}</span>
</div>
<span className="font-mono">{formatTokenNumber(item.value || 0)}</span>
</div>
))}
</div>
)
})
TokenUsageDetails.displayName = "TokenUsageDetails"
export const ContextWindowSummary: React.FC<TaskContextWindowButtonsProps> = ({
contextWindow,
tokenUsed,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
percentage,
autoCompactThreshold = 0,
}) => {
// Accordion state
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set())
const toggleSection = useCallback((section: string, event?: React.MouseEvent) => {
if (event) {
event.preventDefault()
event.stopPropagation()
}
setExpandedSections((prev) => {
const newSet = new Set(prev)
if (newSet.has(section)) {
newSet.delete(section)
} else {
newSet.add(section)
}
return newSet
})
}, [])
const totalTokens = (tokensIn || 0) + (tokensOut || 0)
return (
<div className="context-window-tooltip-content flex flex-col gap-2 bg-menu rounded shadow-sm border border-menu-border z-100 w-60 p-4">
{autoCompactThreshold > 0 && (
<AccordionItem
isExpanded={expandedSections.has("threshold")}
onToggle={(event) => toggleSection("threshold", event)}
title="Auto Condense Threshold"
value={<span className="text-muted-foreground">{`${(autoCompactThreshold * 100).toFixed(0)}%`}</span>}>
<div className="space-y-1">
<p className="text-xs leading-relaxed text-white">
Click on the context window bar to set a new threshold.
</p>
<p className="text-xs leading-relaxed mt-0 mb-0">
When the context window usage exceeds this threshold, the task will be automatically condensed.
</p>
</div>
</AccordionItem>
)}
<AccordionItem
isExpanded={expandedSections.has("context")}
onToggle={(event) => toggleSection("context", event)}
title="Context Window"
value={percentage ? `${percentage.toFixed(1)}% used` : contextWindow}>
<div className="space-y-1">
<div className="flex justify-between">
<span>Used:</span>
<span className="font-mono">{tokenUsed}</span>
</div>
<div className="flex justify-between">
<span>Total:</span>
<span className="font-mono">{contextWindow}</span>
</div>
<div className="flex justify-between">
<span>Remaining:</span>
<span className="font-mono">
{formatTokenNumber(parseInt(contextWindow.replace(/,/g, "")) - parseInt(tokenUsed.replace(/,/g, "")))}
</span>
</div>
</div>
</AccordionItem>
{totalTokens > 0 && (
<AccordionItem
isExpanded={expandedSections.has("tokens")}
onToggle={(event) => toggleSection("tokens", event)}
title="Token Usage"
value={`${formatTokenNumber(totalTokens)} total`}>
<TokenUsageDetails
cacheReads={cacheReads}
cacheWrites={cacheWrites}
tokensIn={tokensIn}
tokensOut={tokensOut}
/>
</AccordionItem>
)}
</div>
)
}
@@ -1,215 +0,0 @@
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"
@@ -1,76 +0,0 @@
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 = "12px"
const TIMELINE_HEIGHT = "13px"
const BLOCK_WIDTH = "13px"
const BLOCK_GAP = "4px"
const BLOCK_GAP = "3px"
const _TOOLTIP_MARGIN = 32 // 32px margin on each side
interface TaskTimelineProps {
@@ -21,7 +21,6 @@ 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) {
@@ -104,22 +103,10 @@ 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%",
@@ -127,14 +114,12 @@ 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, hoveredIndex],
[taskTimelinePropsMessages, messageIndexMap, onBlockClick],
)
// Scroll to the end when messages change
@@ -1,35 +0,0 @@
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 Compact</kbd> in the settings and set the 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,16 +1,13 @@
import { Button } from "@heroui/button"
import { cn } from "@heroui/react"
import { CheckIcon, CopyIcon } from "lucide-react"
import { useCallback, useState } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import HeroTooltip from "@/components/common/HeroTooltip"
const CopyTaskButton: React.FC<{
taskText?: string
className?: string
}> = ({ taskText, className }) => {
}> = ({ taskText }) => {
const [copied, setCopied] = useState(false)
const handleCopy = useCallback(() => {
const handleCopy = () => {
if (!taskText) {
return
}
@@ -19,19 +16,20 @@ const CopyTaskButton: React.FC<{
setCopied(true)
setTimeout(() => setCopied(false), 1500)
})
}, [taskText])
}
return (
<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 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>
)
}
@@ -1,29 +1,32 @@
import { Button, cn } from "@heroui/react"
import { StringArrayRequest } from "@shared/proto/cline/common"
import { TrashIcon } from "lucide-react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/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?: 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>
}> = ({ 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>
</HeroTooltip>
)
DeleteTaskButton.displayName = "DeleteTaskButton"
export default DeleteTaskButton
@@ -1,30 +0,0 @@
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,12 +1,11 @@
import { Button, cn } from "@heroui/react"
import { StringRequest } from "@shared/proto/cline/common"
import { ArrowDownToLineIcon } from "lucide-react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import HeroTooltip from "@/components/common/HeroTooltip"
import { FileServiceClient } from "@/services/grpc-client"
const OpenDiskTaskHistoryButton: React.FC<{
taskId?: string
className?: string
}> = ({ taskId, className }) => {
}> = ({ taskId }) => {
const handleOpenDiskTaskHistory = () => {
if (!taskId) {
return
@@ -18,16 +17,18 @@ const OpenDiskTaskHistoryButton: React.FC<{
}
return (
<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>
<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>
)
}
@@ -1,6 +1,4 @@
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 {
@@ -104,18 +102,31 @@ const ChecklistRenderer: React.FC<ChecklistRendererProps> = ({ text }) => {
overflowY: items.length >= 10 ? "auto" : "visible",
}}>
{items.map((item, index) => (
// 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} />}
<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 ? "✓" : "○"}
</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 { cn, Tooltip } from "@heroui/react"
import React, { useMemo } from "react"
import { Tooltip } from "@heroui/react"
import React from "react"
interface HeroTooltipProps {
content: React.ReactNode
@@ -8,8 +8,6 @@ interface HeroTooltipProps {
delay?: number
closeDelay?: number
placement?: "top" | "bottom" | "left" | "right"
showArrow?: boolean
disabled?: boolean
}
/**
@@ -20,41 +18,39 @@ 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 = useMemo(() => {
return typeof content === "string" ? (
const formattedContent =
typeof content === "string" ? (
<div
className={cn(
"bg-code-background text-code-foreground border border-code-foreground/20 rounded shadow-md max-w-[250px] text-sm",
className,
"p-2",
)}>
<span className="whitespace-pre-wrap break-words overflow-y-auto">{content}</span>
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>
</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={closeDelay}
closeDelay={0}
content={formattedContent} // Immediate close when cursor moves away
delay={delay}
disableAnimation={true}
isDisabled={disabled}
isDisabled={false}
placement={placement} // Disable animation for immediate appearance/disappearance
showArrow={showArrow}>
showArrow={false}>
{children}
</Tooltip>
)
@@ -55,7 +55,6 @@ export interface ExtensionStateContextType extends ExtensionState {
showAccount: boolean
showAnnouncement: boolean
showChatModelSelector: boolean
expandTaskHeader: boolean
// Setters
setDictationSettings: (value: DictationSettings) => void
@@ -76,7 +75,6 @@ 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
@@ -212,7 +210,6 @@ export const ExtensionStateContextProvider: React.FC<{
yoloModeToggled: false,
customPrompt: undefined,
useAutoCondense: false,
autoCondenseThreshold: undefined,
favoritedModelIds: [],
// NEW: Add workspace information with defaults
@@ -220,7 +217,6 @@ 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>>({
@@ -725,8 +721,6 @@ export const ExtensionStateContextProvider: React.FC<{
refreshOpenRouterModels,
onRelinquishControl,
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
expandTaskHeader,
setExpandTaskHeader,
setDictationSettings: (value: DictationSettings) =>
setState((prevState) => ({
...prevState,
-8
View File
@@ -82,22 +82,14 @@ 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))",
},
},
},