diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index e8e5ba1d..6e21bc57 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1,3 +1,4 @@ + /*-------------------------------------------------------------------------------------- * Copyright 2025 Glass Devtools, Inc. All rights reserved. * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. @@ -7,7 +8,6 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdownRender.js'; @@ -19,13 +19,14 @@ import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; -import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; -import { ChevronRight, Pencil, X } from 'lucide-react'; +import { ChevronRight, Pencil, X, AlertTriangle } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; +import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; +import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; +import { ToolName } from '../../../toolsService.js'; -import { ToolResultType, ToolName } from '../../../toolsService.js'; @@ -151,7 +152,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization featureName: FeatureName; @@ -190,20 +191,24 @@ export const VoidChatArea: React.FC = ({ return (
{ onClickAnywhere?.() }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'Escape' && isStreaming && onAbort) { + onAbort(); + } + }} > {/* Selections section */} {showSelections && selections && setSelections && ( @@ -443,7 +448,7 @@ export const SelectedFiles = ( border rounded-sm ${isThisSelectionProspective ? 'border-void-border-2' : isThisSelectionOpened - ? 'border-void-border-1 ring-1 ring-[#007FD4]' + ? 'border-void-border-1 ring-1 ring-void-blue' : 'border-void-border-1' } hover:border-void-border-1 @@ -502,7 +507,7 @@ export const SelectedFiles = (
{ e.stopPropagation(); // don't focus input box @@ -528,42 +533,33 @@ export const SelectedFiles = ( } -const actionTitleOfToolName: { [T in ToolName]: string } = { - 'read_file': 'Read file', - 'list_dir': 'Inspected folder', - 'pathname_search': 'Searched filename', - 'search': 'Searched', - - 'create_uri': 'Created URI', - 'delete_uri': 'Deleted URI', - 'edit': 'Edited file', - 'terminal_command': 'Ran terminal command', -} - - -const ToolResult = ({ - toolName, - actionParam, - actionNumResults, - children, - onClick, -}: { - toolName: ToolName; - actionParam: string; - actionNumResults?: number; +interface DropdownComponentProps { + title: string; + desc1: string; + desc2?: string; + numResults?: number; children?: React.ReactNode; onClick?: () => void; -}) => { +} + +const DropdownComponent = ({ + title, + desc1, + desc2, + numResults, + children, + onClick, +}: DropdownComponentProps) => { const [isExpanded, setIsExpanded] = useState(false); const isDropdown = !!children const isClickable = !!isDropdown || !!onClick return ( -
+
)}
- {actionTitleOfToolName[toolName]} - {actionParam} - {actionNumResults !== undefined && ( + {title} + {desc1} + {desc2 && + {desc2} + } + {numResults !== undefined && ( - {`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`} + {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} )}
@@ -591,7 +590,9 @@ const ToolResult = ({ // the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition. className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`} > - {children} +
+ {children} +
@@ -599,252 +600,9 @@ const ToolResult = ({ }; - -const ToolError = ({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => { - return - - -} - - -const toolResultToComponent: { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } = { - 'read_file': ({ message }) => { - - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - if (message.result.type === 'error') return - - const { value, params } = message.result - return ( - -
-
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > - - {params.uri.fsPath} -
- {value.hasNextPage && (
AI can scroll for more content...
)} -
-
- ) - }, - 'list_dir': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const explorerService = accessor.get('IExplorerService') - // message.result.hasNextPage = true - // message.result.itemsRemaining = 400 - if (message.result.type === 'error') return - - const { value, params } = message.result - return ( - -
- {value.children?.map((child, i) => ( -
{ - commandService.executeCommand('workbench.view.explorer'); - explorerService.select(child.uri, true); - }} - > - - {`${child.name}${child.isDirectory ? '/' : ''}`} -
- ))} - {value.hasNextPage && (
{value.itemsRemaining} more items...
)} -
-
- ) - }, - 'pathname_search': ({ message }) => { - - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - if (message.result.type === 'error') return - - const { value, params } = message.result - return ( - -
- {value.uris.map((uri, i) => ( -
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} - > - - {uri.fsPath.split('/').pop()} -
- ))} - {value.hasNextPage && (
More results available...
)} -
-
- ) - }, - 'search': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - if (message.result.type === 'error') return - - const { value, params } = message.result - return ( - -
- {value.uris.map((uri, i) => ( -
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} - > - - {uri.fsPath.split('/').pop()} -
- ))} - {value.hasNextPage && (
More results available...
)} -
-
- ) - }, - - // --- - - 'create_uri': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - if (message.result.type === 'error') return - - const { params } = message.result - return ( - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > -
-
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > - - {params.uri.fsPath.split('/').pop()} -
-
-
- ) - }, - 'delete_uri': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - if (message.result.type === 'error') return - - const { params } = message.result - return ( - -
-
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > - - {params.uri.fsPath.split('/').pop()} -
-
-
- ) - }, - 'edit': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - if (message.result.type === 'error') return - - const { params } = message.result - return ( - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > -
-
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - > - - {params.uri.fsPath.split('/').pop()} -
-
-
- ) - }, - 'terminal_command': ({ message }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - - if (message.result.type === 'error') return - - const { params } = message.result - return ( - -
-
- - - -
-
-
- ) - } - -}; - - - -type ChatBubbleMode = 'display' | 'edit' -const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, }) => { +const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'user' } }) => { const role = chatMessage.role - // Only show reasoning dropdown when there's actual content - const reasoningStr = (chatMessage.role === 'assistant' && chatMessage.reasoning?.trim()) || null - const hasReasoning = !!reasoningStr - - const [isReasoningOpen, setIsReasoningOpen] = useState(false) const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -889,7 +647,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM } }, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) - const EditSymbol = mode === 'display' ? Pencil : X + const onOpenEdit = () => { setIsBeingEdited(true) chatThreadsService.setFocusedMessageIdx(messageIdx) @@ -902,155 +660,95 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatThreadsService.setFocusedMessageIdx(undefined) } - // set chat bubble contents + + const EditSymbol = mode === 'display' ? Pencil : X + + let chatbubbleContents: React.ReactNode - if (role === 'user') { - if (mode === 'display') { - chatbubbleContents = <> - - {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 - const thread = chatThreadsService.getCurrentThread() - chatThreadsService.cancelStreaming(thread.id) - - // update state - setIsBeingEdited(false) - chatThreadsService.setFocusedMessageIdx(undefined) - chatThreadsService.closeStagingSelectionsInMessage(messageIdx) - - // stream the edit - const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) - } - - const onAbort = () => { - const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.cancelStreaming(threadId) - } - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCloseEdit() - } - if (e.key === 'Enter' && !e.shiftKey) { - onSubmit() - } - } - - if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show) - return null - } - - chatbubbleContents = <> - - setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - - - } - } - else if (role === 'assistant') { - const thread = chatThreadsService.getCurrentThread() - - const chatMessageLocation: ChatMessageLocation = { - threadId: thread.id, - messageIdx: messageIdx, - } - - - const reasoningDropdown = hasReasoning ? ( -
-
-
setIsReasoningOpen(!isReasoningOpen)} - > - -
- Reasoning - Model's step-by-step thinking -
-
-
-
- -
-
-
-
- ) : null - - chatbubbleContents = (<> - {/* Reasoning dropdown (conditional) */} - {reasoningDropdown} - {/* Main content */} - - ) - } - else if (role === 'tool') { - - const ToolComponent = toolResultToComponent[chatMessage.name] as ({ message }: { message: any }) => React.ReactNode // ts isnt smart enough to deal with the types here... - - chatbubbleContents = - - console.log('tool result:', chatMessage.name, chatMessage.paramsStr, chatMessage.result) - - } - else if (role === 'tool_request') { + if (mode === 'display') { chatbubbleContents = <> - {JSON.stringify(chatMessage.name, null, 2)}
- {JSON.stringify(chatMessage.params, null, 2)} -
{ chatThreadsService.approveTool(chatMessage.voidToolId) }}>Accept
-
{ chatThreadsService.rejectTool(chatMessage.voidToolId) }}>Reject
+ + {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 + const thread = chatThreadsService.getCurrentThread() + chatThreadsService.cancelStreaming(thread.id) + + // update state + setIsBeingEdited(false) + chatThreadsService.setFocusedMessageIdx(undefined) + chatThreadsService.closeStagingSelectionsInMessage(messageIdx) + + // stream the edit + const userMessage = textAreaRefState.value; + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) + } + + const onAbort = () => { + const threadId = chatThreadsService.state.currentThreadId + chatThreadsService.cancelStreaming(threadId) + } + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCloseEdit() + } + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit() + } + } + + if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show). + return null + } + + chatbubbleContents = + setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true) + chatThreadsService.setFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false) + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + + } + + return
setIsHovered(true)} @@ -1059,29 +757,21 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
{ - if (role === 'user' && mode === 'display') { - onOpenEdit() - } - }} + onClick={() => { if (mode === 'display') { onOpenEdit() } }} > {chatbubbleContents} - {isLoading && }
- {/* edit button */} + {role === 'user' && } + +
+ + + +} + + +const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => { + + const accessor = useAccessor() + const chatThreadsService = accessor.get('IChatThreadService') + + + const reasoningStr = chatMessage.reasoning?.trim() || null + const hasReasoning = !!reasoningStr + const thread = chatThreadsService.getCurrentThread() + + const chatMessageLocation: ChatMessageLocation = { + threadId: thread.id, + messageIdx: messageIdx, + } + + return
+ + {/* reasoning token (anthropic) */} + {hasReasoning && + + } + + {/* assistant message */} + + + {isLoading && } + +
+ +} + + + +const ToolError = ({ title, errorMessage }: { title: string, errorMessage: string }) => { + return ( +
+ +
+ {title} +
{'Error: ' + errorMessage}
+
+
+ ) +} + + +const toolNameToTitle: Record = { + 'read_file': 'Read file', + 'list_dir': 'Inspected folder', + 'pathname_search': 'Searched filename', + 'search': 'Searched', + 'create_uri': 'Created file', + 'delete_uri': 'Deleted file', + 'edit': 'Edited file', + 'terminal_command': 'Ran terminal command' +} + + + + +const toolNameToComponent: { [T in ToolName]: (props: { chatMessage: ToolMessage }) => React.ReactNode } = { + 'read_file': ({ chatMessage }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + + if (chatMessage.result.type === 'error') return + + const { value, params } = chatMessage.result + return ( + { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + > +
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + > +
+ {params.uri.fsPath} +
+ {value.hasNextPage && (
AI can scroll for more content...
)} +
+ ) + }, + 'list_dir': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const explorerService = accessor.get('IExplorerService') + const title = toolNameToTitle[chatMessage.name] + // message.result.hasNextPage = true + // message.result.itemsRemaining = 400 + if (chatMessage.result.type === 'error') return + + const { value, params } = chatMessage.result + return ( + + + {value.children?.map((child, i) => ( +
{ + commandService.executeCommand('workbench.view.explorer'); + explorerService.select(child.uri, true); + }} + > +
+ {`${child.name}${child.isDirectory ? '/' : ''}`} +
+ ))} + {value.hasNextPage && ( +
+ {value.itemsRemaining} more items... +
+ )} +
+ + ) + }, + 'pathname_search': ({ chatMessage }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + if (chatMessage.result.type === 'error') return + + const { value, params } = chatMessage.result + return ( + + {value.uris.map((uri, i) => ( +
{ + commandService.executeCommand('vscode.open', uri, { preview: true }) + }} + > +
+ {uri.fsPath.split('/').pop()} +
+ )) + } + {value.hasNextPage && ( +
+ More results available... +
+ )} +
+ ) + }, + 'search': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + if (chatMessage.result.type === 'error') return + + const { value, params } = chatMessage.result + return ( + + {value.uris.map((uri, i) => ( +
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} + > +
+ {uri.fsPath.split('/').pop()} +
+ ))} + {value.hasNextPage && (
More results available...
)} +
+ ) + }, + + 'create_uri': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + + if (chatMessage.result.type === 'error') return + + const { params } = chatMessage.result + return ( + { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> + ) + }, + 'delete_uri': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + + if (chatMessage.result.type === 'error') return + + const { params } = chatMessage.result + return ( + { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> + ) + }, + 'edit': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + + if (chatMessage.result.type === 'error') return + + const { params } = chatMessage.result + return ( + { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> + ) + }, + 'terminal_command': ({ chatMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + const title = toolNameToTitle[chatMessage.name] + + if (chatMessage.result.type === 'error') return + + const { params } = chatMessage.result + return ( + +
+
+ +
+
+ ) + } + +}; + + +type ChatBubbleMode = 'display' | 'edit' +type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, } +const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) => { + + const role = chatMessage.role + + if (role === 'user') { + + return + + } + + else if (role === 'assistant') { + + return + + } + else if (role === 'tool') { + + + const ToolMessageComponent = toolNameToComponent[chatMessage.name] as React.FC<{ chatMessage: any, messageIdx: any, isLoading: any }> // ts isnt smart enough... + + return + + } + + } @@ -1225,14 +1257,14 @@ export const SidebarChat = () => { key={currentThread.id} // force rerender on all children if id changes scrollContainerRef={scrollContainerRef} className={` - w-full h-auto - flex flex-col - overflow-x-hidden - overflow-y-auto - py-4 - ${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} - `} - style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights + flex flex-col + px-4 py-4 space-y-4 + w-full h-auto + overflow-x-hidden + overflow-y-auto + ${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} + `} + style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - (25) }} // the height of the previousMessages is determined by all other heights > {/* previous messages */} {allMessagesHTML} @@ -1260,8 +1292,10 @@ export const SidebarChat = () => { const onKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { onSubmit() + } else if (e.key === 'Escape' && isStreaming) { + onAbort() } - }, [onSubmit]) + }, [onSubmit, onAbort, isStreaming]) const inputForm =
0 ? 'absolute bottom-0' : ''}`}> { > { chatThreadsService.setFocusedMessageIdx(undefined) }} @@ -1298,5 +1332,3 @@ export const SidebarChat = () => {
} - -