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 e7cb62bebb8..664ab756416 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 @@ -50,6 +50,8 @@ import { PDFMessageRenderer } from '../util/PDFMessageRenderer.js'; import { IconX, IconWarning, IconLoading, TypingCursor } from './shared/icons.js'; import { getBasename, getFolderName, getRelative, voidOpenFileFn } from './shared/pathUtils.js'; import { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './tools/ToolPrimitives.js'; +import { ChatBubble } from './chat/ChatBubble.js'; +import { EditToolSoFar } from './tools/ToolRenderers.js'; // Re-export shared modules for existing consumers (will migrate imports over time). export { IconX, IconWarning, IconLoading, TypingCursor } from './shared/icons.js'; @@ -57,2631 +59,11 @@ export { getBasename, getFolderName, getRelative, voidOpenFileFn } from './share export { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './tools/ToolPrimitives.js'; export { VoidChatArea, ButtonSubmit, ButtonStop } from './composer/VoidChatArea.js'; export { SelectedFiles } from './composer/SelectedFiles.js'; +export { ChatBubble } from './chat/ChatBubble.js'; -type ToolHeaderParams = { - icon?: React.ReactNode; - title: React.ReactNode; - desc1: React.ReactNode; - desc1OnClick?: () => void; - desc2?: React.ReactNode; - isError?: boolean; - info?: string; - desc1Info?: string; - isRejected?: boolean; - numResults?: number; - hasNextPage?: boolean; - children?: React.ReactNode; - bottomChildren?: React.ReactNode; - onClick?: () => void; - desc2OnClick?: () => void; - isOpen?: boolean; - className?: string; -} - -const ToolHeaderWrapper = ({ - icon, - title, - desc1, - desc1OnClick, - desc1Info, - desc2, - numResults, - hasNextPage, - children, - info, - bottomChildren, - isError, - onClick, - desc2OnClick, - isOpen, - isRejected, - className, // applies to the main content -}: ToolHeaderParams) => { - - const [isOpen_, setIsOpen] = useState(false); - const isExpanded = isOpen !== undefined ? isOpen : isOpen_ - - const isDropdown = children !== undefined // null ALLOWS dropdown - const isClickable = !!(isDropdown || onClick) - - const isDesc1Clickable = !!desc1OnClick - - const desc1HTML = {desc1} - - return (
-
- {/* header */} -
-
- {/* left */} -
- {/* title eg "> Edited File" */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - if (onClick) { onClick(); } - }} - > - {isDropdown && ()} - {title} - - {!isDesc1Clickable && desc1HTML} -
- {isDesc1Clickable && desc1HTML} -
- - {/* right */} -
- - {info && } - - {isError && } - {isRejected && } - {desc2 && - {desc2} - } - {numResults !== undefined && ( - - {`${numResults}${hasNextPage ? '+' : ''} result${numResults !== 1 ? 's' : ''}`} - - )} -
-
-
- {/* children */} - {
- {children} -
} -
- {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 { 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 = - - - // JumpToFileButton removed in favor of FileLinkText - } - 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') { - // error - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - } - - return -} - -const SimplifiedToolHeader = ({ - title, - children, -}: { - title: string; - children?: React.ReactNode; -}) => { - const [isOpen, setIsOpen] = useState(false); - const isDropdown = children !== undefined; - return ( -
-
- {/* header */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - }} - > - {isDropdown && ( - - )} -
- {title} -
-
- {/* 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') - - // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - const currentThreadId = chatThreadsState.currentThreadId - - // global state - 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 }) - } - - - // 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) - // initialize on first render, and when edit was just enabled - const _mustInitialize = useRef(true) - const _justEnabledEdit = useRef(false) - useEffect(() => { - 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 - }) - ) - - if (textAreaFnsRef.current) - textAreaFnsRef.current.setValue(chatMessage.displayContent || '') - - textAreaRefState.focus(); - - _justEnabledEdit.current = false - _mustInitialize.current = false - } - - }, [chatMessage, mode, textAreaRefState, setStagingSelections]) - - const onOpenEdit = () => { - 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 - - - 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') { - - 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 - - // 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 - } - - await chatThreadsService.abortRunning(threadId) - - // update state - setIsBeingEdited(false) - chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) - - // stream the edit - const userMessage = textAreaRefState.value; - try { - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, threadId }) - } catch (e) { - console.error('Error while editing message:', e) - } - await chatThreadsService.focusCurrentChat() - requestAnimationFrame(() => _scrollToBottom?.()) - } - - const onAbort = async () => { - // use subscribed state - const threadId = currentThreadId - await chatThreadsService.abortRunning(threadId) - } - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCloseEdit() - } - 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 - } - - 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 - - return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
- - - -
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> -
- - -
- -} - -const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
- {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 && -
- - - {!isCommitted && } - -
- } - - -}, (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} - - -} - -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 - - // 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 - } - - // built-in title - else { - const toolName = t.name as BuiltinToolName - const entry = titleOfBuiltinToolName[toolName] - // Defensive: a name can pass `builtinToolNames.includes()` (a runtime list) - // yet be missing from this title map — e.g. type/runtime drift, or a weak/free - // model emitting an unexpected built-in tool name. A missing entry must never - // crash the whole chat render (it previously threw "reading 'proposed'"). - if (!entry) { - const verb = t.type === 'success' ? 'Called' : t.type === 'running_now' ? 'Calling' : 'Call' - const fallback = `${verb} ${toolName}` - return t.type === 'running_now' ? loadingTitleWrapper(fallback) : fallback - } - if (t.type === 'success') return entry.done - if (t.type === 'running_now') return entry.running - return entry.proposed - } -} - - -const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType): { - desc1: React.ReactNode, - desc1Info?: string, -} => { - - if (!_toolParams) { - return { desc1: '', }; - } - - const x = { - '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'] - return { - desc1: getFolderName(toolParams.uri.fsPath), - desc1Info: getRelative(toolParams.uri, accessor), - }; - }, - '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'] - return { - desc1: `"${toolParams.query}"`, - } - }, - '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'] - return { - 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'] - return { - 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'] - return { - desc1: getBasename(toolParams.uri.fsPath), - desc1Info: getRelative(toolParams.uri, accessor), - } - }, - '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'] - return { - desc1: `"${toolParams.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: '' } - }, - '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'] - return { - desc1: getFolderName(toolParams.uri.fsPath) ?? '/', - desc1Info: getRelative(toolParams.uri, accessor), - } - }, - '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'] - return { - desc1: `"${toolParams.query}"`, - } - }, - 'browse_url': () => { - const toolParams = _toolParams as BuiltinToolCallParams['browse_url'] - return { - desc1: toolParams.url, - desc1Info: new URL(toolParams.url).hostname, - } - } - } - - try { - return x[toolName]?.() || { 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() - - // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - const currentThreadId = chatThreadsState.currentThreadId - - const onAccept = useCallback(() => { - 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]) - - 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]) - - const approveButton = ( - - ) - - const cancelButton = ( - - ) - - const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools' - const approvalToggle = approvalType ?
- -
: null - - return
- {approveButton} - {cancelButton} - {approvalToggle} -
-} - -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}
- ))} -
-} - -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' }} - > - - {title} -
-
-
- {children} -
-
-
- ); -} - - -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 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; - - // wait for the interruptor so we know it's running - - await streamState?.interrupt - const container = divRef.current; - if (!container) return; - - const terminal = terminalToolsService.getTemporaryTerminal(toolMessage.params.terminalId); - if (!terminal) return; - - try { - terminal.attachToElement(container); - 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') { - terminal.layout({ width, height }); - } - }); - - resizeObserver.observe(container); - return () => { terminal.detachFromElement(); resizeObserver?.disconnect(); } - } - - useEffect(() => { - effect() - }, [terminalToolsService, toolMessage, toolMessage.type, type]); - - 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) - } - 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 <> - - -} - -type WrapperProps = { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string } -const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { - const accessor = useAccessor() - const mcpService = accessor.get('IMCPService') - - const title = getTitle(toolMessage) - const desc1 = removeMCPToolNamePrefix(toolMessage.name) - const icon = null - - - if (toolMessage.type === 'running_now') return null // do not show running - - 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) ? [] : {} - for (const k of Object.keys(value)) { - if (SENSITIVE_KEYS.has(k.toLowerCase())) out[k] = redactValue(value[k]) - else out[k] = redactParams(value[k]) - } - return out - } - return value - } - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } - - const redactedParams = redactParams(params) - const paramsStr = JSON.stringify(redactedParams, null, 2) - componentParams.desc2 = - - 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 - if (result) { - const resultStr = mcpService.stringifyResult(result) - // Check if result is text (not JSON) - text events return plain text, others return JSON - // Type guard: check if result has 'event' property and it's 'text' - const isTextResult = typeof result === 'object' && result !== null && 'event' in result && (result as any).event === 'text' - // If it's text, display as markdown; otherwise display as JSON code block - const displayContent = isTextResult ? resultStr : `\`\`\`json\n${resultStr}\n\`\`\`` - componentParams.children = - - - - - } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - - return - -} - -type ResultWrapper = (props: WrapperProps) => React.ReactNode - -const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper, } } = { - 'read_file': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - 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 - - 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] - } - - 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} - - - } - - return - }, - }, - 'get_dir_tree': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - 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 - - 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}` - } - - if (toolMessage.type === 'success') { - const { result } = toolMessage - componentParams.children = - - - - - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - - return - - } - }, - '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 - - 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 (params.uri) { - 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} - - - } - - return - } - }, - '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 - - 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, } - - if (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} - - - } - - return - } - }, - '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 - - 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, } - - if (params.searchInFolder || params.isRegex) { - let info: string[] = [] - if (params.searchInFolder) { - const rel = getRelative(params.searchInFolder, accessor) - if (rel) info.push(`Only search in ${rel}`) - } - if (params.isRegex) { info.push(`Uses regex search`) } - 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 - } - }, - - 'search_in_file': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor(); - 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 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 { rawParams, params } = toolMessage; - 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('; ') - - 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') { - const { result } = toolMessage; - componentParams.bottomChildren = - - {result} - - - } - - return ; - } - }, - - 'read_lint_errors': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - const title = getTitle(toolMessage) - - 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 - - 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 - - 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 - // JumpToFileButton removed in favor of FileLinkText - componentParams.bottomChildren = - - {result} - - - } - - return - }, - }, - - // --- - - '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 { 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') { - // nothing more is needed - } - else if (toolMessage.type === 'tool_request') { - // nothing more is needed - } - - return - } - }, - '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) } - } - - return - } - }, - 'rewrite_file': { - resultWrapper: (params) => { - return - } - }, - 'edit_file': { - resultWrapper: (params) => { - return - } - }, - - // --- - - 'run_command': { - resultWrapper: (params) => { - return - } - }, - - 'run_persistent_command': { - resultWrapper: (params) => { - return - } - }, - 'open_persistent_terminal': { - resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() - 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, } - - 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} - - - } - - return - }, - }, - '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} - - - } - - return - }, - }, - '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') { - // Show loading indicator - 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, } - - 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. -
-
- } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - - return - }, - }, - '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') { - // Show loading indicator - 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, } - - if (toolMessage.type === 'success') { - const { result } = toolMessage - const urlStr = result.url || params.url - - componentParams.onClick = () => { - if (urlStr) { - window.open(urlStr, '_blank', 'noopener,noreferrer') - } - } - 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} -
- )} - {urlStr && ( - - {urlStr} - - )} -
- {contentPreview} -
-
-
- } else { - componentParams.children = -
- No content extracted from URL. -
-
- } - } - else if (toolMessage.type === 'tool_error') { - const { result } = toolMessage - componentParams.bottomChildren = - - {result} - - - } - - 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() - - // Subscribe to thread state changes properly - const chatThreadsState = useChatThreadsState() - - const isRunning = useChatThreadsStreamState(threadId)?.isRunning - const isDisabled = useMemo(() => { - 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]) - - // Memoize message count lookup to avoid direct state access in render - 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': 'cortex-tooltip', - 'data-tooltip-content': `Disabled ${isRunning ? 'when running' : 'because another thread is running'}`, - 'data-tooltip-place': 'top', - } : {}} - > - Checkpoint -
-
-} - - -type ChatBubbleMode = 'display' | 'edit' -type ChatBubbleProps = { - 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) - } - } - 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
- } - - switch (status) { - case 'succeeded': - return ( -
- -
- ) - case 'failed': - return ( -
- -
- ) - case 'running': - return ( -
- -
- ) - case 'paused': - return ( -
- -
- ) - case 'skipped': - return ( -
- -
- ) - default: // queued - return ( -
-
-
- ) - } - } - - 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 handleApprove = () => { - if (isCheckpointGhost || isBusy) return - chatThreadService.approvePlan({ threadId, messageIdx }) - } - - const handleReject = () => { - if (isCheckpointGhost || isBusy) return - chatThreadService.rejectPlan({ threadId, messageIdx }) - } - - const handleToggleStep = (stepNumber: number) => { - if (isCheckpointGhost || isBusy) return - chatThreadService.toggleStepDisabled({ threadId, messageIdx, stepNumber }) - } - - 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 - - )} -
-
- - {!isCollapsed && ( -
- {progressText} - {approvalState === 'pending' && isIdleLike && ( -
- - -
- )} - {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)} -
- - {/* Actions Row */} - {(approvalState === 'pending' || (approvalState === 'executing' && status === 'failed')) && !isCheckpointGhost && ( -
- {approvalState === 'pending' && !isRunning && ( - - )} - {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} - - ))} -
-
- )} - {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} -
- )} - {isSuccess && toolMsg.result && ( -
- View result -
-																					{typeof toolMsg.result === 'string'
-																						? toolMsg.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.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 -}); - -// Review Component - Shows summary after execution -const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessage, isCheckpointGhost: boolean }) => { - return ( -
-
-
-
- {message.completed ? ( - - ) : ( - - )} -

- {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.summary}

- - {message.filesChanged && message.filesChanged.length > 0 && ( -
-

Files Changed:

-
- {message.filesChanged.map((file, i) => ( -
- {file.changeType === 'created' && } - {file.changeType === 'modified' && } - {file.changeType === 'deleted' && } - {file.path} -
- ))} -
-
- )} - - {message.issues && message.issues.length > 0 && ( -
- {message.issues.map((issue, i) => ( -
- {issue.severity === 'error' ? ( - - ) : issue.severity === 'warning' ? ( - - ) : ( - - )} -
-

- {issue.message} -

- {issue.file && ( -

- - {issue.file} -

- )} -
-
- ))} -
- )} - - {message.nextSteps && message.nextSteps.length > 0 && ( -
-

Recommended Next Steps:

-
    - {message.nextSteps.map((step, i) => ( -
  • - - {step} -
  • - ))} -
-
- )} -
-
- ); -}; - -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
- -
- } - - 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
- -
- } - - 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() @@ -2947,43 +329,6 @@ const CommandBarInChat = () => { -const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { - - if (!isABuiltinToolName(toolCallSoFar.name)) return null - - const accessor = useAccessor() - - const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined - - const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed - - const uriDone = toolCallSoFar.doneParams.includes('uri') - const desc1 = - {uriDone ? - getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') - : `Generating`} - - - - const desc1OnClick = () => { uri && voidOpenFileFn(uri, accessor) } - - // If URI has not been specified - return - - - - -} - - export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/ChatBubble.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/ChatBubble.tsx new file mode 100644 index 00000000000..538bf1c83b7 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/ChatBubble.tsx @@ -0,0 +1,720 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Pencil, X, Check, ChevronRight, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Undo, Undo2, Ban, AlertTriangle } from 'lucide-react'; +import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useFullChatThreadsStreamState } from '../../util/services.js'; +import { ChatMarkdownRender, ChatMessageLocation } from '../../markdown/ChatMarkdownRender.js'; +import { ChatMessage, CheckpointEntry, PlanMessage, ReviewMessage, PlanStep, StepStatus, PlanApprovalState } from '../../../../common/chatThreadServiceTypes.js'; +import { BuiltinToolName, ToolName } from '../../../../common/toolsServiceTypes.js'; +import { isABuiltinToolName } from '../../../../common/prompt/prompts.js'; +import ErrorBoundary from '../ErrorBoundary.js'; +import { IconLoading, TypingCursor } from '../shared/icons.js'; +import { getBasename, voidOpenFileFn } from '../shared/pathUtils.js'; +import { AssistantMessageComponent, ProseWrapper, ReasoningWrapper, SmallProseWrapper } from './proseWrappers.js'; +import { UserMessageComponent } from './UserMessageComponent.js'; +import { builtinToolNameToComponent, CanceledTool, InvalidTool, SimplifiedToolHeader, ResultWrapper } from '../tools/ToolRenderers.js'; + +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 isRunning = useChatThreadsStreamState(threadId)?.isRunning + const isDisabled = useMemo(() => { + 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]) + + // Memoize message count lookup to avoid direct state access in render + 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': 'cortex-tooltip', + 'data-tooltip-content': `Disabled ${isRunning ? 'when running' : 'because another thread is running'}`, + 'data-tooltip-place': 'top', + } : {}} + > + Checkpoint +
+
+} + + +type ChatBubbleMode = 'display' | 'edit' +export type ChatBubbleProps = { + 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) + } + } + 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
+ } + + switch (status) { + case 'succeeded': + return ( +
+ +
+ ) + case 'failed': + return ( +
+ +
+ ) + case 'running': + return ( +
+ +
+ ) + case 'paused': + return ( +
+ +
+ ) + case 'skipped': + return ( +
+ +
+ ) + default: // queued + return ( +
+
+
+ ) + } + } + + 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 handleApprove = () => { + if (isCheckpointGhost || isBusy) return + chatThreadService.approvePlan({ threadId, messageIdx }) + } + + const handleReject = () => { + if (isCheckpointGhost || isBusy) return + chatThreadService.rejectPlan({ threadId, messageIdx }) + } + + const handleToggleStep = (stepNumber: number) => { + if (isCheckpointGhost || isBusy) return + chatThreadService.toggleStepDisabled({ threadId, messageIdx, stepNumber }) + } + + 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 + + )} +
+
+ + {!isCollapsed && ( +
+ {progressText} + {approvalState === 'pending' && isIdleLike && ( +
+ + +
+ )} + {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)} +
+ + {/* Actions Row */} + {(approvalState === 'pending' || (approvalState === 'executing' && status === 'failed')) && !isCheckpointGhost && ( +
+ {approvalState === 'pending' && !isRunning && ( + + )} + {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} + + ))} +
+
+ )} + {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} +
+ )} + {isSuccess && toolMsg.result && ( +
+ View result +
+																					{typeof toolMsg.result === 'string'
+																						? toolMsg.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.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 +}); + +// Review Component - Shows summary after execution +const ReviewComponent = ({ message, isCheckpointGhost }: { message: ReviewMessage, isCheckpointGhost: boolean }) => { + return ( +
+
+
+
+ {message.completed ? ( + + ) : ( + + )} +

+ {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.summary}

+ + {message.filesChanged && message.filesChanged.length > 0 && ( +
+

Files Changed:

+
+ {message.filesChanged.map((file, i) => ( +
+ {file.changeType === 'created' && } + {file.changeType === 'modified' && } + {file.changeType === 'deleted' && } + {file.path} +
+ ))} +
+
+ )} + + {message.issues && message.issues.length > 0 && ( +
+ {message.issues.map((issue, i) => ( +
+ {issue.severity === 'error' ? ( + + ) : issue.severity === 'warning' ? ( + + ) : ( + + )} +
+

+ {issue.message} +

+ {issue.file && ( +

+ + {issue.file} +

+ )} +
+
+ ))} +
+ )} + + {message.nextSteps && message.nextSteps.length > 0 && ( +
+

Recommended Next Steps:

+
    + {message.nextSteps.map((step, i) => ( +
  • + + {step} +
  • + ))} +
+
+ )} +
+
+ ); +}; + +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
+ +
+ } + + 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
+ +
+ } + + 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 +}) + + +export { ChatBubble }; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/UserMessageComponent.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/UserMessageComponent.tsx new file mode 100644 index 00000000000..aebb967c09d --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/UserMessageComponent.tsx @@ -0,0 +1,262 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { Pencil, X } from 'lucide-react'; +import { useAccessor, useChatThreadsState } from '../../util/services.js'; +import { ChatMessage } from '../../../../common/chatThreadServiceTypes.js'; +import { VoidInputBox2, TextAreaFns } from '../../util/inputs.js'; +import { VoidChatArea } from '../composer/VoidChatArea.js'; +import { SelectedFiles } from '../composer/SelectedFiles.js'; +import { ImageMessageRenderer } from '../../util/ImageMessageRenderer.js'; +import { PDFMessageRenderer } from '../../util/PDFMessageRenderer.js'; + +export 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 + + // global state + 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 }) + } + + + // 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) + // initialize on first render, and when edit was just enabled + const _mustInitialize = useRef(true) + const _justEnabledEdit = useRef(false) + useEffect(() => { + 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 + }) + ) + + if (textAreaFnsRef.current) + textAreaFnsRef.current.setValue(chatMessage.displayContent || '') + + textAreaRefState.focus(); + + _justEnabledEdit.current = false + _mustInitialize.current = false + } + + }, [chatMessage, mode, textAreaRefState, setStagingSelections]) + + const onOpenEdit = () => { + 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 + + + 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') { + + 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 + + // 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 + } + + await chatThreadsService.abortRunning(threadId) + + // update state + setIsBeingEdited(false) + chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) + + // stream the edit + const userMessage = textAreaRefState.value; + try { + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, threadId }) + } catch (e) { + console.error('Error while editing message:', e) + } + await chatThreadsService.focusCurrentChat() + requestAnimationFrame(() => _scrollToBottom?.()) + } + + const onAbort = async () => { + // use subscribed state + const threadId = currentThreadId + await chatThreadsService.abortRunning(threadId) + } + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCloseEdit() + } + 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 + } + + 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 + + return
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
{ if (mode === 'display') { onOpenEdit() } }} + > + {chatbubbleContents} +
+ + + +
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
+ + +
+ +} + diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/proseWrappers.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/proseWrappers.tsx new file mode 100644 index 00000000000..d8805deb166 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chat/proseWrappers.tsx @@ -0,0 +1,192 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useEffect, useMemo, useState } from 'react'; +import { useAccessor } from '../../util/services.js'; +import { ChatMarkdownRender, ChatMessageLocation } from '../../markdown/ChatMarkdownRender.js'; +import { ChatMessage } from '../../../../common/chatThreadServiceTypes.js'; +import { IconLoading, TypingCursor } from '../shared/icons.js'; +import { ToolChildrenWrapper } from '../tools/ToolPrimitives.js'; +import { ToolHeaderWrapper } from '../tools/ToolHeader.js'; + +export const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { + return
+ {children} +
+} + +export const ProseWrapper = ({ children }: { children: React.ReactNode }) => { + return
+ {children} +
+} +export 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 && +
+ + + {!isCommitted && } + +
+ } + + +}, (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 +}) + +export 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". + diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolHeader.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolHeader.tsx new file mode 100644 index 00000000000..45f9a9e6140 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolHeader.tsx @@ -0,0 +1,191 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useState } from 'react'; +import { ChevronRight, AlertTriangle, Ban, CircleEllipsis } from 'lucide-react'; + +export type ToolHeaderParams = { + icon?: React.ReactNode; + title: React.ReactNode; + desc1: React.ReactNode; + desc1OnClick?: () => void; + desc2?: React.ReactNode; + isError?: boolean; + info?: string; + desc1Info?: string; + isRejected?: boolean; + numResults?: number; + hasNextPage?: boolean; + children?: React.ReactNode; + bottomChildren?: React.ReactNode; + onClick?: () => void; + desc2OnClick?: () => void; + isOpen?: boolean; + className?: string; +} + +export const ToolHeaderWrapper = ({ + icon, + title, + desc1, + desc1OnClick, + desc1Info, + desc2, + numResults, + hasNextPage, + children, + info, + bottomChildren, + isError, + onClick, + desc2OnClick, + isOpen, + isRejected, + className, // applies to the main content +}: ToolHeaderParams) => { + + const [isOpen_, setIsOpen] = useState(false); + const isExpanded = isOpen !== undefined ? isOpen : isOpen_ + + const isDropdown = children !== undefined // null ALLOWS dropdown + const isClickable = !!(isDropdown || onClick) + + const isDesc1Clickable = !!desc1OnClick + + const desc1HTML = {desc1} + + return (
+
+ {/* header */} +
+
+ {/* left */} +
+ {/* title eg "> Edited File" */} +
{ + if (isDropdown) { setIsOpen(v => !v); } + if (onClick) { onClick(); } + }} + > + {isDropdown && ()} + {title} + + {!isDesc1Clickable && desc1HTML} +
+ {isDesc1Clickable && desc1HTML} +
+ + {/* right */} +
+ + {info && } + + {isError && } + {isRejected && } + {desc2 && + {desc2} + } + {numResults !== undefined && ( + + {`${numResults}${hasNextPage ? '+' : ''} result${numResults !== 1 ? 's' : ''}`} + + )} +
+
+
+ {/* children */} + {
+ {children} +
} +
+ {bottomChildren} +
); +}; + + + +export const SimplifiedToolHeader = ({ + title, + children, +}: { + title: string; + children?: React.ReactNode; +}) => { + const [isOpen, setIsOpen] = useState(false); + const isDropdown = children !== undefined; + return ( +
+
+ {/* header */} +
{ + if (isDropdown) { setIsOpen(v => !v); } + }} + > + {isDropdown && ( + + )} +
+ {title} +
+
+ {/* children */} + {
+ {children} +
} +
+
+ ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolRenderers.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolRenderers.tsx new file mode 100644 index 00000000000..864e7fe5785 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/tools/ToolRenderers.tsx @@ -0,0 +1,1379 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ChevronRight, AlertTriangle, Ban, Check, Dot, FileIcon, Folder, Text, Undo2, X } from 'lucide-react'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState } from '../../util/services.js'; +import { ChatMarkdownRender, getApplyBoxId } from '../../markdown/ChatMarkdownRender.js'; +import { VoidDiffEditor } from '../../util/inputs.js'; +import { ChatMessage, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; +import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'; +import { CopyButton, EditToolAcceptRejectButtonsHTML, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../../markdown/ApplyBlockHoverButtons.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js'; +import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; +import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; +import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js'; +import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; +import { IconLoading } from '../shared/icons.js'; +import { getBasename, getFolderName, getRelative, voidOpenFileFn } from '../shared/pathUtils.js'; +import { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './ToolPrimitives.js'; +import { SmallProseWrapper } from '../chat/proseWrappers.js'; +import { ToolApprovalTypeSwitch } from '../../settings/Settings.js'; +import { ToolHeaderWrapper, SimplifiedToolHeader, ToolHeaderParams } from './ToolHeader.js'; + +export type { ToolHeaderParams }; + +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 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') { + // 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') { + // error + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + } + + return +} + +const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { + 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 + + // 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 + } + + // built-in title + else { + const toolName = t.name as BuiltinToolName + const entry = titleOfBuiltinToolName[toolName] + // Defensive: a name can pass `builtinToolNames.includes()` (a runtime list) + // yet be missing from this title map — e.g. type/runtime drift, or a weak/free + // model emitting an unexpected built-in tool name. A missing entry must never + // crash the whole chat render (it previously threw "reading 'proposed'"). + if (!entry) { + const verb = t.type === 'success' ? 'Called' : t.type === 'running_now' ? 'Calling' : 'Call' + const fallback = `${verb} ${toolName}` + return t.type === 'running_now' ? loadingTitleWrapper(fallback) : fallback + } + if (t.type === 'success') return entry.done + if (t.type === 'running_now') return entry.running + return entry.proposed + } +} + + +const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType): { + desc1: React.ReactNode, + desc1Info?: string, +} => { + + if (!_toolParams) { + return { desc1: '', }; + } + + const x = { + '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'] + return { + desc1: getFolderName(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + }; + }, + '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'] + return { + desc1: `"${toolParams.query}"`, + } + }, + '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'] + return { + 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'] + return { + 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'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + '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'] + return { + desc1: `"${toolParams.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: '' } + }, + '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'] + return { + desc1: getFolderName(toolParams.uri.fsPath) ?? '/', + desc1Info: getRelative(toolParams.uri, accessor), + } + }, + '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'] + return { + desc1: `"${toolParams.query}"`, + } + }, + 'browse_url': () => { + const toolParams = _toolParams as BuiltinToolCallParams['browse_url'] + return { + desc1: toolParams.url, + desc1Info: new URL(toolParams.url).hostname, + } + } + } + + try { + return x[toolName]?.() || { 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() + + // Subscribe to thread state changes properly + const chatThreadsState = useChatThreadsState() + const currentThreadId = chatThreadsState.currentThreadId + + const onAccept = useCallback(() => { + 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]) + + 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]) + + const approveButton = ( + + ) + + const cancelButton = ( + + ) + + const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools' + const approvalToggle = approvalType ?
+ +
: null + + return
+ {approveButton} + {cancelButton} + {approvalToggle} +
+} + +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}
+ ))} +
+} + +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' }} + > + + {title} +
+
+
+ {children} +
+
+
+ ); +} + + +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 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; + + // wait for the interruptor so we know it's running + + await streamState?.interrupt + const container = divRef.current; + if (!container) return; + + const terminal = terminalToolsService.getTemporaryTerminal(toolMessage.params.terminalId); + if (!terminal) return; + + try { + terminal.attachToElement(container); + 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') { + terminal.layout({ width, height }); + } + }); + + resizeObserver.observe(container); + return () => { terminal.detachFromElement(); resizeObserver?.disconnect(); } + } + + useEffect(() => { + effect() + }, [terminalToolsService, toolMessage, toolMessage.type, type]); + + 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) + } + + 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 <> + + +} + +export type WrapperProps = { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string } +const MCPToolWrapper = ({ toolMessage }: WrapperProps) => { + const accessor = useAccessor() + const mcpService = accessor.get('IMCPService') + + const title = getTitle(toolMessage) + const desc1 = removeMCPToolNamePrefix(toolMessage.name) + const icon = null + + + if (toolMessage.type === 'running_now') return null // do not show running + + 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) ? [] : {} + for (const k of Object.keys(value)) { + if (SENSITIVE_KEYS.has(k.toLowerCase())) out[k] = redactValue(value[k]) + else out[k] = redactParams(value[k]) + } + return out + } + return value + } + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, } + + const redactedParams = redactParams(params) + const paramsStr = JSON.stringify(redactedParams, null, 2) + componentParams.desc2 = + + 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 + if (result) { + const resultStr = mcpService.stringifyResult(result) + // Check if result is text (not JSON) - text events return plain text, others return JSON + // Type guard: check if result has 'event' property and it's 'text' + const isTextResult = typeof result === 'object' && result !== null && 'event' in result && (result as any).event === 'text' + // If it's text, display as markdown; otherwise display as JSON code block + const displayContent = isTextResult ? resultStr : `\`\`\`json\n${resultStr}\n\`\`\`` + componentParams.children = + + + + + } + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + + return + +} + +export type ResultWrapper = (props: WrapperProps) => React.ReactNode + +const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper, } } = { + 'read_file': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + 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 + + 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] + } + + 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} + + + } + + return + }, + }, + 'get_dir_tree': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + 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 + + 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}` + } + + if (toolMessage.type === 'success') { + const { result } = toolMessage + componentParams.children = + + + + + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + + return + + } + }, + '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 + + 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 (params.uri) { + 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} + + + } + + return + } + }, + '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 + + 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, } + + if (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} + + + } + + return + } + }, + '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 + + 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, } + + if (params.searchInFolder || params.isRegex) { + let info: string[] = [] + if (params.searchInFolder) { + const rel = getRelative(params.searchInFolder, accessor) + if (rel) info.push(`Only search in ${rel}`) + } + if (params.isRegex) { info.push(`Uses regex search`) } + 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 + } + }, + + 'search_in_file': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor(); + 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 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 { rawParams, params } = toolMessage; + 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('; ') + + 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') { + const { result } = toolMessage; + componentParams.bottomChildren = + + {result} + + + } + + return ; + } + }, + + 'read_lint_errors': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + const title = getTitle(toolMessage) + + 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 + + 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 + + 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 + // JumpToFileButton removed in favor of FileLinkText + componentParams.bottomChildren = + + {result} + + + } + + return + }, + }, + + // --- + + '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 { 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') { + // nothing more is needed + } + else if (toolMessage.type === 'tool_request') { + // nothing more is needed + } + + return + } + }, + '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) } + } + + return + } + }, + 'rewrite_file': { + resultWrapper: (params) => { + return + } + }, + 'edit_file': { + resultWrapper: (params) => { + return + } + }, + + // --- + + 'run_command': { + resultWrapper: (params) => { + return + } + }, + + 'run_persistent_command': { + resultWrapper: (params) => { + return + } + }, + 'open_persistent_terminal': { + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + 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, } + + 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} + + + } + + return + }, + }, + '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} + + + } + + return + }, + }, + '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') { + // Show loading indicator + 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, } + + 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. +
+
+ } + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + + return + }, + }, + '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') { + // Show loading indicator + 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, } + + if (toolMessage.type === 'success') { + const { result } = toolMessage + const urlStr = result.url || params.url + + componentParams.onClick = () => { + if (urlStr) { + window.open(urlStr, '_blank', 'noopener,noreferrer') + } + } + 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} +
+ )} + {urlStr && ( + + {urlStr} + + )} +
+ {contentPreview} +
+
+
+ } else { + componentParams.children = +
+ No content extracted from URL. +
+
+ } + } + else if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.bottomChildren = + + {result} + + + } + + return + }, + }, +}; + + + + +const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { + + if (!isABuiltinToolName(toolCallSoFar.name)) return null + + const accessor = useAccessor() + + const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined + + const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed + + const uriDone = toolCallSoFar.doneParams.includes('uri') + const desc1 = + {uriDone ? + getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') + : `Generating`} + + + + const desc1OnClick = () => { uri && voidOpenFileFn(uri, accessor) } + + // If URI has not been specified + return + + + + +} + +export { ToolHeaderWrapper, SimplifiedToolHeader, InvalidTool, CanceledTool, builtinToolNameToComponent, EditToolSoFar };