From 96015e6056898ec8e3a045acb9007bc3887ecacc Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Fri, 19 Dec 2025 21:08:40 +0000 Subject: [PATCH 1/4] feat: Improve chat UI to match codebase design system - Refactor SidebarChat component with improved design - Restore rounded corners and improve visual consistency - Update styles and CSS for better UI/UX - Improve input components and command bar styling - Polish overall chat interface appearance --- .../cortexide/browser/media/cortexide.css | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 8389 ++++++++++------- .../cortexide/browser/react/src/styles.css | 10 +- .../browser/react/src/util/inputs.tsx | 20 +- .../VoidCommandBar.tsx | 33 +- 5 files changed, 5155 insertions(+), 3303 deletions(-) diff --git a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css index 931701be728..7950dd2b3e1 100644 --- a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css +++ b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css @@ -49,9 +49,9 @@ --cortex-space-xl: 20px; --cortex-space-2xl: 24px; - /* Radii */ - --cortex-radius-xs: 3px; - --cortex-radius-sm: 4px; + /* Radii - subtle rounded corners for modern aesthetic */ + --cortex-radius-xs: 4px; + --cortex-radius-sm: 6px; --cortex-radius-md: 8px; --cortex-radius-lg: 12px; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx index c628e9e5398..2fb6f73df5e 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -3,70 +3,187 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; - - -import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js'; -import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; - -import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; -import { URI } from '../../../../../../../base/common/uri.js'; -import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { ErrorDisplay } from './ErrorDisplay.js'; -import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js'; -import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; -import { PastThreadsList } from './SidebarThreadSelector.js'; -import { CORTEXIDE_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; -import { CORTEXIDE_OPEN_SETTINGS_ACTION_ID } from '../../../cortexideSettingsPane.js'; -import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled, isValidProviderModelSelection } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js'; -import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; -import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { getModelCapabilities, getIsReasoningEnabledState, getReservedOutputTokenSpace } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text, Image as ImageIcon, FileText } from 'lucide-react'; -import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, PlanMessage, ReviewMessage, PlanStep, StepStatus, PlanApprovalState } from '../../../../common/chatThreadServiceTypes.js'; -import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; -import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; -import { IsRunningType } from '../../../chatThreadService.js'; -import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; -import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js'; -import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; -import ErrorBoundary from './ErrorBoundary.js'; -import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; - -import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; -import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js'; -import { useImageAttachments } from '../util/useImageAttachments.js'; -import { usePDFAttachments } from '../util/usePDFAttachments.js'; -import { PDFAttachmentList } from '../util/PDFAttachmentList.js'; -import { ImageAttachmentList } from '../util/ImageAttachmentList.js'; -import { ChatImageAttachment, ChatPDFAttachment } from '../../../../common/chatThreadServiceTypes.js'; -import { ImageMessageRenderer } from '../util/ImageMessageRenderer.js'; -import { PDFMessageRenderer } from '../util/PDFMessageRenderer.js'; - - - -export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { +import React, { + ButtonHTMLAttributes, + FormEvent, + FormHTMLAttributes, + Fragment, + KeyboardEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { + useAccessor, + useChatThreadsState, + useChatThreadsStreamState, + useSettingsState, + useActiveURI, + useCommandBarState, + useFullChatThreadsStreamState, +} from "../util/services.js"; +import { ScrollType } from "../../../../../../../editor/common/editorCommon.js"; + +import { + ChatMarkdownRender, + ChatMessageLocation, + getApplyBoxId, +} from "../markdown/ChatMarkdownRender.js"; +import { URI } from "../../../../../../../base/common/uri.js"; +import { IDisposable } from "../../../../../../../base/common/lifecycle.js"; +import { ErrorDisplay } from "./ErrorDisplay.js"; +import { + BlockCode, + TextAreaFns, + VoidCustomDropdownBox, + VoidInputBox2, + VoidSlider, + VoidSwitch, + VoidDiffEditor, +} from "../util/inputs.js"; +import { ModelDropdown } from "../void-settings-tsx/ModelDropdown.js"; +import { PastThreadsList } from "./SidebarThreadSelector.js"; +import { CORTEXIDE_CTRL_L_ACTION_ID } from "../../../actionIDs.js"; +import { CORTEXIDE_OPEN_SETTINGS_ACTION_ID } from "../../../cortexideSettingsPane.js"; +import { + ChatMode, + displayInfoOfProviderName, + FeatureName, + isFeatureNameDisabled, + isValidProviderModelSelection, +} from "../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js"; +import { ICommandService } from "../../../../../../../platform/commands/common/commands.js"; +import { WarningBox } from "../void-settings-tsx/WarningBox.js"; +import { + getModelCapabilities, + getIsReasoningEnabledState, + getReservedOutputTokenSpace, +} from "../../../../common/modelCapabilities.js"; +import { + AlertTriangle, + File, + Ban, + Check, + ChevronRight, + Dot, + FileIcon, + Pencil, + Undo, + Undo2, + X, + Flag, + Copy as CopyIcon, + Info, + CirclePlus, + Ellipsis, + CircleEllipsis, + Folder, + ALargeSmall, + TypeOutline, + Text, + Image as ImageIcon, + FileText, + Plus, +} from "lucide-react"; +import { + ChatMessage, + CheckpointEntry, + StagingSelectionItem, + ToolMessage, + PlanMessage, + ReviewMessage, + PlanStep, + StepStatus, + PlanApprovalState, +} from "../../../../common/chatThreadServiceTypes.js"; +import { + approvalTypeOfBuiltinToolName, + BuiltinToolCallParams, + BuiltinToolName, + ToolName, + LintErrorItem, + ToolApprovalType, + toolApprovalTypes, +} from "../../../../common/toolsServiceTypes.js"; +import { + CopyButton, + EditToolAcceptRejectButtonsHTML, + IconShell1, + JumpToFileButton, + JumpToTerminalButton, + StatusIndicator, + StatusIndicatorForApplyButton, + useApplyStreamState, + useEditToolStreamState, +} from "../markdown/ApplyBlockHoverButtons.js"; +import { IsRunningType } from "../../../chatThreadService.js"; +import { + acceptAllBg, + acceptBorder, + buttonFontSize, + buttonTextColor, + rejectAllBg, + rejectBg, + rejectBorder, +} from "../../../../common/helpers/colors.js"; +import { + builtinToolNames, + isABuiltinToolName, + MAX_FILE_CHARS_PAGE, + MAX_TERMINAL_INACTIVE_TIME, +} from "../../../../common/prompt/prompts.js"; +import { RawToolCallObj } from "../../../../common/sendLLMMessageTypes.js"; +import ErrorBoundary from "./ErrorBoundary.js"; +import { ToolApprovalTypeSwitch } from "../void-settings-tsx/Settings.js"; + +import { persistentTerminalNameOfId } from "../../../terminalToolService.js"; +import { removeMCPToolNamePrefix } from "../../../../common/mcpServiceTypes.js"; +import { useImageAttachments } from "../util/useImageAttachments.js"; +import { usePDFAttachments } from "../util/usePDFAttachments.js"; +import { PDFAttachmentList } from "../util/PDFAttachmentList.js"; +import { ImageAttachmentList } from "../util/ImageAttachmentList.js"; +import { + ChatImageAttachment, + ChatPDFAttachment, +} from "../../../../common/chatThreadServiceTypes.js"; +import { ImageMessageRenderer } from "../util/ImageMessageRenderer.js"; +import { PDFMessageRenderer } from "../util/PDFMessageRenderer.js"; + +export const IconX = ({ + size, + className = "", + ...props +}: { size: number; className?: string } & React.SVGProps) => { return ( ); }; -const IconArrowUp = ({ size, className = '' }: { size: number, className?: string }) => { +const IconArrowUp = ({ + size, + className = "", +}: { + size: number; + className?: string; +}) => { return ( { +const IconSquare = ({ + size, + className = "", +}: { + size: number; + className?: string; +}) => { return ( { +export const IconWarning = ({ + size, + className = "", +}: { + size: number; + className?: string; +}) => { return ( { + return ( + + ); +}; + +// Pulsing dots animation for a softer loading indicator +const PulsingDots = ({ className = "" }: { className?: string }) => { + return ( + + ); +}; -export const IconLoading = ({ className = '', showTokenCount }: { className?: string, showTokenCount?: number }) => { +export const IconLoading = ({ + className = "", + showTokenCount, + showSpinner = false, + size = 14, +}: { + className?: string; + showTokenCount?: number; + showSpinner?: boolean; + size?: number; +}) => { const [dots, setDots] = useState(1); useEffect(() => { @@ -138,7 +323,7 @@ export const IconLoading = ({ className = '', showTokenCount }: { className?: st const animate = () => { const now = Date.now(); if (now - lastUpdate >= 400) { - setDots(prev => prev >= 3 ? 1 : prev + 1); + setDots((prev) => (prev >= 3 ? 1 : prev + 1)); lastUpdate = now; } frameId = requestAnimationFrame(animate); @@ -148,160 +333,240 @@ export const IconLoading = ({ className = '', showTokenCount }: { className?: st return () => cancelAnimationFrame(frameId); }, []); - const dotsText = '.'.repeat(dots); - const tokenText = showTokenCount !== undefined ? ` (${showTokenCount} tokens)` : ''; - - return
{dotsText}{tokenText}
; -} - + const dotsText = ".".repeat(dots); + const tokenText = + showTokenCount !== undefined ? ` (${showTokenCount} tokens)` : ""; + return ( +
+ {showSpinner ? ( + + ) : ( + + )} + {tokenText && ( + {tokenText} + )} +
+ ); +}; // SLIDER ONLY: -const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { - const accessor = useAccessor() +const ReasoningOptionSlider = ({ + featureName, +}: { + featureName: FeatureName; +}) => { + const accessor = useAccessor(); - const cortexideSettingsService = accessor.get('ICortexideSettingsService') - const voidSettingsState = useSettingsState() + const cortexideSettingsService = accessor.get("ICortexideSettingsService"); + const voidSettingsState = useSettingsState(); - const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName] - const overridesOfModel = voidSettingsState.overridesOfModel + const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName]; + const overridesOfModel = voidSettingsState.overridesOfModel; - if (!modelSelection) return null + if (!modelSelection) return null; // Skip "auto" - it's not a real provider if (!isValidProviderModelSelection(modelSelection)) { return null; } - const { modelName, providerName } = modelSelection - const { reasoningCapabilities } = getModelCapabilities(providerName, modelName, overridesOfModel) - const { canTurnOffReasoning, reasoningSlider: reasoningBudgetSlider } = reasoningCapabilities || {} - - const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] - const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) - - if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider - return
- Thinking - { - const isOff = canTurnOffReasoning && !newVal - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff }) - }} - /> -
- } + const { modelName, providerName } = modelSelection; + const { reasoningCapabilities } = getModelCapabilities( + providerName, + modelName, + overridesOfModel, + ); + const { canTurnOffReasoning, reasoningSlider: reasoningBudgetSlider } = + reasoningCapabilities || {}; + + const modelSelectionOptions = + voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[ + modelName + ]; + const isReasoningEnabled = getIsReasoningEnabledState( + featureName, + providerName, + modelName, + modelSelectionOptions, + overridesOfModel, + ); - if (reasoningBudgetSlider?.type === 'budget_slider') { // if it's a slider - const { min: min_, max, default: defaultVal } = reasoningBudgetSlider - - const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters - const stepSize = Math.round((max - min_) / nSteps) - - const valueIfOff = min_ - stepSize - const min = canTurnOffReasoning ? valueIfOff : min_ - const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal - : valueIfOff - - return
- Thinking - { - if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; - const isOff = canTurnOffReasoning && newVal === valueIfOff - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningBudget: newVal }) - }} - /> - {isReasoningEnabled ? `${value} tokens` : 'Thinking disabled'} -
+ if (canTurnOffReasoning && !reasoningBudgetSlider) { + // if it's just a on/off toggle without a power slider + return ( +
+ + Thinking + + { + const isOff = canTurnOffReasoning && !newVal; + cortexideSettingsService.setOptionsOfModelSelection( + featureName, + modelSelection.providerName, + modelSelection.modelName, + { reasoningEnabled: !isOff }, + ); + }} + /> +
+ ); } - if (reasoningBudgetSlider?.type === 'effort_slider') { - - const { values, default: defaultVal } = reasoningBudgetSlider - - const min = canTurnOffReasoning ? -1 : 0 - const max = values.length - 1 - - const currentEffort = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEffort ?? defaultVal - const valueIfOff = -1 - const value = isReasoningEnabled && currentEffort ? values.indexOf(currentEffort) : valueIfOff - - const currentEffortCapitalized = currentEffort.charAt(0).toUpperCase() + currentEffort.slice(1, Infinity) - - return
- Thinking - { - if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; - const isOff = canTurnOffReasoning && newVal === valueIfOff - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningEffort: values[newVal] ?? undefined }) - }} - /> - {isReasoningEnabled ? `${currentEffortCapitalized}` : 'Thinking disabled'} -
+ if (reasoningBudgetSlider?.type === "budget_slider") { + // if it's a slider + const { min: min_, max, default: defaultVal } = reasoningBudgetSlider; + + const nSteps = 8; // only used in calculating stepSize, stepSize is what actually matters + const stepSize = Math.round((max - min_) / nSteps); + + const valueIfOff = min_ - stepSize; + const min = canTurnOffReasoning ? valueIfOff : min_; + const value = isReasoningEnabled + ? (voidSettingsState.optionsOfModelSelection[featureName][ + modelSelection.providerName + ]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal) + : valueIfOff; + + return ( +
+ + Thinking + + { + if ( + modelSelection.providerName === "auto" && + modelSelection.modelName === "auto" + ) + return; + const isOff = canTurnOffReasoning && newVal === valueIfOff; + cortexideSettingsService.setOptionsOfModelSelection( + featureName, + modelSelection.providerName, + modelSelection.modelName, + { reasoningEnabled: !isOff, reasoningBudget: newVal }, + ); + }} + /> + + {isReasoningEnabled ? `${value} tokens` : "Thinking disabled"} + +
+ ); } - return null -} - + if (reasoningBudgetSlider?.type === "effort_slider") { + const { values, default: defaultVal } = reasoningBudgetSlider; + + const min = canTurnOffReasoning ? -1 : 0; + const max = values.length - 1; + + const currentEffort = + voidSettingsState.optionsOfModelSelection[featureName][ + modelSelection.providerName + ]?.[modelSelection.modelName]?.reasoningEffort ?? defaultVal; + const valueIfOff = -1; + const value = + isReasoningEnabled && currentEffort + ? values.indexOf(currentEffort) + : valueIfOff; + + const currentEffortCapitalized = + currentEffort.charAt(0).toUpperCase() + currentEffort.slice(1, Infinity); + + return ( +
+ + Thinking + + { + if ( + modelSelection.providerName === "auto" && + modelSelection.modelName === "auto" + ) + return; + const isOff = canTurnOffReasoning && newVal === valueIfOff; + cortexideSettingsService.setOptionsOfModelSelection( + featureName, + modelSelection.providerName, + modelSelection.modelName, + { + reasoningEnabled: !isOff, + reasoningEffort: values[newVal] ?? undefined, + }, + ); + }} + /> + + {isReasoningEnabled + ? `${currentEffortCapitalized}` + : "Thinking disabled"} + +
+ ); + } + return null; +}; const nameOfChatMode = { - 'normal': 'Chat', - 'gather': 'Gather', - 'agent': 'Agent', -} + normal: "Chat", + gather: "Gather", + agent: "Agent", +}; const detailOfChatMode = { - 'normal': 'Normal chat', - 'gather': 'Reads files, but can\'t edit', - 'agent': 'Edits files and uses tools', -} - + normal: "Normal chat", + gather: "Reads files, but can't edit", + agent: "Edits files and uses tools", +}; const ChatModeDropdown = ({ className }: { className: string }) => { - const accessor = useAccessor() - - const cortexideSettingsService = accessor.get('ICortexideSettingsService') - const settingsState = useSettingsState() - - const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'agent'], []) - - const onChangeOption = useCallback((newVal: ChatMode) => { - cortexideSettingsService.setGlobalSetting('chatMode', newVal) - }, [cortexideSettingsService]) - - return nameOfChatMode[val]} - getOptionDropdownName={(val) => nameOfChatMode[val]} - getOptionDropdownDetail={(val) => detailOfChatMode[val]} - getOptionsEqual={(a, b) => a === b} - /> - -} + const accessor = useAccessor(); + const cortexideSettingsService = accessor.get("ICortexideSettingsService"); + const settingsState = useSettingsState(); + const options: ChatMode[] = useMemo(() => ["normal", "gather", "agent"], []); + const onChangeOption = useCallback( + (newVal: ChatMode) => { + cortexideSettingsService.setGlobalSetting("chatMode", newVal); + }, + [cortexideSettingsService], + ); + return ( + nameOfChatMode[val]} + getOptionDropdownName={(val) => nameOfChatMode[val]} + getOptionDropdownDetail={(val) => detailOfChatMode[val]} + getOptionsEqual={(a, b) => a === b} + /> + ); +}; interface CortexideChatAreaProps { // Required @@ -321,8 +586,8 @@ interface CortexideChatAreaProps { showProspectiveSelections?: boolean; loadingIcon?: React.ReactNode; - selections?: StagingSelectionItem[] - setSelections?: (s: StagingSelectionItem[]) => void + selections?: StagingSelectionItem[]; + setSelections?: (s: StagingSelectionItem[]) => void; // selections?: any[]; // onSelectionsChange?: (selections: any[]) => void; @@ -350,7 +615,7 @@ export const VoidChatArea: React.FC = ({ divRef, isStreaming = false, isDisabled = false, - className = '', + className = "", showModelDropdown = true, showSelections = false, showProspectiveSelections = false, @@ -370,7 +635,7 @@ export const VoidChatArea: React.FC = ({ const pdfInputRef = React.useRef(null); const containerRef = React.useRef(null); - // Handle paste + // Handle paste React.useEffect(() => { const handlePaste = (e: ClipboardEvent) => { const items = Array.from(e.clipboardData?.items || []); @@ -378,12 +643,12 @@ export const VoidChatArea: React.FC = ({ const pdfFiles: File[] = []; for (const item of items) { - if (item.type.startsWith('image/')) { + if (item.type.startsWith("image/")) { const file = item.getAsFile(); if (file) { imageFiles.push(file); } - } else if (item.type === 'application/pdf') { + } else if (item.type === "application/pdf") { const file = item.getAsFile(); if (file) { pdfFiles.push(file); @@ -403,9 +668,9 @@ export const VoidChatArea: React.FC = ({ const container = containerRef.current || divRef?.current; if (container) { - container.addEventListener('paste', handlePaste); + container.addEventListener("paste", handlePaste); return () => { - container.removeEventListener('paste', handlePaste); + container.removeEventListener("paste", handlePaste); }; } }, [divRef, onImagePaste]); @@ -425,8 +690,9 @@ export const VoidChatArea: React.FC = ({ } lastDragOverTimeRef.current = now; - const hasFiles = Array.from(e.dataTransfer.items).some(item => - item.type.startsWith('image/') || item.type === 'application/pdf' + const hasFiles = Array.from(e.dataTransfer.items).some( + (item) => + item.type.startsWith("image/") || item.type === "application/pdf", ); if (hasFiles) { setIsDragOver(true); @@ -444,11 +710,11 @@ export const VoidChatArea: React.FC = ({ e.stopPropagation(); setIsDragOver(false); - const imageFiles = Array.from(e.dataTransfer.files).filter(file => - file.type.startsWith('image/') + const imageFiles = Array.from(e.dataTransfer.files).filter((file) => + file.type.startsWith("image/"), ); - const pdfFiles = Array.from(e.dataTransfer.files).filter(file => - file.type === 'application/pdf' + const pdfFiles = Array.from(e.dataTransfer.files).filter( + (file) => file.type === "application/pdf", ); if (imageFiles.length > 0 && onImageDrop) { @@ -468,30 +734,30 @@ export const VoidChatArea: React.FC = ({ }; const handleImageInputChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []).filter(file => - file.type.startsWith('image/') + const files = Array.from(e.target.files || []).filter((file) => + file.type.startsWith("image/"), ); if (files.length > 0 && onImageDrop) { onImageDrop(files); } - e.target.value = ''; // Reset input + e.target.value = ""; // Reset input }; const handlePDFInputChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []).filter(file => - file.type === 'application/pdf' + const files = Array.from(e.target.files || []).filter( + (file) => file.type === "application/pdf", ); if (files.length > 0 && onPDFDrop) { onPDFDrop(files); } - e.target.value = ''; // Reset input + e.target.value = ""; // Reset input }; return (
{ if (divRef) { - if (typeof divRef === 'function') { + if (typeof divRef === "function") { divRef(node); } else { divRef.current = node; @@ -501,17 +767,17 @@ export const VoidChatArea: React.FC = ({ }} className={` gap-x-1 - flex flex-col p-2.5 relative input text-left shrink-0 - rounded-2xl - bg-[#030304] + flex flex-col p-2.5 relative input text-left shrink-0 + rounded-2xl + bg-void-bg-1 transition-all duration-200 - border border-[rgba(255,255,255,0.08)] focus-within:border-[rgba(255,255,255,0.12)] hover:border-[rgba(255,255,255,0.12)] - ${isDragOver ? 'border-blue-500 bg-blue-500/10' : ''} + border border-void-border-3 focus-within:border-void-border-2 hover:border-void-border-2 + ${isDragOver ? "border-void-link-color bg-void-link-color/10" : ""} max-h-[80vh] overflow-y-auto - ${className} - `} + ${className} + `} onClick={(e) => { - onClickAnywhere?.() + onClickAnywhere?.(); }} onDragOver={handleDragOver} onDragLeave={handleDragLeave} @@ -544,7 +810,7 @@ export const VoidChatArea: React.FC = ({ {/* Selections section */} {showSelections && selections && setSelections && ( = ({ {/* Input section - Modern Cursor-style layout */}
-
- {children} -
+
{children}
{/* Right-side icon bar - Cursor style */}
@@ -585,16 +849,13 @@ export const VoidChatArea: React.FC = ({ {isStreaming ? ( ) : ( - + )}
{/* Close button (X) if onClose is provided */} {onClose && ( -
+
= ({
{/* Bottom row - Model selector and settings */} -
+
{showModelDropdown && ( -
- {featureName === 'Chat' && } - +
+ {featureName === "Chat" && ( + + )} +
)} {/* Loading indicator */} {isStreaming && loadingIcon && ( -
- {loadingIcon} -
+
{loadingIcon}
)}
); }; - - - -type ButtonProps = ButtonHTMLAttributes +type ButtonProps = ButtonHTMLAttributes; const DEFAULT_BUTTON_SIZE = 22; -export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { - - return -} + // data-tooltip-id='void-tooltip' + // data-tooltip-content={'Send'} + // data-tooltip-place='left' + {...props} + > + + + ); +}; -export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { - return -} - - + type="button" + {...props} + > + + + ); +}; const scrollToBottom = (divRef: { current: HTMLElement | null }) => { if (divRef.current) { @@ -668,20 +937,27 @@ const scrollToBottom = (divRef: { current: HTMLElement | null }) => { } }; - - -const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject }) => { +const ScrollToBottomContainer = ({ + children, + className, + style, + scrollContainerRef, +}: { + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; + scrollContainerRef: React.MutableRefObject; +}) => { const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom - const divRef = scrollContainerRef + const divRef = scrollContainerRef; const onScroll = () => { const div = divRef.current; if (!div) return; - const isBottom = Math.abs( - div.scrollHeight - div.clientHeight - div.scrollTop - ) < 4; + const isBottom = + Math.abs(div.scrollHeight - div.clientHeight - div.scrollTop) < 4; setIsAtBottom(isBottom); }; @@ -699,63 +975,63 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe }, []); return ( -
+
{children}
); }; -export const getRelative = (uri: URI, accessor: ReturnType) => { - const workspaceContextService = accessor.get('IWorkspaceContextService') - let path: string - const isInside = workspaceContextService.isInsideWorkspace(uri) +export const getRelative = ( + uri: URI, + accessor: ReturnType, +) => { + const workspaceContextService = accessor.get("IWorkspaceContextService"); + let path: string; + const isInside = workspaceContextService.isInsideWorkspace(uri); if (isInside) { - const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath)) - if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') } - else { path = uri.fsPath } - } - else { - path = uri.fsPath + const f = workspaceContextService + .getWorkspace() + .folders.find((f) => uri.fsPath?.startsWith(f.uri.fsPath)); + if (f) { + path = uri.fsPath.replace(f.uri.fsPath, ""); + } else { + path = uri.fsPath; + } + } else { + path = uri.fsPath; } - return path || undefined -} + return path || undefined; +}; export const getFolderName = (pathStr: string) => { // 'unixify' path - pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / - const parts = pathStr.split('/') // split on / + pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with / + const parts = pathStr.split("/"); // split on / // Filter out empty parts (the last element will be empty if path ends with /) - const nonEmptyParts = parts.filter(part => part.length > 0) - if (nonEmptyParts.length === 0) return '/' // Root directory - if (nonEmptyParts.length === 1) return nonEmptyParts[0] + '/' // Only one folder + const nonEmptyParts = parts.filter((part) => part.length > 0); + if (nonEmptyParts.length === 0) return "/"; // Root directory + if (nonEmptyParts.length === 1) return nonEmptyParts[0] + "/"; // Only one folder // Get the last two parts - const lastTwo = nonEmptyParts.slice(-2) - return lastTwo.join('/') + '/' -} + const lastTwo = nonEmptyParts.slice(-2); + return lastTwo.join("/") + "/"; +}; export const getBasename = (pathStr: string, parts: number = 1) => { // 'unixify' path - pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / - const allParts = pathStr.split('/') // split on / - if (allParts.length === 0) return pathStr - return allParts.slice(-parts).join('/') -} - - + pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with / + const allParts = pathStr.split("/"); // split on / + if (allParts.length === 0) return pathStr; + return allParts.slice(-parts).join("/"); +}; // Open file utility function export const voidOpenFileFn = ( uri: URI, accessor: ReturnType, - range?: [number, number] + range?: [number, number], ) => { - const commandService = accessor.get('ICommandService') - const editorService = accessor.get('ICodeEditorService') + const commandService = accessor.get("ICommandService"); + const editorService = accessor.get("ICodeEditorService"); // Get editor selection from CodeSelection range let editorSelection = undefined; @@ -771,205 +1047,245 @@ export const voidOpenFileFn = ( } // open the file - commandService.executeCommand('vscode.open', uri).then(() => { - + commandService.executeCommand("vscode.open", uri).then(() => { // select the text setTimeout(() => { if (!editorSelection) return; - const editor = editorService.getActiveCodeEditor() + const editor = editorService.getActiveCodeEditor(); if (!editor) return; - editor.setSelection(editorSelection) - editor.revealRange(editorSelection, ScrollType.Immediate) - - }, 50) // needed when document was just opened and needs to initialize - - }) - + editor.setSelection(editorSelection); + editor.revealRange(editorSelection, ScrollType.Immediate); + }, 50); // needed when document was just opened and needs to initialize + }); }; - -export const SelectedFiles = ( - { type, selections, setSelections, showProspectiveSelections, messageIdx, }: - | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } - | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean, messageIdx?: number } -) => { - - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const modelReferenceService = accessor.get('ICortexideModelService') - - - +export const SelectedFiles = ({ + type, + selections, + setSelections, + showProspectiveSelections, + messageIdx, +}: + | { + type: "past"; + selections: StagingSelectionItem[]; + setSelections?: undefined; + showProspectiveSelections?: undefined; + messageIdx: number; + } + | { + type: "staging"; + selections: StagingSelectionItem[]; + setSelections: (newSelections: StagingSelectionItem[]) => void; + showProspectiveSelections?: boolean; + messageIdx?: number; + }) => { + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const modelReferenceService = accessor.get("ICortexideModelService"); // state for tracking prospective files - const { uri: currentURI } = useActiveURI() - const [recentUris, setRecentUris] = useState([]) - const maxRecentUris = 10 - const maxProspectiveFiles = 3 - useEffect(() => { // handle recent files - if (!currentURI) return - setRecentUris(prev => { - const withoutCurrent = prev.filter(uri => uri.fsPath !== currentURI.fsPath) // remove duplicates - const withCurrent = [currentURI, ...withoutCurrent] - return withCurrent.slice(0, maxRecentUris) - }) - }, [currentURI]) - const [prospectiveSelections, setProspectiveSelections] = useState([]) - + const { uri: currentURI } = useActiveURI(); + const [recentUris, setRecentUris] = useState([]); + const maxRecentUris = 10; + const maxProspectiveFiles = 3; + useEffect(() => { + // handle recent files + if (!currentURI) return; + setRecentUris((prev) => { + const withoutCurrent = prev.filter( + (uri) => uri.fsPath !== currentURI.fsPath, + ); // remove duplicates + const withCurrent = [currentURI, ...withoutCurrent]; + return withCurrent.slice(0, maxRecentUris); + }); + }, [currentURI]); + const [prospectiveSelections, setProspectiveSelections] = useState< + StagingSelectionItem[] + >([]); // handle prospective files useEffect(() => { const computeRecents = async () => { const prospectiveURIs = recentUris - .filter(uri => !selections.find(s => s.type === 'File' && s.uri.fsPath === uri.fsPath)) - .slice(0, maxProspectiveFiles) + .filter( + (uri) => + !selections.find( + (s) => s.type === "File" && s.uri.fsPath === uri.fsPath, + ), + ) + .slice(0, maxProspectiveFiles); - const answer: StagingSelectionItem[] = [] + const answer: StagingSelectionItem[] = []; for (const uri of prospectiveURIs) { answer.push({ - type: 'File', + type: "File", uri: uri, - language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext', + language: + ( + await modelReferenceService.getModelSafe(uri) + ).model?.getLanguageId() || "plaintext", state: { wasAddedAsCurrentFile: false }, - }) + }); } - return answer - } + return answer; + }; // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet - if (type === 'staging' && showProspectiveSelections) { - computeRecents().then((a) => setProspectiveSelections(a)) + if (type === "staging" && showProspectiveSelections) { + computeRecents().then((a) => setProspectiveSelections(a)); + } else { + setProspectiveSelections([]); } - else { - setProspectiveSelections([]) - } - }, [recentUris, selections, type, showProspectiveSelections]) - + }, [recentUris, selections, type, showProspectiveSelections]); - const allSelections = [...selections, ...prospectiveSelections] + const allSelections = [...selections, ...prospectiveSelections]; if (allSelections.length === 0) { - return null + return null; } return ( -
- +
{allSelections.map((selection, i) => { + const isThisSelectionProspective = i > selections.length - 1; + + const thisKey = + selection.type === "CodeSelection" + ? selection.type + + selection.language + + selection.range + + selection.state.wasAddedAsCurrentFile + + selection.uri.fsPath + : selection.type === "File" + ? selection.type + + selection.language + + selection.state.wasAddedAsCurrentFile + + selection.uri.fsPath + : selection.type === "Folder" + ? selection.type + + selection.language + + selection.state + + selection.uri.fsPath + : i; + + const SelectionIcon = + selection.type === "File" + ? File + : selection.type === "Folder" + ? Folder + : selection.type === "CodeSelection" + ? Text + : (undefined as never); - const isThisSelectionProspective = i > selections.length - 1 - - const thisKey = selection.type === 'CodeSelection' ? selection.type + selection.language + selection.range + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath - : selection.type === 'File' ? selection.type + selection.language + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath - : selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath - : i - - const SelectionIcon = ( - selection.type === 'File' ? File - : selection.type === 'Folder' ? Folder - : selection.type === 'CodeSelection' ? Text - : (undefined as never) - ) - - return
- {/* tooltip for file path */} - - {/* summarybox */} -
+ {/* summarybox */} +
{ - if (type !== 'staging') return; // (never) - if (isThisSelectionProspective) { // add prospective selection to selections - setSelections([...selections, selection]) - } - else if (selection.type === 'File') { // open files - voidOpenFileFn(selection.uri, accessor); - - const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile - if (wasAddedAsCurrentFile) { - // make it so the file is added permanently, not just as the current file - const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } } - setSelections([ - ...selections.slice(0, i), - newSelection, - ...selections.slice(i + 1) - ]) + onClick={() => { + if (type !== "staging") return; // (never) + if (isThisSelectionProspective) { + // add prospective selection to selections + setSelections([...selections, selection]); + } else if (selection.type === "File") { + // open files + voidOpenFileFn(selection.uri, accessor); + + const wasAddedAsCurrentFile = + selection.state.wasAddedAsCurrentFile; + if (wasAddedAsCurrentFile) { + // make it so the file is added permanently, not just as the current file + const newSelection: StagingSelectionItem = { + ...selection, + state: { + ...selection.state, + wasAddedAsCurrentFile: false, + }, + }; + setSelections([ + ...selections.slice(0, i), + newSelection, + ...selections.slice(i + 1), + ]); + } + } else if (selection.type === "CodeSelection") { + voidOpenFileFn(selection.uri, accessor, selection.range); + } else if (selection.type === "Folder") { + // TODO!!! reveal in tree } + }} + > + {} + + { + // file name and range + getBasename(selection.uri.fsPath) + + (selection.type === "CodeSelection" + ? ` (${selection.range[0]}-${selection.range[1]})` + : "") } - else if (selection.type === 'CodeSelection') { - voidOpenFileFn(selection.uri, accessor, selection.range); - } - else if (selection.type === 'Folder') { - // TODO!!! reveal in tree - } - }} - > - {} - - { // file name and range - getBasename(selection.uri.fsPath) - + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') - } - - {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? - - {`(Current File)`} - - : null - } - - {type === 'staging' && !isThisSelectionProspective ? // X button -
{ - e.stopPropagation(); // don't open/close selection - if (type !== 'staging') return; - setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) - }} - > - -
- : <> - } -
- -
+ {selection.type === "File" && + selection.state.wasAddedAsCurrentFile && + messageIdx === undefined && + currentURI?.fsPath === selection.uri.fsPath ? ( + + {`(Current File)`} + + ) : null} + + {type === "staging" && !isThisSelectionProspective ? ( // X button +
{ + e.stopPropagation(); // don't open/close selection + if (type !== "staging") return; + setSelections([ + ...selections.slice(0, i), + ...selections.slice(i + 1), + ]); + }} + > + +
+ ) : ( + <> + )} +
+ +
+ ); })} - -
- - ) -} - + ); +}; type ToolHeaderParams = { icon?: React.ReactNode; @@ -989,7 +1305,7 @@ type ToolHeaderParams = { desc2OnClick?: () => void; isOpen?: boolean; className?: string; -} +}; const ToolHeaderWrapper = ({ icon, @@ -1010,183 +1326,234 @@ const ToolHeaderWrapper = ({ isRejected, className, // applies to the main content }: ToolHeaderParams) => { - const [isOpen_, setIsOpen] = useState(false); - const isExpanded = isOpen !== undefined ? isOpen : isOpen_ + const isExpanded = isOpen !== undefined ? isOpen : isOpen_; - const isDropdown = children !== undefined // null ALLOWS dropdown - const isClickable = !!(isDropdown || onClick) + const isDropdown = children !== undefined; // null ALLOWS dropdown + const isClickable = !!(isDropdown || onClick); - const isDesc1Clickable = !!desc1OnClick + const isDesc1Clickable = !!desc1OnClick; - const desc1HTML = {desc1} - - return (
-
- {/* header */} -
-
- {/* left */} -
+ {desc1} + + ); + + return ( +
+
+ {/* header */} +
+
- {/* title eg "> Edited File" */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - if (onClick) { onClick(); } - }} + {/* left */} +
- {isDropdown && ( Edited File" */} +
{ + if (isDropdown) { + setIsOpen((v) => !v); + } + if (onClick) { + onClick(); + } + }} + > + {isDropdown && ( + )} - {title} + /> + )} + {title} - {!isDesc1Clickable && desc1HTML} + {!isDesc1Clickable && desc1HTML} +
+ {isDesc1Clickable && desc1HTML}
- {isDesc1Clickable && desc1HTML} -
- {/* right */} -
- - {info && } - - {isError && } - {isRejected && } - {desc2 && - {desc2} - } - {numResults !== undefined && ( - - {`${numResults}${hasNextPage ? '+' : ''} result${numResults !== 1 ? 's' : ''}`} - - )} -
-
+ {/* right */} +
+ {info && ( + + )} + + {isError && ( + + )} + {isRejected && ( + + )} + {desc2 && ( + + {desc2} + + )} + {numResults !== undefined && ( + + {`${numResults}${hasNextPage ? "+" : ""} result${numResults !== 1 ? "s" : ""}`} + + )} +
+
+
+ {/* children */} + { +
+ {children} +
+ }
- {/* children */} - {
- {children} -
} + {bottomChildren}
- {bottomChildren} -
); + ); }; +const EditTool = ({ + toolMessage, + threadId, + messageIdx, + content, +}: Parameters>[0] & { + content: string; +}) => { + const accessor = useAccessor(); + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const title = getTitle(toolMessage); -const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters>[0] & { content: string }) => { - const accessor = useAccessor() - const isError = false - const isRejected = toolMessage.type === 'rejected' - - const title = getTitle(toolMessage) - - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - const { rawParams, params, name } = toolMessage - const desc1OnClick = () => voidOpenFileFn(params.uri, accessor) - const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, } - + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; + + const { rawParams, params, name } = toolMessage; + const desc1OnClick = () => voidOpenFileFn(params.uri, accessor); + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1OnClick, + desc1Info, + isError, + icon, + isRejected, + }; - const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite' - if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - componentParams.children = - - + const editToolType = toolMessage.name === "edit_file" ? "diff" : "rewrite"; + if ( + toolMessage.type === "running_now" || + toolMessage.type === "tool_request" + ) { + componentParams.children = ( + + + + ); // JumpToFileButton removed in favor of FileLinkText - } - else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { + } else if ( + toolMessage.type === "success" || + toolMessage.type === "rejected" || + toolMessage.type === "tool_error" + ) { // add apply box const applyBoxId = getApplyBoxId({ threadId: threadId, messageIdx: messageIdx, - tokenIdx: 'N/A', - }) - componentParams.desc2 = - - // add children - componentParams.children = - - + ); - if (toolMessage.type === 'success' || toolMessage.type === 'rejected') { - const { result } = toolMessage - componentParams.bottomChildren = - {result?.lintErrors?.map((error, i) => ( -
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
- ))} -
- } - else if (toolMessage.type === 'tool_error') { + // add children + componentParams.children = ( + + + + ); + + if (toolMessage.type === "success" || toolMessage.type === "rejected") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result?.lintErrors?.map((error, i) => ( +
+ Lines {error.startLineNumber}-{error.endLineNumber}:{" "} + {error.message} +
+ ))} +
+ ); + } else if (toolMessage.type === "tool_error") { // error - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } } - return -} + return ; +}; const SimplifiedToolHeader = ({ title, @@ -1202,14 +1569,16 @@ const SimplifiedToolHeader = ({
{/* header */}
{ - if (isDropdown) { setIsOpen(v => !v); } + if (isDropdown) { + setIsOpen((v) => !v); + } }} > {isDropdown && ( )}
@@ -1217,268 +1586,312 @@ const SimplifiedToolHeader = ({
{/* children */} - {
- {children} -
} + { +
+ {children} +
+ }
); }; - - - -const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, currCheckpointIdx, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, currCheckpointIdx: number | undefined, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => { - - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') +const UserMessageComponent = ({ + chatMessage, + messageIdx, + isCheckpointGhost, + currCheckpointIdx, + _scrollToBottom, +}: { + chatMessage: ChatMessage & { role: "user" }; + messageIdx: number; + currCheckpointIdx: number | undefined; + isCheckpointGhost: boolean; + _scrollToBottom: (() => void) | null; +}) => { + const accessor = useAccessor(); + const chatThreadsService = accessor.get("IChatThreadService"); // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - const currentThreadId = chatThreadsState.currentThreadId + const chatThreadsState = useChatThreadsState(); + const currentThreadId = chatThreadsState.currentThreadId; // global state - let isBeingEdited = false - let stagingSelections: StagingSelectionItem[] = [] - let setIsBeingEdited = (_: boolean) => { } - let setStagingSelections = (_: StagingSelectionItem[]) => { } + let isBeingEdited = false; + let stagingSelections: StagingSelectionItem[] = []; + let setIsBeingEdited = (_: boolean) => {}; + let setStagingSelections = (_: StagingSelectionItem[]) => {}; if (messageIdx !== undefined) { - const _state = chatThreadsService.getCurrentMessageState(messageIdx) - isBeingEdited = _state.isBeingEdited - stagingSelections = _state.stagingSelections - setIsBeingEdited = (v) => chatThreadsService.setCurrentMessageState(messageIdx, { isBeingEdited: v }) - setStagingSelections = (s) => chatThreadsService.setCurrentMessageState(messageIdx, { stagingSelections: s }) + const _state = chatThreadsService.getCurrentMessageState(messageIdx); + isBeingEdited = _state.isBeingEdited; + stagingSelections = _state.stagingSelections; + setIsBeingEdited = (v) => + chatThreadsService.setCurrentMessageState(messageIdx, { + isBeingEdited: v, + }); + setStagingSelections = (s) => + chatThreadsService.setCurrentMessageState(messageIdx, { + stagingSelections: s, + }); } - // local state - const mode: ChatBubbleMode = isBeingEdited ? 'edit' : 'display' - const [isFocused, setIsFocused] = useState(false) - const [isHovered, setIsHovered] = useState(false) - const [isDisabled, setIsDisabled] = useState(false) - const [textAreaRefState, setTextAreaRef] = useState(null) - const textAreaFnsRef = useRef(null) + const mode: ChatBubbleMode = isBeingEdited ? "edit" : "display"; + const [isFocused, setIsFocused] = useState(false); + const [isHovered, setIsHovered] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const [textAreaRefState, setTextAreaRef] = + useState(null); + const textAreaFnsRef = useRef(null); // initialize on first render, and when edit was just enabled - const _mustInitialize = useRef(true) - const _justEnabledEdit = useRef(false) + const _mustInitialize = useRef(true); + const _justEnabledEdit = useRef(false); useEffect(() => { - const canInitialize = mode === 'edit' && textAreaRefState - const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current + const canInitialize = mode === "edit" && textAreaRefState; + const shouldInitialize = + _justEnabledEdit.current || _mustInitialize.current; if (canInitialize && shouldInitialize) { setStagingSelections( - (chatMessage.selections || []).map(s => { // quick hack so we dont have to do anything more - if (s.type === 'File') return { ...s, state: { ...s.state, wasAddedAsCurrentFile: false, } } - else return s - }) - ) + (chatMessage.selections || []).map((s) => { + // quick hack so we dont have to do anything more + if (s.type === "File") + return { + ...s, + state: { ...s.state, wasAddedAsCurrentFile: false }, + }; + else return s; + }), + ); if (textAreaFnsRef.current) - textAreaFnsRef.current.setValue(chatMessage.displayContent || '') + textAreaFnsRef.current.setValue(chatMessage.displayContent || ""); textAreaRefState.focus(); - _justEnabledEdit.current = false - _mustInitialize.current = false + _justEnabledEdit.current = false; + _mustInitialize.current = false; } - - }, [chatMessage, mode, textAreaRefState, setStagingSelections]) + }, [chatMessage, mode, textAreaRefState, setStagingSelections]); const onOpenEdit = () => { - setIsBeingEdited(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx) - _justEnabledEdit.current = true - } + setIsBeingEdited(true); + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + _justEnabledEdit.current = true; + }; const onCloseEdit = () => { - setIsFocused(false) - setIsHovered(false) - setIsBeingEdited(false) - chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) - - } - - const EditSymbol = mode === 'display' ? Pencil : X + setIsFocused(false); + setIsHovered(false); + setIsBeingEdited(false); + chatThreadsService.setCurrentlyFocusedMessageIdx(undefined); + }; + const EditSymbol = mode === "display" ? Pencil : X; - let chatbubbleContents: React.ReactNode - if (mode === 'display') { + let chatbubbleContents: React.ReactNode; + if (mode === "display") { const hasImages = chatMessage.images && chatMessage.images.length > 0; const hasPDFs = chatMessage.pdfs && chatMessage.pdfs.length > 0; const hasAttachments = hasImages || hasPDFs; - chatbubbleContents = <> - - {hasImages && ( -
- -
- )} - {hasPDFs && ( -
- -
- )} - {chatMessage.displayContent && ( - {chatMessage.displayContent} - )} - - } - else if (mode === 'edit') { - + chatbubbleContents = ( + <> + + {hasImages && ( +
+ +
+ )} + {hasPDFs && ( +
+ +
+ )} + {chatMessage.displayContent && ( + {chatMessage.displayContent} + )} + + ); + } else if (mode === "edit") { const onSubmit = async () => { - if (isDisabled) return; if (!textAreaRefState) return; if (messageIdx === undefined) return; // cancel any streams on this thread - use subscribed state - const threadId = currentThreadId + const threadId = currentThreadId; // Defensive check: verify the message is still a user message before editing - const thread = chatThreadsState.allThreads[threadId] - if (!thread || !thread.messages || thread.messages[messageIdx]?.role !== 'user') { - console.error('Error while editing message: Message is not a user message or no longer exists') - setIsBeingEdited(false) - chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) - return + const thread = chatThreadsState.allThreads[threadId]; + if ( + !thread || + !thread.messages || + thread.messages[messageIdx]?.role !== "user" + ) { + console.error( + "Error while editing message: Message is not a user message or no longer exists", + ); + setIsBeingEdited(false); + chatThreadsService.setCurrentlyFocusedMessageIdx(undefined); + return; } - await chatThreadsService.abortRunning(threadId) + await chatThreadsService.abortRunning(threadId); // update state - setIsBeingEdited(false) - chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) + setIsBeingEdited(false); + chatThreadsService.setCurrentlyFocusedMessageIdx(undefined); // stream the edit const userMessage = textAreaRefState.value; try { - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, threadId }) + await chatThreadsService.editUserMessageAndStreamResponse({ + userMessage, + messageIdx, + threadId, + }); } catch (e) { - console.error('Error while editing message:', e) + console.error("Error while editing message:", e); } - await chatThreadsService.focusCurrentChat() - requestAnimationFrame(() => _scrollToBottom?.()) - } + await chatThreadsService.focusCurrentChat(); + requestAnimationFrame(() => _scrollToBottom?.()); + }; const onAbort = async () => { // use subscribed state - const threadId = currentThreadId - await chatThreadsService.abortRunning(threadId) - } + const threadId = currentThreadId; + await chatThreadsService.abortRunning(threadId); + }; const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCloseEdit() + if (e.key === "Escape") { + onCloseEdit(); } - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { - onSubmit() + if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { + onSubmit(); } - } + }; - if (!chatMessage.content) { // don't show if empty and not loading (if loading, want to show). - return null + if (!chatMessage.content) { + // don't show if empty and not loading (if loading, want to show). + return null; } - chatbubbleContents = - setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - + chatbubbleContents = ( + + setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true); + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false); + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + + ); } - const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 + const isMsgAfterCheckpoint = + currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1; - return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > + return (
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
- - -
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} > - { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() + if (mode === "display") { + onOpenEdit(); } }} - /> -
- - -
+ > + {chatbubbleContents} +
-} +
+ { + if (mode === "display") { + onOpenEdit(); + } else if (mode === "edit") { + onCloseEdit(); + } + }} + /> +
+
+ ); +}; const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
- {children} -
-} +" + > + {children} +
+ ); +}; const ProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
- {children} -
-} -const AssistantMessageComponent = React.memo(({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { - - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') - - const reasoningStr = chatMessage.reasoning?.trim() || null - const hasReasoning = !!reasoningStr - const isDoneReasoning = !!chatMessage.displayContent - const thread = chatThreadsService.getCurrentThread() - - - const chatMessageLocation: ChatMessageLocation = useMemo(() => ({ - threadId: thread.id, - messageIdx: messageIdx, - }), [thread.id, messageIdx]) - - const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning - if (isEmpty) return null - - return <> - {/* reasoning token */} - {hasReasoning && -
- - - - - -
- } - - {/* assistant message */} - {chatMessage.displayContent && -
- - - -
- } - - -}, (prev, next) => { - // Custom comparison: only re-render if message content, checkpoint state, or committed state changes - return prev.chatMessage.displayContent === next.chatMessage.displayContent && - prev.chatMessage.reasoning === next.chatMessage.reasoning && - prev.isCheckpointGhost === next.isCheckpointGhost && - prev.isCommitted === next.isCommitted && - prev.messageIdx === next.messageIdx -}) - -const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneReasoning: boolean, isStreaming: boolean, children: React.ReactNode }) => { - const isDone = isDoneReasoning || !isStreaming - const isWriting = !isDone - const [isOpen, setIsOpen] = useState(isWriting) - useEffect(() => { - if (!isWriting) setIsOpen(false) // if just finished reasoning, close - }, [isWriting]) - return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - -
- {children} -
-
-
-} +" + > + {children} +
+ ); +}; +const AssistantMessageComponent = React.memo( + ({ + chatMessage, + isCheckpointGhost, + isCommitted, + messageIdx, + }: { + chatMessage: ChatMessage & { role: "assistant" }; + isCheckpointGhost: boolean; + messageIdx: number; + isCommitted: boolean; + }) => { + const accessor = useAccessor(); + const chatThreadsService = accessor.get("IChatThreadService"); + + const reasoningStr = chatMessage.reasoning?.trim() || null; + const hasReasoning = !!reasoningStr; + const isDoneReasoning = !!chatMessage.displayContent; + const thread = chatThreadsService.getCurrentThread(); + + const chatMessageLocation: ChatMessageLocation = useMemo( + () => ({ + threadId: thread.id, + messageIdx: messageIdx, + }), + [thread.id, messageIdx], + ); + const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning; + if (isEmpty) return null; + + return ( + <> + {/* reasoning token */} + {hasReasoning && ( +
+ + + + + +
+ )} + {/* assistant message */} + {chatMessage.displayContent && ( +
+ + + +
+ )} + + ); + }, + (prev, next) => { + // Custom comparison: only re-render if message content, checkpoint state, or committed state changes + return ( + prev.chatMessage.displayContent === next.chatMessage.displayContent && + prev.chatMessage.reasoning === next.chatMessage.reasoning && + prev.isCheckpointGhost === next.isCheckpointGhost && + prev.isCommitted === next.isCommitted && + prev.messageIdx === next.messageIdx + ); + }, +); +const ReasoningWrapper = ({ + isDoneReasoning, + isStreaming, + children, +}: { + isDoneReasoning: boolean; + isStreaming: boolean; + children: React.ReactNode; +}) => { + const isDone = isDoneReasoning || !isStreaming; + const isWriting = !isDone; + const [isOpen, setIsOpen] = useState(isWriting); + useEffect(() => { + if (!isWriting) setIsOpen(false); // if just finished reasoning, close + }, [isWriting]); + return ( + : ""} + isOpen={isOpen} + onClick={() => setIsOpen((v) => !v)} + > + +
{children}
+
+
+ ); +}; // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { - return - {item} - - -} + return ( + + {item} + + + ); +}; const titleOfBuiltinToolName = { - 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, - 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, - 'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') }, - 'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, - 'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, - 'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) }, - 'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) }, - 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, - 'rewrite_file': { done: `Wrote file`, proposed: 'Write file', running: loadingTitleWrapper('Writing file') }, - 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, - 'run_persistent_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, - - 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, - 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, - - 'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') }, - 'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') }, - 'web_search': { done: 'Searched the web', proposed: 'Search the web', running: loadingTitleWrapper('Searching the web') }, - 'browse_url': { done: 'Fetched web page', proposed: 'Fetch web page', running: loadingTitleWrapper('Fetching web page') }, -} as const satisfies Record - - -const getTitle = (toolMessage: Pick): React.ReactNode => { - const t = toolMessage + read_file: { + done: "Read file", + proposed: "Read file", + running: loadingTitleWrapper("Reading file"), + }, + ls_dir: { + done: "Inspected folder", + proposed: "Inspect folder", + running: loadingTitleWrapper("Inspecting folder"), + }, + get_dir_tree: { + done: "Inspected folder tree", + proposed: "Inspect folder tree", + running: loadingTitleWrapper("Inspecting folder tree"), + }, + search_pathnames_only: { + done: "Searched by file name", + proposed: "Search by file name", + running: loadingTitleWrapper("Searching by file name"), + }, + search_for_files: { + done: "Searched", + proposed: "Search", + running: loadingTitleWrapper("Searching"), + }, + create_file_or_folder: { + done: `Created`, + proposed: `Create`, + running: loadingTitleWrapper(`Creating`), + }, + delete_file_or_folder: { + done: `Deleted`, + proposed: `Delete`, + running: loadingTitleWrapper(`Deleting`), + }, + edit_file: { + done: `Edited file`, + proposed: "Edit file", + running: loadingTitleWrapper("Editing file"), + }, + rewrite_file: { + done: `Wrote file`, + proposed: "Write file", + running: loadingTitleWrapper("Writing file"), + }, + run_command: { + done: `Ran terminal`, + proposed: "Run terminal", + running: loadingTitleWrapper("Running terminal"), + }, + run_persistent_command: { + done: `Ran terminal`, + proposed: "Run terminal", + running: loadingTitleWrapper("Running terminal"), + }, + + open_persistent_terminal: { + done: `Opened terminal`, + proposed: "Open terminal", + running: loadingTitleWrapper("Opening terminal"), + }, + kill_persistent_terminal: { + done: `Killed terminal`, + proposed: "Kill terminal", + running: loadingTitleWrapper("Killing terminal"), + }, + + read_lint_errors: { + done: `Read lint errors`, + proposed: "Read lint errors", + running: loadingTitleWrapper("Reading lint errors"), + }, + search_in_file: { + done: "Searched in file", + proposed: "Search in file", + running: loadingTitleWrapper("Searching in file"), + }, + web_search: { + done: "Searched the web", + proposed: "Search the web", + running: loadingTitleWrapper("Searching the web"), + }, + browse_url: { + done: "Fetched web page", + proposed: "Fetch web page", + running: loadingTitleWrapper("Fetching web page"), + }, +} as const satisfies Record< + BuiltinToolName, + { done: any; proposed: any; running: any } +>; + +const getTitle = ( + toolMessage: Pick< + ChatMessage & { role: "tool" }, + "name" | "type" | "mcpServerName" + >, +): React.ReactNode => { + const t = toolMessage; // non-built-in title if (!builtinToolNames.includes(t.name as BuiltinToolName)) { // descriptor of Running or Ran etc const descriptor = - t.type === 'success' ? 'Called' - : t.type === 'running_now' ? 'Calling' - : t.type === 'tool_request' ? 'Call' - : t.type === 'rejected' ? 'Call' - : t.type === 'invalid_params' ? 'Call' - : t.type === 'tool_error' ? 'Call' - : 'Call' - - - const title = `${descriptor} ${toolMessage.mcpServerName || 'MCP'}` - if (t.type === 'running_now' || t.type === 'tool_request') - return loadingTitleWrapper(title) - return title + t.type === "success" + ? "Called" + : t.type === "running_now" + ? "Calling" + : t.type === "tool_request" + ? "Call" + : t.type === "rejected" + ? "Call" + : t.type === "invalid_params" + ? "Call" + : t.type === "tool_error" + ? "Call" + : "Call"; + + const title = `${descriptor} ${toolMessage.mcpServerName || "MCP"}`; + if (t.type === "running_now" || t.type === "tool_request") + return loadingTitleWrapper(title); + return title; } // built-in title else { - const toolName = t.name as BuiltinToolName - if (t.type === 'success') return titleOfBuiltinToolName[toolName].done - if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running - return titleOfBuiltinToolName[toolName].proposed + const toolName = t.name as BuiltinToolName; + if (t.type === "success") return titleOfBuiltinToolName[toolName].done; + if (t.type === "running_now") + return titleOfBuiltinToolName[toolName].running; + return titleOfBuiltinToolName[toolName].proposed; } -} - +}; -const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType): { - desc1: React.ReactNode, - desc1Info?: string, +const toolNameToDesc = ( + toolName: BuiltinToolName, + _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, + accessor: ReturnType, +): { + desc1: React.ReactNode; + desc1Info?: string; } => { - if (!_toolParams) { - return { desc1: '', }; + return { desc1: "" }; } const x = { - 'read_file': () => { - const toolParams = _toolParams as BuiltinToolCallParams['read_file'] + read_file: () => { + const toolParams = _toolParams as BuiltinToolCallParams["read_file"]; return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), }; }, - 'ls_dir': () => { - const toolParams = _toolParams as BuiltinToolCallParams['ls_dir'] + ls_dir: () => { + const toolParams = _toolParams as BuiltinToolCallParams["ls_dir"]; return { desc1: getFolderName(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), }; }, - 'search_pathnames_only': () => { - const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only'] + search_pathnames_only: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["search_pathnames_only"]; return { desc1: `"${toolParams.query}"`, - } + }; }, - 'search_for_files': () => { - const toolParams = _toolParams as BuiltinToolCallParams['search_for_files'] + search_for_files: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["search_for_files"]; return { desc1: `"${toolParams.query}"`, - } + }; }, - 'search_in_file': () => { - const toolParams = _toolParams as BuiltinToolCallParams['search_in_file']; + search_in_file: () => { + const toolParams = _toolParams as BuiltinToolCallParams["search_in_file"]; return { desc1: `"${toolParams.query}"`, desc1Info: getRelative(toolParams.uri, accessor), }; }, - 'create_file_or_folder': () => { - const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder'] + create_file_or_folder: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["create_file_or_folder"]; return { - desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), + desc1: toolParams.isFolder + ? (getFolderName(toolParams.uri.fsPath) ?? "/") + : getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'delete_file_or_folder': () => { - const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder'] + delete_file_or_folder: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["delete_file_or_folder"]; return { - desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath), + desc1: toolParams.isFolder + ? (getFolderName(toolParams.uri.fsPath) ?? "/") + : getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'rewrite_file': () => { - const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file'] + rewrite_file: () => { + const toolParams = _toolParams as BuiltinToolCallParams["rewrite_file"]; return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'edit_file': () => { - const toolParams = _toolParams as BuiltinToolCallParams['edit_file'] + edit_file: () => { + const toolParams = _toolParams as BuiltinToolCallParams["edit_file"]; return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'run_command': () => { - const toolParams = _toolParams as BuiltinToolCallParams['run_command'] + run_command: () => { + const toolParams = _toolParams as BuiltinToolCallParams["run_command"]; return { desc1: `"${toolParams.command}"`, - } + }; }, - 'run_persistent_command': () => { - const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command'] + run_persistent_command: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["run_persistent_command"]; return { desc1: `"${toolParams.command}"`, - } + }; }, - 'open_persistent_terminal': () => { - const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal'] - return { desc1: '' } + open_persistent_terminal: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["open_persistent_terminal"]; + return { desc1: "" }; }, - 'kill_persistent_terminal': () => { - const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal'] - return { desc1: toolParams.persistentTerminalId } + kill_persistent_terminal: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["kill_persistent_terminal"]; + return { desc1: toolParams.persistentTerminalId }; }, - 'get_dir_tree': () => { - const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree'] + get_dir_tree: () => { + const toolParams = _toolParams as BuiltinToolCallParams["get_dir_tree"]; return { - desc1: getFolderName(toolParams.uri.fsPath) ?? '/', + desc1: getFolderName(toolParams.uri.fsPath) ?? "/", desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'read_lint_errors': () => { - const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors'] + read_lint_errors: () => { + const toolParams = + _toolParams as BuiltinToolCallParams["read_lint_errors"]; return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), - } + }; }, - 'web_search': () => { - const toolParams = _toolParams as BuiltinToolCallParams['web_search'] + web_search: () => { + const toolParams = _toolParams as BuiltinToolCallParams["web_search"]; return { desc1: `"${toolParams.query}"`, - } + }; }, - 'browse_url': () => { - const toolParams = _toolParams as BuiltinToolCallParams['browse_url'] + browse_url: () => { + const toolParams = _toolParams as BuiltinToolCallParams["browse_url"]; return { desc1: toolParams.url, desc1Info: new URL(toolParams.url).hostname, - } - } - } + }; + }, + }; try { - return x[toolName]?.() || { desc1: '' } + return x[toolName]?.() || { desc1: "" }; + } catch { + return { desc1: "" }; } - catch { - return { desc1: '' } - } -} +}; -const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => { - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') - const metricsService = accessor.get('IMetricsService') - const cortexideSettingsService = accessor.get('ICortexideSettingsService') - const voidSettingsState = useSettingsState() +const ToolRequestAcceptRejectButtons = ({ + toolName, +}: { + toolName: ToolName; +}) => { + const accessor = useAccessor(); + const chatThreadsService = accessor.get("IChatThreadService"); + const metricsService = accessor.get("IMetricsService"); + const cortexideSettingsService = accessor.get("ICortexideSettingsService"); + const voidSettingsState = useSettingsState(); // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - const currentThreadId = chatThreadsState.currentThreadId + const chatThreadsState = useChatThreadsState(); + const currentThreadId = chatThreadsState.currentThreadId; const onAccept = useCallback(() => { - try { // this doesn't need to be wrapped in try/catch anymore + try { + // this doesn't need to be wrapped in try/catch anymore // use subscribed state - chatThreadsService.approveLatestToolRequest(currentThreadId) - metricsService.capture('Tool Request Accepted', {}) - } catch (e) { console.error('Error while approving message in chat:', e) } - }, [chatThreadsService, metricsService, currentThreadId]) + chatThreadsService.approveLatestToolRequest(currentThreadId); + metricsService.capture("Tool Request Accepted", {}); + } catch (e) { + console.error("Error while approving message in chat:", e); + } + }, [chatThreadsService, metricsService, currentThreadId]); const onReject = useCallback(() => { try { // use subscribed state - chatThreadsService.rejectLatestToolRequest(currentThreadId) - } catch (e) { console.error('Error while approving message in chat:', e) } - metricsService.capture('Tool Request Rejected', {}) - }, [chatThreadsService, metricsService, currentThreadId]) + chatThreadsService.rejectLatestToolRequest(currentThreadId); + } catch (e) { + console.error("Error while approving message in chat:", e); + } + metricsService.capture("Tool Request Rejected", {}); + }, [chatThreadsService, metricsService, currentThreadId]); const approveButton = ( - ) + ); const cancelButton = ( - ) - - const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools' - const approvalToggle = approvalType ?
- -
: null - - return
- {approveButton} - {cancelButton} - {approvalToggle} -
-} + ); -export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { - return
-
- {children} + const approvalType = isABuiltinToolName(toolName) + ? approvalTypeOfBuiltinToolName[toolName] + : "MCP tools"; + const approvalToggle = approvalType ? ( +
+
-
-} -export const CodeChildren = ({ children, className }: { children: React.ReactNode, className?: string }) => { - return
-
- {children} + ) : null; + + return ( +
+ {approveButton} + {cancelButton} + {approvalToggle}
-
-} + ); +}; + +export const ToolChildrenWrapper = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+
{children}
+
+ ); +}; +export const CodeChildren = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+
{children}
+
+ ); +}; -export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { - return
void; + isSmall?: boolean; + className?: string; + showDot?: boolean; +}) => { + return ( +
- {showDot === false ? null :
} -
{name}
-
-} - - - -const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => { - - const content = type === 'diff' ? - - : - - return
- - {content} - -
+ onClick={onClick} + > + {showDot === false ? null : ( +
+ + + +
+ )} +
+ {name} +
+
+ ); +}; -} +const EditToolChildren = ({ + uri, + code, + type, +}: { + uri: URI | undefined; + code: string; + type: "diff" | "rewrite"; +}) => { + const content = + type === "diff" ? ( + + ) : ( + + ); + return ( +
+ {content} +
+ ); +}; const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { - return
- {lintErrors.map((error, i) => ( -
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
- ))} -
-} + return ( +
+ {lintErrors.map((error, i) => ( +
+ Lines {error.startLineNumber}-{error.endLineNumber}: {error.message} +
+ ))} +
+ ); +}; -const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => { +const BottomChildren = ({ + children, + title, +}: { + children: React.ReactNode; + title: string; +}) => { const [isOpen, setIsOpen] = useState(false); if (!children) return null; return (
setIsOpen(o => !o)} - style={{ background: 'none' }} + onClick={() => setIsOpen((o) => !o)} + style={{ background: "none" }} > - {title} + + {title} +
{children} @@ -1989,1410 +2613,1948 @@ const BottomChildren = ({ children, title }: { children: React.ReactNode, title:
); -} - - -const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }: { threadId: string, applyBoxId: string, uri: URI, codeStr: string, toolName: 'edit_file' | 'rewrite_file' }) => { - const { streamState } = useEditToolStreamState({ applyBoxId, uri }) - return
- {/* */} - {/* */} - {streamState === 'idle-no-changes' && } - -
-} - - - -const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => { - const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName }) - const desc1 = 'Invalid parameters' - const icon = null - const isError = true - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - - componentParams.children = - - {message} - - - return -} - -const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => { - const accessor = useAccessor() - const title = getTitle({ name: toolName, type: 'rejected', mcpServerName }) - const desc1 = '' - const icon = null - const isRejected = true - const componentParams: ToolHeaderParams = { title, desc1, icon, isRejected } - return -} - - -const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ - toolMessage: Exclude, { type: 'invalid_params' }> - type: 'run_command' -} | { - toolMessage: Exclude, { type: 'invalid_params' }> - type: | 'run_persistent_command' -})) => { - const accessor = useAccessor() +}; - const commandService = accessor.get('ICommandService') - const terminalToolsService = accessor.get('ITerminalToolService') - const toolsService = accessor.get('IToolsService') - const isError = false - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - const streamState = useChatThreadsStreamState(threadId) +const EditToolHeaderButtons = ({ + applyBoxId, + uri, + codeStr, + toolName, + threadId, +}: { + threadId: string; + applyBoxId: string; + uri: URI; + codeStr: string; + toolName: "edit_file" | "rewrite_file"; +}) => { + const { streamState } = useEditToolStreamState({ applyBoxId, uri }); + return ( +
+ {/* */} + {/* */} + {streamState === "idle-no-changes" && ( + + )} + +
+ ); +}; - const divRef = useRef(null) +const InvalidTool = ({ + toolName, + message, + mcpServerName, +}: { + toolName: ToolName; + message: string; + mcpServerName: string | undefined; +}) => { + const accessor = useAccessor(); + const title = getTitle({ + name: toolName, + type: "invalid_params", + mcpServerName, + }); + const desc1 = "Invalid parameters"; + const icon = null; + const isError = true; + const componentParams: ToolHeaderParams = { title, desc1, isError, icon }; + + componentParams.children = ( + + {message} + + ); + return ; +}; - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } +const CanceledTool = ({ + toolName, + mcpServerName, +}: { + toolName: ToolName; + mcpServerName: string | undefined; +}) => { + const accessor = useAccessor(); + const title = getTitle({ name: toolName, type: "rejected", mcpServerName }); + const desc1 = ""; + const icon = null; + const isRejected = true; + const componentParams: ToolHeaderParams = { title, desc1, icon, isRejected }; + return ; +}; +const CommandTool = ({ + toolMessage, + type, + threadId, +}: { threadId: string } & ( + | { + toolMessage: Exclude< + ToolMessage<"run_command">, + { type: "invalid_params" } + >; + type: "run_command"; + } + | { + toolMessage: Exclude< + ToolMessage<"run_persistent_command">, + { type: "invalid_params" } + >; + type: "run_persistent_command"; + } +)) => { + const accessor = useAccessor(); + + const commandService = accessor.get("ICommandService"); + const terminalToolsService = accessor.get("ITerminalToolService"); + const toolsService = accessor.get("IToolsService"); + const isError = false; + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; + const streamState = useChatThreadsStreamState(threadId); + + const divRef = useRef(null); + + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; const effect = async () => { - if (streamState?.isRunning !== 'tool') return - if (type !== 'run_command' || toolMessage.type !== 'running_now') return; + if (streamState?.isRunning !== "tool") return; + if (type !== "run_command" || toolMessage.type !== "running_now") return; // wait for the interruptor so we know it's running - await streamState?.interrupt + await streamState?.interrupt; const container = divRef.current; if (!container) return; - const terminal = terminalToolsService.getTemporaryTerminal(toolMessage.params.terminalId); + const terminal = terminalToolsService.getTemporaryTerminal( + toolMessage.params.terminalId, + ); if (!terminal) return; try { terminal.attachToElement(container); - terminal.setVisible(true) - } catch { - } + terminal.setVisible(true); + } catch {} // Listen for size changes of the container and keep the terminal layout in sync. const resizeObserver = new ResizeObserver((entries) => { const height = entries[0].borderBoxSize[0].blockSize; const width = entries[0].borderBoxSize[0].inlineSize; - if (typeof terminal.layout === 'function') { + if (typeof terminal.layout === "function") { terminal.layout({ width, height }); } }); resizeObserver.observe(container); - return () => { terminal.detachFromElement(); resizeObserver?.disconnect(); } - } + return () => { + terminal.detachFromElement(); + resizeObserver?.disconnect(); + }; + }; useEffect(() => { - effect() + effect(); }, [terminalToolsService, toolMessage, toolMessage.type, type]); - if (toolMessage.type === 'success') { - const { result } = toolMessage + if (toolMessage.type === "success") { + const { result } = toolMessage; // it's unclear that this is a button and not an icon. // componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} // /> - let msg: string - if (type === 'run_command') msg = toolsService.stringOfResult['run_command'](toolMessage.params, result) - else msg = toolsService.stringOfResult['run_persistent_command'](toolMessage.params, result) - - if (type === 'run_persistent_command') { - componentParams.info = persistentTerminalNameOfId(toolMessage.params.persistentTerminalId) + let msg: string; + if (type === "run_command") + msg = toolsService.stringOfResult["run_command"]( + toolMessage.params, + result, + ); + else + msg = toolsService.stringOfResult["run_persistent_command"]( + toolMessage.params, + result, + ); + + if (type === "run_persistent_command") { + componentParams.info = persistentTerminalNameOfId( + toolMessage.params.persistentTerminalId, + ); } - componentParams.children = -
- -
-
- } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - else if (toolMessage.type === 'running_now') { - if (type === 'run_command') - componentParams.children =
- } - else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_request') { + componentParams.children = ( + +
+ +
+
+ ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); + } else if (toolMessage.type === "running_now") { + if (type === "run_command") + componentParams.children = ( +
+ ); + } else if ( + toolMessage.type === "rejected" || + toolMessage.type === "tool_request" + ) { } - return <> - - -} + return ( + <> + + + ); +}; -type WrapperProps = { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string } +type WrapperProps = { + toolMessage: Exclude, { type: "invalid_params" }>; + messageIdx: number; + threadId: string; +}; const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { - const accessor = useAccessor() - const mcpService = accessor.get('IMCPService') + const accessor = useAccessor(); + const mcpService = accessor.get("IMCPService"); - const title = getTitle(toolMessage) - const desc1 = removeMCPToolNamePrefix(toolMessage.name) - const icon = null + const title = getTitle(toolMessage); + const desc1 = removeMCPToolNamePrefix(toolMessage.name); + const icon = null; + if (toolMessage.type === "running_now") return null; // do not show running - if (toolMessage.type === 'running_now') return null // do not show running - - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; // Redact sensitive values in params before display/copy const redactParams = (value: any): any => { - const SENSITIVE_KEYS = new Set(['token', 'apiKey', 'apikey', 'password', 'authorization', 'auth', 'secret', 'clientSecret', 'accessToken', 'bearer']) - const redactValue = (v: any) => (typeof v === 'string' ? (v.length > 6 ? v.slice(0, 3) + '***' + v.slice(-2) : '***') : v) - if (Array.isArray(value)) return value.map(redactParams) - if (value && typeof value === 'object') { - const out: any = Array.isArray(value) ? [] : {} + const SENSITIVE_KEYS = new Set([ + "token", + "apiKey", + "apikey", + "password", + "authorization", + "auth", + "secret", + "clientSecret", + "accessToken", + "bearer", + ]); + const redactValue = (v: any) => + typeof v === "string" + ? v.length > 6 + ? v.slice(0, 3) + "***" + v.slice(-2) + : "***" + : v; + if (Array.isArray(value)) return value.map(redactParams); + if (value && typeof value === "object") { + const out: any = Array.isArray(value) ? [] : {}; for (const k of Object.keys(value)) { - if (SENSITIVE_KEYS.has(k.toLowerCase())) out[k] = redactValue(value[k]) - else out[k] = redactParams(value[k]) + if (SENSITIVE_KEYS.has(k.toLowerCase())) out[k] = redactValue(value[k]); + else out[k] = redactParams(value[k]); } - return out + return out; } - return value - } - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } + return value; + }; + const componentParams: ToolHeaderParams = { + title, + desc1, + isError, + icon, + isRejected, + }; - const redactedParams = redactParams(params) - const paramsStr = JSON.stringify(redactedParams, null, 2) - componentParams.desc2 = + const redactedParams = redactParams(params); + const paramsStr = JSON.stringify(redactedParams, null, 2); + componentParams.desc2 = ( + + ); - componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined + componentParams.info = !toolMessage.mcpServerName + ? "MCP tool not found" + : undefined; // Add copy inputs button in desc2 - - if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') { - const { result } = toolMessage - const resultStr = result ? mcpService.stringifyResult(result) : 'null' - componentParams.children = - - - - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + if (toolMessage.type === "success" || toolMessage.type === "tool_request") { + const { result } = toolMessage; + const resultStr = result ? mcpService.stringifyResult(result) : "null"; + componentParams.children = ( + + + + + + ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return - -} + return ; +}; -type ResultWrapper = (props: WrapperProps) => React.ReactNode +type ResultWrapper = ( + props: WrapperProps, +) => React.ReactNode; -const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper, } } = { - 'read_file': { +const builtinToolNameToComponent: { + [T in BuiltinToolName]: { resultWrapper: ResultWrapper }; +} = { + read_file: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); - const title = getTitle(toolMessage) + const title = getTitle(toolMessage); - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); - const icon = null + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - let range: [number, number] | undefined = undefined - if (toolMessage.params.startLine !== null || toolMessage.params.endLine !== null) { - const start = toolMessage.params.startLine === null ? `1` : `${toolMessage.params.startLine}` - const end = toolMessage.params.endLine === null ? `` : `${toolMessage.params.endLine}` - const addStr = `(${start}-${end})` - componentParams.desc1 += ` ${addStr}` - range = [params.startLine || 1, params.endLine || 1] + let range: [number, number] | undefined = undefined; + if ( + toolMessage.params.startLine !== null || + toolMessage.params.endLine !== null + ) { + const start = + toolMessage.params.startLine === null + ? `1` + : `${toolMessage.params.startLine}`; + const end = + toolMessage.params.endLine === null + ? `` + : `${toolMessage.params.endLine}`; + const addStr = `(${start}-${end})`; + componentParams.desc1 += ` ${addStr}`; + range = [params.startLine || 1, params.endLine || 1]; } - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor, range) } - if (result.hasNextPage && params.pageNumber === 1) // first page - componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)` - else if (params.pageNumber > 1) // subsequent pages - componentParams.desc2 = `(part ${params.pageNumber})` - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor, range); + }; + if (result.hasNextPage && params.pageNumber === 1) + // first page + componentParams.desc2 = `(truncated after ${Math.round(MAX_FILE_CHARS_PAGE) / 1000}k)`; + else if (params.pageNumber > 1) + // subsequent pages + componentParams.desc2 = `(part ${params.pageNumber})`; + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; // JumpToFileButton removed in favor of FileLinkText - componentParams.bottomChildren = - - {result} - - + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, - 'get_dir_tree': { + get_dir_tree: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; if (params.uri) { - const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only search in ${rel}` + const rel = getRelative(params.uri, accessor); + if (rel) componentParams.info = `Only search in ${rel}`; } - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.children = - - - - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.children = ( + + + + + + ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return - - } + return ; + }, }, - 'ls_dir': { + ls_dir: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const explorerService = accessor.get('IExplorerService') - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const explorerService = accessor.get("IExplorerService"); + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; if (params.uri) { - const rel = getRelative(params.uri, accessor) - if (rel) componentParams.info = `Only search in ${rel}` + const rel = getRelative(params.uri, accessor); + if (rel) componentParams.info = `Only search in ${rel}`; } - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.numResults = result.children?.length - componentParams.hasNextPage = result.hasNextPage - componentParams.children = !result.children || (result.children.length ?? 0) === 0 ? undefined - : - {result.children.map((child, i) => ( { - voidOpenFileFn(child.uri, accessor) - // commandService.executeCommand('workbench.view.explorer'); // open in explorer folders view instead - // explorerService.select(child.uri, true); - }} - />))} - {result.hasNextPage && - - } - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.numResults = result.children?.length; + componentParams.hasNextPage = result.hasNextPage; + componentParams.children = + !result.children || + (result.children.length ?? 0) === 0 ? undefined : ( + + {result.children.map((child, i) => ( + { + voidOpenFileFn(child.uri, accessor); + // commandService.executeCommand('workbench.view.explorer'); // open in explorer folders view instead + // explorerService.select(child.uri, true); + }} + /> + ))} + {result.hasNextPage && ( + + )} + + ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return - } + return ; + }, }, - 'search_pathnames_only': { + search_pathnames_only: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const isError = false - const isRejected = toolMessage.type === 'rejected' - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; if (params.includePattern) { - componentParams.info = `Only search in ${params.includePattern}` + componentParams.info = `Only search in ${params.includePattern}`; } - if (toolMessage.type === 'success') { - const { result, rawParams } = toolMessage - componentParams.numResults = result.uris.length - componentParams.hasNextPage = result.hasNextPage - componentParams.children = result.uris.length === 0 ? undefined - : - {result.uris.map((uri, i) => ( { voidOpenFileFn(uri, accessor) }} - />))} - {result.hasNextPage && - - } - - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + if (toolMessage.type === "success") { + const { result, rawParams } = toolMessage; + componentParams.numResults = result.uris.length; + componentParams.hasNextPage = result.hasNextPage; + componentParams.children = + result.uris.length === 0 ? undefined : ( + + {result.uris.map((uri, i) => ( + { + voidOpenFileFn(uri, accessor); + }} + /> + ))} + {result.hasNextPage && ( + + )} + + ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return - } + return ; + }, }, - 'search_for_files': { + search_for_files: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const isError = false - const isRejected = toolMessage.type === 'rejected' - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; if (params.searchInFolder || params.isRegex) { - let info: string[] = [] + let info: string[] = []; if (params.searchInFolder) { - const rel = getRelative(params.searchInFolder, accessor) - if (rel) info.push(`Only search in ${rel}`) + const rel = getRelative(params.searchInFolder, accessor); + if (rel) info.push(`Only search in ${rel}`); + } + if (params.isRegex) { + info.push(`Uses regex search`); } - if (params.isRegex) { info.push(`Uses regex search`) } - componentParams.info = info.join('; ') + componentParams.info = info.join("; "); } - if (toolMessage.type === 'success') { - const { result, rawParams } = toolMessage - componentParams.numResults = result.uris.length - componentParams.hasNextPage = result.hasNextPage - componentParams.children = result.uris.length === 0 ? undefined - : - {result.uris.map((uri, i) => ( { voidOpenFileFn(uri, accessor) }} - />))} - {result.hasNextPage && - - } - - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - return - } - }, + if (toolMessage.type === "success") { + const { result, rawParams } = toolMessage; + componentParams.numResults = result.uris.length; + componentParams.hasNextPage = result.hasNextPage; + componentParams.children = + result.uris.length === 0 ? undefined : ( + + {result.uris.map((uri, i) => ( + { + voidOpenFileFn(uri, accessor); + }} + /> + ))} + {result.hasNextPage && ( + + )} + + ); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); + } + return ; + }, + }, - 'search_in_file': { + search_in_file: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor(); - const toolsService = accessor.get('IToolsService'); + const toolsService = accessor.get("IToolsService"); const title = getTitle(toolMessage); - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running const { rawParams, params } = toolMessage; - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected }; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - const infoarr: string[] = [] - const uriStr = getRelative(params.uri, accessor) - if (uriStr) infoarr.push(uriStr) - if (params.isRegex) infoarr.push('Uses regex search') - componentParams.info = infoarr.join('; ') + const infoarr: string[] = []; + const uriStr = getRelative(params.uri, accessor); + if (uriStr) infoarr.push(uriStr); + if (params.isRegex) infoarr.push("Uses regex search"); + componentParams.info = infoarr.join("; "); - if (toolMessage.type === 'success') { + if (toolMessage.type === "success") { const { result } = toolMessage; // result is array of snippets componentParams.numResults = result.lines.length; - componentParams.children = result.lines.length === 0 ? undefined : - - -
-								{toolsService.stringOfResult['search_in_file'](params, result)}
-							
-
-
- } - else if (toolMessage.type === 'tool_error') { + componentParams.children = + result.lines.length === 0 ? undefined : ( + + +
+									{toolsService.stringOfResult["search_in_file"](
+										params,
+										result,
+									)}
+								
+
+
+ ); + } else if (toolMessage.type === "tool_error") { const { result } = toolMessage; - componentParams.bottomChildren = - - {result} - - + componentParams.bottomChildren = ( + + {result} + + ); } return ; - } + }, }, - 'read_lint_errors': { + read_lint_errors: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); - const title = getTitle(toolMessage) + const title = getTitle(toolMessage); - const { uri } = toolMessage.params ?? {} - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null + const { uri } = toolMessage.params ?? {}; + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - componentParams.info = getRelative(uri, accessor) // full path + componentParams.info = getRelative(uri, accessor); // full path - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; if (result.lintErrors) - componentParams.children = - else - componentParams.children = `No lint errors found.` - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage + componentParams.children = ( + + ); + else componentParams.children = `No lint errors found.`; + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; // JumpToFileButton removed in favor of FileLinkText - componentParams.bottomChildren = - - {result} - - + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, // --- - 'create_file_or_folder': { + create_file_or_folder: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const isError = false - const isRejected = toolMessage.type === 'rejected' - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - componentParams.info = getRelative(params.uri, accessor) // full path + componentParams.info = getRelative(params.uri, accessor); // full path - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } - } - else if (toolMessage.type === 'rejected') { - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - if (params) { componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } } - componentParams.bottomChildren = - - {result} - - - } - else if (toolMessage.type === 'running_now') { + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } else if (toolMessage.type === "rejected") { + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + if (params) { + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } + componentParams.bottomChildren = ( + + {result} + + ); + } else if (toolMessage.type === "running_now") { // nothing more is needed - } - else if (toolMessage.type === 'tool_request') { + } else if (toolMessage.type === "tool_request") { // nothing more is needed } - return - } + return ; + }, }, - 'delete_file_or_folder': { + delete_file_or_folder: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const isFolder = toolMessage.params?.isFolder ?? false - const isError = false - const isRejected = toolMessage.type === 'rejected' - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } - - componentParams.info = getRelative(params.uri, accessor) // full path - - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } - } - else if (toolMessage.type === 'rejected') { - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - if (params) { componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } } - componentParams.bottomChildren = - - {result} - - - } - else if (toolMessage.type === 'running_now') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } - } - else if (toolMessage.type === 'tool_request') { - const { result } = toolMessage - componentParams.onClick = () => { voidOpenFileFn(params.uri, accessor) } + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const isFolder = toolMessage.params?.isFolder ?? false; + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; + + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; + + componentParams.info = getRelative(params.uri, accessor); // full path + + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } else if (toolMessage.type === "rejected") { + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + if (params) { + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } + componentParams.bottomChildren = ( + + {result} + + ); + } else if (toolMessage.type === "running_now") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; + } else if (toolMessage.type === "tool_request") { + const { result } = toolMessage; + componentParams.onClick = () => { + voidOpenFileFn(params.uri, accessor); + }; } - return - } + return ; + }, }, - 'rewrite_file': { + rewrite_file: { resultWrapper: (params) => { - return - } + return ( + + ); + }, }, - 'edit_file': { + edit_file: { resultWrapper: (params) => { - return - } + return ( + + ); + }, }, // --- - 'run_command': { + run_command: { resultWrapper: (params) => { - return - } + return ; + }, }, - 'run_persistent_command': { + run_persistent_command: { resultWrapper: (params) => { - return - } + return ; + }, }, - 'open_persistent_terminal': { + open_persistent_terminal: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const terminalToolsService = accessor.get('ITerminalToolService') + const accessor = useAccessor(); + const terminalToolsService = accessor.get("ITerminalToolService"); - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const title = getTitle(toolMessage) - const icon = null + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const title = getTitle(toolMessage); + const icon = null; - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - const relativePath = params.cwd ? getRelative(URI.file(params.cwd), accessor) : '' - componentParams.info = relativePath ? `Running in ${relativePath}` : undefined + const relativePath = params.cwd + ? getRelative(URI.file(params.cwd), accessor) + : ""; + componentParams.info = relativePath + ? `Running in ${relativePath}` + : undefined; - if (toolMessage.type === 'success') { - const { result } = toolMessage - const { persistentTerminalId } = result - componentParams.desc1 = persistentTerminalNameOfId(persistentTerminalId) - componentParams.onClick = () => terminalToolsService.focusPersistentTerminal(persistentTerminalId) - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + if (toolMessage.type === "success") { + const { result } = toolMessage; + const { persistentTerminalId } = result; + componentParams.desc1 = + persistentTerminalNameOfId(persistentTerminalId); + componentParams.onClick = () => + terminalToolsService.focusPersistentTerminal(persistentTerminalId); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, - 'kill_persistent_terminal': { + kill_persistent_terminal: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const terminalToolsService = accessor.get('ITerminalToolService') - - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const title = getTitle(toolMessage) - const icon = null - - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') return null // do not show running - - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } - - if (toolMessage.type === 'success') { - const { persistentTerminalId } = params - componentParams.desc1 = persistentTerminalNameOfId(persistentTerminalId) - componentParams.onClick = () => terminalToolsService.focusPersistentTerminal(persistentTerminalId) - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const terminalToolsService = accessor.get("ITerminalToolService"); + + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const title = getTitle(toolMessage); + const icon = null; + + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") return null; // do not show running + + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; + + if (toolMessage.type === "success") { + const { persistentTerminalId } = params; + componentParams.desc1 = + persistentTerminalNameOfId(persistentTerminalId); + componentParams.onClick = () => + terminalToolsService.focusPersistentTerminal(persistentTerminalId); + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, - 'web_search': { + web_search: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const toolsService = accessor.get('IToolsService') - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') { + const accessor = useAccessor(); + const toolsService = accessor.get("IToolsService"); + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; + + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") { // Show loading indicator - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError: false, icon, isRejected: false } - componentParams.children = -
- - Searching the web... -
-
- return + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError: false, + icon, + isRejected: false, + }; + componentParams.children = ( + +
+ + Searching the web... +
+
+ ); + return ; } - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.numResults = result.results?.length || 0 + if (toolMessage.type === "success") { + const { result } = toolMessage; + componentParams.numResults = result.results?.length || 0; if (result.results && result.results.length > 0) { - componentParams.children = -
- {result.results.map((r: { title: string, snippet: string, url: string }, i: number) => ( - + + ); } else { - componentParams.children = -
- No search results found. -
-
+ componentParams.children = ( + +
+ No search results found. +
+
+ ); } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, - 'browse_url': { + browse_url: { resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const toolsService = accessor.get('IToolsService') - const title = getTitle(toolMessage) - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - if (toolMessage.type === 'tool_request') return null // do not show past requests - if (toolMessage.type === 'running_now') { + const accessor = useAccessor(); + const toolsService = accessor.get("IToolsService"); + const title = getTitle(toolMessage); + const { desc1, desc1Info } = toolNameToDesc( + toolMessage.name, + toolMessage.params, + accessor, + ); + const icon = null; + + if (toolMessage.type === "tool_request") return null; // do not show past requests + if (toolMessage.type === "running_now") { // Show loading indicator - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError: false, icon, isRejected: false } - componentParams.children = -
- - Fetching content from URL... -
-
- return + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError: false, + icon, + isRejected: false, + }; + componentParams.children = ( + +
+ + Fetching content from URL... +
+
+ ); + return ; } - const isError = false - const isRejected = toolMessage.type === 'rejected' - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + const isError = false; + const isRejected = toolMessage.type === "rejected"; + const { rawParams, params } = toolMessage; + const componentParams: ToolHeaderParams = { + title, + desc1, + desc1Info, + isError, + icon, + isRejected, + }; - if (toolMessage.type === 'success') { - const { result } = toolMessage - const urlStr = result.url || params.url + if (toolMessage.type === "success") { + const { result } = toolMessage; + const urlStr = result.url || params.url; componentParams.onClick = () => { if (urlStr) { - window.open(urlStr, '_blank', 'noopener,noreferrer') + window.open(urlStr, "_blank", "noopener,noreferrer"); } - } - componentParams.info = urlStr ? `Source: ${new URL(urlStr).hostname}` : undefined + }; + componentParams.info = urlStr + ? `Source: ${new URL(urlStr).hostname}` + : undefined; if (result.content) { - const contentPreview = result.content.length > 2000 - ? result.content.substring(0, 2000) + '\n\n... (content truncated)' - : result.content - - componentParams.children = -
- {result.title && ( -
- {result.title} -
- )} - {result.metadata?.publishedDate && ( -
- Published: {result.metadata.publishedDate} + const contentPreview = + result.content.length > 2000 + ? result.content.substring(0, 2000) + + "\n\n... (content truncated)" + : result.content; + + componentParams.children = ( + +
+ {result.title && ( +
+ {result.title} +
+ )} + {result.metadata?.publishedDate && ( +
+ Published: {result.metadata.publishedDate} +
+ )} + {urlStr && ( + + {urlStr} + + )} +
+ {contentPreview}
- )} - {urlStr && ( - - {urlStr} - - )} -
- {contentPreview}
-
-
+ + ); } else { - componentParams.children = -
- No content extracted from URL. -
-
+ componentParams.children = ( + +
+ No content extracted from URL. +
+
+ ); } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - + } else if (toolMessage.type === "tool_error") { + const { result } = toolMessage; + componentParams.bottomChildren = ( + + {result} + + ); } - return + return ; }, }, }; - -const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => { - const accessor = useAccessor() - const chatThreadService = accessor.get('IChatThreadService') - const streamState = useFullChatThreadsStreamState() +const Checkpoint = ({ + message, + threadId, + messageIdx, + isCheckpointGhost, + threadIsRunning, +}: { + message: CheckpointEntry; + threadId: string; + messageIdx: number; + isCheckpointGhost: boolean; + threadIsRunning: boolean; +}) => { + const accessor = useAccessor(); + const chatThreadService = accessor.get("IChatThreadService"); + const streamState = useFullChatThreadsStreamState(); // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() + const chatThreadsState = useChatThreadsState(); - const isRunning = useChatThreadsStreamState(threadId)?.isRunning + const isRunning = useChatThreadsStreamState(threadId)?.isRunning; const isDisabled = useMemo(() => { - if (isRunning) return true + if (isRunning) return true; // Use Object.values().some() instead of Object.keys().find() for better performance - return Object.values(streamState).some(threadState => threadState?.isRunning) - }, [isRunning, streamState]) + return Object.values(streamState).some( + (threadState) => threadState?.isRunning, + ); + }, [isRunning, streamState]); // Memoize message count lookup to avoid direct state access in render - const threadMessagesLength = chatThreadsState.allThreads[threadId]?.messages.length ?? 0 + const threadMessagesLength = + chatThreadsState.allThreads[threadId]?.messages.length ?? 0; - return
-
{ - if (threadIsRunning) return - if (isDisabled) return - chatThreadService.jumpToCheckpointBeforeMessageIdx({ - threadId, - messageIdx, - jumpToUserModified: messageIdx === threadMessagesLength - 1 - }) - }} - {...isDisabled ? { - 'data-tooltip-id': 'void-tooltip', - 'data-tooltip-content': `Disabled ${isRunning ? 'when running' : 'because another thread is running'}`, - 'data-tooltip-place': 'top', - } : {}} - > - Checkpoint + return ( +
+
{ + if (threadIsRunning) return; + if (isDisabled) return; + chatThreadService.jumpToCheckpointBeforeMessageIdx({ + threadId, + messageIdx, + jumpToUserModified: messageIdx === threadMessagesLength - 1, + }); + }} + {...(isDisabled + ? { + "data-tooltip-id": "void-tooltip", + "data-tooltip-content": `Disabled ${isRunning ? "when running" : "because another thread is running"}`, + "data-tooltip-place": "top", + } + : {})} + > + Checkpoint +
-
-} - + ); +}; -type ChatBubbleMode = 'display' | 'edit' +type ChatBubbleMode = "display" | "edit"; type ChatBubbleProps = { - chatMessage: ChatMessage, - messageIdx: number, - isCommitted: boolean, - chatIsRunning: IsRunningType, - threadId: string, - currCheckpointIdx: number | undefined, - _scrollToBottom: (() => void) | null, -} + chatMessage: ChatMessage; + messageIdx: number; + isCommitted: boolean; + chatIsRunning: IsRunningType; + threadId: string; + currCheckpointIdx: number | undefined; + _scrollToBottom: (() => void) | null; +}; // Plan Component - Shows structured execution plan as a todo list -const PlanComponent = React.memo(({ message, isCheckpointGhost, threadId, messageIdx }: { message: PlanMessage, isCheckpointGhost: boolean, threadId: string, messageIdx: number }) => { - const accessor = useAccessor() - const chatThreadService = accessor.get('IChatThreadService') - const [expandedSteps, setExpandedSteps] = useState>(new Set()) - const [isCollapsed, setIsCollapsed] = useState(false) - - // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - const approvalState = message.approvalState || 'pending' - const isRunning = useChatThreadsStreamState(threadId)?.isRunning - const isBusy = isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'preparing' - const isIdleLike = isRunning === undefined || isRunning === 'idle' - - // Get thread messages with proper subscription - const thread = chatThreadsState.allThreads[threadId] - const threadMessages = thread?.messages ?? [] - - // Memoize tool message lookup map for O(1) access instead of O(n) searches - const toolMessagesMap = useMemo(() => { - const map = new Map>() - for (const msg of threadMessages) { - if (msg.role === 'tool') { - const toolMsg = msg as ToolMessage - map.set(toolMsg.id, toolMsg) +const PlanComponent = React.memo( + ({ + message, + isCheckpointGhost, + threadId, + messageIdx, + }: { + message: PlanMessage; + isCheckpointGhost: boolean; + threadId: string; + messageIdx: number; + }) => { + const accessor = useAccessor(); + const chatThreadService = accessor.get("IChatThreadService"); + const [expandedSteps, setExpandedSteps] = useState>(new Set()); + const [isCollapsed, setIsCollapsed] = useState(false); + + // Subscribe to thread state changes properly + const chatThreadsState = useChatThreadsState(); + const approvalState = message.approvalState || "pending"; + const isRunning = useChatThreadsStreamState(threadId)?.isRunning; + const isBusy = + isRunning === "LLM" || isRunning === "tool" || isRunning === "preparing"; + const isIdleLike = isRunning === undefined || isRunning === "idle"; + + // Get thread messages with proper subscription + const thread = chatThreadsState.allThreads[threadId]; + const threadMessages = thread?.messages ?? []; + + // Memoize tool message lookup map for O(1) access instead of O(n) searches + const toolMessagesMap = useMemo(() => { + const map = new Map>(); + for (const msg of threadMessages) { + if (msg.role === "tool") { + const toolMsg = msg as ToolMessage; + map.set(toolMsg.id, toolMsg); + } } - } - return map - }, [threadMessages]) - - // Calculate progress - memoize to avoid recalculating on every render - const totalSteps = message.steps.length - const completedSteps = useMemo(() => - message.steps.filter(s => s.status === 'succeeded' || s.status === 'skipped').length - , [message.steps]) - const progressText = useMemo(() => - `${completedSteps} of ${totalSteps} ${totalSteps === 1 ? 'Step' : 'Steps'} Completed` - , [completedSteps, totalSteps]) - - // Memoize hasPausedSteps to avoid recalculating on every render - const hasPausedSteps = useMemo(() => - message.steps.some(s => s.status === 'paused') - , [message.steps]) - - const getCheckmarkIcon = (status?: StepStatus, isDisabled?: boolean) => { - if (isDisabled) { - return
- } + return map; + }, [threadMessages]); + + // Calculate progress - memoize to avoid recalculating on every render + const totalSteps = message.steps.length; + const completedSteps = useMemo( + () => + message.steps.filter( + (s) => s.status === "succeeded" || s.status === "skipped", + ).length, + [message.steps], + ); + const progressText = useMemo( + () => + `${completedSteps} of ${totalSteps} ${totalSteps === 1 ? "Step" : "Steps"} Completed`, + [completedSteps, totalSteps], + ); - switch (status) { - case 'succeeded': - return ( -
- -
- ) - case 'failed': - return ( -
- -
- ) - case 'running': - return ( -
- -
- ) - case 'paused': - return ( -
- -
- ) - case 'skipped': - return ( -
- -
- ) - default: // queued + // Memoize hasPausedSteps to avoid recalculating on every render + const hasPausedSteps = useMemo( + () => message.steps.some((s) => s.status === "paused"), + [message.steps], + ); + + const getCheckmarkIcon = (status?: StepStatus, isDisabled?: boolean) => { + if (isDisabled) { return ( -
-
-
- ) - } - } +
+ ); + } - const toggleStepExpanded = (stepNumber: number) => { - setExpandedSteps(prev => { - const next = new Set(prev) - if (next.has(stepNumber)) { - next.delete(stepNumber) - } else { - next.add(stepNumber) + switch (status) { + case "succeeded": + return ( +
+ +
+ ); + case "failed": + return ( +
+ +
+ ); + case "running": + return ( +
+ +
+ ); + case "paused": + return ( +
+ +
+ ); + case "skipped": + return ( +
+ +
+ ); + default: // queued + return ( +
+
+
+ ); } - return next - }) - } + }; - const handleApprove = () => { - if (isCheckpointGhost || isBusy) return - chatThreadService.approvePlan({ threadId, messageIdx }) - } + const toggleStepExpanded = (stepNumber: number) => { + setExpandedSteps((prev) => { + const next = new Set(prev); + if (next.has(stepNumber)) { + next.delete(stepNumber); + } else { + next.add(stepNumber); + } + return next; + }); + }; - const handleReject = () => { - if (isCheckpointGhost || isBusy) return - chatThreadService.rejectPlan({ threadId, messageIdx }) - } + const handleApprove = () => { + if (isCheckpointGhost || isBusy) return; + chatThreadService.approvePlan({ threadId, messageIdx }); + }; - const handleToggleStep = (stepNumber: number) => { - if (isCheckpointGhost || isBusy) return - chatThreadService.toggleStepDisabled({ threadId, messageIdx, stepNumber }) - } + const handleReject = () => { + if (isCheckpointGhost || isBusy) return; + chatThreadService.rejectPlan({ threadId, messageIdx }); + }; - const getStatusBadge = (status?: StepStatus) => { - switch (status) { - case 'running': - return Running - case 'failed': - return Failed - case 'paused': - return Paused - case 'skipped': - return Skipped - default: - return null - } - } + const handleToggleStep = (stepNumber: number) => { + if (isCheckpointGhost || isBusy) return; + chatThreadService.toggleStepDisabled({ + threadId, + messageIdx, + stepNumber, + }); + }; - return ( -
-
- {/* Header */} -
-
-
- + const getStatusBadge = (status?: StepStatus) => { + switch (status) { + case "running": + return ( + + Running + + ); + case "failed": + return ( + + Failed + + ); + case "paused": + return ( + + Paused + + ); + case "skipped": + return ( + + Skipped + + ); + default: + return null; + } + }; + + return ( +
+
+ {/* Header */} +
+
-

{message.summary}

- {approvalState === 'pending' && ( - - Pending Approval - - )} - {approvalState === 'executing' && ( - - - Executing - - )} - {approvalState === 'completed' && ( - - - Completed - - )} + +
+

+ {message.summary} +

+ {approvalState === "pending" && ( + + Pending Approval + + )} + {approvalState === "executing" && ( + + + Executing + + )} + {approvalState === "completed" && ( + + + Completed + + )} +
-
- {!isCollapsed && ( -
- {progressText} - {approvalState === 'pending' && isIdleLike && ( -
+ {!isCollapsed && ( +
+ + {progressText} + + {approvalState === "pending" && isIdleLike && ( +
+ + +
+ )} + {approvalState === "executing" && isBusy && ( + )} + {hasPausedSteps && !isBusy && ( -
- )} - {approvalState === 'executing' && isBusy && ( - - )} - {hasPausedSteps && !isBusy && ( - - )} -
- )} + )} +
+ )} +
-
- {/* Todo List */} - {!isCollapsed && ( -
- {message.steps.map((step, idx) => { - const isExpanded = expandedSteps.has(step.stepNumber) - const isDisabled = step.disabled - const status = step.status || 'queued' - const hasDetails = step.tools || step.files || step.error || step.toolCalls - - return ( -
- {/* Checkmark */} -
- {getCheckmarkIcon(status, isDisabled)} -
- - {/* Content */} -
-
-

- {step.description} -

- - {/* Status Badge */} - {getStatusBadge(status)} + {/* Todo List */} + {!isCollapsed && ( +
+ {message.steps.map((step, idx) => { + const isExpanded = expandedSteps.has(step.stepNumber); + const isDisabled = step.disabled; + const status = step.status || "queued"; + const hasDetails = + step.tools || step.files || step.error || step.toolCalls; + + return ( +
+ {/* Checkmark */} +
+ {getCheckmarkIcon(status, isDisabled)}
- {/* Actions Row */} - {(approvalState === 'pending' || (approvalState === 'executing' && status === 'failed')) && !isCheckpointGhost && ( -
- {approvalState === 'pending' && !isRunning && ( - - )} - {approvalState === 'executing' && status === 'failed' && ( - <> - - - {step.checkpointIdx !== undefined && step.checkpointIdx !== null && ( - )} - + {approvalState === "executing" && + status === "failed" && ( + <> + + + {step.checkpointIdx !== undefined && + step.checkpointIdx !== null && ( + + )} + + )} +
)} -
- )} - {/* Expandable Details */} - {hasDetails && ( - - )} - - {/* Expanded Content */} - {isExpanded && hasDetails && ( -
- {step.tools && step.tools.length > 0 && ( -
-
Expected Tools:
-
- {step.tools.map((tool, i) => ( - - {tool} - - ))} + {/* Expandable Details */} + {hasDetails && ( + + )} + + {/* Expanded Content */} + {isExpanded && hasDetails && ( +
+ {step.tools && step.tools.length > 0 && ( +
+
+ Expected Tools: +
+
+ {step.tools.map((tool, i) => ( + + {tool} + + ))} +
-
- )} - {step.toolCalls && step.toolCalls.length > 0 && ( -
-
Tool Calls Executed {step.toolCalls.length}
-
- {step.toolCalls.map((toolId, i) => { - // Use memoized map for O(1) lookup instead of O(n) find - const toolMsg = toolMessagesMap.get(toolId) - if (!toolMsg) return null - - const isSuccess = toolMsg.type === 'success' - const isError = toolMsg.type === 'tool_error' - - return ( -
-
- {toolMsg.name} - {isSuccess && } - {isError && } -
- {isError && toolMsg.result && ( -
- {toolMsg.result} + )} + {step.toolCalls && step.toolCalls.length > 0 && ( +
+
+ Tool Calls Executed{" "} + + {step.toolCalls.length} + +
+
+ {step.toolCalls.map((toolId, i) => { + // Use memoized map for O(1) lookup instead of O(n) find + const toolMsg = toolMessagesMap.get(toolId); + if (!toolMsg) return null; + + const isSuccess = toolMsg.type === "success"; + const isError = toolMsg.type === "tool_error"; + + return ( +
+
+ + {toolMsg.name} + + {isSuccess && ( + + )} + {isError && ( + + )}
- )} - {isSuccess && toolMsg.result && typeof toolMsg.result === 'object' && ( -
- View result -
-																					{JSON.stringify(toolMsg.result, null, 2)}
-																				
-
- )} - {isError && toolMsg.params && ( -
- View params -
-																					{JSON.stringify(toolMsg.params, null, 2)}
-																				
-
- )} -
- ) - })} + {isError && toolMsg.result && ( +
+ {toolMsg.result} +
+ )} + {isSuccess && + toolMsg.result && + typeof toolMsg.result === "object" && ( +
+ + View result + +
+																							{JSON.stringify(
+																								toolMsg.result,
+																								null,
+																								2,
+																							)}
+																						
+
+ )} + {isError && toolMsg.params && ( +
+ + View params + +
+																						{JSON.stringify(
+																							toolMsg.params,
+																							null,
+																							2,
+																						)}
+																					
+
+ )} +
+ ); + })} +
-
- )} - {step.files && step.files.length > 0 && ( -
-
Files Affected:
-
- {step.files.map((file, i) => ( - - - {file.split('/').pop()} - - ))} + )} + {step.files && step.files.length > 0 && ( +
+
+ Files Affected: +
+
+ {step.files.map((file, i) => ( + + + {file.split("/").pop()} + + ))} +
-
- )} - {step.error && ( -
- - {step.error} -
- )} - {(step.startTime && step.endTime) && ( -
- Duration: {((step.endTime - step.startTime) / 1000).toFixed(1)}s -
- )} - {step.checkpointIdx !== undefined && step.checkpointIdx !== null && ( -
- Checkpoint: #{step.checkpointIdx} -
- )} -
- )} + )} + {step.error && ( +
+ + {step.error} +
+ )} + {step.startTime && step.endTime && ( +
+ Duration:{" "} + {((step.endTime - step.startTime) / 1000).toFixed( + 1, + )} + s +
+ )} + {step.checkpointIdx !== undefined && + step.checkpointIdx !== null && ( +
+ Checkpoint: #{step.checkpointIdx} +
+ )} +
+ )} +
-
- ) - })} -
- )} + ); + })} +
+ )} +
-
- ); -}, (prev, next) => { - // Custom comparison: only re-render if plan message, checkpoint state, or thread changes - return prev.message === next.message && - prev.isCheckpointGhost === next.isCheckpointGhost && - prev.threadId === next.threadId && - prev.messageIdx === next.messageIdx -}); + ); + }, + (prev, next) => { + // Custom comparison: only re-render if plan message, checkpoint state, or thread changes + return ( + prev.message === next.message && + prev.isCheckpointGhost === next.isCheckpointGhost && + prev.threadId === next.threadId && + prev.messageIdx === next.messageIdx + ); + }, +); // Review Component - Shows summary after execution -const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessage, isCheckpointGhost: boolean }) => { +const ReviewComponent = ({ + message, + isCheckpointGhost, +}: { + message: ReviewMessage; + isCheckpointGhost: boolean; +}) => { return ( -
-
+
+
{message.completed ? ( @@ -3400,20 +4562,24 @@ const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessag ) : ( )} -

- {message.completed ? 'Review Complete' : 'Review: Issues Found'} +

+ {message.completed ? "Review Complete" : "Review: Issues Found"}

{(message.executionTime || message.stepsCompleted !== undefined) && (
- {message.executionTime && `${(message.executionTime / 1000).toFixed(1)}s`} - {message.stepsCompleted !== undefined && message.stepsTotal !== undefined && ( - - {message.stepsCompleted}/{message.stepsTotal} steps - - )} + {message.executionTime && + `${(message.executionTime / 1000).toFixed(1)}s`} + {message.stepsCompleted !== undefined && + message.stepsTotal !== undefined && ( + + {message.stepsCompleted}/{message.stepsTotal} steps + + )}
)}
@@ -3421,13 +4587,21 @@ const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessag {message.filesChanged && message.filesChanged.length > 0 && (
-

Files Changed:

+

+ Files Changed: +

{message.filesChanged.map((file, i) => (
- {file.changeType === 'created' && } - {file.changeType === 'modified' && } - {file.changeType === 'deleted' && } + {file.changeType === "created" && ( + + )} + {file.changeType === "modified" && ( + + )} + {file.changeType === "deleted" && ( + + )} {file.path}
))} @@ -3438,24 +4612,39 @@ const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessag {message.issues && message.issues.length > 0 && (
{message.issues.map((issue, i) => ( -
- {issue.severity === 'error' ? ( +
+ {issue.severity === "error" ? ( - ) : issue.severity === 'warning' ? ( - + ) : issue.severity === "warning" ? ( + ) : ( - + )}
-

+

{issue.message}

{issue.file && ( @@ -3472,10 +4661,15 @@ const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessag {message.nextSteps && message.nextSteps.length > 0 && (
-

Recommended Next Steps:

+

+ Recommended Next Steps: +

    {message.nextSteps.map((step, i) => ( -
  • +
  • {step}
  • @@ -3488,126 +4682,166 @@ const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessag ); }; -const ChatBubble = React.memo((props: ChatBubbleProps) => { - return - <_ChatBubble {...props} /> - -}, (prev, next) => { - // Custom comparison: only re-render if props actually changed - return prev.chatMessage === next.chatMessage && - prev.messageIdx === next.messageIdx && - prev.isCommitted === next.isCommitted && - prev.chatIsRunning === next.chatIsRunning && - prev.currCheckpointIdx === next.currCheckpointIdx && - prev.threadId === next.threadId && - prev._scrollToBottom === next._scrollToBottom -}) - -const _ChatBubble = React.memo(({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => { - const role = chatMessage.role - - const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts) - - if (role === 'user') { - return - } - else if (role === 'assistant') { - return - } - else if (role === 'tool') { +const ChatBubble = React.memo( + (props: ChatBubbleProps) => { + return ( + + <_ChatBubble {...props} /> + + ); + }, + (prev, next) => { + // Custom comparison: only re-render if props actually changed + return ( + prev.chatMessage === next.chatMessage && + prev.messageIdx === next.messageIdx && + prev.isCommitted === next.isCommitted && + prev.chatIsRunning === next.chatIsRunning && + prev.currCheckpointIdx === next.currCheckpointIdx && + prev.threadId === next.threadId && + prev._scrollToBottom === next._scrollToBottom + ); + }, +); + +const _ChatBubble = React.memo( + ({ + threadId, + chatMessage, + currCheckpointIdx, + isCommitted, + messageIdx, + chatIsRunning, + _scrollToBottom, + }: ChatBubbleProps) => { + const role = chatMessage.role; + + const isCheckpointGhost = + messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning; // whether to show as gray (if chat is running, for good measure just dont show any ghosts) + + if (role === "user") { + return ( + + ); + } else if (role === "assistant") { + return ( + + ); + } else if (role === "tool") { + if (chatMessage.type === "invalid_params") { + return ( +
    + +
    + ); + } - if (chatMessage.type === 'invalid_params') { - return
    - -
    - } + const toolName = chatMessage.name; + const isBuiltInTool = isABuiltinToolName(toolName); + const ToolResultWrapper = isBuiltInTool + ? (builtinToolNameToComponent[toolName] + ?.resultWrapper as ResultWrapper) + : (MCPToolWrapper as ResultWrapper); - const toolName = chatMessage.name - const isBuiltInTool = isABuiltinToolName(toolName) - const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper - : MCPToolWrapper as ResultWrapper - - if (ToolResultWrapper) - return <> -
    - +
    + +
    + {chatMessage.type === "tool_request" ? ( +
    + +
    + ) : null} + + ); + return null; + } else if (role === "interrupted_streaming_tool") { + return ( +
    +
    - {chatMessage.type === 'tool_request' ? -
    - -
    : null} - - return null - } - - else if (role === 'interrupted_streaming_tool') { - return
    - -
    - } - - else if (role === 'checkpoint') { - return - } - - else if (role === 'plan') { - return - } - - else if (role === 'review') { - return - } - -}, (prev, next) => { - // Custom comparison for _ChatBubble - return prev.chatMessage === next.chatMessage && - prev.messageIdx === next.messageIdx && - prev.isCommitted === next.isCommitted && - prev.chatIsRunning === next.chatIsRunning && - prev.currCheckpointIdx === next.currCheckpointIdx && - prev.threadId === next.threadId && - prev._scrollToBottom === next._scrollToBottom -}) + ); + } else if (role === "checkpoint") { + return ( + + ); + } else if (role === "plan") { + return ( + + ); + } else if (role === "review") { + return ( + + ); + } + }, + (prev, next) => { + // Custom comparison for _ChatBubble + return ( + prev.chatMessage === next.chatMessage && + prev.messageIdx === next.messageIdx && + prev.isCommitted === next.isCommitted && + prev.chatIsRunning === next.chatIsRunning && + prev.currCheckpointIdx === next.currCheckpointIdx && + prev.threadId === next.threadId && + prev._scrollToBottom === next._scrollToBottom + ); + }, +); const CommandBarInChat = () => { - const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - const numFilesChanged = sortedCommandBarURIs.length - - const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') - const commandService = accessor.get('ICommandService') - const chatThreadsState = useChatThreadsState() - const commandBarState = useCommandBarState() - const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) + const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = + useCommandBarState(); + const numFilesChanged = sortedCommandBarURIs.length; + + const accessor = useAccessor(); + const editCodeService = accessor.get("IEditCodeService"); + const commandService = accessor.get("ICommandService"); + const chatThreadsState = useChatThreadsState(); + const commandBarState = useCommandBarState(); + const chatThreadsStreamState = useChatThreadsStreamState( + chatThreadsState.currentThreadId, + ); // ( // { // /> // ) - const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState<'auto-opened' | 'auto-closed' | 'user-opened' | 'user-closed'>('auto-closed'); - const isFileDetailsOpened = fileDetailsOpenedState === 'auto-opened' || fileDetailsOpenedState === 'user-opened'; - + const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState< + "auto-opened" | "auto-closed" | "user-opened" | "user-closed" + >("auto-closed"); + const isFileDetailsOpened = + fileDetailsOpenedState === "auto-opened" || + fileDetailsOpenedState === "user-opened"; useEffect(() => { // close the file details if there are no files // this converts 'user-closed' to 'auto-closed' if (numFilesChanged === 0) { - setFileDetailsOpenedState('auto-closed') + setFileDetailsOpenedState("auto-closed"); } // open the file details if it hasnt been closed - if (numFilesChanged > 0 && fileDetailsOpenedState !== 'user-closed') { - setFileDetailsOpenedState('auto-opened') + if (numFilesChanged > 0 && fileDetailsOpenedState !== "user-closed") { + setFileDetailsOpenedState("auto-opened"); } - }, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged]) + }, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged]); - - const isFinishedMakingThreadChanges = ( + const isFinishedMakingThreadChanges = // there are changed files - commandBarState.sortedURIs.length !== 0 + commandBarState.sortedURIs.length !== 0 && // none of the files are streaming - && commandBarState.sortedURIs.every(uri => !commandBarState.stateOfURI[uri.fsPath]?.isStreaming) - ) + commandBarState.sortedURIs.every( + (uri) => !commandBarState.stateOfURI[uri.fsPath]?.isStreaming, + ); // ======== status of agent ======== // This icon answers the question "is the LLM doing work on this thread?" @@ -3650,179 +4887,225 @@ const CommandBarInChat = () => { // orange = Requires action // dark = Done - const threadStatus = ( - chatThreadsStreamState?.isRunning === 'awaiting_user' - ? { title: 'Needs Approval', color: 'yellow', } as const - : (chatThreadsStreamState?.isRunning === 'LLM' || chatThreadsStreamState?.isRunning === 'tool' || chatThreadsStreamState?.isRunning === 'preparing') - ? { title: chatThreadsStreamState?.isRunning === 'preparing' ? 'Preparing' : 'Running', color: 'orange', } as const - : { title: 'Done', color: 'dark', } as const - ) - - - const threadStatusHTML = - + const threadStatus = + chatThreadsStreamState?.isRunning === "awaiting_user" + ? ({ title: "Needs Approval", color: "yellow" } as const) + : chatThreadsStreamState?.isRunning === "LLM" || + chatThreadsStreamState?.isRunning === "tool" || + chatThreadsStreamState?.isRunning === "preparing" + ? ({ + title: + chatThreadsStreamState?.isRunning === "preparing" + ? "Preparing" + : "Running", + color: "orange", + } as const) + : ({ title: "Done", color: "dark" } as const); + + const threadStatusHTML = ( + + ); // ======== info about changes ======== // num files changed // acceptall + rejectall // popup info about each change (each with num changes + acceptall + rejectall of their own) - const numFilesChangedStr = numFilesChanged === 0 ? 'No files with changes' - : `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? '' : 's'} with changes` - - - + const numFilesChangedStr = + numFilesChanged === 0 + ? "No files with changes" + : `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? "" : "s"} with changes`; - const acceptRejectAllButtons =
    - { - sortedCommandBarURIs.forEach(uri => { - editCodeService.acceptOrRejectAllDiffAreas({ - uri, - removeCtrlKs: true, - behavior: "reject", - _addToHistory: true, + const acceptRejectAllButtons = ( +
    + { + sortedCommandBarURIs.forEach((uri) => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "reject", + _addToHistory: true, + }); }); - }); - }} - data-tooltip-id='void-tooltip' - data-tooltip-place='top' - data-tooltip-content='Reject all' - /> + }} + data-tooltip-id="void-tooltip" + data-tooltip-place="top" + data-tooltip-content="Reject all" + /> - { - sortedCommandBarURIs.forEach(uri => { - editCodeService.acceptOrRejectAllDiffAreas({ - uri, - removeCtrlKs: true, - behavior: "accept", - _addToHistory: true, + { + sortedCommandBarURIs.forEach((uri) => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "accept", + _addToHistory: true, + }); }); - }); - }} - data-tooltip-id='void-tooltip' - data-tooltip-place='top' - data-tooltip-content='Accept all' - /> - - - -
    - + }} + data-tooltip-id="void-tooltip" + data-tooltip-place="top" + data-tooltip-content="Accept all" + /> +
    + ); // !select-text cursor-auto - const fileDetailsContent =
    - {sortedCommandBarURIs.map((uri, i) => { - const basename = getBasename(uri.fsPath) - - const { sortedDiffIds, isStreaming } = commandBarStateOfURI[uri.fsPath] ?? {} - const isFinishedMakingFileChanges = !isStreaming + const fileDetailsContent = ( +
    + {sortedCommandBarURIs.map((uri, i) => { + const basename = getBasename(uri.fsPath); - const numDiffs = sortedDiffIds?.length || 0 - - const fileStatus = (isFinishedMakingFileChanges - ? { title: 'Done', color: 'dark', } as const - : { title: 'Running', color: 'orange', } as const - ) - - const fileNameHTML =
    voidOpenFileFn(uri, accessor)} - > - {/* */} - {basename} -
    + const { sortedDiffIds, isStreaming } = + commandBarStateOfURI[uri.fsPath] ?? {}; + const isFinishedMakingFileChanges = !isStreaming; + const numDiffs = sortedDiffIds?.length || 0; + const fileStatus = isFinishedMakingFileChanges + ? ({ title: "Done", color: "dark" } as const) + : ({ title: "Running", color: "orange" } as const); + const fileNameHTML = ( +
    voidOpenFileFn(uri, accessor)} + > + {/* */} + {basename} +
    + ); - const detailsContent =
    - {numDiffs} diff{numDiffs !== 1 ? 's' : ''} -
    + const detailsContent = ( +
    + + {numDiffs} diff{numDiffs !== 1 ? "s" : ""} + +
    + ); - const acceptRejectButtons =
    - {/* + {/* */} - { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }} - data-tooltip-id='void-tooltip' - data-tooltip-place='top' - data-tooltip-content='Reject file' - - /> - { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }} - data-tooltip-id='void-tooltip' - data-tooltip-place='top' - data-tooltip-content='Accept file' - /> - -
    + { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "reject", + _addToHistory: true, + }); + }} + data-tooltip-id="void-tooltip" + data-tooltip-place="top" + data-tooltip-content="Reject file" + /> + { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "accept", + _addToHistory: true, + }); + }} + data-tooltip-id="void-tooltip" + data-tooltip-place="top" + data-tooltip-content="Accept file" + /> +
    + ); - const fileStatusHTML = + const fileStatusHTML = ( + + ); - return ( - // name, details -
    -
    - {fileNameHTML} - {detailsContent} -
    -
    - {acceptRejectButtons} - {fileStatusHTML} + return ( + // name, details +
    +
    + {fileNameHTML} + {detailsContent} +
    +
    + {acceptRejectButtons} + {fileStatusHTML} +
    -
    - ) - })} -
    + ); + })} +
    + ); const fileDetailsButton = ( - ) + ); return ( <> {/* file details */} -
    +
    { text-void-fg-3 text-xs text-nowrap overflow-hidden transition-all duration-200 ease-in-out - ${isFileDetailsOpened ? 'max-h-24' : 'max-h-0'} + ${isFileDetailsOpened ? "max-h-24" : "max-h-0"} `} > {fileDetailsContent} @@ -3848,91 +5131,101 @@ const CommandBarInChat = () => { justify-between `} > -
    - {fileDetailsButton} -
    +
    {fileDetailsButton}
    {acceptRejectAllButtons} {threadStatusHTML}
    - ) -} - - - -const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { + ); +}; - if (!isABuiltinToolName(toolCallSoFar.name)) return null +const EditToolSoFar = ({ + toolCallSoFar, +}: { + toolCallSoFar: RawToolCallObj; +}) => { + if (!isABuiltinToolName(toolCallSoFar.name)) return null; - const accessor = useAccessor() + const accessor = useAccessor(); - const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined + const uri = toolCallSoFar.rawParams.uri + ? URI.file(toolCallSoFar.rawParams.uri) + : undefined; - const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed + const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed; - const uriDone = toolCallSoFar.doneParams.includes('uri') - const desc1 = - {uriDone ? - getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') - : `Generating`} - - + const uriDone = toolCallSoFar.doneParams.includes("uri"); + const desc1 = ( + + {uriDone + ? getBasename(toolCallSoFar.rawParams["uri"] ?? "unknown") + : `Generating`} + + + ); - const desc1OnClick = () => { uri && voidOpenFileFn(uri, accessor) } + const desc1OnClick = () => { + uri && voidOpenFileFn(uri, accessor); + }; // If URI has not been specified - return - - - - -} - + return ( + + + + + ); +}; export const SidebarChat = () => { - const textAreaRef = useRef(null) - const textAreaFnsRef = useRef(null) + const textAreaRef = useRef(null); + const textAreaFnsRef = useRef(null); - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const chatThreadsService = accessor.get('IChatThreadService') + const accessor = useAccessor(); + const commandService = accessor.get("ICommandService"); + const chatThreadsService = accessor.get("IChatThreadService"); - const settingsState = useSettingsState() + const settingsState = useSettingsState(); // ----- HIGHER STATE ----- // threads state - const chatThreadsState = useChatThreadsState() + const chatThreadsState = useChatThreadsState(); - const currentThread = chatThreadsService.getCurrentThread() - const previousMessages = currentThread?.messages ?? [] + const currentThread = chatThreadsService.getCurrentThread(); + const previousMessages = currentThread?.messages ?? []; - const selections = currentThread.state.stagingSelections - const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadState({ stagingSelections: s }) } + const selections = currentThread.state.stagingSelections; + const setSelections = (s: StagingSelectionItem[]) => { + chatThreadsService.setCurrentThreadState({ stagingSelections: s }); + }; // stream state - const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - const isRunning = currThreadStreamState?.isRunning - const latestError = currThreadStreamState?.error - const { displayContentSoFar, toolCallSoFar, reasoningSoFar } = currThreadStreamState?.llmInfo ?? {} + const currThreadStreamState = useChatThreadsStreamState( + chatThreadsState.currentThreadId, + ); + const isRunning = currThreadStreamState?.isRunning; + const latestError = currThreadStreamState?.error; + const { displayContentSoFar, toolCallSoFar, reasoningSoFar } = + currThreadStreamState?.llmInfo ?? {}; // this is just if it's currently being generated, NOT if it's currently running - const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone // show loading for slow tools (right now just edit) + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone; // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- // state of current message - const initVal = '' - const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal) + const initVal = ""; + const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal); // Image attachments management const { @@ -3962,416 +5255,547 @@ export const SidebarChat = () => { // Wrapper to check vision capabilities before adding PDFs // PDFs are more forgiving than images - they can work with non-vision models via text extraction - const addPDFs = useCallback(async (files: File[]) => { - const currentModelSel = settingsState.modelSelectionOfFeature['Chat']; + const addPDFs = useCallback( + async (files: File[]) => { + const currentModelSel = settingsState.modelSelectionOfFeature["Chat"]; + + // In auto mode, skip vision capability check - the router will select an appropriate model + // PDFs can also work with non-vision models via text extraction, so we're more lenient + if ( + currentModelSel?.providerName === "auto" && + currentModelSel?.modelName === "auto" + ) { + await addPDFsRaw(files); + return; + } - // In auto mode, skip vision capability check - the router will select an appropriate model - // PDFs can also work with non-vision models via text extraction, so we're more lenient - if (currentModelSel?.providerName === 'auto' && currentModelSel?.modelName === 'auto') { + // For non-auto mode, allow PDFs even without vision models (they can use text extraction) + // But we could optionally warn if no vision models are available await addPDFsRaw(files); - return; - } - - // For non-auto mode, allow PDFs even without vision models (they can use text extraction) - // But we could optionally warn if no vision models are available - await addPDFsRaw(files); - }, [addPDFsRaw, settingsState]); + }, + [addPDFsRaw, settingsState], + ); // Wrapper to check vision capabilities before adding images - const addImages = useCallback(async (files: File[]) => { - const currentModelSel = settingsState.modelSelectionOfFeature['Chat']; - - // In auto mode, skip vision capability check - the router will select an appropriate model - if (currentModelSel?.providerName === 'auto' && currentModelSel?.modelName === 'auto') { - await addImagesRaw(files); - return; - } + const addImages = useCallback( + async (files: File[]) => { + const currentModelSel = settingsState.modelSelectionOfFeature["Chat"]; + + // In auto mode, skip vision capability check - the router will select an appropriate model + if ( + currentModelSel?.providerName === "auto" && + currentModelSel?.modelName === "auto" + ) { + await addImagesRaw(files); + return; + } - // Check if user has vision-capable API keys or Ollama vision models - const { isSelectedModelVisionCapable, checkOllamaModelVisionCapable, hasVisionCapableApiKey, hasOllamaVisionModel, isOllamaAccessible } = await import('../util/visionModelHelper.js'); + // Check if user has vision-capable API keys or Ollama vision models + const { + isSelectedModelVisionCapable, + checkOllamaModelVisionCapable, + hasVisionCapableApiKey, + hasOllamaVisionModel, + isOllamaAccessible, + } = await import("../util/visionModelHelper.js"); + + // First, check if the currently selected model is vision-capable (synchronous check) + let selectedIsVision = isSelectedModelVisionCapable( + currentModelSel, + settingsState.settingsOfProvider, + ); + + // If Ollama model and not detected by name, query Ollama API directly (async) + if (!selectedIsVision && currentModelSel?.providerName === "ollama") { + const ollamaAccessible = await isOllamaAccessible(); + if (ollamaAccessible) { + selectedIsVision = await checkOllamaModelVisionCapable( + currentModelSel.modelName, + ); + } + } - // First, check if the currently selected model is vision-capable (synchronous check) - let selectedIsVision = isSelectedModelVisionCapable(currentModelSel, settingsState.settingsOfProvider); + if (selectedIsVision) { + // User has selected a vision-capable model, proceed + await addImagesRaw(files); + return; + } - // If Ollama model and not detected by name, query Ollama API directly (async) - if (!selectedIsVision && currentModelSel?.providerName === 'ollama') { + // If not selected, check if they have any vision-capable options available + const hasApiKey = hasVisionCapableApiKey( + settingsState.settingsOfProvider, + currentModelSel, + ); const ollamaAccessible = await isOllamaAccessible(); - if (ollamaAccessible) { - selectedIsVision = await checkOllamaModelVisionCapable(currentModelSel.modelName); + const hasOllamaVision = + ollamaAccessible && (await hasOllamaVisionModel()); + + if (!hasApiKey && !hasOllamaVision) { + // Show notification with option to open Ollama setup + const notificationService = accessor.get("INotificationService"); + const commandService = accessor.get("ICommandService"); + + notificationService.notify({ + severity: 2, // Severity.Warning + message: + "No vision-capable models available. Please set up an API key (Anthropic, OpenAI, or Gemini) or install an Ollama vision model (e.g., llava, bakllava).", + actions: { + primary: [ + { + id: "void.vision.setup", + label: "Setup Ollama Vision Models", + tooltip: "", + class: undefined, + enabled: true, + run: () => + commandService.executeCommand( + CORTEXIDE_OPEN_SETTINGS_ACTION_ID, + ), + }, + ], + }, + }); + return; } - } - if (selectedIsVision) { - // User has selected a vision-capable model, proceed + // User has vision support available but not selected, proceed anyway (they might switch models) await addImagesRaw(files); - return; - } - - // If not selected, check if they have any vision-capable options available - const hasApiKey = hasVisionCapableApiKey(settingsState.settingsOfProvider, currentModelSel); - const ollamaAccessible = await isOllamaAccessible(); - const hasOllamaVision = ollamaAccessible && await hasOllamaVisionModel(); - - if (!hasApiKey && !hasOllamaVision) { - // Show notification with option to open Ollama setup - const notificationService = accessor.get('INotificationService'); - const commandService = accessor.get('ICommandService'); - - notificationService.notify({ - severity: 2, // Severity.Warning - message: 'No vision-capable models available. Please set up an API key (Anthropic, OpenAI, or Gemini) or install an Ollama vision model (e.g., llava, bakllava).', - actions: { - primary: [{ - id: 'void.vision.setup', - label: 'Setup Ollama Vision Models', - tooltip: '', - class: undefined, - enabled: true, - run: () => commandService.executeCommand(CORTEXIDE_OPEN_SETTINGS_ACTION_ID), - }], - }, - }); - return; - } - - // User has vision support available but not selected, proceed anyway (they might switch models) - await addImagesRaw(files); - }, [addImagesRaw, settingsState, accessor]); + }, + [addImagesRaw, settingsState, accessor], + ); - const isDisabled = (instructionsAreEmpty && imageAttachments.length === 0 && pdfAttachments.length === 0) || !!isFeatureNameDisabled('Chat', settingsState) + const isDisabled = + (instructionsAreEmpty && + imageAttachments.length === 0 && + pdfAttachments.length === 0) || + !!isFeatureNameDisabled("Chat", settingsState); - const sidebarRef = useRef(null) - const scrollContainerRef = useRef(null) + const sidebarRef = useRef(null); + const scrollContainerRef = useRef(null); // Memoize scrollToBottom callback to prevent unnecessary re-renders const scrollToBottomCallback = useCallback(() => { - scrollToBottom(scrollContainerRef) - }, [scrollContainerRef]) - - const onSubmit = useCallback(async (_forceSubmit?: string) => { + scrollToBottom(scrollContainerRef); + }, [scrollContainerRef]); - if (isDisabled && !_forceSubmit) return - if (isRunning) return + const onSubmit = useCallback( + async (_forceSubmit?: string) => { + if (isDisabled && !_forceSubmit) return; + if (isRunning) return; - // use subscribed state - currentThread.id is already from subscribed state - const threadId = currentThread.id + // use subscribed state - currentThread.id is already from subscribed state + const threadId = currentThread.id; - - // send message to LLM - const userMessage = _forceSubmit || textAreaRef.current?.value || '' + // send message to LLM + const userMessage = _forceSubmit || textAreaRef.current?.value || ""; // Resolve @references in the input into staging selections before sending // Supports tokens like: @"src/app/file.ts", @path/to/file.ts, @folder, @workspace, @recent, @selection try { - const toolsService = accessor.get('IToolsService') - const workspaceService = accessor.get('IWorkspaceContextService') - const editorService = accessor.get('IEditorService') - const languageService = accessor.get('ILanguageService') - const historyService = accessor.get('IHistoryService') - const notificationService = accessor.get('INotificationService') - let outlineService: any = undefined - try { outlineService = accessor.get('IOutlineModelService') } catch {} - - // Collect existing URIs to avoid duplicate attachments - const existing = new Set() - const existingSelections = chatThreadsState.allThreads[currentThread.id]?.state?.stagingSelections || [] - for (const s of existingSelections) existing.add(s.uri?.fsPath || '') - - const addFileSelection = async (uri: any) => { - if (!uri) return - const key = uri.fsPath || uri.path || '' - if (key && existing.has(key)) return - existing.add(key) - const newSel = { - type: 'File', - uri, - language: languageService.guessLanguageIdByFilepathOrFirstLine(uri) || '', - state: { wasAddedAsCurrentFile: false }, - } - await chatThreadsService.addNewStagingSelection(newSel) - } - - const addFolderSelection = async (uri: any) => { - if (!uri) return - const key = uri.fsPath || uri.path || '' - if (key && existing.has(key)) return - existing.add(key) - const newSel = { - type: 'Folder', - uri, - language: undefined, - state: undefined, - } - await chatThreadsService.addNewStagingSelection(newSel) - } - - const tokens: string[] = [] - { - // Extract quoted paths first: @"..." - const quoted = [...userMessage.matchAll(/@"([^"]+)"/g)].map(m => m[1]) - tokens.push(...quoted) - // Extract bare @word-like tokens (stop at whitespace or punctuation) - for (const m of userMessage.matchAll(/@([\w\.\-_/]+(?::\d+(?:-\d+)?)?)/g)) { - const t = m[1] - if (t) tokens.push(t) + const toolsService = accessor.get("IToolsService"); + const workspaceService = accessor.get("IWorkspaceContextService"); + const editorService = accessor.get("IEditorService"); + const languageService = accessor.get("ILanguageService"); + const historyService = accessor.get("IHistoryService"); + const notificationService = accessor.get("INotificationService"); + let outlineService: any = undefined; + try { + outlineService = accessor.get("IOutlineModelService"); + } catch {} + + // Collect existing URIs to avoid duplicate attachments + const existing = new Set(); + const existingSelections = + chatThreadsState.allThreads[currentThread.id]?.state + ?.stagingSelections || []; + for (const s of existingSelections) existing.add(s.uri?.fsPath || ""); + + const addFileSelection = async (uri: any) => { + if (!uri) return; + const key = uri.fsPath || uri.path || ""; + if (key && existing.has(key)) return; + existing.add(key); + const newSel = { + type: "File", + uri, + language: + languageService.guessLanguageIdByFilepathOrFirstLine(uri) || "", + state: { wasAddedAsCurrentFile: false }, + }; + await chatThreadsService.addNewStagingSelection(newSel); + }; + + const addFolderSelection = async (uri: any) => { + if (!uri) return; + const key = uri.fsPath || uri.path || ""; + if (key && existing.has(key)) return; + existing.add(key); + const newSel = { + type: "Folder", + uri, + language: undefined, + state: undefined, + }; + await chatThreadsService.addNewStagingSelection(newSel); + }; + + const tokens: string[] = []; + { + // Extract quoted paths first: @"..." + const quoted = [...userMessage.matchAll(/@"([^"]+)"/g)].map( + (m) => m[1], + ); + tokens.push(...quoted); + // Extract bare @word-like tokens (stop at whitespace or punctuation) + for (const m of userMessage.matchAll( + /@([\w\.\-_/]+(?::\d+(?:-\d+)?)?)/g, + )) { + const t = m[1]; + if (t) tokens.push(t); + } } - } - const special = new Set(['selection', 'workspace', 'recent', 'folder']) - - // Track unresolved references for error reporting - const unresolvedRefs: string[] = [] - - for (const raw of tokens) { - // Handle special tokens - if (raw === 'selection') { - const active = editorService.activeTextEditorControl - const activeResource = editorService.activeEditor?.resource - const sel = active?.getSelection?.() - if (activeResource && sel && !sel.isEmpty()) { - const newSel = { - type: 'File', - uri: activeResource, - language: languageService.guessLanguageIdByFilepathOrFirstLine(activeResource) || '', - state: { wasAddedAsCurrentFile: false }, - range: sel, - } - const key = activeResource.fsPath || '' - if (!existing.has(key)) { - existing.add(key) - await chatThreadsService.addNewStagingSelection(newSel) + const special = new Set(["selection", "workspace", "recent", "folder"]); + + // Track unresolved references for error reporting + const unresolvedRefs: string[] = []; + + for (const raw of tokens) { + // Handle special tokens + if (raw === "selection") { + const active = editorService.activeTextEditorControl; + const activeResource = editorService.activeEditor?.resource; + const sel = active?.getSelection?.(); + if (activeResource && sel && !sel.isEmpty()) { + const newSel = { + type: "File", + uri: activeResource, + language: + languageService.guessLanguageIdByFilepathOrFirstLine( + activeResource, + ) || "", + state: { wasAddedAsCurrentFile: false }, + range: sel, + }; + const key = activeResource.fsPath || ""; + if (!existing.has(key)) { + existing.add(key); + await chatThreadsService.addNewStagingSelection(newSel); + } + } else { + unresolvedRefs.push("@selection (no active selection)"); } - } else { - unresolvedRefs.push('@selection (no active selection)') + continue; } - continue - } - if (raw === 'workspace') { - for (const folder of workspaceService.getWorkspace().folders) { - await addFolderSelection(folder.uri) + if (raw === "workspace") { + for (const folder of workspaceService.getWorkspace().folders) { + await addFolderSelection(folder.uri); + } + continue; } - continue - } - if (raw === 'recent') { - for (const h of historyService.getHistory()) { - if (h.resource) await addFileSelection(h.resource) + if (raw === "recent") { + for (const h of historyService.getHistory()) { + if (h.resource) await addFileSelection(h.resource); + } + continue; } - continue - } - // Handle explicit symbol: @sym:Name or @symbol:Name - if (raw.startsWith('sym:') || raw.startsWith('symbol:')) { - const symName = raw.replace(/^symbol?:/,'') - let symbolFound = false - if (outlineService && typeof outlineService.getCachedModels === 'function') { - try { - const models = outlineService.getCachedModels() - for (const om of models) { - const list = typeof om.asListOfDocumentSymbols === 'function' ? om.asListOfDocumentSymbols() : [] - for (const s of list) { - if ((s?.name || '').toLowerCase() === symName.toLowerCase()) { - symbolFound = true - const uri = om.uri - const range = s.range - const key = uri?.fsPath || '' - if (!existing.has(key)) { - existing.add(key) - await chatThreadsService.addNewStagingSelection({ - type: 'File', - uri, - language: languageService.guessLanguageIdByFilepathOrFirstLine(uri) || '', - state: { wasAddedAsCurrentFile: false }, - range, - }) + // Handle explicit symbol: @sym:Name or @symbol:Name + if (raw.startsWith("sym:") || raw.startsWith("symbol:")) { + const symName = raw.replace(/^symbol?:/, ""); + let symbolFound = false; + if ( + outlineService && + typeof outlineService.getCachedModels === "function" + ) { + try { + const models = outlineService.getCachedModels(); + for (const om of models) { + const list = + typeof om.asListOfDocumentSymbols === "function" + ? om.asListOfDocumentSymbols() + : []; + for (const s of list) { + if ( + (s?.name || "").toLowerCase() === symName.toLowerCase() + ) { + symbolFound = true; + const uri = om.uri; + const range = s.range; + const key = uri?.fsPath || ""; + if (!existing.has(key)) { + existing.add(key); + await chatThreadsService.addNewStagingSelection({ + type: "File", + uri, + language: + languageService.guessLanguageIdByFilepathOrFirstLine( + uri, + ) || "", + state: { wasAddedAsCurrentFile: false }, + range, + }); + } } } } + } catch (err) { + // Service error - log but continue + console.warn("Error resolving symbol:", err); } - } catch (err) { - // Service error - log but continue - console.warn('Error resolving symbol:', err) } + if (!symbolFound) { + unresolvedRefs.push(`@${raw} (symbol not found)`); + } + continue; } - if (!symbolFound) { - unresolvedRefs.push(`@${raw} (symbol not found)`) - } - continue - } - // Handle explicit folder keyword like: @folder:path or plain name that matches a folder - let query = raw - let isFolderHint = false - if (raw.startsWith('folder:')) { - isFolderHint = true - query = raw.slice('folder:'.length) - } + // Handle explicit folder keyword like: @folder:path or plain name that matches a folder + let query = raw; + let isFolderHint = false; + if (raw.startsWith("folder:")) { + isFolderHint = true; + query = raw.slice("folder:".length); + } - // Use tools service to resolve best match in workspace - let resolved = false - try { - const res = await (await toolsService.callTool.search_pathnames_only({ query, includePattern: null, pageNumber: 1 })).result - const [first] = res.uris || [] - if (first) { - resolved = true - // Heuristic: if hint says folder or resolved path ends with '/', treat as folder - if (isFolderHint) await addFolderSelection(first) - else await addFileSelection(first) + // Use tools service to resolve best match in workspace + let resolved = false; + try { + const res = await ( + await toolsService.callTool.search_pathnames_only({ + query, + includePattern: null, + pageNumber: 1, + }) + ).result; + const [first] = res.uris || []; + if (first) { + resolved = true; + // Heuristic: if hint says folder or resolved path ends with '/', treat as folder + if (isFolderHint) await addFolderSelection(first); + else await addFileSelection(first); + } + } catch (err) { + // Service error - log but continue + console.warn("Error resolving reference:", err); + } + if (!resolved) { + unresolvedRefs.push(`@${raw}`); } - } catch (err) { - // Service error - log but continue - console.warn('Error resolving reference:', err) } - if (!resolved) { - unresolvedRefs.push(`@${raw}`) + + // Report unresolved references to user + if (unresolvedRefs.length > 0) { + const refList = unresolvedRefs.slice(0, 3).join(", "); + const moreText = + unresolvedRefs.length > 3 + ? ` and ${unresolvedRefs.length - 3} more` + : ""; + notificationService.warn( + `Could not resolve reference${unresolvedRefs.length > 1 ? "s" : ""}: ${refList}${moreText}. Please check the file path or symbol name.`, + ); } + } catch (err) { + // Best-effort; do not block send, but log error + console.warn("Error resolving @references:", err); } - // Report unresolved references to user - if (unresolvedRefs.length > 0) { - const refList = unresolvedRefs.slice(0, 3).join(', ') - const moreText = unresolvedRefs.length > 3 ? ` and ${unresolvedRefs.length - 3} more` : '' - notificationService.warn(`Could not resolve reference${unresolvedRefs.length > 1 ? 's' : ''}: ${refList}${moreText}. Please check the file path or symbol name.`) + // Convert image attachments to ChatImageAttachment format + const images: ChatImageAttachment[] = imageAttachments + .filter((att) => att.uploadStatus === "success" || !att.uploadStatus) + .map((att) => ({ + id: att.id, + data: att.data, + mimeType: att.mimeType, + filename: att.filename, + width: att.width, + height: att.height, + size: att.size, + })); + + // Check if any PDFs are still processing + const processingPDFs = pdfAttachments.filter( + (att) => + att.uploadStatus === "uploading" || att.uploadStatus === "processing", + ); + + if (processingPDFs.length > 0) { + const processingNames = processingPDFs + .map((p) => p.filename) + .join(", "); + notificationService.warn( + `Some PDFs are still processing: ${processingNames}. They will be sent but may not have extracted text available yet.`, + ); } - } catch (err) { - // Best-effort; do not block send, but log error - console.warn('Error resolving @references:', err) - } - - // Convert image attachments to ChatImageAttachment format - const images: ChatImageAttachment[] = imageAttachments - .filter(att => att.uploadStatus === 'success' || !att.uploadStatus) - .map(att => ({ - id: att.id, - data: att.data, - mimeType: att.mimeType, - filename: att.filename, - width: att.width, - height: att.height, - size: att.size, - })); - - // Check if any PDFs are still processing - const processingPDFs = pdfAttachments.filter( - att => att.uploadStatus === 'uploading' || att.uploadStatus === 'processing' - ); - if (processingPDFs.length > 0) { - const processingNames = processingPDFs.map(p => p.filename).join(', '); - notificationService.warn(`Some PDFs are still processing: ${processingNames}. They will be sent but may not have extracted text available yet.`); - } - - // Convert PDF attachments to ChatPDFAttachment format - // Include PDFs that are successful, have no status, or are still processing (they might have partial data) - // Exclude only failed PDFs - const pdfs: ChatPDFAttachment[] = pdfAttachments - .filter(att => att.uploadStatus !== 'failed') - .map(att => ({ - id: att.id, - data: att.data, - filename: att.filename, - size: att.size, - pageCount: att.pageCount, - selectedPages: att.selectedPages, - extractedText: att.extractedText, - pagePreviews: att.pagePreviews, - })); - - // Validate that model supports vision/PDFs if attachments are present - const currentModelSel = settingsState.modelSelectionOfFeature['Chat']; - if ((images.length > 0 || pdfs.length > 0) && currentModelSel) { - const { isSelectedModelVisionCapable, checkOllamaModelVisionCapable, hasVisionCapableApiKey, hasOllamaVisionModel, isOllamaAccessible } = await import('../util/visionModelHelper.js'); - - // In auto mode, check if user has any vision-capable models available - if (currentModelSel.providerName === 'auto' && currentModelSel.modelName === 'auto') { - // Images require vision-capable models (no fallback) - if (images.length > 0) { - const hasApiKey = hasVisionCapableApiKey(settingsState.settingsOfProvider, currentModelSel); - const ollamaAccessible = await isOllamaAccessible(); - const hasOllamaVision = ollamaAccessible && await hasOllamaVisionModel(); - - if (!hasApiKey && !hasOllamaVision) { - notificationService.error('No vision-capable models available. Please set up an API key (Anthropic, OpenAI, or Gemini) or install an Ollama vision model (e.g., llava, bakllava) to use images.'); - return; + // Convert PDF attachments to ChatPDFAttachment format + // Include PDFs that are successful, have no status, or are still processing (they might have partial data) + // Exclude only failed PDFs + const pdfs: ChatPDFAttachment[] = pdfAttachments + .filter((att) => att.uploadStatus !== "failed") + .map((att) => ({ + id: att.id, + data: att.data, + filename: att.filename, + size: att.size, + pageCount: att.pageCount, + selectedPages: att.selectedPages, + extractedText: att.extractedText, + pagePreviews: att.pagePreviews, + })); + + // Validate that model supports vision/PDFs if attachments are present + const currentModelSel = settingsState.modelSelectionOfFeature["Chat"]; + if ((images.length > 0 || pdfs.length > 0) && currentModelSel) { + const { + isSelectedModelVisionCapable, + checkOllamaModelVisionCapable, + hasVisionCapableApiKey, + hasOllamaVisionModel, + isOllamaAccessible, + } = await import("../util/visionModelHelper.js"); + + // In auto mode, check if user has any vision-capable models available + if ( + currentModelSel.providerName === "auto" && + currentModelSel.modelName === "auto" + ) { + // Images require vision-capable models (no fallback) + if (images.length > 0) { + const hasApiKey = hasVisionCapableApiKey( + settingsState.settingsOfProvider, + currentModelSel, + ); + const ollamaAccessible = await isOllamaAccessible(); + const hasOllamaVision = + ollamaAccessible && (await hasOllamaVisionModel()); + + if (!hasApiKey && !hasOllamaVision) { + notificationService.error( + "No vision-capable models available. Please set up an API key (Anthropic, OpenAI, or Gemini) or install an Ollama vision model (e.g., llava, bakllava) to use images.", + ); + return; + } } - } - // PDFs can work with non-vision models via text extraction, so we allow them even without vision-capable models - // If vision-capable models are available, router will select appropriate model - } else { - // For non-auto mode, check if the selected model is vision-capable - let isVisionCapable = isSelectedModelVisionCapable(currentModelSel, settingsState.settingsOfProvider); - - // If Ollama, check via API - if (!isVisionCapable && currentModelSel.providerName === 'ollama') { - const ollamaAccessible = await isOllamaAccessible(); - if (ollamaAccessible) { - isVisionCapable = await checkOllamaModelVisionCapable(currentModelSel.modelName); + // PDFs can work with non-vision models via text extraction, so we allow them even without vision-capable models + // If vision-capable models are available, router will select appropriate model + } else { + // For non-auto mode, check if the selected model is vision-capable + let isVisionCapable = isSelectedModelVisionCapable( + currentModelSel, + settingsState.settingsOfProvider, + ); + + // If Ollama, check via API + if (!isVisionCapable && currentModelSel.providerName === "ollama") { + const ollamaAccessible = await isOllamaAccessible(); + if (ollamaAccessible) { + isVisionCapable = await checkOllamaModelVisionCapable( + currentModelSel.modelName, + ); + } } - } - - // If not vision-capable, show error - if (!isVisionCapable) { - const hasApiKey = hasVisionCapableApiKey(settingsState.settingsOfProvider, currentModelSel); - const ollamaAccessible = await isOllamaAccessible(); - const hasOllamaVision = ollamaAccessible && await hasOllamaVisionModel(); - if (!hasApiKey && !hasOllamaVision) { - notificationService.error('The selected model does not support images or PDFs. Please select a vision-capable model (e.g., Claude, GPT-4, Gemini, or an Ollama vision model like llava).'); - return; - } else { - notificationService.warn('The selected model may not support images or PDFs. Consider switching to a vision-capable model for better results.'); + // If not vision-capable, show error + if (!isVisionCapable) { + const hasApiKey = hasVisionCapableApiKey( + settingsState.settingsOfProvider, + currentModelSel, + ); + const ollamaAccessible = await isOllamaAccessible(); + const hasOllamaVision = + ollamaAccessible && (await hasOllamaVisionModel()); + + if (!hasApiKey && !hasOllamaVision) { + notificationService.error( + "The selected model does not support images or PDFs. Please select a vision-capable model (e.g., Claude, GPT-4, Gemini, or an Ollama vision model like llava).", + ); + return; + } else { + notificationService.warn( + "The selected model may not support images or PDFs. Consider switching to a vision-capable model for better results.", + ); + } } } } - } - // Capture staging selections BEFORE clearing them, so they're included in the message - const stagingSelections = chatThreadsState.allThreads[currentThread.id]?.state?.stagingSelections || [] + // Capture staging selections BEFORE clearing them, so they're included in the message + const stagingSelections = + chatThreadsState.allThreads[currentThread.id]?.state + ?.stagingSelections || []; - // Optimistic UI: Clear input and attachments immediately for perceived responsiveness - setSelections([]) // clear staging - if (textAreaFnsRef.current) { - textAreaFnsRef.current.setValue('') - } - clearImages() // clear image attachments - clearPDFs() // clear PDF attachments - textAreaRef.current?.focus() // focus input after submit - - // Send message (non-blocking for UI responsiveness) - try { - await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, threadId, images, pdfs, _chatSelections: stagingSelections }) - } catch (e) { - console.error('Error while sending message in chat:', e) - } + // Optimistic UI: Clear input and attachments immediately for perceived responsiveness + setSelections([]); // clear staging + if (textAreaFnsRef.current) { + textAreaFnsRef.current.setValue(""); + } + clearImages(); // clear image attachments + clearPDFs(); // clear PDF attachments + textAreaRef.current?.focus(); // focus input after submit - }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState, imageAttachments, pdfAttachments, clearImages, clearPDFs, currentThread.id]) + // Send message (non-blocking for UI responsiveness) + try { + await chatThreadsService.addUserMessageAndStreamResponse({ + userMessage, + threadId, + images, + pdfs, + _chatSelections: stagingSelections, + }); + } catch (e) { + console.error("Error while sending message in chat:", e); + } + }, + [ + chatThreadsService, + isDisabled, + isRunning, + textAreaRef, + textAreaFnsRef, + setSelections, + settingsState, + imageAttachments, + pdfAttachments, + clearImages, + clearPDFs, + currentThread.id, + ], + ); const onAbort = async () => { - const threadId = currentThread.id - await chatThreadsService.abortRunning(threadId) - } - - const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(CORTEXIDE_CTRL_L_ACTION_ID)?.getLabel() - - const threadId = currentThread.id - const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity) + const threadId = currentThread.id; + await chatThreadsService.abortRunning(threadId); + }; + const keybindingString = accessor + .get("IKeybindingService") + .lookupKeybinding(CORTEXIDE_CTRL_L_ACTION_ID) + ?.getLabel(); + const threadId = currentThread.id; + const currCheckpointIdx = + chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? + undefined; // if not exist, treat like checkpoint is last message (infinity) // resolve mount info // Accessing .current is safe - refs don't trigger re-renders when changed - const mountedInfo = chatThreadsState.allThreads[threadId]?.state.mountedInfo - const isResolved = mountedInfo?.mountedIsResolvedRef.current + const mountedInfo = chatThreadsState.allThreads[threadId]?.state.mountedInfo; + const isResolved = mountedInfo?.mountedIsResolvedRef.current; useEffect(() => { - if (isResolved) return + if (isResolved) return; mountedInfo?._whenMountedResolver?.({ textAreaRef: textAreaRef, scrollToBottom: scrollToBottomCallback, - }) - - }, [threadId, textAreaRef, scrollContainerRef, isResolved, mountedInfo, scrollToBottomCallback]) - - - + }); + }, [ + threadId, + textAreaRef, + scrollContainerRef, + isResolved, + mountedInfo, + scrollToBottomCallback, + ]); const previousMessagesHTML = useMemo(() => { // const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') @@ -4379,415 +5803,628 @@ export const SidebarChat = () => { // Use stable keys based on message ID or index for better React reconciliation return previousMessages.map((message, i) => { // Use message ID if available, otherwise fall back to index - const messageKey = (message as any).id || `msg-${i}` - return - }) - }, [previousMessages, threadId, currCheckpointIdx, isRunning, scrollToBottomCallback]) - - const streamingChatIdx = previousMessagesHTML.length + const messageKey = (message as any).id || `msg-${i}`; + return ( + + ); + }); + }, [ + previousMessages, + threadId, + currCheckpointIdx, + isRunning, + scrollToBottomCallback, + ]); + + const streamingChatIdx = previousMessagesHTML.length; // Memoize chatMessage object to avoid recreating on every render - const streamingChatMessage = useMemo(() => ({ - role: 'assistant' as const, - displayContent: displayContentSoFar ?? '', - reasoning: reasoningSoFar ?? '', - anthropicReasoning: null, - }), [displayContentSoFar, reasoningSoFar]) + const streamingChatMessage = useMemo( + () => ({ + role: "assistant" as const, + displayContent: displayContentSoFar ?? "", + reasoning: reasoningSoFar ?? "", + anthropicReasoning: null, + }), + [displayContentSoFar, reasoningSoFar], + ); // Only show streaming message when actively streaming (LLM, tool, or preparing) // Don't show when idle/undefined to prevent duplicate messages and never-ending loading // Only show stop button when actively running (LLM, tool, preparing), not when idle - const isActivelyStreaming = isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'preparing' - const currStreamingMessageHTML = isActivelyStreaming && (reasoningSoFar || displayContentSoFar) ? - : null + const isActivelyStreaming = + isRunning === "LLM" || isRunning === "tool" || isRunning === "preparing"; + const activityStatus = useMemo(() => { + if (!isActivelyStreaming) { + return null; + } + + // Prefer explicit status text from the stream state when preparing + if (isRunning === "preparing") { + const primary = + displayContentSoFar && displayContentSoFar.trim().length > 0 + ? displayContentSoFar + : "Preparing your request…"; + return { primary, secondary: undefined as string | undefined }; + } + + // Tool phase – surface which tool is running when possible + if (isRunning === "tool" || toolIsGenerating) { + let toolLabel = "Running tools…"; + if (toolCallSoFar?.name) { + toolLabel = `Running tool: ${toolCallSoFar.name}`; + } + return { + primary: toolLabel, + secondary: "Applying changes and reading from your project.", + }; + } + + // LLM phase – distinguish thinking vs. writing + if (isRunning === "LLM") { + if (!displayContentSoFar && !reasoningSoFar) { + return { + primary: "Thinking through your request…", + secondary: "Analyzing your code, context, and instructions.", + }; + } + + if (reasoningSoFar && !displayContentSoFar) { + return { + primary: "Planning the best set of changes…", + secondary: undefined, + }; + } + + if (displayContentSoFar) { + return { + primary: "Writing the answer…", + secondary: undefined, + }; + } + } + + return { + primary: "Working on your request…", + secondary: undefined, + }; + }, [ + isActivelyStreaming, + isRunning, + displayContentSoFar, + reasoningSoFar, + toolIsGenerating, + toolCallSoFar, + ]); + + const currStreamingMessageHTML = + isActivelyStreaming && (reasoningSoFar || displayContentSoFar) ? ( + + ) : null; // the tool currently being generated - const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'edit_file' || toolCallSoFar.name === 'rewrite_file' ? - : null - : null + const generatingTool = toolIsGenerating ? ( + toolCallSoFar.name === "edit_file" || + toolCallSoFar.name === "rewrite_file" ? ( + + ) : null + ) : null; - const messagesHTML = - {/* previous messages */} - {previousMessagesHTML} - {currStreamingMessageHTML} - - {/* Generating tool */} - {generatingTool} - - {/* loading indicator with token count */} - {(isRunning === 'LLM' || isRunning === 'preparing') ? - - : null} + > + {/* previous messages */} + {previousMessagesHTML} + {currStreamingMessageHTML} + {/* Generating tool */} + {generatingTool} - {/* error message */} - {latestError === undefined ? null : -
    - { chatThreadsService.dismissStreamError(currentThread.id) }} - showDismiss={true} - /> + {/* Claude-style activity banner with spinner + loading indicator with token count */} + {activityStatus ? ( + +
    +
    + +
    +
    + + {activityStatus.primary} + + {activityStatus.secondary && ( + + {activityStatus.secondary} + + )} +
    +
    +
    + ) : null} - { commandService.executeCommand(CORTEXIDE_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' /> -
    - } -
    + {/* error message */} + {latestError === undefined ? null : ( +
    + { + chatThreadsService.dismissStreamError(currentThread.id); + }} + showDismiss={true} + /> + { + commandService.executeCommand(CORTEXIDE_OPEN_SETTINGS_ACTION_ID); + }} + text="Open settings" + /> +
    + )} + + ); - const onChangeText = useCallback((newStr: string) => { - setInstructionsAreEmpty(!newStr) - }, [setInstructionsAreEmpty]) - const onKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { - onSubmit() - } else if (e.key === 'Escape' && isRunning) { - onAbort() - } - }, [onSubmit, onAbort, isRunning]) + const onChangeText = useCallback( + (newStr: string) => { + setInstructionsAreEmpty(!newStr); + }, + [setInstructionsAreEmpty], + ); + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { + onSubmit(); + } else if (e.key === "Escape" && isRunning) { + onAbort(); + } + }, + [onSubmit, onAbort, isRunning], + ); // Context usage calculation + warning (partially memoized - draft tokens calculated on each render) - const [ctxWarned, setCtxWarned] = useState(false) - const estimateTokens = useCallback((s: string) => Math.ceil((s || '').length / 4), []) - const modelSel = settingsState.modelSelectionOfFeature['Chat'] + const [ctxWarned, setCtxWarned] = useState(false); + const estimateTokens = useCallback( + (s: string) => Math.ceil((s || "").length / 4), + [], + ); + const modelSel = settingsState.modelSelectionOfFeature["Chat"]; // Memoize context budget and messages tokens (only recalculate when messages or model changes) const { contextBudget, messagesTokens } = useMemo(() => { - let budget = 0 - let tokens = 0 + let budget = 0; + let tokens = 0; if (modelSel && isValidProviderModelSelection(modelSel)) { - const { providerName, modelName } = modelSel - const caps = getModelCapabilities(providerName, modelName, settingsState.overridesOfModel) - const contextWindow = caps.contextWindow - const msOpts = settingsState.optionsOfModelSelection['Chat'][providerName]?.[modelName] - const isReasoningEnabled2 = getIsReasoningEnabledState('Chat', providerName, modelName, msOpts, settingsState.overridesOfModel) - const rot = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled: isReasoningEnabled2, overridesOfModel: settingsState.overridesOfModel }) || 0 - budget = Math.max(256, Math.floor(contextWindow * 0.8) - rot) + const { providerName, modelName } = modelSel; + const caps = getModelCapabilities( + providerName, + modelName, + settingsState.overridesOfModel, + ); + const contextWindow = caps.contextWindow; + const msOpts = + settingsState.optionsOfModelSelection["Chat"][providerName]?.[ + modelName + ]; + const isReasoningEnabled2 = getIsReasoningEnabledState( + "Chat", + providerName, + modelName, + msOpts, + settingsState.overridesOfModel, + ); + const rot = + getReservedOutputTokenSpace(providerName, modelName, { + isReasoningEnabled: isReasoningEnabled2, + overridesOfModel: settingsState.overridesOfModel, + }) || 0; + budget = Math.max(256, Math.floor(contextWindow * 0.8) - rot); tokens = previousMessages.reduce((acc, m) => { - if (m.role === 'user') return acc + estimateTokens(m.content || '') - if (m.role === 'assistant') return acc + estimateTokens((m.displayContent as string) || (m.content || '') || '') - return acc - }, 0) + if (m.role === "user") return acc + estimateTokens(m.content || ""); + if (m.role === "assistant") + return ( + acc + + estimateTokens( + (m.displayContent as string) || m.content || "" || "", + ) + ); + return acc; + }, 0); } - return { contextBudget: budget, messagesTokens: tokens } - }, [modelSel, previousMessages, settingsState.overridesOfModel, estimateTokens]) + return { contextBudget: budget, messagesTokens: tokens }; + }, [ + modelSel, + previousMessages, + settingsState.overridesOfModel, + estimateTokens, + ]); // Calculate draft tokens and total on each render (draft changes frequently) - const draftTokens = estimateTokens(textAreaRef.current?.value || '') - const contextTotal = messagesTokens + draftTokens - const contextPct = contextBudget > 0 ? contextTotal / contextBudget : 0 + const draftTokens = estimateTokens(textAreaRef.current?.value || ""); + const contextTotal = messagesTokens + draftTokens; + const contextPct = contextBudget > 0 ? contextTotal / contextBudget : 0; useEffect(() => { if (contextPct > 0.8 && contextPct < 1 && !ctxWarned) { - try { accessor.get('INotificationService').info(`Context nearing limit: ~${contextTotal} / ${contextBudget} tokens. Older messages may be summarized.`) } catch {} - setCtxWarned(true) - } - if (contextPct < 0.6 && ctxWarned) setCtxWarned(false) - }, [contextPct, ctxWarned, contextTotal, contextBudget, accessor]) - - const inputChatArea = onSubmit()} - onAbort={onAbort} - isStreaming={isActivelyStreaming} - isDisabled={isDisabled} - showSelections={true} - // showProspectiveSelections={previousMessagesHTML.length === 0} - selections={selections} - setSelections={setSelections} - onClickAnywhere={() => { textAreaRef.current?.focus() }} - imageAttachments={ - imageAttachments.length > 0 ? ( - <> - - {imageValidationError && ( -
    - {imageValidationError.message} -
    - )} - - ) : null - } - onImagePaste={addImages} - onImageDrop={addImages} - onPDFDrop={addPDFs} - pdfAttachments={ - pdfAttachments.length > 0 ? ( - <> - - {pdfValidationError && ( -
    - {pdfValidationError} -
    - )} - - ) : null + try { + accessor + .get("INotificationService") + .info( + `Context nearing limit: ~${contextTotal} / ${contextBudget} tokens. Older messages may be summarized.`, + ); + } catch {} + setCtxWarned(true); } - > - { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} - ref={textAreaRef} - fnsRef={textAreaFnsRef} - multiline={true} - /> + if (contextPct < 0.6 && ctxWarned) setCtxWarned(false); + }, [contextPct, ctxWarned, contextTotal, contextBudget, accessor]); - {/* Context chips for current selections */} - {selections.length > 0 && ( -
    - {selections.map((sel, idx) => { - const name = sel.type === 'Folder' - ? (sel.uri?.path?.split('/').filter(Boolean).pop() || 'folder') - : (sel.uri?.path?.split('/').pop() || 'file') - const fullPath = sel.uri?.fsPath || sel.uri?.path || name - const rangeLabel = (sel as any).range ? ` • ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber}` : '' - const tooltipText = (sel as any).range - ? `${fullPath} (lines ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber})` - : fullPath - return ( - - {sel.type === 'Folder' ? 'Folder' : 'File'} - {name} - {rangeLabel && {rangeLabel}} - - - ) - })} -
    - )} + const inputChatArea = ( + onSubmit()} + onAbort={onAbort} + isStreaming={isActivelyStreaming} + isDisabled={isDisabled} + showSelections={true} + // showProspectiveSelections={previousMessagesHTML.length === 0} + selections={selections} + setSelections={setSelections} + onClickAnywhere={() => { + textAreaRef.current?.focus(); + }} + imageAttachments={ + imageAttachments.length > 0 ? ( + <> + + {imageValidationError && ( +
    + {imageValidationError.message} +
    + )} + + ) : null + } + onImagePaste={addImages} + onImageDrop={addImages} + onPDFDrop={addPDFs} + pdfAttachments={ + pdfAttachments.length > 0 ? ( + <> + + {pdfValidationError && ( +
    + {pdfValidationError} +
    + )} + + ) : null + } + > + { + chatThreadsService.setCurrentlyFocusedMessageIdx(undefined); + }} + ref={textAreaRef} + fnsRef={textAreaFnsRef} + multiline={true} + /> -
    + {/* Context chips for current selections */} + {selections.length > 0 && ( +
    + {selections.map((sel, idx) => { + const name = + sel.type === "Folder" + ? sel.uri?.path?.split("/").filter(Boolean).pop() || "folder" + : sel.uri?.path?.split("/").pop() || "file"; + const fullPath = sel.uri?.fsPath || sel.uri?.path || name; + const rangeLabel = (sel as any).range + ? ` • ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber}` + : ""; + const tooltipText = (sel as any).range + ? `${fullPath} (lines ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber})` + : fullPath; + return ( + + + {sel.type === "Folder" ? "Folder" : "File"} + + {name} + {rangeLabel && {rangeLabel}} + + + ); + })} +
    + )} +
    + ); + const isLandingPage = previousMessages.length === 0; - const isLandingPage = previousMessages.length === 0 + const initiallySuggestedPromptsHTML = ( +
    + {[ + "Summarize my codebase", + "How do types work in Rust?", + "Create a .voidrules file for me", + ].map((text, index) => ( +
    onSubmit(text)} + > + {text} +
    + ))} +
    + ); + const threadPageInput = ( +
    +
    + +
    +
    + {inputChatArea} + + {/* Context usage indicator */} + {modelSel + ? (() => { + const pctNum = Math.max( + 0, + Math.min(100, Math.round(contextPct * 100)), + ); + const color = + contextPct >= 1 + ? "text-red-500" + : contextPct > 0.8 + ? "text-amber-500" + : "text-void-fg-3"; + const barColor = + contextPct >= 1 + ? "bg-red-500" + : contextPct > 0.8 + ? "bg-amber-500" + : "bg-void-fg-3/60"; + return ( +
    +
    + Context ~{contextTotal} / {contextBudget} tokens ({pctNum}%) +
    +
    +
    +
    +
    + ); + })() + : null} +
    +
    + ); - const initiallySuggestedPromptsHTML =
    - {[ - 'Summarize my codebase', - 'How do types work in Rust?', - 'Create a .voidrules file for me' - ].map((text, index) => ( -
    onSubmit(text)} - > - {text} + const landingPageInput = ( +
    +
    + {inputChatArea} + {modelSel + ? (() => { + const pctNum = Math.max( + 0, + Math.min(100, Math.round(contextPct * 100)), + ); + const color = + contextPct >= 1 + ? "text-red-500" + : contextPct > 0.8 + ? "text-amber-500" + : "text-void-fg-3"; + const barColor = + contextPct >= 1 + ? "bg-red-500" + : contextPct > 0.8 + ? "bg-amber-500" + : "bg-void-fg-3/60"; + return ( +
    +
    + Context ~{contextTotal} / {contextBudget} tokens ({pctNum}%) +
    +
    +
    +
    +
    + ); + })() + : null}
    - ))} -
    +
    + ); + const keybindingService = accessor.get("IKeybindingService"); + const quickActions: { id: string; label: string }[] = [ + { id: "void.explainCode", label: "Explain" }, + { id: "void.refactorCode", label: "Refactor" }, + { id: "void.addTests", label: "Add Tests" }, + { id: "void.fixTests", label: "Fix Tests" }, + { id: "void.writeDocstring", label: "Docstring" }, + { id: "void.optimizeCode", label: "Optimize" }, + { id: "void.debugCode", label: "Debug" }, + ]; + + const QuickActionsBar = () => ( +
    + {quickActions.map(({ id, label }) => { + const kb = keybindingService.lookupKeybinding(id)?.getLabel(); + return ( + + ); + })} +
    + ); + // Lightweight context chips: active file and model + const ContextChipsBar = () => { + const editorService = accessor.get("IEditorService"); + const activeEditor = editorService?.activeEditor; + // Try best-effort file label + const activeResource = activeEditor?.resource; + const activeFileLabel = activeResource + ? activeResource.path?.split("/").pop() + : undefined; + const modelSel = settingsState.modelSelectionOfFeature["Chat"]; + const modelLabel = modelSel + ? `${modelSel.providerName}:${modelSel.modelName}` + : undefined; + if (!activeFileLabel && !modelLabel) return null; + return ( +
    + {activeFileLabel && ( + + File + {activeFileLabel} + + )} + {modelLabel && ( + + Model + {modelLabel} + + )} +
    + ); + }; - const threadPageInput =
    -
    - -
    -
    - {inputChatArea} - - {/* Context usage indicator */} - {modelSel ? ( - (() => { - const pctNum = Math.max(0, Math.min(100, Math.round(contextPct * 100))) - const color = contextPct >= 1 ? 'text-red-500' : contextPct > 0.8 ? 'text-amber-500' : 'text-void-fg-3' - const barColor = contextPct >= 1 ? 'bg-red-500' : contextPct > 0.8 ? 'bg-amber-500' : 'bg-void-fg-3/60' - return
    -
    Context ~{contextTotal} / {contextBudget} tokens ({pctNum}%)
    -
    -
    -
    -
    - })() - ) : null} -
    -
    - - const landingPageInput =
    -
    - {inputChatArea} - {modelSel ? ( - (() => { - const pctNum = Math.max(0, Math.min(100, Math.round(contextPct * 100))) - const color = contextPct >= 1 ? 'text-red-500' : contextPct > 0.8 ? 'text-amber-500' : 'text-void-fg-3' - const barColor = contextPct >= 1 ? 'bg-red-500' : contextPct > 0.8 ? 'bg-amber-500' : 'bg-void-fg-3/60' - return
    -
    Context ~{contextTotal} / {contextBudget} tokens ({pctNum}%)
    -
    -
    -
    -
    - })() - ) : null} -
    -
    - - const keybindingService = accessor.get('IKeybindingService') - const quickActions: { id: string, label: string }[] = [ - { id: 'void.explainCode', label: 'Explain' }, - { id: 'void.refactorCode', label: 'Refactor' }, - { id: 'void.addTests', label: 'Add Tests' }, - { id: 'void.fixTests', label: 'Fix Tests' }, - { id: 'void.writeDocstring', label: 'Docstring' }, - { id: 'void.optimizeCode', label: 'Optimize' }, - { id: 'void.debugCode', label: 'Debug' }, - ] - - const QuickActionsBar = () => ( -
    - {quickActions.map(({ id, label }) => { - const kb = keybindingService.lookupKeybinding(id)?.getLabel() - return ( - - ) - })} -
    - ) - - // Lightweight context chips: active file and model - const ContextChipsBar = () => { - const editorService = accessor.get('IEditorService') - const activeEditor = editorService?.activeEditor - // Try best-effort file label - const activeResource = activeEditor?.resource - const activeFileLabel = activeResource ? activeResource.path?.split('/').pop() : undefined - const modelSel = settingsState.modelSelectionOfFeature['Chat'] - const modelLabel = modelSel ? `${modelSel.providerName}:${modelSel.modelName}` : undefined - if (!activeFileLabel && !modelLabel) return null - return ( -
    - {activeFileLabel && ( - - File - {activeFileLabel} - - )} - {modelLabel && ( - - Model - {modelLabel} - - )} -
    - ) - } - - const landingPageContent =
    - - {landingPageInput} - - - {/* Context chips */} - - - - - {/* Quick Actions shortcuts */} - - - - - {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads + const landingPageContent = ( +
    + {landingPageInput} + + {/* Context chips */} -
    Previous Threads
    - +
    - : + + {/* Quick Actions shortcuts */} -
    Suggestions
    - {initiallySuggestedPromptsHTML} +
    - } -
    + {Object.keys(chatThreadsState.allThreads).length > 1 ? ( // show if there are threads + +
    + Previous Threads +
    + +
    + ) : ( + +
    + Suggestions +
    + {initiallySuggestedPromptsHTML} +
    + )} +
    + ); // const threadPageContent =
    // {/* Thread content */} @@ -4802,26 +6439,230 @@ export const SidebarChat = () => { // //
    //
    - const threadPageContent =
    + const threadPageContent = ( +
    + {messagesHTML} + {threadPageInput} +
    + ); + + // Chat Tabs Bar Component - displays all threads as tabs + const ChatTabsBar = () => { + // Track threads that have been accessed in the current session + const accessedThreadsRef = useRef>(new Set()); + + // Update accessed threads when switching + useEffect(() => { + if (chatThreadsState.currentThreadId) { + accessedThreadsRef.current.add(chatThreadsState.currentThreadId); + } + }, [chatThreadsState.currentThreadId]); - - {messagesHTML} - - - {threadPageInput} - -
    + // Only show threads that are actively being used, not all historical threads + // This prevents closed tabs from reappearing + const MAX_TABS_TO_SHOW = 12; + const now = Date.now(); + const RECENT_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + + const allThreadIds = Object.keys(chatThreadsState.allThreads) + .filter((threadId) => { + const thread = chatThreadsState.allThreads[threadId]; + if (!thread) return false; + + // Always include the current thread + if (threadId === chatThreadsState.currentThreadId) return true; + + // Only include threads that have messages (active chats) + if (thread.messages.length === 0) return false; + + // Include threads accessed in current session + if (accessedThreadsRef.current.has(threadId)) return true; + + // Include threads accessed recently (within last 24 hours) + const lastModified = new Date(thread.lastModified).getTime(); + return (now - lastModified) < RECENT_THRESHOLD; + }) + .sort((a, b) => { + const threadA = chatThreadsState.allThreads[a]; + const threadB = chatThreadsState.allThreads[b]; + if (!threadA || !threadB) return 0; + + // Prioritize current thread + if (a === chatThreadsState.currentThreadId) return -1; + if (b === chatThreadsState.currentThreadId) return 1; + + // Then prioritize threads accessed in session + const aAccessed = accessedThreadsRef.current.has(a); + const bAccessed = accessedThreadsRef.current.has(b); + if (aAccessed && !bAccessed) return -1; + if (!aAccessed && bAccessed) return 1; + + // Finally sort by last modified + return ( + new Date(threadB.lastModified).getTime() - + new Date(threadA.lastModified).getTime() + ); + }) + .slice(0, MAX_TABS_TO_SHOW); + + const getThreadTitle = (threadId: string) => { + const thread = chatThreadsState.allThreads[threadId]; + if (!thread) return "New Chat"; + const firstUserMsg = thread.messages.find((msg) => msg.role === "user"); + if (firstUserMsg && firstUserMsg.displayContent) { + const title = firstUserMsg.displayContent.trim(); + return title.length > 28 ? title.substring(0, 28) + "..." : title; + } + return "New Chat"; + }; + + const handleCloseTab = useCallback( + (e: React.MouseEvent, threadId: string) => { + e.stopPropagation(); + const allThreadIds = Object.keys(chatThreadsState.allThreads); + + // If this is the last tab, don't close it (always keep at least one tab) + if (allThreadIds.length === 1) { + return; + } + + // Remove from accessed threads set + accessedThreadsRef.current.delete(threadId); + + // If this is the active tab, switch to another one first + if (threadId === chatThreadsState.currentThreadId) { + // Find the next available thread from all threads + const otherThreadId = allThreadIds.find((id) => id !== threadId); + if (otherThreadId) { + accessedThreadsRef.current.add(otherThreadId); + chatThreadsService.switchToThread(otherThreadId); + } + } + // Delete the thread + chatThreadsService.deleteThread(threadId); + }, + [chatThreadsService, chatThreadsState], + ); + + return ( +
    +
    + {allThreadIds.map((threadId, index) => { + const isActive = threadId === chatThreadsState.currentThreadId; + const title = getThreadTitle(threadId); + const thread = chatThreadsState.allThreads[threadId]; + const hasMessages = thread && thread.messages.length > 0; + const messageCount = thread + ? thread.messages.filter( + (m) => m.role === "user" || m.role === "assistant", + ).length + : 0; + + return ( +
    + + {/* Close button - always visible on active tab, hover on others */} + {allThreadIds.length > 1 && ( + + )} +
    + ); + })} +
    +
    + ); + }; return ( - - {isLandingPage ? - landingPageContent - : threadPageContent} +
    + {/* Tabs Bar - always visible when there are threads */} + {Object.keys(chatThreadsState.allThreads).length > 0 && ( + + + + )} + {/* Content */} +
    + {isLandingPage ? landingPageContent : threadPageContent} +
    +
    - ) -} + ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css b/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css index 0b595a28823..fecc52cbe1b 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css @@ -21,6 +21,8 @@ outline: none !important; } +/* Allow rounded corners - removed global override */ + .inherit-bg-all-restyle > * { background-color: inherit !important; } @@ -73,10 +75,10 @@ } /* rounded corners utilities */ -.void-rounded-sm { border-radius: var(--void-radius-sm); } -.void-rounded-md { border-radius: var(--void-radius-md); } -.void-rounded-lg { border-radius: var(--void-radius-lg); } -.void-rounded-xl { border-radius: var(--void-radius-xl); } +.void-rounded-sm { border-radius: 4px; } +.void-rounded-md { border-radius: 6px; } +.void-rounded-lg { border-radius: 8px; } +.void-rounded-xl { border-radius: 12px; } /* Code Review Decorations */ .monaco-editor .code-review-error { diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/util/inputs.tsx index 4f05984927c..9e9ea7c67d4 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/util/inputs.tsx @@ -842,7 +842,7 @@ export const VoidInputBox2 = forwardRef(fun disabled={!isEnabled} - className={`w-full resize-none max-h-[500px] overflow-y-auto ${appearanceClasses} ${className}`} + className={`void-focus-ring w-full resize-none max-h-[500px] overflow-y-auto transition-colors ${appearanceClasses} ${className}`} style={{ ...baseStyle, ...style }} onInput={useCallback((event: React.FormEvent) => { @@ -893,7 +893,7 @@ export const VoidInputBox2 = forwardRef(fun {isMenuOpen && (
    >({ } diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index 1dda6dc3598..cb88c68cf53 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -38,11 +38,14 @@ export const VoidCommandBarMain = ({ uri, editor }: CortexideCommandBarProps) => export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes) => (
    }
    From 00308bb0b403e79d7191cd9b8414d29b204ee057 Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Fri, 19 Dec 2025 21:09:20 +0000 Subject: [PATCH 2/4] chore: Apply additional SidebarChat improvements from WIP --- .../cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx index 2fb6f73df5e..fd0a51da981 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -4870,7 +4870,8 @@ const CommandBarInChat = () => { if (numFilesChanged > 0 && fileDetailsOpenedState !== "user-closed") { setFileDetailsOpenedState("auto-opened"); } - }, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fileDetailsOpenedState, numFilesChanged]); // setFileDetailsOpenedState is stable, no need in deps const isFinishedMakingThreadChanges = // there are changed files @@ -5376,7 +5377,8 @@ export const SidebarChat = () => { // Memoize scrollToBottom callback to prevent unnecessary re-renders const scrollToBottomCallback = useCallback(() => { scrollToBottom(scrollContainerRef); - }, [scrollContainerRef]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // scrollContainerRef is a ref, stable and doesn't need to be in deps const onSubmit = useCallback( async (_forceSubmit?: string) => { From e70e401c16b68383c3c1da5fbb3afc5e61aa9d03 Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Fri, 19 Dec 2025 22:36:12 +0000 Subject: [PATCH 3/4] fix: Remove unused code and ensure Tailwind animations work - Remove unused dots state and animation logic from IconLoading component - Add custom CSS animations (spin, pulse) to ensure Tailwind animations work - Support both prefixed and unprefixed animation classes - Fixes potential memory leak from unused requestAnimationFrame loop - Fix unicode characters in comments to pass linting --- .../react/src/sidebar-tsx/SidebarChat.tsx | 25 ++--------------- .../cortexide/browser/react/src/styles.css | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx index fd0a51da981..105c8e5261a 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -313,27 +313,6 @@ export const IconLoading = ({ showSpinner?: boolean; size?: number; }) => { - const [dots, setDots] = useState(1); - - useEffect(() => { - // Optimized: Use requestAnimationFrame for smoother animation, update every 400ms - let frameId: number; - let lastUpdate = Date.now(); - - const animate = () => { - const now = Date.now(); - if (now - lastUpdate >= 400) { - setDots((prev) => (prev >= 3 ? 1 : prev + 1)); - lastUpdate = now; - } - frameId = requestAnimationFrame(animate); - }; - - frameId = requestAnimationFrame(animate); - return () => cancelAnimationFrame(frameId); - }, []); - - const dotsText = ".".repeat(dots); const tokenText = showTokenCount !== undefined ? ` (${showTokenCount} tokens)` : ""; @@ -5859,7 +5838,7 @@ export const SidebarChat = () => { return { primary, secondary: undefined as string | undefined }; } - // Tool phase – surface which tool is running when possible + // Tool phase - surface which tool is running when possible if (isRunning === "tool" || toolIsGenerating) { let toolLabel = "Running tools…"; if (toolCallSoFar?.name) { @@ -5871,7 +5850,7 @@ export const SidebarChat = () => { }; } - // LLM phase – distinguish thinking vs. writing + // LLM phase - distinguish thinking vs. writing if (isRunning === "LLM") { if (!displayContentSoFar && !reasoningSoFar) { return { diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css b/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css index fecc52cbe1b..40178842ae0 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/styles.css @@ -80,6 +80,33 @@ .void-rounded-lg { border-radius: 8px; } .void-rounded-xl { border-radius: 12px; } +/* Animation utilities - ensure Tailwind animations work even with prefix */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Support both prefixed and unprefixed animation classes */ +.animate-spin, +.void-animate-spin { + animation: spin 1s linear infinite; +} + +.animate-pulse, +.void-animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + /* Code Review Decorations */ .monaco-editor .code-review-error { background-color: rgba(255, 0, 0, 0.1); @@ -138,6 +165,7 @@ } .monaco-editor .error-detection-glyph-error::before { + // allow-any-unicode-next-line content: '❌'; font-size: 14px; color: var(--vscode-errorForeground); From 210caebbf857ef6ffa5cbe91540809c5c8499b7f Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Fri, 19 Dec 2025 22:52:44 +0000 Subject: [PATCH 4/4] fix: remove unused Plus import from lucide-react - Removed unused Plus import that was added but never used - Cleanup after PR review --- .../cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx index 105c8e5261a..8b2949d23dd 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -86,7 +86,6 @@ import { Text, Image as ImageIcon, FileText, - Plus, } from "lucide-react"; import { ChatMessage,