redesign how tool use loop works

This commit is contained in:
Andrew Pareles 2025-03-16 02:54:56 -07:00
parent 4a9af17736
commit 97b44a94e4
12 changed files with 292 additions and 301 deletions

View file

@ -637,6 +637,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
token: CancellationToken,
): Promise<InlineCompletion[]> {
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
if (!isEnabled) return []
const testMode = false
const docUriStr = model.uri.toString();
@ -792,10 +795,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = isEnabled ? new Promise((resolve, reject) => reject('Autocomplete is disabled')) : new Promise((resolve, reject) => {
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
messagesType: 'FIMMessage',

View file

@ -26,6 +26,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe
import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IMetricsService } from '../common/metricsService.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
for (let i = arr.length - 1; i >= 0; i--) {
@ -104,10 +105,14 @@ export type ThreadsState = {
export type ThreadStreamState = {
[threadId: string]: undefined | {
// state related
isRunning?: undefined | true; // 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
streamingToken?: string;
messageSoFar?: string;
reasoningSoFar?: string;
streamingToken?: string;
}
}
@ -135,7 +140,7 @@ export interface IChatThreadService {
readonly _serviceBrand: undefined;
readonly state: ThreadsState;
readonly streamState: ThreadStreamState;
readonly streamState: ThreadStreamState; // not persistent
onDidChangeCurrentThread: Event<void>;
onDidChangeStreamState: Event<{ threadId: string }>
@ -167,18 +172,18 @@ export interface IChatThreadService {
closeStagingSelectionsInMessage(messageIdx: number): void;
cancelStreaming(threadId: string): void;
stopRunning(threadId: string): void;
dismissStreamError(threadId: string): void;
// call to edit a message - CAN THROW
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }): Promise<void>;
// call to add a message - CAN THROW
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
addUserMessageAndStreamResponse({ userMessage }: { userMessage: string }): Promise<void>;
// approve/reject - CAN THROW
approveTool(toolId: string): void;
rejectTool(toolId: string): void;
approveTool(threadId: string): void;
rejectTool(threadId: string): void;
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
@ -205,6 +210,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ITextModelService private readonly _textModelService: ITextModelService,
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
@IMetricsService private readonly _metricsService: IMetricsService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@ -266,7 +272,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// name: 'pathname_search',
// params: { queryStr: 'hello', pageNumber: 0 },
// paramsStr: '{"query": "hello", "pageNumber": 0}',
// voidToolId: 'request-1',
// id: 'request-1',
// } satisfies ToolRequestApproval<'pathname_search'>,
{
@ -295,7 +301,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// name: 'list_dir',
// params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 },
// paramsStr: '{"uri": "/Users/username/Documents"}',
// voidToolId: 'request-2',
// id: 'request-2',
// } satisfies ToolRequestApproval<'list_dir'>,
{
@ -316,7 +322,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// name: 'read_file',
// params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 },
// paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
// voidToolId: 'request-3',
// id: 'request-3',
// } satisfies ToolRequestApproval<'read_file'>,
{
@ -344,7 +350,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// name: 'search',
// params: { queryStr: 'function main', pageNumber: 0 },
// paramsStr: '{"query": "function main"}',
// voidToolId: 'request-4',
// id: 'request-4',
// } satisfies ToolRequestApproval<'search'>,
// ---
@ -366,7 +372,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
name: 'edit',
params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
voidToolId: 'request-5',
id: 'request-5',
} satisfies ToolRequestApproval<'edit'>,
{
@ -386,7 +392,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
name: 'create_uri',
params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false },
paramsStr: '{"uri": "/Users/username/Project/new-file.js"}',
voidToolId: 'request-6',
id: 'request-6',
} satisfies ToolRequestApproval<'create_uri'>,
{
@ -406,7 +412,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
name: 'delete_uri',
params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
voidToolId: 'request-7',
id: 'request-7',
} satisfies ToolRequestApproval<'delete_uri'>,
{
@ -431,7 +437,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
name: 'terminal_command',
params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
voidToolId: 'request-8',
id: 'request-8',
} satisfies ToolRequestApproval<'terminal_command'>,
@ -493,11 +499,23 @@ class ChatThreadService extends Disposable implements IChatThreadService {
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
...state
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>, behavior: 'set' | 'merge') {
if (state === undefined)
delete this.streamState[threadId]
else {
if (behavior === 'merge') {
this.streamState[threadId] = {
...this.streamState[threadId],
...state
}
}
else if (behavior === 'set') {
this.streamState[threadId] = state
}
}
this._onDidChangeStreamState.fire({ threadId })
}
@ -506,7 +524,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
async editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }) {
const thread = this.getCurrentThread()
@ -531,46 +549,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}, true)
// re-add the message and stream it
this.addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections: { prevSelns, currSelns } })
this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: { prevSelns, currSelns } })
}
private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {}
// CAN THROW ERRORS
approveTool(toolId: string) {
const chatMode = this._settingsService.state.globalSettings.chatMode
// if not streaming, approveToolAndStreamResponse
const threadId = this.getCurrentThread().id
const isStreaming = !!this.streamState[threadId]?.streamingToken
if (!isStreaming) {
this._approveToolAndStreamResponse_NotStreamingNow({ chatMode })
}
else {
const resRej = this.resRejOfToolAwaitingApproval[toolId]
delete this.resRejOfToolAwaitingApproval[toolId]
resRej?.res()
}
}
rejectTool(toolId: string) {
// if not streaming, rejecttool
const threadId = this.getCurrentThread().id
const isStreaming = !!this.streamState[threadId]?.streamingToken
if (!isStreaming) {
this._rejectTool_NotStreamingNow({})
}
else {
const resRej = this.resRejOfToolAwaitingApproval[toolId]
delete this.resRejOfToolAwaitingApproval[toolId]
resRej?.rej()
}
}
private _currentModelSelectionProps = () => {
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
const featureName: FeatureName = 'Chat'
@ -579,8 +562,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
return { modelSelection, modelSelectionOptions }
}
approveTool(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 lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user')
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen
const instructions = lastUserMessage.displayContent || ''
const prevSelns: StagingSelectionItem[] = this._getAllSelections()
const currSelns: StagingSelectionItem[] = []
const callThisToolFirst: ToolRequestApproval<ToolName> = lastMessage
this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
}
rejectTool(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 { name, params, paramsStr, id } = lastMessage
const errorMessage = this.errMsgs.rejected
this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
}
stopRunning(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role === 'tool_request') {
// interrupt tool request
this.rejectTool(threadId)
}
else {
// interrupt assistant message
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
}
this._setStreamState(threadId, {}, 'set')
}
private _tools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
: undefined
@ -597,20 +635,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// CAN THROW ERRORS
private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent, callThisTool }: {
tools: InternalToolInfo[] | undefined,
private async _chatAgentLoop({
threadId,
prevSelns,
currSelns,
modelSelection,
modelSelectionOptions,
userMessageContent,
callThisToolFirst,
}: {
threadId: string,
prevSelns: StagingSelectionItem[],
currSelns: StagingSelectionItem[],
modelSelection: ModelSelection | null,
modelSelectionOptions: ModelSelectionOptions | undefined,
chatMode: ChatMode,
userMessageContent: string, // content of LATEST user message
callThisTool?: ToolRequestApproval<ToolName>
callThisToolFirst?: ToolRequestApproval<ToolName>
}) {
// define helper functions so we can tell what's going on
const getLatestMessages = async () => {
// recompute files in last message
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped
@ -619,7 +663,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// replace last userMessage with userMessageFullContent (which contains all the files too)
const messages_ = toLLMChatMessages(this.getCurrentThread().messages)
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!)
// system message
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
@ -638,9 +682,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const handleToolCall = async (tool: ToolCallType) => {
shouldSendAnotherMessage = true
// returns true when the tool call is waiting for user approval
const handleToolCall = async (
tool: ToolCallType,
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
): Promise<boolean> => {
const toolName: ToolName = tool.name
const toolParamsStr = tool.paramsStr
const toolId = tool.id
@ -650,31 +696,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
let toolResult: ToolResultType[typeof toolName]
let toolResultStr: string
// 1. validate tool params
try {
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
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 }, })
return false
}
// 2. if tool requires approval, await the approval
if (toolNamesThatRequireApproval.has(toolName)) {
const voidToolId = generateUuid()
const toolApprovalPromise = new Promise<void>((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } })
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, voidToolId: voidToolId })
if (!opts?.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
await toolApprovalPromise
// accepted tool
}
catch (e) {
shouldSendAnotherMessage = false // interrupt flow by rejecting
const errorMessage = this.errMsgs.rejected
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'rejected', params: toolParams }, })
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
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 }, })
return false
}
// 2. if tool requires approval, break from the loop, awaiting approval
const requiresApproval = true // TODO!!!
if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) {
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
return true
}
}
else {
toolParams = opts.toolParams
}
// 3. call the tool
@ -697,61 +738,32 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 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 true
return false
};
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
const tools = this._tools(chatMode)
// CALL GIVEN TOOL before entering agent loop
const handleFirstToolCall = async (callThisTool: ToolRequestApproval<ToolName>) => {
const toolName: ToolName = callThisTool.name
const toolParamsStr = callThisTool.paramsStr
const toolId = callThisTool.voidToolId
const toolParams = callThisTool.params
let toolResult: ToolResultType[typeof toolName]
let toolResultStr: string
// 3. call the tool
try {
toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad...
} catch (error) {
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
}
// 4. stringify the result to give to the LLM
try {
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 }, })
return 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 true
}
this._setStreamState(threadId, { error: undefined }) // clear any previous error
// clear any previous error + set running
this._setStreamState(threadId, { isRunning: true, error: undefined }, 'set')
let nMessagesSent = 0
let shouldSendAnotherMessage = true
let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval'
if (callThisTool) {
const keepGoing = await handleFirstToolCall(callThisTool)
if (!keepGoing) {
this._setStreamState(threadId, { streamingToken: undefined })
return
}
// before enter loop, call tool
if (callThisToolFirst) {
await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
}
// tool use loop
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false // false by default
// false by default each iteration
shouldSendAnotherMessage = false
exitReason = 'end'
nMessagesSent += 1
let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval)
@ -765,33 +777,34 @@ class ChatThreadService extends Disposable implements IChatThreadService {
tools: tools,
modelSelection,
modelSelectionOptions,
logging: { loggingName: `Agent` },
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) },
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') },
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, }) // added to history, so clear messages so far
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge')
// if no tool, finish
// call tool if there is one
const tool: ToolCallType | undefined = toolCalls?.[0]
if (!tool) {
this._setStreamState(threadId, { streamingToken: undefined })
resMessageIsDonePromise()
return
}
else {
const keepGoing = await handleToolCall(tool)
if (!keepGoing) { this._setStreamState(threadId, { streamingToken: undefined }) }
resMessageIsDonePromise()
return
if (tool) {
const awaitingUserApproval = await handleToolCall(tool)
if (awaitingUserApproval) {
exitReason = 'awaitingToolApproval'
} else {
shouldSendAnotherMessage = true
}
}
resMessageIsDonePromise()
return
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
})
@ -799,62 +812,31 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// should never happen, just for safety
if (llmCancelToken === null) {
this._setStreamState(threadId, {
messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined,
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
})
}, 'set')
break
}
this._setStreamState(threadId, { streamingToken: llmCancelToken }) // new stream token for the new message
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
await messageIsDonePromise
} // end while
// if awaiting user approval, keep isRunning true, else end isRunning
if (exitReason === 'end')
this._setStreamState(threadId, { isRunning: undefined }, 'merge')
// TODO!!! metrics on nMessagesSent and all the file extensions sent here
// capture number of messages sent
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
}
private async _rejectTool_NotStreamingNow({ }) {
const thread = this.getCurrentThread()
const threadId = thread.id
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role !== 'tool_request') return // should never happen
const { name, params, paramsStr, voidToolId, } = lastMessage
const errorMessage = this.errMsgs.rejected
this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id: voidToolId, content: errorMessage, result: { type: 'rejected', params: params }, })
}
// called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened
private async _approveToolAndStreamResponse_NotStreamingNow({ chatMode }: { chatMode: ChatMode }) {
const thread = this.getCurrentThread()
const threadId = thread.id
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role !== 'tool_request') return // should never happen
const lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user')
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen
const instructions = lastUserMessage.displayContent || ''
const prevSelns: StagingSelectionItem[] = this._getAllSelections()
const currSelns: StagingSelectionItem[] = []
const tools = this._tools(chatMode)
const callThisTool: ToolRequestApproval<ToolName> = lastMessage
this._agentLoop({ callThisTool, tools, prevSelns, currSelns, threadId, chatMode, userMessageContent: instructions, ...this._currentModelSelectionProps() })
}
async addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
async addUserMessageAndStreamResponse({ userMessage, _chatSelections }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
const thread = this.getCurrentThread()
const threadId = thread.id
@ -870,22 +852,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
const tools = this._tools(chatMode)
this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...this._currentModelSelectionProps(), })
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined })
this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), })
}
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined })
this._setStreamState(threadId, { error: undefined }, 'merge')
}
@ -901,10 +872,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )`
let target = _codespanStr // the string to search for
let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable';
if (target.includes('.')) {
let codespanType: 'file-or-folder' | 'function-or-class' | 'unsearchable' = 'unsearchable';
if (target.includes('.') || target.includes('/')) {
codespanType = 'file'
codespanType = 'file-or-folder'
target = _codespanStr
} else if (functionOrMethodPattern.test(target)) {
@ -933,7 +904,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
.reverse()
if (codespanType === 'file') {
if (codespanType === 'file-or-folder') {
const doesUriMatchTarget = (uri: URI) => uri.path.includes(target)

View file

@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { LucideIcon, RotateCw } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
enum CopyButtonText {
@ -278,17 +278,27 @@ export const BlockCodeApplyWrapper = ({
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
return <div
className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3 my-1'
>
const name = uri !== 'current' ?
<ListableToolItem
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
// TODO!!! this uri is not correct, it is not recognized as an actual file for some stupid reason
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
: <span>{language}</span>
return <div className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3 my-1'>
{/* header */}
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
<div className="flex items-center">
{statusIndicatorHTML}
<span className="text-[13px] font-light text-void-fg-3">
{uri !== 'current' ? getBasename(uri.fsPath) : language || 'text'}
{name}
</span>
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>

View file

@ -12,6 +12,7 @@ import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { getBasename } from '../sidebar-tsx/SidebarChat.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
export type ChatMessageLocation = {
@ -25,6 +26,9 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati
return `${threadId}-${messageIdx}-${tokenIdx}`
}
function isValidUri(s: string): boolean {
return s.includes('/') && s.length > 5 && isAbsolute(s)
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
@ -122,25 +126,24 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
if (t.type === "code") {
const [firstLine, remainingContents] = getFirstLine(t.text)
const firstLineIsURI = URI.isUri(firstLine)
const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents
const firstLineIsURI = isValidUri(firstLine)
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
// figure out langauge and URI
let uri: URI | null
let language: string | undefined = undefined
let uri: URI | undefined = undefined
if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
uri = codeURI ?? URI.from(URI.file(firstLine))
}
else {
uri = codeURI || null
}
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
uri = codeURI
language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell)
}
else { // no language provided - fallback
if (firstLineIsURI) { // get lang from the uri in the markdown
uri = codeURI ?? URI.file(firstLine)
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
}
else { // get lang from the given URI and contents
uri = codeURI
language = getLanguage(languageService, { uri: codeURI ?? null, fileContents: remainingContents ?? undefined })
}
else { // no language provided - fallback - get lang from the uri and contents
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
}
if (options.isApplyEnabled && chatMessageLocation) {

View file

@ -96,7 +96,6 @@ export const QuickEditChat = ({
isStreaming={isStreamingRef.current}
loadingIcon={loadingIcon}
isDisabled={isDisabled}
className="py-2 w-full"
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2

View file

@ -26,11 +26,10 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
import { dirname } from '../../../../../../../base/common/resources.js';
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
import { DiffZone } from '../../../editCodeService.js';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
@ -211,15 +210,15 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) =>
const nameOfChatMode = {
'chat': 'Chat',
'normal': 'Normal',
'gather': 'Gather',
'agent': 'Agent',
}
const detailOfChatMode = {
'chat': 'Normal chat',
'gather': 'Read and search only',
'agent': 'Full tool use',
'normal': 'Normal chat',
'gather': 'Discover relevant files',
'agent': 'Edit files and use tools',
}
@ -227,9 +226,8 @@ const ChatModeDropdown = ({ className }: { className: string }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const options: ChatMode[] = useMemo(() => ['chat', 'gather', 'agent'], [])
const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'agent'], [])
const onChangeOption = useCallback((newVal: ChatMode) => {
voidSettingsService.setGlobalSetting('chatMode', newVal)
@ -304,7 +302,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
<div
ref={divRef}
className={`
gap-1
gap-x-1
flex flex-col p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
@ -351,12 +349,12 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
{/* Bottom row */}
<div className='flex flex-row justify-between items-end gap-1'>
{showModelDropdown && (
<div className='flex flex-col gap-1'>
<div className='flex flex-col gap-y-1'>
<ReasoningOptionSlider featureName={featureName} />
<div className='flex items-center flex-wrap gap-x-1 gap-y-1'>
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
<ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
<div className='flex items-center flex-wrap gap-x-2 gap-y-1'>
{featureName === 'Chat' && <ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-2 rounded py-0.5 px-1' />}
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 rounded' />
</div>
</div>
)}
@ -667,6 +665,7 @@ type ToolHeaderParams = {
desc1: React.ReactNode;
desc2?: React.ReactNode;
isError?: boolean;
isRejected?: boolean;
numResults?: number;
children?: React.ReactNode;
onClick?: () => void;
@ -683,6 +682,7 @@ const ToolHeaderWrapper = ({
isError,
onClick,
isOpen,
isRejected,
}: ToolHeaderParams) => {
const [isExpanded_, setIsExpanded] = useState(false);
@ -692,7 +692,7 @@ const ToolHeaderWrapper = ({
const isClickable = !!(isDropdown || onClick)
return (<div className=''>
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden">
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
{/* header */}
<div
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
@ -706,7 +706,7 @@ const ToolHeaderWrapper = ({
className={`text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
/>
)}
<div className="flex items-center w-full gap-x-2 overflow-hidden justify-between">
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
{/* left */}
<div className="flex items-center gap-x-2 min-w-0 overflow-hidden">
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
@ -723,7 +723,8 @@ const ToolHeaderWrapper = ({
{`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`}
</span>
)}
{isError && <AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={12} />}
{isError && <AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={14} />}
{isRejected && <Ban className='text-void-fg-4 opacity-90 flex-shrink-0' size={14} />}
</div>
</div>
</div>
@ -820,7 +821,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
// cancel any streams on this thread
const thread = chatThreadsService.getCurrentThread()
chatThreadsService.cancelStreaming(thread.id)
chatThreadsService.stopRunning(thread.id)
// update state
setIsBeingEdited(false)
@ -830,7 +831,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
// stream the edit
const userMessage = textAreaRefState.value;
try {
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, })
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, })
} catch (e) {
console.error('Error while editing message:', e)
}
@ -838,7 +839,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
const onAbort = () => {
const threadId = chatThreadsService.state.currentThreadId
chatThreadsService.cancelStreaming(threadId)
chatThreadsService.stopRunning(threadId)
}
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
@ -1014,15 +1015,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
// 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 toolNameToTitle: Record<ToolName, { past: string, current: string, proposed: string }> = {
'read_file': { past: 'Read file', current: 'Reading file', proposed: 'Read file' },
'list_dir': { past: 'Inspected folder', current: 'Inspecting folder', proposed: 'Inspect folder' },
'pathname_search': { past: 'Searched by file name', current: 'Searching by file name', proposed: 'Search by file name' },
'search': { past: 'Searched', current: 'Searching', proposed: 'Search' },
'create_uri': { past: 'Created file', current: 'Creating file', proposed: 'Create file' },
'delete_uri': { past: 'Deleted file', current: 'Deleting file', proposed: 'Delete file' },
'edit': { past: 'Edited file', current: 'Editing file', proposed: 'Edit file' },
'terminal_command': { past: 'Ran terminal command', current: 'Running terminal command', proposed: 'Run terminal command' }
const toolNameToTitle: Record<ToolName, { past: string, proposed: string }> = {
'read_file': { past: 'Read file', proposed: 'Read file' },
'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' },
'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' },
'search': { past: 'Searched', proposed: 'Search' },
'create_uri': { past: 'Created file', proposed: 'Create file' },
'delete_uri': { past: 'Deleted file', proposed: 'Delete file' },
'edit': { past: 'Edited file', proposed: 'Edit file' },
'terminal_command': { past: 'Ran terminal command', proposed: 'Run terminal command' }
}
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
@ -1060,24 +1061,27 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
}
const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) => {
const ToolRequestAcceptRejectButtons = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const metricsService = accessor.get('IMetricsService')
const onAccept = useCallback(() => {
try {
chatThreadsService.approveTool(voidToolId)
try { // this doesn't need to be wrapped in try/catch anymore
const threadId = chatThreadsService.state.currentThreadId
chatThreadsService.approveTool(threadId)
metricsService.capture('Tool Request Accepted', {})
} catch (e) { console.error('Error while approving message in chat:', e) }
}, [chatThreadsService, voidToolId, metricsService])
}, [chatThreadsService, metricsService])
const onReject = useCallback(() => {
try {
chatThreadsService.rejectTool(voidToolId)
const threadId = chatThreadsService.state.currentThreadId
chatThreadsService.rejectTool(threadId)
} catch (e) { console.error('Error while approving message in chat:', e) }
metricsService.capture('Tool Request Rejected', {})
}, [chatThreadsService, voidToolId, metricsService])
}, [chatThreadsService, metricsService])
const approveButton = (
<button
@ -1126,7 +1130,7 @@ export const ToolContentsWrapper = ({ children, className }: { children: React.R
</div>
</div>
}
const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => {
export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => {
return <div
className={`
${onClick ? 'hover:brightness-125 hover:cursor-pointer transition-all duration-200 ' : ''}
@ -1159,10 +1163,8 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
className='w-full overflow-auto py-1'
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
<div className='border border-void-border-1 rounded p-1'>
<div className='!select-text cursor-auto'>
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
</div>
<div className='!select-text cursor-auto my-4'>
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
</div>
</ToolContentsWrapper>
}
@ -1183,12 +1185,10 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: {
className='w-full overflow-auto py-1'
onClick={() => terminalToolsService.openTerminal(terminalId)}
/>
<div className='border border-void-border-1 rounded p-1'>
<div className='!select-text cursor-auto'>
{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}
{result}
{resultStr}
</div>
<div className='!select-text cursor-auto my-4'>
{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}
{result}
{resultStr}
</div>
</ToolContentsWrapper>
}
@ -1203,7 +1203,7 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close
}, [isWriting])
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen}>
<ToolContentsWrapper className='bg-void-bg-3'>
<ToolContentsWrapper className='bg-void-bg-3 prose-sm'>
<div className='!select-text cursor-auto'>
{children}
</div>
@ -1396,7 +1396,8 @@ const toolNameToComponent: { [T in ToolName]: {
const isError = toolMessage.result.type === 'error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
const isRejected = toolMessage.result.type === 'rejected'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { params } = toolMessage.result
@ -1434,12 +1435,13 @@ const toolNameToComponent: { [T in ToolName]: {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolMessage.name].past
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
const isError = toolMessage.result.type === 'error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
const isRejected = toolMessage.result.type === 'rejected'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { params } = toolMessage.result
@ -1480,12 +1482,13 @@ const toolNameToComponent: { [T in ToolName]: {
resultWrapper: ({ toolMessage, messageIdx }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const title = toolNameToTitle[toolMessage.name].past
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
const isError = toolMessage.result.type === 'error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
const isRejected = toolMessage.result.type === 'rejected'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') {
const { params } = toolMessage.result
@ -1540,12 +1543,13 @@ const toolNameToComponent: { [T in ToolName]: {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const terminalToolsService = accessor.get('ITerminalToolService')
const title = toolNameToTitle[toolMessage.name].past
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
const icon = null
const isError = toolMessage.result.type === 'error'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
const isRejected = toolMessage.result.type === 'rejected'
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
if (toolMessage.result.type === 'success') {
const { command } = toolMessage.result.params
@ -1605,11 +1609,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
}
else if (role === 'tool_request') {
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
// if (!isLast) return null
if (!isLast) return null
if (!ToolRequestWrapper) return null
return <>
<ToolRequestWrapper toolRequest={chatMessage} />
<ToolRequestAcceptRejectButtons voidToolId={chatMessage.voidToolId} />
<ToolRequestAcceptRejectButtons />
</>
}
else if (role === 'tool') {
@ -1838,7 +1842,7 @@ export const SidebarChat = () => {
// stream state
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isStreaming = !!currThreadStreamState?.streamingToken
const isRunning = !!currThreadStreamState?.isRunning
const latestError = currThreadStreamState?.error
const messageSoFar = currThreadStreamState?.messageSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
@ -1861,7 +1865,7 @@ export const SidebarChat = () => {
const onSubmit = useCallback(async () => {
if (isDisabled) return
if (isStreaming) return
if (isRunning) return
// update state
chatThreadsService.closeStagingSelectionsInCurrentThread() // close all selections
@ -1871,10 +1875,8 @@ export const SidebarChat = () => {
// getModelCapabilities() // TODO!!! check if can go into agent mode
const chatMode = settingsState.globalSettings.chatMode
try {
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode })
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage })
} catch (e) {
console.error('Error while sending message in chat:', e)
}
@ -1883,11 +1885,11 @@ export const SidebarChat = () => {
textAreaFnsRef.current?.setValue('')
textAreaRef.current?.focus() // focus input after submit
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections])
}, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState])
const onAbort = () => {
const threadId = currentThread.id
chatThreadsService.cancelStreaming(threadId)
chatThreadsService.stopRunning(threadId)
}
// const [_test_messages, _set_test_messages] = useState<string[]>([])
@ -1910,7 +1912,7 @@ export const SidebarChat = () => {
}, [previousMessages, currentThread, numMessages])
const streamingChatIdx = previousMessagesHTML.length
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
messageIdx={streamingChatIdx}
chatMessage={{
@ -1919,7 +1921,7 @@ export const SidebarChat = () => {
reasoning: reasoningSoFar ?? '',
anthropicReasoning: null,
}}
isLoading={isStreaming}
isLoading={isRunning}
isLast={true}
/> : null
@ -1970,10 +1972,10 @@ export const SidebarChat = () => {
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
} else if (e.key === 'Escape' && isStreaming) {
} else if (e.key === 'Escape' && isRunning) {
onAbort()
}
}, [onSubmit, onAbort, isStreaming])
}, [onSubmit, onAbort, isRunning])
const inputForm = <div
key={'input' + chatThreadsState.currentThreadId}
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
@ -1982,7 +1984,7 @@ export const SidebarChat = () => {
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={isStreaming}
isStreaming={isRunning}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={previousMessagesHTML.length === 0}
@ -1991,7 +1993,8 @@ export const SidebarChat = () => {
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`}
// className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`}
className={`min-h-[81px] px-0.5 py-0.5`}
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}

View file

@ -21,7 +21,7 @@ export type ToolRequestApproval<T extends ToolName> = {
name: T; // internal use
params: ToolCallParams[T]; // internal use
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
voidToolId: string; // internal id Void uses
id: string; // proposed tool's id
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.

View file

@ -8,6 +8,7 @@ import { URI } from '../../../../../base/common/uri.js';
import { os } from '../helpers/systemInfo.js';
import { IVoidFileService } from '../voidFileService.js';
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
// this is just for ease of readability
@ -21,8 +22,8 @@ Do NOT output the whole file here if possible, and try to write as LITTLE code a
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}.
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase by providing specific references to files and content'}.
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid.
@ -56,6 +57,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri
Misc:
- Do not make things up.
- Do not be lazy.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
`

View file

@ -81,7 +81,7 @@ export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
modelSelection: ModelSelection | null;
modelSelectionOptions: ModelSelectionOptions | undefined;
} & SendLLMType;
@ -91,7 +91,7 @@ export type SendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
abortRef: AbortRef;
aiInstructions: string;

View file

@ -99,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
'Chat': { filter: (o, { chatMode }) => chatMode === 'chat' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
}

View file

@ -378,7 +378,7 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V
export type ChatMode = 'agent' | 'gather' | 'chat'
export type ChatMode = 'agent' | 'gather' | 'normal'
export type GlobalSettings = {

View file

@ -17,7 +17,7 @@ export const sendLLMMessage = ({
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
logging: { loggingName },
logging: { loggingName, loggingExtras },
settingsOfProvider,
modelSelection,
modelSelectionOptions,
@ -48,6 +48,7 @@ export const sendLLMMessage = ({
suffixLength: messages_.suffix.length,
} : {},
...loggingExtras,
...extras,
})
}