diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts
index 328f4fe9..77fec1ba 100644
--- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts
+++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts
@@ -446,6 +446,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// if (Math.random() > 0) throw new Error('TESTING')
const errorMessage = 'Tool call was rejected by the user.'
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
+ shouldSendAnotherMessage = false // interrupt flow by rejecting
res_()
return
}
diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts
index 7535dadc..ae16d111 100644
--- a/src/vs/workbench/contrib/void/browser/editCodeService.ts
+++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts
@@ -50,10 +50,10 @@ const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
-const greenBG = new Color(new RGBA(155, 185, 85, .3)); // default is RGBA(155, 185, 85, .2)
+const greenBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2)
registerColor('void.greenBG', configOfBG(greenBG), '', true);
-const redBG = new Color(new RGBA(255, 0, 0, .3)); // default is RGBA(255, 0, 0, .2)
+const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2)
registerColor('void.redBG', configOfBG(redBG), '', true);
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
@@ -1481,7 +1481,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by pressing undo.')) }
})
- // TODO replace these with whatever block we're on initially if already started (caching apply)
+ // TODO replace these with whatever block we're on initially if already started (if add caching of apply S/R blocks)
type SearchReplaceDiffAreaMetadata = {
originalBounds: [number, number], // 1-indexed
@@ -1607,6 +1607,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
shouldUpdateOrigStreamStyle = false
}
}
+ else {
+ // TODO!!! test this
+ // starting line is at least the number of lines in the generated code minus 1
+ const numLinesInOrig = block.orig.split('\n').length - 1
+ diffZone._streamState.line = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1)
+ }
// must be done writing original to move on to writing streamed content
continue
}
@@ -1615,37 +1621,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
- console.log('finding text in code...', { orig: block.orig })
const originalBounds = findTextInCode(block.orig, originalFileCode)
-
// if error
if (typeof originalBounds === 'string') {
+ console.log('TEXT NOT FOUND')
const content = errMsgOfInvalidStr(originalBounds, block.orig)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
)
-
- // REVERT
- // TODO!!!!! don't actually revert - we want to change this so it doesn't revert but isntead gives the current file contents
- const numLines = this._getNumLines(uri)
- if (numLines !== null) this._writeText(uri, originalFileCode,
- { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER },
- { shouldRealignDiffAreas: false }
- )
- // reset state
- diffZone.startLine = 1
- diffZone.endLine = numLines ?? 1
- if (diffZone._streamState.isStreaming) {
- diffZone._streamState.line = 1
- }
-
- currStreamingBlockNum = 0
+ // REVERT THIS ONE BLOCK
+ // TODO!!! test this
latestStreamLocationMutable = null
shouldUpdateOrigStreamStyle = true
- oldBlocks = []
- addedTrackingZoneOfBlockNum.splice(0, Infinity) // clear the array
+ blocks.splice(blockNum, Infinity) // remove all blocks at and after this one
+ oldBlocks = blocks
// abort and resolve
shouldSendAnotherMessage = true
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
index 686a86d2..1ee518e1 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -914,7 +914,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
}
-const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => {
+const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@@ -930,8 +930,9 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB
messageIdx: messageIdx,
}
- const isEmpty = !chatMessage.content && !chatMessage.reasoning // && !(isLast && isLoading) // TODO!!!!
- if (isEmpty) return null
+ const isEmpty = !chatMessage.content && !chatMessage.reasoning
+ const isLastAndLoading = isLoading && isLast
+ if (isEmpty && !isLastAndLoading) return null
return <>
= {
'create_uri': 'Create file',
'delete_uri': 'Delete file',
'edit': 'Edit file',
- 'terminal_command': 'Ran terminal command'
+ 'terminal_command': 'Run terminal command'
}
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
@@ -1335,8 +1336,10 @@ const toolNameToComponent: { [T in ToolName]: {
const commandService = accessor.get('ICommandService')
const title = toolNameToTitle[toolRequest.name]
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
- return
},
resultWrapper: ({ toolMessage }) => {
@@ -1349,19 +1352,32 @@ const toolNameToComponent: { [T in ToolName]: {
return
}
- const { params } = toolMessage.result
+
+ const { command } = toolMessage.result.params
+ const { terminalId, resolveReason, result } = toolMessage.result.value
return (
-
-
+
+
+ {resolveReason.type === 'bgtask' ? 'Result so far:' : null}
+
+ {
+ resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `Error: exit code ${resolveReason.exitCode}` : null)
+ : resolveReason.type === 'bgtask' ? null :
+ resolveReason.type === 'timeout' ? `(partial results; request timed out)` :
+ resolveReason.type === 'toofull' ? `(truncated)`
+ : null
+ }
+
)
@@ -1371,8 +1387,9 @@ const toolNameToComponent: { [T in ToolName]: {
type ChatBubbleMode = 'display' | 'edit'
-type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, }
-const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) => {
+type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, isLast: boolean }
+
+const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps) => {
const role = chatMessage.role
@@ -1381,6 +1398,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) =>
chatMessage={chatMessage}
messageIdx={messageIdx}
isLoading={isLoading}
+ isLast={isLast}
/>
}
else if (role === 'assistant') {
@@ -1388,6 +1406,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) =>
chatMessage={chatMessage}
messageIdx={messageIdx}
isLoading={isLoading}
+ isLast={isLast}
/>
}
else if (role === 'tool_request') {
@@ -1508,7 +1527,7 @@ export const SidebarChat = () => {
const pastMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) =>
-
+
)
}, [previousMessages, currentThread])
@@ -1524,6 +1543,7 @@ export const SidebarChat = () => {
anthropicReasoning: null,
}}
isLoading={isStreaming}
+ isLast={true}
/> : null
const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML]
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
index bdf2ae0d..8da66169 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
@@ -33,7 +33,7 @@ export const SidebarThreadSelector = () => {
.filter(threadId => allThreads![threadId].messages.length !== 0)
return (
-
+
{/* title */}
diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts
index 55d0e225..9d418cfe 100644
--- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts
+++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts
@@ -3,23 +3,36 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
-import { Disposable } from '../../../../base/common/lifecycle.js';
+import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
-import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
-import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
+import { ITerminalService, ITerminalInstance, ITerminalGroupService } from '../../../../workbench/contrib/terminal/browser/terminal.js';
+import { ResolveReason } from '../common/toolsServiceTypes.js';
+import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
+
+
export interface ITerminalToolService {
readonly _serviceBrand: undefined;
- runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, contents: string }>;
+ runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: ResolveReason }>;
listTerminalIds(): string[];
}
export const ITerminalToolService = createDecorator('TerminalToolService');
+
+function isCommandComplete(output: string) {
+ // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
+ const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
+ if (!completionMatch) { return false }
+ if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
+ return { exitCode: 0 }
+}
+
+
const nameOfId = (id: string) => {
if (id === '1') return 'Void Agent'
return `Void Agent (${id})`
@@ -40,11 +53,11 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
constructor(
@ITerminalService private readonly terminalService: ITerminalService,
+ @ITerminalGroupService private readonly terminalGroupService: ITerminalGroupService,
) {
super();
// initialize any terminals that are already open
-
for (const terminal of terminalService.instances) {
const proposedTerminalId = idOfName(terminal.title)
if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal
@@ -82,7 +95,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Panel,
config: { name: nameOfId(terminalId), title: nameOfId(terminalId) }
- });
+ })
this.terminalInstanceOfId[terminalId] = terminal
return { terminalId, didCreateTerminal: true }
}
@@ -95,37 +108,71 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
const terminal = this.terminalInstanceOfId[terminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
+ this.terminalGroupService.focusInstance(terminal)
- if (!waitForCompletion) {
- console.log('NOT WAITING FOR COMPLETION')
- await terminal.sendText(command, true);
- return { terminalId, didCreateTerminal, contents: '(command is running in background...)' };
- }
+ let result: string = ''
+ let resolveReason: ResolveReason | undefined = undefined
- // stream
+ const disposables: IDisposable[] = []
- let data = ''
- const d1 = terminal.onData(newData => { data += newData })
-
- // terminal.onExit(() => {
- // console.log('TERMINALEXIT')
- // })
-
- await terminal.sendText(command, true);
- // wait for the command to finish
- const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection);
- if (commandDetection) {
- const d2 = commandDetection.onCommandFinished(() => {
- console.log('FINISHED', data)
- d1.dispose()
- d2.dispose()
- return { terminalId, didCreateTerminal, contents: data }
+ // onFullPage
+ const waitUntilFullPage = new Promise((res, rej) => {
+ const d1 = terminal.onData(async newData => {
+ if (resolveReason) return
+ result += newData
+ if (result.length > MAX_TERMINAL_CHARS_PAGE) {
+ result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
+ await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
+ resolveReason = { type: 'toofull' }
+ res()
+ return
+ }
})
- }
+ disposables.push(d1)
+ })
- console.log('didnot wait', data)
- d1.dispose()
- return { terminalId, didCreateTerminal, contents: 'Could not await data...' }
+ // onDone
+ const waitUntilDone = new Promise((res, rej) => {
+ const d2 = terminal.onData(newData => {
+ if (resolveReason) return
+ const isDone = isCommandComplete(result)
+ if (isDone) {
+ resolveReason = { type: 'done', exitCode: isDone.exitCode }
+ res()
+ return
+ }
+ })
+ disposables.push(d2)
+ })
+
+
+ // send the command here
+ await terminal.sendText(command, true)
+
+ // timeout promise
+ const waitUntilTimeout = new Promise((res, rej) => {
+ setTimeout(async () => {
+ if (resolveReason) return
+ await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
+ resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
+ res()
+ }, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
+ })
+
+ await Promise.any([
+ waitUntilDone,
+ waitUntilFullPage,
+ waitUntilTimeout,
+ ])
+
+ disposables.forEach(d => d.dispose())
+
+ if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
+
+ console.log('res', { terminalId, didCreateTerminal, result, resolveReason })
+
+
+ return { terminalId, didCreateTerminal, result, resolveReason }
}
@@ -133,3 +180,5 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
+
+
diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts
index cc851e10..9e87bfb7 100644
--- a/src/vs/workbench/contrib/void/browser/toolsService.ts
+++ b/src/vs/workbench/contrib/void/browser/toolsService.ts
@@ -27,6 +27,9 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Tool
// pagination info
const MAX_FILE_CHARS_PAGE = 50_000
const MAX_CHILDREN_URIs_PAGE = 500
+export const MAX_TERMINAL_CHARS_PAGE = 50_000
+export const TERMINAL_TIMEOUT_TIME = 15
+export const TERMINAL_BG_WAIT_TIME = 1
@@ -322,8 +325,8 @@ export class ToolsService implements IToolsService {
return {}
},
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
- const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
- return { terminalId, didCreateTerminal }
+ const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
+ return { terminalId, didCreateTerminal, result, resolveReason }
},
}
@@ -353,10 +356,32 @@ export class ToolsService implements IToolsService {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit: (params, result) => {
- return `Change successfully made ${params.uri.fsPath} successfully deleted.`
+ return `Change successfully made to ${params.uri.fsPath}.`
},
terminal_command: (params, result) => {
- return `Terminal command "${params.command}" successfully executed in terminal ${result.terminalId}${result.didCreateTerminal ? `(a newly-created terminal)` : ''}.`
+
+ const {
+ terminalId,
+ didCreateTerminal,
+ resolveReason,
+ result: result_,
+ } = result
+
+ const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
+
+ if (resolveReason.type === 'timeout') {
+ return `Terminal command ran in ${terminalDesc}, but timed out after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
+ }
+ else if (resolveReason.type === 'bgtask') {
+ return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
+ }
+ else if (resolveReason.type === 'toofull') {
+ return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
+ }
+ else if (resolveReason.type === 'done') {
+ return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
+ }
+ throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
}
diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
index b4d00691..3c17853d 100644
--- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
+++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
@@ -25,6 +25,9 @@ export type ToolDirectoryItem = {
}
+export type ResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
+
+
@@ -158,6 +161,6 @@ export type ToolResultType = {
'edit': {},
'create_uri': {},
'delete_uri': {},
- 'terminal_command': { terminalId: string, didCreateTerminal: boolean },
+ 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: ResolveReason; },
}
diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts
index 6ff7906e..33c62f3d 100644
--- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts
+++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts
@@ -368,6 +368,7 @@ const prepareMessages_noEmptyMessage = ({ messages }: { messages: PrepareMessage
else if (c.type === 'tool_use') { }
else if (c.type === 'tool_result') { }
}
+ if (currMsg.content.length === 0) currMsg.content = [{ type: 'text', text: EMPTY_MESSAGE }]
}
}