diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c4b127a1..9c4e79e4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -132,6 +132,7 @@ export type StartApplyingOpts = { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; + uri: 'current' | URI; } @@ -1226,6 +1227,7 @@ class EditCodeService extends Disposable implements IEditCodeService { + // throws if there's an error public startApplying(opts: StartApplyingOpts) { if (opts.type === 'rewrite') { const addedDiffArea = this._initializeWriteoverStream(opts) @@ -1448,11 +1450,18 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr } = opts + const { applyStr, uri: givenURI } = opts + let uri: URI + + if (givenURI === 'current') { + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + } + else { + uri = givenURI + } - const uri_ = this._getActiveEditorURI() - if (!uri_) return - const uri = uri_ // generate search/replace block text const originalFileCode = this._voidFileService.readModel(uri) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index a7259020..801abcb6 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -15,6 +15,14 @@ import { IVoidFileService } from '../../common/voidFileService.js'; // this is just for ease of readability export const tripleTick = ['```', '```'] +export const editToolDesc_toolDescription = `\ +A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \ +Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \ +However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \ +Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.` + + + export const chat_systemMessage = (workspaces: string[]) => `\ You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index b31bfb7b..8ce22b2f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -86,6 +86,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, + uri: 'current', }) applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) 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 a77aba9a..05cf7fc0 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 @@ -151,7 +151,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization featureName: FeatureName; @@ -528,22 +528,36 @@ export const SelectedFiles = ( } -type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } -interface ToolResultProps { - actionTitle: string; - actionParam: string; - actionNumResults?: number; - children?: React.ReactNode; - onClick?: () => void; +const actionTitleOfToolName: { [T in ToolName]: string } = { + 'read_file': 'Read file', + 'list_dir': 'Inspected folder', + 'pathname_search': 'Searched filename', + 'search': 'Searched', + + 'create_uri': 'Created URI', + 'delete_uri': 'Deleted URI', + 'edit': 'Edited file', + 'terminal_command': 'Ran terminal command', } + + + +type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } + const ToolResult = ({ - actionTitle, + toolName, actionParam, actionNumResults, children, onClick, -}: ToolResultProps) => { +}: { + toolName: ToolName; + actionParam: string; + actionNumResults?: number; + children?: React.ReactNode; + onClick?: () => void; +}) => { const [isExpanded, setIsExpanded] = useState(false); const isDropdown = !!children @@ -565,7 +579,7 @@ const ToolResult = ({ /> )}
- {actionTitle} + {actionTitleOfToolName[toolName]} {actionParam} {actionNumResults !== undefined && ( @@ -587,18 +601,38 @@ const ToolResult = ({ +const ToolError = ({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => { + return +} + + const toolResultToComponent: ToolResultToComponent = { 'read_file': ({ message }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + if (message.result.type === 'error') return + const { value } = message.result return ( { commandService.executeCommand('vscode.open', message.result.uri, { preview: true }) }} - /> + toolName='read_file' + actionParam={'View file'} + > +
+
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > + + {getBasename(value.uri.fsPath)} +
+ {value.hasNextPage && (
AI can scroll for more content...
)} +
+
) }, 'list_dir': ({ message }) => { @@ -607,16 +641,18 @@ const toolResultToComponent: ToolResultToComponent = { const explorerService = accessor.get('IExplorerService') // message.result.hasNextPage = true // message.result.itemsRemaining = 400 + if (message.result.type === 'error') return + + const { value } = message.result return (
- {message.result.children?.map((child, i) => ( -
( +
{ commandService.executeCommand('workbench.view.explorer'); @@ -627,11 +663,7 @@ const toolResultToComponent: ToolResultToComponent = { {`${child.name}${child.isDirectory ? '/' : ''}`}
))} - {message.result.hasNextPage && ( -
- {message.result.itemsRemaining} more items... -
- )} + {value.hasNextPage && (
{value.itemsRemaining} more items...
)}
) @@ -640,74 +672,162 @@ const toolResultToComponent: ToolResultToComponent = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + if (message.result.type === 'error') return + const { value } = message.result return (
- {Array.isArray(message.result.uris) ? - message.result.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > - - {uri.fsPath.split('/').pop()} -
- )) : -
{message.result.uris}
- } - {message.result.hasNextPage && ( -
- More results available... + {value.uris.map((uri, i) => ( +
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} + > + + {uri.fsPath.split('/').pop()}
- )} + ))} + {value.hasNextPage && (
More results available...
)}
) }, 'search': ({ message }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + if (message.result.type === 'error') return + const { value } = message.result + return ( + +
+ {value.uris.map((uri, i) => ( +
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} + > + + {uri.fsPath.split('/').pop()} +
+ ))} + {value.hasNextPage && (
More results available...
)} +
+
+ ) + }, + + // --- + + 'create_uri': ({ message }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + if (message.result.type === 'error') return + + const { value } = message.result return ( { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} >
- {Array.isArray(message.result.uris) ? - message.result.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > - - {uri.fsPath.split('/').pop()} -
- )) : -
{message.result.uris}
- } - {message.result.hasNextPage && ( -
- More results available... -
- )} +
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > + + {value.uri.fsPath.split('/').pop()} +
+
+
+ ) + }, + 'delete_uri': ({ message }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + if (message.result.type === 'error') return + + const { value } = message.result + return ( + { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > +
+
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > + + {value.uri.fsPath.split('/').pop()} +
+
+
+ ) + }, + 'edit': ({ message }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + if (message.result.type === 'error') return + + const { value } = message.result + return ( + { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > +
+
{ commandService.executeCommand('vscode.open', value.uri, { preview: true }) }} + > + + {value.uri.fsPath.split('/').pop()} +
+
+
+ ) + }, + 'terminal_command': ({ message }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + if (message.result.type === 'error') return + + const { value } = message.result + return ( + +
+
+ + + +
) } + }; diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts new file mode 100644 index 00000000..2fe64d11 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -0,0 +1,71 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +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 { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; + +export interface ITerminalToolService { + readonly _serviceBrand: undefined; + + createNewTerminal(terminalId: string): Promise; + runCommand(command: string, terminalId?: string): Promise; + focus(terminalId: string): Promise; +} + +export const ITerminalToolService = createDecorator('TerminalToolService'); + +export class TerminalToolService extends Disposable implements ITerminalToolService { + readonly _serviceBrand: undefined; + + private terminalInstances: Record = {} + + constructor( + @ITerminalService private readonly terminalService: ITerminalService + ) { + super(); + } + + async createNewTerminal() { + const terminalId = generateUuid(); + + this.terminalService.createTerminal({}); + const terminal = await this.terminalService.createTerminal({ + location: TerminalLocation.Editor, + config: { name: `Void Agent (${terminalId})`, } + }); + + this.terminalInstances[terminalId] = terminal + return terminalId; + } + + async runCommand(command: string, terminalId?: string) { + + if (!terminalId) { + terminalId = await this.createNewTerminal(); + } + + const terminal = this.terminalInstances[terminalId]; + if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`); + + terminal.sendText(command, true); + return; + } + + async focus(terminalId: string) { + const terminal = this.terminalInstances[terminalId]; + if (!terminal) throw new Error(`That terminal was closed.`); + + + terminal.focus(true); + return; + } + +} + +registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts index d95e2666..dc16b52d 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -13,11 +13,12 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from './llmMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js'; -import { toLLMChatMessage } from './llmMessageTypes.js'; +import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolName, voidTools } from './toolsService.js'; +import { toLLMChatMessage, ToolCallType } from './llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from './voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; +import { getErrorMessage } from '../../../../base/common/errors.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -59,11 +60,9 @@ export type ToolMessage = { name: T; // internal use params: string; // internal use id: string; // apis require this tool use id - content: string; // result - result: ToolCallReturnType[T]; // text message of result + content: string; // give this result to LLM + result: { type: 'success'; value: ToolCallReturnType[T], } | { type: 'error'; value: string }; // give this result to user } - - // 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. export type ChatMessage = { @@ -390,37 +389,44 @@ class ChatThreadService extends Disposable implements IChatThreadService { else { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message - for (const tool of toolCalls ?? []) { - const toolName = tool.name as ToolName - // 1. - let toolResult: Awaited> - let toolResultVal: ToolCallReturnType[ToolName] - try { - toolResult = await this._toolsService.toolFns[toolName](tool.params) - toolResultVal = toolResult - } catch (error) { - this._setStreamState(threadId, { error }) - shouldSendAnotherMessage = false - break - } + // deal with the tool + const tool: ToolCallType | undefined = toolCalls?.[0] + if (!tool) { + res_() + return + } + const toolName = tool.name - // 2. - let toolResultStr: string - try { - toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - } catch (error) { - this._setStreamState(threadId, { error }) - shouldSendAnotherMessage = false - break - } - - this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, }) + // 1. + let toolResultVal: ToolCallReturnType[typeof toolName] + try { + const val = await this._toolsService.toolFns[toolName](tool.params) + toolResultVal = val + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) shouldSendAnotherMessage = true + res_() + return } + // 2. + let toolResultStr: string + try { + toolResultStr = this._toolsService.toolResultToString[toolName](toolResultVal as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here + } catch (error) { + // treat as irrecoverable error + this._setStreamState(threadId, { error }) + res_() + return + } + + this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: { type: 'success', value: toolResultVal }, }) + shouldSendAnotherMessage = true + res_() } - res_() + }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' @@ -436,7 +442,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } - agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs + agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve) } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 4f767928..4257e616 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -22,6 +22,11 @@ export const errorDetails = (fullError: Error | null): string | null => { return null } +export const getErrorMessage: (error: unknown) => string = (error) => { + if (error instanceof Error) return `${error.name}: ${error.message}` + return error + '' +} + export type LLMChatMessage = { role: 'system' | 'user'; @@ -60,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { else if (c.role === 'tool') return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' } else { - throw 1 + throw new Error(`Role ${(c as any).role} not recognized.`) } } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 95dcb045..cf6052dd 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -6,7 +6,10 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js' import { ISearchService } from '../../../../workbench/services/search/common/search.js' +import { IEditCodeService } from '../browser/editCodeService.js' +import { editToolDesc_toolDescription } from '../browser/prompt/prompts.js' import { IVoidFileService } from './voidFileService.js' +import { ITerminalToolService } from '../browser/terminalToolService.js' // tool use for AI @@ -29,6 +32,8 @@ const paginationHelper = { } as const export const voidTools = { + // --- context-gathering (read/search/list) --- + read_file: { name: 'read_file', description: `Returns file contents of a given URI. ${paginationHelper.desc}`, @@ -68,44 +73,50 @@ export const voidTools = { required: ['query'], }, + // --- editing (create/delete) --- - // create_file: { - // name: 'create_file', - // description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`, - // params: { - // uri: { type: 'string', description: undefined }, - // }, - // required: ['uri'], - // }, + create_uri: { + name: 'create_uri', + description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + params: { + uri: { type: 'string', description: undefined }, + }, + required: ['uri'], + }, - // create_folder: { - // name: 'create_folder', - // description: `Creates a folder at the given path. Fails gracefully if the folder already exists by doing nothing.`, - // params: { - // uri: { type: 'string', description: undefined }, - // }, - // required: ['uri'], - // }, + delete_uri: { + name: 'delete_uri', + description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + }, + required: ['uri', 'params'], + }, + + edit: { // APPLY TOOL + name: 'edit', + description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + changeDescription: { type: 'string', description: editToolDesc_toolDescription } + }, + required: ['uri', 'changeDescription'], + }, + + terminal_command: { + name: 'terminal_command', + description: `Executes a terminal command.`, + params: { + command: { type: 'string', description: 'The terminal command to execute.' } + }, + required: ['command'], + }, - // go_to_definition: { + // go_to_definition + // go_to_usages - // }, - - // go_to_usages: - - // create_file: { - // name: 'create_file', - // description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.` - // params: { - // uri: { type: 'string', description: undefined }, - // } - // } - - // semantic_search: { - // description: 'Searches files semantically for the given string query.', - // // RAG - // }, } satisfies { [name: string]: InternalToolInfo } export type ToolName = keyof typeof voidTools @@ -124,9 +135,13 @@ export type ToolParamsObj = { [paramName in ToolParamNames { return o } catch (e) { - throw new Error(`Tool parameter was not a valid JSON: "${s}".`) + throw new Error(`Tool parameter was not a string of a valid JSON: "${s}".`) } } -const validateQueryStr = (queryStr: unknown) => { - if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.') - return queryStr +const validateStr = (argName: string, value: unknown) => { + if (typeof value !== 'string') throw new Error(`Error: ${argName} must be a string.`) + return value } // TODO!!!! check to make sure in workspace const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.') + if (typeof uriStr !== 'string') throw new Error('Error: provided uri must be a string.') const uri = URI.file(uriStr) return uri } const validatePageNum = (pageNumberUnknown: unknown) => { - const proposedPageNum = Number.parseInt(pageNumberUnknown + '') - const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1 - const pageNumber = num < 1 ? 1 : num - return pageNumber + if (!pageNumberUnknown) return 1 + const parsedInt = Number.parseInt(pageNumberUnknown + '') + if (!Number.isInteger(parsedInt)) throw new Error(`Page number was not an integer: "${pageNumberUnknown}".`) + if (parsedInt < 1) throw new Error(`Specified page number must be 1 or greater: "${pageNumberUnknown}".`) + return parsedInt } + +const validateRecursiveParamStr = (paramsUnknown: unknown) => { + if (typeof paramsUnknown !== 'string') throw new Error('Error calling tool: provided params must be a string.') + const params = paramsUnknown + const isRecursive = params.includes('r') + return isRecursive +} + export interface IToolsService { readonly _serviceBrand: undefined; toolFns: ToolFns; @@ -256,8 +280,8 @@ export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; - public toolFns: ToolFns - public toolResultToString: ToolResultToString + public toolFns: ToolFns; + public toolResultToString: ToolResultToString; constructor( @@ -266,15 +290,17 @@ export class ToolsService implements IToolsService { @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, @IVoidFileService voidFileService: IVoidFileService, + @IEditCodeService editCodeService: IEditCodeService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); this.toolFns = { - read_file: async (s: string) => { + read_file: async (params: string) => { console.log('read_file') - const o = validateJSON(s) + const o = validateJSON(params) const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) @@ -293,25 +319,21 @@ export class ToolsService implements IToolsService { return { uri, fileContents, hasNextPage } }, - list_dir: async (s: string) => { - console.log('list_dir') - const o = validateJSON(s) + list_dir: async (params: string) => { + const o = validateJSON(params) const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) const dirResult = await computeDirectoryResult(fileService, uri, pageNumber) - console.log('list_dir result:', dirResult) - return dirResult }, - pathname_search: async (s: string) => { - console.log('pathname_search') - const o = validateJSON(s) + pathname_search: async (params: string) => { + const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o - const queryStr = validateQueryStr(queryUnknown) + const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) @@ -324,19 +346,13 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - console.log('pathname_search result:', uris) - return { queryStr, uris, hasNextPage } }, - search: async (s: string) => { - - - console.log('search') - - const o = validateJSON(s) + search: async (params: string) => { + const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o - const queryStr = validateQueryStr(queryUnknown) + const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) @@ -349,17 +365,62 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - - console.log('search result:', uris) - return { queryStr, uris, hasNextPage } }, + // --- + + create_uri: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr } = o + const uri = validateURI(uriStr) + await fileService.createFile(uri) + return { uri } + }, + + delete_uri: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr, params: paramsStr } = o + const uri = validateURI(uriStr) + const isRecursive = validateRecursiveParamStr(paramsStr) + await fileService.del(uri, { recursive: isRecursive }) + return { uri } + }, + + edit: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o + const uri = validateURI(uriStr) + const changeDescription = validateStr('changeDescription', changeDescriptionUnknown) + + const applyId = editCodeService.startApplying({ uri, applyStr: changeDescription, from: 'ClickApply', type: 'rewrite' }) + + // // TODO!!! + + // await // await apply done before moving on + + return { uri, changeDescription } + }, + + terminal_command: async (s: string) => { + const o = validateJSON(s) + const { command: commandUnknown } = o + const command = validateStr('command', commandUnknown) + + // TODO!!!! + // await // Await user confirmation and then command execution before resolving + + + return { command } + }, + + } const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' + // given to the LLM after the call this.toolResultToString = { read_file: (result) => { return nextPageStr(result.hasNextPage) @@ -369,13 +430,25 @@ export class ToolsService implements IToolsService { return dirTreeStr + nextPageStr(result.hasNextPage) }, pathname_search: (result) => { - if (typeof result.uris === 'string') return result.uris return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, search: (result) => { - if (typeof result.uris === 'string') return result.uris return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, + // --- + create_uri: (result) => { + return `URI ${result.uri.fsPath} successfully created.` + }, + delete_uri: (result) => { + return `URI ${result.uri.fsPath} successfully deleted.` + }, + edit: (result) => { + return `Change successfully made ${result.uri.fsPath} successfully deleted.` + }, + terminal_command: (result) => { + return `Terminal command "${result.command}" successfully executed.` + }, + }