mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
ancilary terminal tool
This commit is contained in:
parent
217a33cc2e
commit
65be47d970
6 changed files with 100 additions and 42 deletions
|
|
@ -26,6 +26,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
|
|||
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
|
|
@ -203,6 +204,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
|
@ -379,8 +381,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
|
||||
|
||||
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
const terminalIds = this.terminalToolService.listTerminalIds()
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath), chatMode), },
|
||||
{ role: 'system', content: chat_systemMessage(workspaceFolders, terminalIds, chatMode), },
|
||||
...messages_.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages_.slice(lastUserMsgIdx + 1, Infinity),
|
||||
|
|
|
|||
|
|
@ -1527,11 +1527,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
const errHelper = (erroneousOriginal: string) => `All previous SEARCH/REPLACE blocks (if any) have been applied except the latest erroneous one. Please continue outputting SEARCH/REPLACE blocks. The ORIGINAL code with an error was: ${JSON.stringify(erroneousOriginal)}`
|
||||
const errMsgOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string) => {
|
||||
return str === `Not found` ?
|
||||
`The ORIGINAL code provided could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}`
|
||||
`The ORIGINAL code provided could not be found in the file. You should make sure the text in ORIGINAL matches lines of code EXACTLY. ${errHelper(blockOrig)}`
|
||||
: str === `Not unique` ?
|
||||
`The ORIGINAL code provided shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}`
|
||||
`The ORIGINAL code provided shows up multiple times in the file. We recommend making the ORIGINAL portion bigger so we can find a unique match. ${errHelper(blockOrig)}`
|
||||
: ``
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,36 +6,65 @@
|
|||
import { Disposable } 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, ITerminalInstanceService } from '../../../../workbench/contrib/terminal/browser/terminal.js';
|
||||
|
||||
export interface ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>;
|
||||
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, contents: string }>;
|
||||
listTerminalIds(): string[];
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
|
||||
|
||||
const nameOfId = (id: string) => {
|
||||
if (id === '1') return 'Void Agent'
|
||||
return `Void Agent (${id})`
|
||||
}
|
||||
const idOfName = (name: string) => {
|
||||
if (name === 'Void Agent') return '1'
|
||||
|
||||
const match = name.match(/Void Agent \((\d+)\)/)
|
||||
if (!match) return null
|
||||
if (Number.isInteger(match[1]) && Number(match[1]) >= 1) return match[1]
|
||||
return null
|
||||
}
|
||||
|
||||
export class TerminalToolService extends Disposable implements ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private terminalInstanceOfId: Record<string, ITerminalInstance> = {}
|
||||
|
||||
constructor(
|
||||
@ITerminalService private readonly terminalService: ITerminalService,
|
||||
@ITerminalInstanceService private readonly terminalInstanceService: ITerminalInstanceService,
|
||||
) {
|
||||
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
|
||||
}
|
||||
console.log('Initialized terminal instances:', this.terminalInstanceOfId)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
listTerminalIds() {
|
||||
return Object.keys(this.terminalInstanceOfId)
|
||||
}
|
||||
|
||||
getValidNewTerminalId(): string {
|
||||
// {1 2 3} # size 3, new=4
|
||||
// {1 3 4} # size 3, new=2
|
||||
// 1 <= newTerminalId <= n + 1
|
||||
const n = Object.keys(this.terminalInstanceOfId).length;
|
||||
if (n === 0) return '1'
|
||||
|
||||
for (let i = 1; i <= n + 1; i++) {
|
||||
const potentialId = i + '';
|
||||
if (!(potentialId in this.terminalInstanceOfId)) return potentialId;
|
||||
|
|
@ -44,45 +73,59 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
|
|||
}
|
||||
|
||||
|
||||
private async _createNewTerminal() {
|
||||
|
||||
private async _getOrCreateTerminal(proposedTerminalId: string) {
|
||||
// if terminal ID exists, return it
|
||||
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
|
||||
// create new terminal and return its ID
|
||||
const terminalId = this.getValidNewTerminalId();
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
location: TerminalLocation.Panel,
|
||||
config: { name: `Void Agent (${terminalId})`, }
|
||||
config: { name: nameOfId(terminalId), title: nameOfId(terminalId) }
|
||||
});
|
||||
this.terminalInstanceOfId[terminalId] = terminal
|
||||
return terminalId;
|
||||
}
|
||||
|
||||
private async _getValidTerminalId(proposedTerminalId: string) {
|
||||
// if there is no terminal ID provided, create one
|
||||
if (proposedTerminalId in this.terminalInstanceOfId)
|
||||
return { terminalId: proposedTerminalId, didCreateTerminal: false }
|
||||
const terminalId = await this._createNewTerminal()
|
||||
return { terminalId, didCreateTerminal: true }
|
||||
}
|
||||
|
||||
private async _focus(terminalId: string) {
|
||||
const terminal = this.terminalInstanceOfId[terminalId];
|
||||
if (!terminal) return
|
||||
terminal.focus(true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
async runCommand(command: string, proposedTerminalId: string) {
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
|
||||
await this.terminalService.whenConnected;
|
||||
const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId)
|
||||
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
|
||||
const terminal = this.terminalInstanceOfId[terminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
|
||||
this._focus(terminalId)
|
||||
|
||||
|
||||
if (!waitForCompletion) {
|
||||
console.log('NOT WAITING FOR COMPLETION')
|
||||
await terminal.sendText(command, true);
|
||||
return { terminalId, didCreateTerminal, contents: '(command is running in background...)' };
|
||||
}
|
||||
|
||||
// stream
|
||||
|
||||
let data = ''
|
||||
const d1 = terminal.onData(newData => { data += newData })
|
||||
|
||||
await terminal.sendText(command, true);
|
||||
// terminal.onData(data => console.log('DATA!!', data));
|
||||
// terminal.onProcessReplayComplete(data => console.log('REPLAY!!', data));
|
||||
// terminal.onDidSendText(data => console.log('SEND!!', data));
|
||||
return { terminalId, didCreateTerminal };
|
||||
// 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 }
|
||||
})
|
||||
}
|
||||
|
||||
console.log('didnot wait', data)
|
||||
d1.dispose()
|
||||
return { terminalId, didCreateTerminal, contents: 'Could not await data...' }
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
|
||||
|
|
|
|||
|
|
@ -137,11 +137,18 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
|||
}
|
||||
|
||||
const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
|
||||
if (!terminalIdUnknown) return '1'
|
||||
const terminalId = terminalIdUnknown + ''
|
||||
if (!terminalId) return ''
|
||||
return terminalId
|
||||
}
|
||||
|
||||
const validateWaitForCompletion = (b: unknown) => {
|
||||
if (typeof b === 'string') {
|
||||
if (b === 'true') return true
|
||||
if (b === 'false') return false
|
||||
}
|
||||
return true // default is true
|
||||
}
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateParams;
|
||||
|
|
@ -238,10 +245,11 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
terminal_command: async (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown } = o
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
return { command, proposedTerminalId }
|
||||
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
},
|
||||
|
||||
}
|
||||
|
|
@ -313,8 +321,8 @@ export class ToolsService implements IToolsService {
|
|||
await applyDonePromise
|
||||
return {}
|
||||
},
|
||||
terminal_command: async ({ command, proposedTerminalId }) => {
|
||||
const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId)
|
||||
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { terminalId, didCreateTerminal }
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,17 @@ Do NOT output the whole file if possible, and try to write as LITTLE as needed t
|
|||
|
||||
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}.
|
||||
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. The user's query is never invalid.
|
||||
|
||||
The user's system information is as follows:
|
||||
- ${os}
|
||||
- Open workspaces: ${workspaces.join(', ')}
|
||||
|
||||
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
|
||||
${(mode === 'agent' || mode === 'gather') && runningTerminalIds.length !== 0 ? `\
|
||||
- Running terminal IDs: ${runningTerminalIds.join(', ')}
|
||||
`: '\n'}
|
||||
${mode === 'agent' || mode === 'gather' /* tool use */ ? `\
|
||||
You will be given tools you can call.
|
||||
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
|
||||
|
|
@ -45,8 +47,7 @@ You're allowed to ask for more context. For example, if the user only gives you
|
|||
`}
|
||||
|
||||
${mode === 'agent' /* code blocks */ ? `\
|
||||
Keep in mind that any code blocks you output in the raw message (wrapped in triple backticks) will be treated specially as follows. This does NOT apply to code blocks in tool calls.
|
||||
- Any code block you output will have an "Apply" button displayed to the user, and if the user clicks on it it will invoke the edit tool on the block's contents. As a result, all code blocks should describe relevant changes.
|
||||
If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code.
|
||||
`: `\
|
||||
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
|
||||
- The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created.
|
||||
|
|
|
|||
|
|
@ -112,7 +112,8 @@ export const voidTools = {
|
|||
description: `Executes a terminal command.`,
|
||||
params: {
|
||||
command: { type: 'string', description: 'The terminal command to execute.' },
|
||||
terminalId: { type: 'string', description: 'Optional. The terminal ID to execute the command in. Must be a number starting at 1. If a terminal does not exist with this ID, a new one will be created (not necessarily with the same ID as the provided one). ' },
|
||||
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
|
||||
terminalId: { type: 'string', description: 'Optional (if provided, value must be an integer >= 1). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
|
|
@ -144,7 +145,7 @@ export type ToolCallParams = {
|
|||
'edit': { uri: URI, changeDescription: string },
|
||||
'create_uri': { uri: URI },
|
||||
'delete_uri': { uri: URI, isRecursive: boolean },
|
||||
'terminal_command': { command: string, proposedTerminalId: string },
|
||||
'terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue