mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
show current tool running + misc style fixes + chat interrupt stops tool too
This commit is contained in:
parent
9ee14e6a71
commit
a7728a6030
6 changed files with 213 additions and 146 deletions
|
|
@ -106,10 +106,11 @@ export type ThreadsState = {
|
|||
currentThreadId: string; // intended for internal use only
|
||||
}
|
||||
|
||||
export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user'
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
// state related
|
||||
isRunning?: undefined | 'message' | 'tool'; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response)
|
||||
isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response)
|
||||
error?: { message: string, fullError: Error | null, };
|
||||
|
||||
// streaming related
|
||||
|
|
@ -650,6 +651,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
this._cancelToolOfThreadId[threadId]?.()
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role !== 'tool_request') return // should never happen
|
||||
const { name, params, paramsStr, id } = lastMessage
|
||||
|
|
@ -665,6 +668,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
// abort the stream first so it doesn't change any state
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
|
||||
|
||||
// add the correct message to the state
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role === 'tool_request') {
|
||||
// interrupt tool request
|
||||
|
|
@ -675,9 +683,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
}
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
}
|
||||
|
||||
|
|
@ -701,6 +706,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {}
|
||||
|
||||
private async _chatAgentLoop({
|
||||
threadId,
|
||||
prevSelns,
|
||||
|
|
@ -754,7 +761,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const handleToolCall = async (
|
||||
tool: ToolCallType,
|
||||
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
|
||||
): Promise<boolean> => {
|
||||
): Promise<{ awaitingUserApproval: boolean, canceled: boolean }> => {
|
||||
const toolName: ToolName = tool.name
|
||||
const toolParamsStr = tool.paramsStr
|
||||
const toolId = tool.id
|
||||
|
|
@ -772,14 +779,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, })
|
||||
return false
|
||||
return { awaitingUserApproval: false, canceled: false }
|
||||
}
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
const requiresApproval = !this._settingsService.state.globalSettings.autoApprove
|
||||
if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) {
|
||||
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
|
||||
return true
|
||||
return { awaitingUserApproval: true, canceled: false }
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -788,12 +795,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// 3. call the tool
|
||||
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
let canceled = false
|
||||
try {
|
||||
toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad...
|
||||
} catch (error) {
|
||||
const { result, cancel } = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad...
|
||||
this._cancelToolOfThreadId[threadId] = cancel
|
||||
let cancelRes: () => void = () => { }
|
||||
const resolveIfCancel = new Promise<never>((res, rej) => { cancelRes = rej })
|
||||
this._cancelToolOfThreadId[threadId] = () => { cancel?.(); canceled = true; delete this._cancelToolOfThreadId[threadId]; cancelRes() }
|
||||
toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad...
|
||||
}
|
||||
catch (error) {
|
||||
if (canceled) return { awaitingUserApproval: false, canceled: true }
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
return false
|
||||
return { awaitingUserApproval: true, canceled: false }
|
||||
}
|
||||
|
||||
// 4. stringify the result to give to the LLM
|
||||
|
|
@ -802,12 +817,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
} catch (error) {
|
||||
const errorMessage = this.errMsgs.errWhenStringifying(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
return false
|
||||
return { awaitingUserApproval: false, canceled: false }
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, })
|
||||
return false
|
||||
return { awaitingUserApproval: false, canceled: false }
|
||||
};
|
||||
|
||||
// above just defines helpers, below starts the actual function
|
||||
|
|
@ -819,13 +834,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval'
|
||||
let isRunningWhenEnd: IsRunningType = undefined
|
||||
let aborted = false
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
|
||||
await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
|
||||
const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
|
||||
if (canceled) return
|
||||
}
|
||||
|
||||
// tool use loop
|
||||
|
|
@ -834,7 +850,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// false by default each iteration
|
||||
shouldSendAnotherMessage = false
|
||||
exitReason = 'end'
|
||||
isRunningWhenEnd = undefined
|
||||
|
||||
nMessagesSent += 1
|
||||
|
||||
|
|
@ -860,9 +876,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// call tool if there is one
|
||||
const tool: ToolCallType | undefined = toolCalls?.[0]
|
||||
if (tool) {
|
||||
const awaitingUserApproval = await handleToolCall(tool)
|
||||
const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort
|
||||
if (awaitingUserApproval) {
|
||||
exitReason = 'awaitingToolApproval'
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
} else {
|
||||
shouldSendAnotherMessage = true
|
||||
}
|
||||
|
|
@ -900,9 +916,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (aborted) { return }
|
||||
} // end while
|
||||
|
||||
|
||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
if (exitReason === 'end')
|
||||
this._setStreamState(threadId, { isRunning: undefined }, 'merge')
|
||||
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')
|
||||
|
||||
// capture number of messages sent
|
||||
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
|
||||
|
|
@ -1012,7 +1028,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
// else search codebase for `target`
|
||||
const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 })
|
||||
let uris: URI[] = []
|
||||
try {
|
||||
const { result } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 })
|
||||
uris = result.uris
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const [idx, uri] of uris.entries()) {
|
||||
if (doesUriMatchTarget(uri)) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
|||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
|
|
@ -344,9 +344,9 @@ export const BlockCodeApplyWrapper = ({
|
|||
</div>
|
||||
|
||||
{/* contents */}
|
||||
<ToolContentsWrapper>
|
||||
<ToolChildrenWrapper>
|
||||
{children}
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '.
|
|||
import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { DiffZone } from '../../../editCodeService.js';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
import { IsRunningType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -1096,11 +1097,11 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
|
|||
if (!isWriting) setIsOpen(false) // if just finished reasoning, close
|
||||
}, [isWriting])
|
||||
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
|
||||
<ToolContentsWrapper className='bg-void-bg-3'>
|
||||
<ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{children}
|
||||
</div>
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
</ToolHeaderWrapper>
|
||||
}
|
||||
|
||||
|
|
@ -1111,15 +1112,25 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
|
|||
|
||||
const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file'
|
||||
const toolNameToTitle = {
|
||||
'read_file': { done: 'Read file', proposed: 'Read file' },
|
||||
'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder' },
|
||||
'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name' },
|
||||
'text_search': { done: 'Searched', proposed: 'Search text' },
|
||||
'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` },
|
||||
'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` },
|
||||
'edit': { done: 'Edited file', proposed: 'Edit file' },
|
||||
'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command' }
|
||||
} as const satisfies Record<ToolName, { done: any, proposed: any }>
|
||||
'read_file': { done: 'Read file', proposed: 'Read file', running: 'Reading file...' },
|
||||
'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: 'Inspecting folder...' },
|
||||
'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: 'Searching by file name...' },
|
||||
'text_search': { done: 'Searched', proposed: 'Search text', running: 'Searching...' },
|
||||
'create_uri': {
|
||||
done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`,
|
||||
proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`,
|
||||
running: (isFolder: boolean) => `Creating ${folderFileStr(isFolder)}...`
|
||||
},
|
||||
'delete_uri': {
|
||||
done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`,
|
||||
proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`,
|
||||
running: (isFolder: boolean) => `Deleting ${folderFileStr(isFolder)}...`
|
||||
},
|
||||
'edit': { done: 'Edited file', proposed: 'Edit file', running: 'Editing file...' },
|
||||
'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command', running: 'Running terminal command...' }
|
||||
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
|
||||
|
||||
|
||||
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
|
||||
|
||||
|
|
@ -1235,7 +1246,7 @@ const ToolRequestAcceptRejectButtons = () => {
|
|||
</div>
|
||||
}
|
||||
|
||||
export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <div className={`${className ? className : ''} overflow-x-auto cursor-default select-none`}>
|
||||
<div className='px-2 min-w-full'>
|
||||
{children}
|
||||
|
|
@ -1266,21 +1277,25 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes
|
|||
|
||||
|
||||
const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => {
|
||||
return <ToolContentsWrapper className='bg-void-bg-3'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</SmallProseWrapper>
|
||||
</div>
|
||||
</ToolContentsWrapper>
|
||||
return <div className='!select-text cursor-auto'>
|
||||
<SmallProseWrapper>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</SmallProseWrapper>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
type ToolRequestState = 'awaiting_user' | 'running'
|
||||
|
||||
const toolNameToComponent: { [T in ToolName]: {
|
||||
requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval<T> }) => React.ReactNode) : null,
|
||||
resultWrapper: (props: { toolMessage: ToolMessage<T>, messageIdx: number }) => React.ReactNode,
|
||||
} } = {
|
||||
type RequestWrapper<T extends ToolName> = (props: { toolRequest: ToolRequestApproval<T>, messageIdx: number, toolRequestState: ToolRequestState, threadId: string }) => React.ReactNode
|
||||
type ResultWrapper<T extends ToolName> = (props: { toolMessage: ToolMessage<T>, messageIdx: number, threadId: string }) => React.ReactNode
|
||||
|
||||
type ToolComponent<T extends ToolName,> = {
|
||||
requestWrapper: T extends ToolNameWithApproval ? RequestWrapper<T> : null,
|
||||
resultWrapper: ResultWrapper<T>,
|
||||
}
|
||||
|
||||
const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
|
||||
'read_file': {
|
||||
requestWrapper: null,
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
|
|
@ -1299,12 +1314,12 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
if (toolMessage.result.type === 'success') {
|
||||
const { value, params } = toolMessage.result
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
if (toolMessage.result.value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)`
|
||||
if (value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)`
|
||||
}
|
||||
else {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { value, params } = toolMessage.result
|
||||
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
|
@ -1330,7 +1345,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
componentParams.numResults = value.children?.length
|
||||
componentParams.hasNextPage = value.hasNextPage
|
||||
componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined
|
||||
: <ToolContentsWrapper>
|
||||
: <ToolChildrenWrapper>
|
||||
{value.children.map((child, i) => (<ListableToolItem key={i}
|
||||
name={`${child.name}${child.isDirectory ? '/' : ''}`}
|
||||
className='w-full overflow-auto'
|
||||
|
|
@ -1343,12 +1358,11 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
{value.hasNextPage &&
|
||||
<ListableToolItem name={`Results truncated (${value.itemsRemaining} remaining).`} isSmall={true} className='w-full overflow-auto' />
|
||||
}
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { value, params } = toolMessage.result
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
|
@ -1373,7 +1387,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
componentParams.numResults = value.uris.length
|
||||
componentParams.hasNextPage = value.hasNextPage
|
||||
componentParams.children = value.uris.length === 0 ? undefined
|
||||
: <ToolContentsWrapper>
|
||||
: <ToolChildrenWrapper>
|
||||
{value.uris.map((uri, i) => (<ListableToolItem key={i}
|
||||
name={getBasename(uri.fsPath)}
|
||||
className='w-full overflow-auto'
|
||||
|
|
@ -1383,12 +1397,11 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
<ListableToolItem name={'Results truncated.'} isSmall={true} className='w-full overflow-auto' />
|
||||
}
|
||||
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { value, params } = toolMessage.result
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
|
@ -1413,7 +1426,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
componentParams.numResults = value.uris.length
|
||||
componentParams.hasNextPage = value.hasNextPage
|
||||
componentParams.children = value.uris.length === 0 ? undefined
|
||||
: <ToolContentsWrapper>
|
||||
: <ToolChildrenWrapper>
|
||||
{value.uris.map((uri, i) => (<ListableToolItem key={i}
|
||||
name={getBasename(uri.fsPath)}
|
||||
className='w-full overflow-auto'
|
||||
|
|
@ -1423,12 +1436,11 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
<ListableToolItem name={`Results truncated.`} isSmall={true} className='w-full overflow-auto' />
|
||||
}
|
||||
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { value, params } = toolMessage.result
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
|
|
@ -1437,12 +1449,13 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
// ---
|
||||
|
||||
'create_uri': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
requestWrapper: ({ toolRequest, toolRequestState }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const explorerService = accessor.get('IExplorerService')
|
||||
const isError = false
|
||||
const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder)
|
||||
const isFolder = toolRequest.params.isFolder
|
||||
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder)
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
const icon = null
|
||||
|
||||
|
|
@ -1463,7 +1476,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { params } = toolMessage.result
|
||||
const { params, value } = toolMessage.result
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
}
|
||||
else if (toolMessage.result.type === 'rejected') {
|
||||
|
|
@ -1471,20 +1484,21 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
}
|
||||
else if (toolMessage.result.type === 'error') {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { params, value } = toolMessage.result
|
||||
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
},
|
||||
'delete_uri': {
|
||||
requestWrapper: ({ toolRequest, }) => {
|
||||
requestWrapper: ({ toolRequest, toolRequestState }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const isError = false
|
||||
const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder)
|
||||
const isFolder = toolRequest.params.isFolder
|
||||
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder)
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
const icon = null
|
||||
|
||||
|
|
@ -1508,7 +1522,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { params } = toolMessage.result
|
||||
const { params, value } = toolMessage.result
|
||||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
}
|
||||
else if (toolMessage.result.type === 'rejected') {
|
||||
|
|
@ -1516,38 +1530,38 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
|
||||
}
|
||||
else if (toolMessage.result.type === 'error') {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
const { params, value } = toolMessage.result
|
||||
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
},
|
||||
'edit': {
|
||||
requestWrapper: ({ toolRequest, }) => {
|
||||
requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const isError = false
|
||||
const title = toolNameToTitle[toolRequest.name].proposed
|
||||
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
const icon = null
|
||||
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
|
||||
|
||||
const { params } = toolRequest
|
||||
componentParams.children = <EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
/>
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
|
||||
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
},
|
||||
resultWrapper: ({ toolMessage, messageIdx }) => {
|
||||
resultWrapper: ({ toolMessage, messageIdx, threadId }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const isError = toolMessage.result.type === 'error'
|
||||
const isRejected = toolMessage.result.type === 'rejected'
|
||||
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed
|
||||
|
|
@ -1556,42 +1570,58 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') {
|
||||
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') {
|
||||
const { params } = toolMessage.result
|
||||
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: threadId,
|
||||
messageIdx: messageIdx,
|
||||
tokenIdx: 'N/A',
|
||||
})
|
||||
// add apply box
|
||||
if (params) {
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: threadId,
|
||||
messageIdx: messageIdx,
|
||||
tokenIdx: 'N/A',
|
||||
})
|
||||
componentParams.desc2 = <EditToolApplyButton
|
||||
changeDescription={params.changeDescription}
|
||||
applyBoxId={applyBoxId}
|
||||
uri={params.uri}
|
||||
/>
|
||||
}
|
||||
|
||||
componentParams.children = <EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
/>
|
||||
componentParams.desc2 = <EditToolApplyButton
|
||||
changeDescription={params.changeDescription}
|
||||
applyBoxId={applyBoxId}
|
||||
uri={params.uri}
|
||||
/>
|
||||
}
|
||||
else if (toolMessage.result.type === 'error') {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
// add children
|
||||
if (toolMessage.result.type !== 'error') {
|
||||
const { params } = toolMessage.result
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
else {
|
||||
// error
|
||||
const { params, value } = toolMessage.result
|
||||
if (params) {
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
|
||||
{value}
|
||||
<EditToolChildren
|
||||
uri={params.uri}
|
||||
changeDescription={params.changeDescription}
|
||||
/>
|
||||
</ToolChildrenWrapper>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
}
|
||||
},
|
||||
'terminal_command': {
|
||||
requestWrapper: ({ toolRequest, }) => {
|
||||
requestWrapper: ({ toolRequest, toolRequestState }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const terminalToolsService = accessor.get('ITerminalToolService')
|
||||
const isError = false
|
||||
const title = toolNameToTitle[toolRequest.name].proposed
|
||||
const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
const icon = null
|
||||
|
||||
|
|
@ -1618,8 +1648,9 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { command } = toolMessage.result.params
|
||||
const { terminalId, resolveReason, result } = toolMessage.result.value
|
||||
const { value, params } = toolMessage.result
|
||||
const { command } = params
|
||||
const { terminalId, resolveReason, result } = value
|
||||
|
||||
const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null)
|
||||
: resolveReason.type === 'bgtask' ? null :
|
||||
|
|
@ -1627,7 +1658,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
resolveReason.type === 'toofull' ? `\n(truncated)`
|
||||
: null
|
||||
|
||||
componentParams.children = <ToolContentsWrapper className='bg-void-bg-3 font-mono whitespace-pre text-nowrap overflow-auto text-sm'>
|
||||
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3 font-mono whitespace-pre text-nowrap overflow-auto text-sm'>
|
||||
<ListableToolItem
|
||||
showDot={false}
|
||||
name={`$ ${command}`}
|
||||
|
|
@ -1639,23 +1670,25 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
{result}
|
||||
{resultStr}
|
||||
</div>
|
||||
</ToolContentsWrapper>
|
||||
</ToolChildrenWrapper>
|
||||
|
||||
|
||||
if (resolveReason.type === 'bgtask')
|
||||
componentParams.desc2 = '(background task)'
|
||||
}
|
||||
else if (toolMessage.result.type === 'rejected') {
|
||||
const { proposedTerminalId, waitForCompletion } = toolMessage.result.params
|
||||
if (terminalToolsService.terminalExists(proposedTerminalId))
|
||||
componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId)
|
||||
if (!waitForCompletion)
|
||||
componentParams.desc2 = '(background task)'
|
||||
}
|
||||
else if (toolMessage.result.type === 'error') {
|
||||
componentParams.children = <>
|
||||
{toolMessage.result.value}
|
||||
</>
|
||||
else if (toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') {
|
||||
const { params } = toolMessage.result
|
||||
if (params) {
|
||||
const { proposedTerminalId, waitForCompletion } = params
|
||||
if (terminalToolsService.terminalExists(proposedTerminalId))
|
||||
componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId)
|
||||
if (!waitForCompletion)
|
||||
componentParams.desc2 = '(background task)'
|
||||
}
|
||||
if (toolMessage.result.type === 'error') {
|
||||
const { value } = toolMessage.result
|
||||
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
return <ToolHeaderWrapper {...componentParams} />
|
||||
|
|
@ -1670,9 +1703,11 @@ type ChatBubbleProps = {
|
|||
messageIdx: number,
|
||||
isCommitted: boolean,
|
||||
isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message)
|
||||
chatIsRunning: IsRunningType,
|
||||
threadId: string,
|
||||
}
|
||||
|
||||
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubbleProps) => {
|
||||
const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => {
|
||||
const role = chatMessage.role
|
||||
|
||||
if (role === 'user') {
|
||||
|
|
@ -1691,18 +1726,23 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubble
|
|||
/>
|
||||
}
|
||||
else if (role === 'tool_request') {
|
||||
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
|
||||
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as RequestWrapper<ToolName>
|
||||
const toolRequestType = (
|
||||
chatIsRunning === 'awaiting_user' ? 'awaiting_user'
|
||||
: chatIsRunning === 'tool' ? 'running'
|
||||
: null
|
||||
)
|
||||
if (ToolRequestWrapper && isLast) { // if it's the last message
|
||||
return <>
|
||||
<ToolRequestWrapper toolRequest={chatMessage} />
|
||||
<ToolRequestAcceptRejectButtons />
|
||||
{toolRequestType !== null && <ToolRequestWrapper toolRequestState={toolRequestType} toolRequest={chatMessage} messageIdx={messageIdx} threadId={threadId} />}
|
||||
{chatIsRunning === 'awaiting_user' && <ToolRequestAcceptRejectButtons />}
|
||||
</>
|
||||
}
|
||||
return null
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough...
|
||||
return <ToolResultWrapper toolMessage={chatMessage} messageIdx={messageIdx} />
|
||||
const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as ResultWrapper<ToolName>
|
||||
return <ToolResultWrapper toolMessage={chatMessage} messageIdx={messageIdx} threadId={threadId} />
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1801,18 +1841,22 @@ export const SidebarChat = () => {
|
|||
const numMessages = previousMessages.length
|
||||
|
||||
const previousMessagesHTML = useMemo(() => {
|
||||
const threadId = currentThread.id
|
||||
return previousMessages.map((message, i) => {
|
||||
const isLast = i === numMessages - 1 && isRunning !== 'tool'
|
||||
const isLast = i === numMessages - 1
|
||||
return <ChatBubble key={getChatBubbleId(currentThread.id, i)}
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
isLast={isLast}
|
||||
isCommitted={true}
|
||||
chatIsRunning={isRunning}
|
||||
isLast={isLast}
|
||||
threadId={threadId}
|
||||
/>
|
||||
}
|
||||
)
|
||||
}, [previousMessages, isRunning, currentThread, numMessages])
|
||||
|
||||
const threadId = currentThread.id
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
|
|
@ -1824,7 +1868,9 @@ export const SidebarChat = () => {
|
|||
}}
|
||||
messageIdx={streamingChatIdx}
|
||||
isCommitted={!isRunning}
|
||||
chatIsRunning={isRunning}
|
||||
isLast={true}
|
||||
threadId={threadId}
|
||||
/> : null
|
||||
|
||||
const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML]
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor
|
|||
const [currUriHasChanges, setCurrUriHasChanges] = useState(false)
|
||||
const anyUriHasChanges = sortedCommandBarURIs.length !== 0
|
||||
useEffect(() => {
|
||||
console.log('uri', uri?.fsPath, sortedCommandBarURIs)
|
||||
const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath)
|
||||
if (i !== -1) {
|
||||
setUriIdx(i)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { basename } from '../../../../base/common/path.js'
|
|||
|
||||
|
||||
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<ToolResultType[T]> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], cancel?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
|
||||
|
||||
|
||||
|
|
@ -179,7 +179,6 @@ export class ToolsService implements IToolsService {
|
|||
public callTool: CallTool;
|
||||
public stringOfResult: ToolResultToString;
|
||||
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
|
|
@ -283,12 +282,12 @@ export class ToolsService implements IToolsService {
|
|||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
|
||||
return { fileContents, hasNextPage }
|
||||
return { result: { fileContents, hasNextPage } }
|
||||
},
|
||||
|
||||
list_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber)
|
||||
return dirResult
|
||||
return { result: dirResult }
|
||||
},
|
||||
|
||||
pathname_search: async ({ queryStr, pageNumber }) => {
|
||||
|
|
@ -304,7 +303,7 @@ export class ToolsService implements IToolsService {
|
|||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { uris, hasNextPage }
|
||||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
text_search: async ({ queryStr, pageNumber }) => {
|
||||
|
|
@ -322,7 +321,7 @@ export class ToolsService implements IToolsService {
|
|||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { queryStr, uris, hasNextPage }
|
||||
return { result: { queryStr, uris, hasNextPage } }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
|
@ -333,12 +332,12 @@ export class ToolsService implements IToolsService {
|
|||
else {
|
||||
await fileService.createFile(uri)
|
||||
}
|
||||
return {}
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
delete_uri: async ({ uri, isRecursive }) => {
|
||||
await fileService.del(uri, { recursive: isRecursive })
|
||||
return {}
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
edit: async ({ uri, changeDescription }) => {
|
||||
|
|
@ -350,13 +349,15 @@ export class ToolsService implements IToolsService {
|
|||
startBehavior: 'keep-conflicts',
|
||||
})
|
||||
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
|
||||
const [_, applyDonePromise] = res
|
||||
await applyDonePromise
|
||||
return {}
|
||||
const [diffZoneURI, applyDonePromise] = res
|
||||
|
||||
const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI })
|
||||
|
||||
return { result: applyDonePromise, cancel }
|
||||
},
|
||||
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { terminalId, didCreateTerminal, result, resolveReason }
|
||||
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
|||
if (e.uri.fsPath !== uri.fsPath) continue
|
||||
// --- sortedURIs: delete if empty, add if not in state yet
|
||||
const diffZones = this._getDiffZonesOnURI(uri)
|
||||
console.log('addordelete diffzone', uri.fsPath, diffZones)
|
||||
if (diffZones.length === 0) {
|
||||
this._deleteURIEntryFromState(uri)
|
||||
this._onDidChangeState.fire({ uri })
|
||||
|
|
|
|||
Loading…
Reference in a new issue