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}
-
-
-
- );
-}
-
-
-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.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.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}
+
+
+
+ );
+}
+
+
+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 };