tool use + checkpoint state updates

This commit is contained in:
Andrew Pareles 2025-04-06 22:08:00 -07:00
parent 5f76fa8d7d
commit 5b0d8f4418
4 changed files with 365 additions and 300 deletions

View file

@ -21,7 +21,7 @@ import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval,
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js';
import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js';
@ -71,7 +71,7 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning })
else if (c.role === 'tool')
llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content })
else if (c.role === 'tool_request') { // pass
else if (c.role === 'decorative_canceled_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
@ -125,7 +125,12 @@ export type ThreadsState = {
currentThreadId: string; // intended for internal use only
}
export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user'
export type IsRunningType =
| 'LLM' // the LLM is currently streaming
| 'tool' // whether a tool is currently running
| 'awaiting_user' // awaiting user call
| undefined
export type ThreadStreamState = {
[threadId: string]: undefined | {
// state related to streaming (not just when streaming)
@ -460,13 +465,33 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
private _swapOutLatestStreamingToolWithResult = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
const messages = this.state.allThreads[threadId]?.messages
if (!messages) return false
const lastMsg = messages[messages.length - 1]
if (!lastMsg) return false
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
this._editMessageInThread(threadId, messages.length - 1, tool)
return true
}
return false
}
private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => {
const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool)
if (swapped) return
this._addMessageToThread(threadId, tool)
}
approveLatestToolRequest(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role !== 'tool_request') return // should never happen
const lastMsg = thread.messages[thread.messages.length - 1]
if (!(
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
)) return // should never happen
const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user')
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
@ -476,7 +501,18 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const prevSelns: StagingSelectionItem[] = this._getAllSelections(threadId)
const currSelns: StagingSelectionItem[] = []
const callThisToolFirst: ToolRequestApproval<ToolName> = lastMessage
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
this._updateLatestToolTo(threadId, {
role: 'tool',
type: 'running_now',
name: lastMsg.name,
paramsStr: lastMsg.paramsStr,
id: lastMsg.id,
params: lastMsg.params,
content: '(value not received yet...)', // this typically shouldn't ever get read
result: null
})
this._wrapRunAgentToNotify(
this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
@ -487,37 +523,64 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role !== 'tool_request') return // should never happen
const { name, params, paramsStr, id } = lastMessage
const lastMsg = thread.messages[thread.messages.length - 1]
let params: ToolCallParams[ToolName]
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
params = lastMsg.params
}
else return
const { name, paramsStr, id } = lastMsg
const errorMessage = this.errMsgs.rejected
this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null })
this._setStreamState(threadId, {}, 'set')
}
// private _rejectLatestStreamingTool(threadId: string) {
// const thread = this.state.allThreads[threadId]
// if (!thread) return // should never happen
// const lastMessage = thread.messages[thread.messages.length - 1]
// if (lastMessage.role !== 'tool') return
// const { name, paramsStr, id, result } = lastMessage
// if (result.type !== 'running_now') return
// const { params } = result
// const errorMessage = this.errMsgs.rejected
// this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
// this._setStreamState(threadId, {}, 'set')
// }
stopRunning(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const isRunning = this.streamState[threadId]?.isRunning
// reject the tool for the user
if (isRunning === 'awaiting_user') {
this.rejectLatestToolRequest(threadId)
}
// interrupt the tool
else if (isRunning === 'tool') {
this._currentlyRunningToolInterruptor[threadId]?.()
}
// reject the tool for the user if relevant
this.rejectLatestToolRequest(threadId)
// interrupt the tool if relevant
this._currentlyRunningToolInterruptor[threadId]?.()
// interrupt assistant message
else if (isRunning === 'message') {
const isRunning = this.streamState[threadId]?.isRunning
if (isRunning === 'LLM') {
// abort the stream first so it doesn't change any state
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolInProgress = this.streamState[threadId]?.toolNameSoFar
console.log('toolInProgress', toolInProgress)
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolInProgress) {
this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress })
}
}
this._setStreamState(threadId, {}, 'set')
@ -559,7 +622,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
modelSelectionOptions: ModelSelectionOptions | undefined,
userMessageContent: string, // content of LATEST user message
callThisToolFirst?: ToolRequestApproval<ToolName>
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) {
const userMessageFullContent = userMessageContent
const getLatestMessages = async () => {
@ -620,16 +683,18 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolParams = params
} 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 }, })
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
if (requiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@ -643,8 +708,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
let interrupted = false
try {
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
@ -655,12 +718,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
catch (error) {
if (interrupted) {
// ideally this should have same implementation as abort - addMessage should get called in stopRunning
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: this.errMsgs.rejected, result: { type: 'rejected', params: toolParams }, })
// the tool result is added when we stop running
return { interrupted: 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 }, })
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
@ -669,12 +731,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} 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 }, })
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, })
return {}
}
// 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 }, })
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, })
return {}
};
@ -708,7 +770,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const messageIsDonePromise = new Promise<ToolCallType[] | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'message' }, 'merge')
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const messages = await getLatestMessages()
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',

View file

@ -22,7 +22,7 @@ import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
@ -176,10 +176,12 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) =>
const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters
const stepSize = Math.round((max - min_) / nSteps)
const min = canTurnOffReasoning ? min_ - stepSize : min_
const value = isReasoningEnabled ? min_ - stepSize
: voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal
const valueIfOff = min_ - stepSize
const min = canTurnOffReasoning ? valueIfOff : min_
const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal
: valueIfOff
return <div className='flex items-center gap-x-2'>
<span className='text-void-fg-3 text-xs pointer-events-none inline-block w-10 pr-1'>Thinking</span>
@ -1137,33 +1139,33 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
// 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) => {
const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => {
return <span className='flex items-center flex-nowrap'>
{item}
<IconLoading className='w-3 text-sm' />
</span>
}
const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file'
const titleOfToolName = {
'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_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') },
'create_file_or_folder': {
done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`,
running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`)
},
'delete_file_or_folder': {
done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`,
proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`,
running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`)
},
'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') },
'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
const t = toolMessage
if (!toolNames.includes(t.name as ToolName)) return t.name // good measure
const toolName = t.name as ToolName
if (t.type === 'success') return titleOfToolName[toolName].done
if (t.type === 'running_now') return titleOfToolName[toolName].running
return titleOfToolName[toolName].proposed
}
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
@ -1329,46 +1331,63 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
</div>
}
type ToolRequestState = 'awaiting_user' | 'running'
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 InvalidTool = ({ toolName }: { toolName: string }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'invalid_params' })
const desc1 = 'Invalid parameters'
const icon = null
const isError = true
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
return <ToolHeaderWrapper {...componentParams} />
}
const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const CanceledTool = ({ toolName }: { toolName: string }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'rejected' })
const desc1 = ''
const icon = null
const isRejected = true
const componentParams: ToolHeaderParams = { title, desc1, icon, isRejected }
return <ToolHeaderWrapper {...componentParams} />
}
type ResultWrapper<T extends ToolName> = (props: { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode
const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>, } } = {
'read_file': {
requestWrapper: null,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const { uri } = toolMessage.result.params ?? {}
const title = getTitle(toolMessage)
const { uri } = toolMessage.params ?? {}
const desc1 = uri ? getBasename(uri.fsPath) : '';
const icon = null
if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const isError = toolMessage.result.type === 'error'
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
if (value.hasNextPage && params.pageNumber === 1) // first page
if (result.hasNextPage && params.pageNumber === 1) // first page
componentParams.desc2 = '(more content available)'
else if (params.pageNumber > 1) // subsequent pages
componentParams.desc2 = `(part ${params.pageNumber})`
}
else {
const { value, params } = toolMessage.result
else if (toolMessage.type === 'tool_error') {
const { params, result } = toolMessage
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1377,26 +1396,27 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
},
},
'get_dir_structure': {
requestWrapper: null,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const isError = toolMessage.result.type === 'error'
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
<SmallProseWrapper>
<ChatMarkdownRender
string={`\`\`\`\n${value.str}\n\`\`\``}
string={`\`\`\`\n${result.str}\n\`\`\``}
chatMessageLocation={undefined}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
@ -1405,10 +1425,10 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
</ToolChildrenWrapper>
}
else {
const { value, params } = toolMessage.result
const { params, result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1418,27 +1438,28 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
},
'ls_dir': {
requestWrapper: null,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const explorerService = accessor.get('IExplorerService')
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const isError = toolMessage.result.type === 'error'
const isError = toolMessage.type === 'tool_error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.children?.length
componentParams.hasNextPage = value.hasNextPage
componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.numResults = result.children?.length
componentParams.hasNextPage = result.hasNextPage
componentParams.children = !result.children || (result.children.length ?? 0) === 0 ? undefined
: <ToolChildrenWrapper>
{value.children.map((child, i) => (<ListableToolItem key={i}
{result.children.map((child, i) => (<ListableToolItem key={i}
name={`${child.name}${child.isDirectory ? '/' : ''}`}
className='w-full overflow-auto'
onClick={() => {
@ -1447,16 +1468,16 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
// explorerService.select(child.uri, true);
}}
/>))}
{value.hasNextPage &&
<ListableToolItem name={`Results truncated (${value.itemsRemaining} remaining).`} isSmall={true} className='w-full overflow-auto' />
{result.hasNextPage &&
<ListableToolItem name={`Results truncated (${result.itemsRemaining} remaining).`} isSmall={true} className='w-full overflow-auto' />
}
</ToolChildrenWrapper>
}
else {
const { value, params } = toolMessage.result
const { params, result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1465,41 +1486,42 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
},
'search_pathnames_only': {
requestWrapper: null,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isError = toolMessage.type === 'tool_error'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.uris.length
componentParams.hasNextPage = value.hasNextPage
componentParams.children = value.uris.length === 0 ? undefined
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.numResults = result.uris.length
componentParams.hasNextPage = result.hasNextPage
componentParams.children = result.uris.length === 0 ? undefined
: <ToolChildrenWrapper>
{value.uris.map((uri, i) => (<ListableToolItem key={i}
{result.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
className='w-full overflow-auto'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>))}
{value.hasNextPage &&
{result.hasNextPage &&
<ListableToolItem name={'Results truncated.'} isSmall={true} className='w-full overflow-auto' />
}
</ToolChildrenWrapper>
}
else {
const { value, params } = toolMessage.result
const { params, result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1508,41 +1530,42 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
},
'search_files': {
requestWrapper: null,
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isError = toolMessage.type === 'tool_error'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'tool_request') return null
if (toolMessage.type === 'rejected') return null // will never happen, not rejectable
if (toolMessage.type === 'running_now') return null // do not show running
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
componentParams.numResults = value.uris.length
componentParams.hasNextPage = value.hasNextPage
componentParams.children = value.uris.length === 0 ? undefined
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.numResults = result.uris.length
componentParams.hasNextPage = result.hasNextPage
componentParams.children = result.uris.length === 0 ? undefined
: <ToolChildrenWrapper>
{value.uris.map((uri, i) => (<ListableToolItem key={i}
{result.uris.map((uri, i) => (<ListableToolItem key={i}
name={getBasename(uri.fsPath)}
className='w-full overflow-auto'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>))}
{value.hasNextPage &&
{result.hasNextPage &&
<ListableToolItem name={`Results truncated.`} isSmall={true} className='w-full overflow-auto' />
}
</ToolChildrenWrapper>
}
else {
const { value, params } = toolMessage.result
const { params, result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
@ -1553,137 +1576,111 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
// ---
'create_file_or_folder': {
requestWrapper: ({ toolRequest, toolRequestState }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const explorerService = accessor.get('IExplorerService')
const isError = false
const isFolder = toolRequest.params.isFolder
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder)
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const isFolder = toolMessage.result.params?.isFolder ?? false
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isError = toolMessage.type === 'tool_error'
const isRejected = toolMessage.type === 'rejected'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { params, value } = toolMessage.result
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.result.type === 'rejected') {
const { params } = toolMessage.result
else if (toolMessage.type === 'rejected') {
const { params } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.result.type === 'error') {
const { params, value } = toolMessage.result
else if (toolMessage.type === 'tool_error') {
const { params, result } = toolMessage
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
componentParams.children = componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
else if (toolMessage.type === 'running_now') {
// nothing more is needed
}
else if (toolMessage.type === 'tool_request') {
// nothing more is needed
}
return <ToolHeaderWrapper {...componentParams} />
}
},
'delete_file_or_folder': {
requestWrapper: ({ toolRequest, toolRequestState }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isError = false
const isFolder = toolRequest.params.isFolder
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder)
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
const { params } = toolRequest
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const isFolder = toolMessage.result.params?.isFolder ?? false
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isFolder = toolMessage.params?.isFolder ?? false
const isError = toolMessage.type === 'tool_error'
const isRejected = toolMessage.type === 'rejected'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { params, value } = toolMessage.result
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.result.type === 'rejected') {
const { params } = toolMessage.result
else if (toolMessage.type === 'rejected') {
const { params } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.result.type === 'error') {
const { params, value } = toolMessage.result
else if (toolMessage.type === 'tool_error') {
const { params, result } = toolMessage
if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } }
componentParams.children = componentParams.children = <ToolChildrenWrapper>
<CodeChildren>
{value}
{result}
</CodeChildren>
</ToolChildrenWrapper>
}
else if (toolMessage.type === 'running_now') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
else if (toolMessage.type === 'tool_request') {
const { params, result } = toolMessage
componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }
}
return <ToolHeaderWrapper {...componentParams} />
}
},
'edit_file': {
requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => {
const accessor = useAccessor()
const isError = false
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[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 = <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, threadId }) => {
const accessor = useAccessor()
const isError = toolMessage.result.type === 'error'
const isRejected = toolMessage.result.type === 'rejected'
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isError = toolMessage.type === 'tool_error'
const isRejected = toolMessage.type === 'rejected'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') {
const { params } = toolMessage.result
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
const { params } = toolMessage
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
changeDescription={params.changeDescription}
/>
</ToolChildrenWrapper>
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
const { params } = toolMessage
// add apply box
if (params) {
@ -1701,8 +1698,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
// add children
if (toolMessage.result.type !== 'error') {
const { params } = toolMessage.result
if (toolMessage.type !== 'tool_error') {
const { params } = toolMessage
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
@ -1712,12 +1709,12 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
else {
// error
const { params, value } = toolMessage.result
const { params, result } = toolMessage
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<CodeChildren>
{value}
{result}
</CodeChildren>
{/* content */}
@ -1729,7 +1726,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
else {
componentParams.children = <CodeChildren>
{value}
{result}
</CodeChildren>
}
}
@ -1739,47 +1736,28 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
}
},
'run_terminal_command': {
requestWrapper: ({ toolRequest, toolRequestState }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const isError = false
const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
const icon = null
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, }
const { proposedTerminalId, waitForCompletion } = toolRequest.params
if (terminalToolsService.terminalExists(proposedTerminalId))
componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId)
if (!waitForCompletion)
componentParams.desc2 = '(background task)'
return <ToolHeaderWrapper {...componentParams} />
},
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const isError = toolMessage.result.type === 'error'
const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const isError = toolMessage.type === 'tool_error'
const title = getTitle(toolMessage)
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params)
const icon = null
const isRejected = toolMessage.result.type === 'rejected'
const isRejected = toolMessage.type === 'rejected'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { value, params } = toolMessage.result
if (toolMessage.type === 'success') {
const { params, result } = toolMessage
const { command } = params
const { terminalId, resolveReason, result } = value
const { terminalId, resolveReason, result: terminalResult } = result
componentParams.desc2 = <JumpToTerminalButton
onClick={() => { terminalToolsService.openTerminal(terminalId) }}
/>
const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null)
const additionalDetailsStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null)
: resolveReason.type === 'bgtask' ? null :
resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` :
resolveReason.type === 'toofull' ? `\n(truncated)`
@ -1795,8 +1773,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
<div>
<span>{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}</span>
<span>{`Result: `}</span>
<span className="text-void-fg-1">{result}</span>
<span className="text-void-fg-1">{resultStr}</span>
<span className="text-void-fg-1">{terminalResult}</span>
<span className="text-void-fg-1">{additionalDetailsStr}</span>
</div>
</div>
</ToolChildrenWrapper>
@ -1805,8 +1783,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
if (resolveReason.type === 'bgtask')
componentParams.desc2 = '(background task)'
}
else if (toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') {
const { params } = toolMessage.result
else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
const { params } = toolMessage
if (params) {
const { proposedTerminalId, waitForCompletion } = params
if (terminalToolsService.terminalExists(proposedTerminalId))
@ -1814,11 +1792,18 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
if (!waitForCompletion)
componentParams.desc2 = '(background task)'
}
if (toolMessage.result.type === 'error') {
const { value } = toolMessage.result
componentParams.children = <ToolChildrenWrapper>{value}</ToolChildrenWrapper>
if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>{result}</ToolChildrenWrapper>
}
}
else if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
const { proposedTerminalId, waitForCompletion } = toolMessage.params
if (terminalToolsService.terminalExists(proposedTerminalId))
componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId)
if (!waitForCompletion)
componentParams.desc2 = '(background task)'
}
return <ToolHeaderWrapper {...componentParams} />
}
@ -1833,7 +1818,7 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs
return <div
className={`
flex items-center justify-center
px-2 text-xs text-void-fg-3
px-2 text-xs text-void-fg-3 transition-all duration-200
${isCheckpointGhost ? 'opacity-50' : ''}
`}
>
@ -1855,14 +1840,13 @@ type ChatBubbleProps = {
chatMessage: ChatMessage,
messageIdx: number,
isCommitted: boolean,
canAcceptReject: boolean,
chatIsRunning: IsRunningType,
threadId: string,
currCheckpointIdx: number,
_scrollToBottom: (() => void) | null,
}
const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, canAcceptReject, chatIsRunning, threadId, _scrollToBottom }: ChatBubbleProps) => {
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
const role = chatMessage.role
const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
@ -1883,46 +1867,61 @@ const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, c
isCommitted={isCommitted}
/>
}
else if (role === 'tool_request') {
const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
const toolRequestState = (
chatIsRunning === 'awaiting_user' ? 'awaiting_user'
: chatIsRunning === 'tool' ? 'running'
: chatIsRunning === 'message' ? null
: null
)
if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
return <>
{toolRequestState !== null &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ToolRequestWrapper
toolRequestState={toolRequestState}
toolRequest={chatMessage}
messageIdx={messageIdx}
threadId={threadId}
/>
</div>}
{chatIsRunning === 'awaiting_user' &&
<div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
<ToolRequestAcceptRejectButtons />
</div>}
</>
}
return null
}
// else if (role === 'tool_request') {
// const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
// const toolRequestState = (
// chatIsRunning === 'awaiting_user' ? 'awaiting_user'
// : chatIsRunning === 'tool' ? 'running'
// : chatIsRunning === 'message' ? null
// : null
// )
// if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
// return <>
// {toolRequestState !== null &&
// <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
// <ToolRequestWrapper
// toolRequestState={toolRequestState}
// toolRequest={chatMessage}
// messageIdx={messageIdx}
// threadId={threadId}
// />
// </div>}
// {chatIsRunning === 'awaiting_user' &&
// <div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
// <ToolRequestAcceptRejectButtons />
// </div>}
// </>
// }
// return null
// }
else if (role === 'tool') {
if (chatMessage.type === 'invalid_params') {
return <InvalidTool toolName={chatMessage.name} />
}
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
if (ToolResultWrapper)
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ToolResultWrapper
toolMessage={chatMessage}
messageIdx={messageIdx}
threadId={threadId}
/>
</div>
return <>
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ToolResultWrapper
toolMessage={chatMessage}
messageIdx={messageIdx}
threadId={threadId}
/>
</div>
{chatMessage.type === 'tool_request' ?
<div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
<ToolRequestAcceptRejectButtons />
</div> : null}
</>
return null
}
else if (role === 'decorative_canceled_tool') {
return <CanceledTool toolName={chatMessage.name} />
}
else if (role === 'checkpoint') {
return <Checkpoint
threadId={threadId}
@ -2062,11 +2061,8 @@ export const SidebarChat = () => {
const previousMessagesHTML = useMemo(() => {
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
console.log('PREVMSGS', previousMessages)
// tool request shows up as Editing... if in progress
return previousMessages.map((message, i) => {
const canAcceptReject = i === lastMessageIdx && message.role === 'tool_request'
return <ChatBubble
key={getChatBubbleId(threadId, i)}
currCheckpointIdx={currCheckpointIdx}
@ -2074,7 +2070,6 @@ export const SidebarChat = () => {
messageIdx={i}
isCommitted={true}
chatIsRunning={isRunning}
canAcceptReject={canAcceptReject}
threadId={threadId}
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
/>
@ -2095,15 +2090,13 @@ export const SidebarChat = () => {
messageIdx={streamingChatIdx}
isCommitted={false}
chatIsRunning={isRunning}
canAcceptReject={false}
threadId={threadId}
_scrollToBottom={null}
/> : null
const proposedToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const generatingToolTitle = typeof proposedToolTitle === 'function' ? proposedToolTitle(null) : proposedToolTitle
const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const messagesHTML = <ScrollToBottomContainer
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes
@ -2128,7 +2121,7 @@ export const SidebarChat = () => {
<ToolHeaderWrapper key={getChatBubbleId(currentThread.id, streamingChatIdx + 1)} title={generatingToolTitle} desc1={<span className='flex items-center'>Generating<IconLoading /></span>} />
: null}
{isRunning === 'message' && !toolIsGenerating ? <ProseWrapper>
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
{/* loading indicator */}
{<IconLoading className='opacity-50 text-sm' />}
</ProseWrapper> : null}

View file

@ -10,25 +10,35 @@ import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js
export type ToolMessage<T extends ToolName> = {
role: 'tool';
name: T; // internal use
paramsStr: string; // internal use
id: string; // apis require this tool use id
content: string; // give this result to LLM
content: string; // give this result to LLM (string of value)
} & (
// in order of events:
| { type: 'invalid_params', result: null, params: null, name: string }
// if rejected, don't show in chat
result:
| { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], }
| { type: 'error'; params: ToolCallParams[T] | undefined; value: string }
| { type: 'rejected'; params: ToolCallParams[T] } // user rejected
}
export type ToolRequestApproval<T extends ToolName> = {
role: 'tool_request';
name: T; // internal use
params: ToolCallParams[T]; // internal use
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
id: string; // proposed tool's id
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
| { type: 'success', result: ToolResultType[T], name: T, params: ToolCallParams[T], }
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T], }
) // user rejected
export type DecorativeCanceledTool = {
role: 'decorative_canceled_tool';
name: string;
}
// export type ToolRequestApproval<T extends ToolName> = {
// role: 'tool_request';
// name: T; // internal use
// params: ToolCallParams[T]; // internal use
// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
// id: string; // proposed tool's id
// }
// checkpoints
export type CheckpointEntry = {
@ -61,7 +71,7 @@ export type ChatMessage =
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
| ToolMessage<ToolName>
| ToolRequestApproval<ToolName>
| DecorativeCanceledTool
| CheckpointEntry

View file

@ -79,7 +79,7 @@ export const voidTools = {
get_dir_structure: {
name: 'get_dir_structure',
description: `Returns a tree diagram of all the files and folders in the given folder URI. Call this to learn more about a folder. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
params: {
...uriParam('folder')
}