diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 8d219f1c..cc9f3a92 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -15,7 +15,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromRegular } from '../common/helpers/extractCodeFromResult.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { isWindows } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 44bfefe0..7ee42b36 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -10,21 +10,21 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; -import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; +import { LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { IToolsService } from './toolsService.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { LocationLink, SymbolKind } from '../../../../editor/common/languages.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -59,85 +59,15 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { } -// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) -export type CodeSelection = { - type: 'Selection'; - fileURI: URI; - selectionStr: string; - range: IRange; - state: { - isOpened: boolean; - }; -} - -export type FileSelection = { - type: 'File'; - fileURI: URI; - selectionStr: null; - range: null; - state: { - isOpened: boolean; - }; -} - -export type StagingSelectionItem = CodeSelection | FileSelection - -export type CodespanLocationLink = { - uri: URI, // we handle serialization for this - selection?: { // store as JSON so dont have to worry about serialization - startLineNumber: number - startColumn: number, - endLineNumber: number - endColumn: number, - } | undefined -} | null - -export type ToolMessage = { - role: 'tool'; - name: T; // internal use - paramsStr: string; // internal use - id: string; // apis require this tool use id - content: string; // give this result to LLM - result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user -} -export type ToolRequestApproval = { - role: 'tool_request'; - name: T; // internal use - params: ToolCallParams[T]; // internal use - voidToolId: string; // internal id Void uses -} - -// 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 = - | { - role: 'user'; - content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) - displayContent: string | null; // content displayed to user - allowed to be '', will be ignored - selections: StagingSelectionItem[] | null; // the user's selection - state: { - stagingSelections: StagingSelectionItem[]; - isBeingEdited: boolean; - } - } | { - role: 'assistant'; - content: string; // content received from LLM - allowed to be '', will be replaced with (empty) - reasoning: string; // reasoning from the LLM, used for step-by-step thinking - - anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning - } - | ToolMessage - | ToolRequestApproval - type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] - -export const defaultMessageState: UserMessageState = { +const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false, } // a 'thread' means a chat message history -export type ChatThreads = { +type ChatThreads = { [id: string]: { id: string; // store the id here too createdAt: string; // ISO string @@ -160,7 +90,7 @@ export type ChatThreads = { type ThreadType = ChatThreads[string] -const defaultThreadState: ThreadType['state'] = { +export const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {}, @@ -483,11 +413,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { res_() return } - const toolName = tool.name + const toolName: ToolName = tool.name shouldSendAnotherMessage = true // 1. validate tool params - let toolParams: ToolCallParams[typeof toolName] + let toolParams: ToolCallParams[ToolName] try { const params = await this._toolsService.validateParams[toolName](tool.paramsStr) toolParams = params diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9ef06ede..5c973961 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,12 +25,12 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; import { filenameToVscodeLanguage } from '../common/helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index fe70caa3..c7ecf1ba 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -93,7 +93,6 @@ export const QuickEditChat = ({ onClose={onX} isStreaming={isStreamingRef.current} isDisabled={isDisabled} - featureName="Ctrl+K" className="py-2 w-full" onClickAnywhere={() => { textAreaRef.current?.focus() }} > diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 9aef4b72..e8aec937 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; -import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js'; import { useSettingsState } from '../util/services.js'; +import { errorDetails } from '../../../../common/sendLLMMessageTypes.js'; export const ErrorDisplay = ({ 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 6bcf5ae8..8f26dd49 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 @@ -25,11 +25,11 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../chatThreadService.js'; import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; -import { ToolName } from '../../../toolsService.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; +import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; +import { ToolName } from '../../../../common/toolsServiceTypes.js'; @@ -274,7 +274,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization className?: string; diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 23974f7e..799f7777 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,7 +11,6 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; @@ -29,6 +28,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { localize2 } from '../../../../nls.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IVoidUriStateService } from './voidUriStateService.js'; +import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; +import { IChatThreadService } from './chatThreadService.js'; // ---------- Register commands and keybindings ---------- diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 2fe64d11..6940151e 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -6,16 +6,13 @@ 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'; +import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - createNewTerminal(terminalId: string): Promise; - runCommand(command: string, terminalId?: string): Promise; - focus(terminalId: string): Promise; + runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>; } export const ITerminalToolService = createDecorator('TerminalToolService'); @@ -23,49 +20,69 @@ export const ITerminalToolService = createDecorator('Termi export class TerminalToolService extends Disposable implements ITerminalToolService { readonly _serviceBrand: undefined; - private terminalInstances: Record = {} + private terminalInstanceOfId: Record = {} constructor( - @ITerminalService private readonly terminalService: ITerminalService + @ITerminalService private readonly terminalService: ITerminalService, ) { super(); } - async createNewTerminal() { - const terminalId = generateUuid(); - this.terminalService.createTerminal({}); + + + 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; + for (let i = 1; i <= n + 1; i++) { + const potentialId = i + ''; + if (!(potentialId in this.terminalInstanceOfId)) return potentialId; + } + throw new Error('This should never be reached by pigeonhole principle'); + } + + + private async _createNewTerminal() { + const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ - location: TerminalLocation.Editor, + location: TerminalLocation.Panel, config: { name: `Void Agent (${terminalId})`, } }); - - this.terminalInstances[terminalId] = terminal + this.terminalInstanceOfId[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; + 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 } } - async focus(terminalId: string) { - const terminal = this.terminalInstances[terminalId]; - if (!terminal) throw new Error(`That terminal was closed.`); - - + private async _focus(terminalId: string) { + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) return terminal.focus(true); return; } + + async runCommand(command: string, proposedTerminalId: string) { + await this.terminalService.whenConnected; + const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId) + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`); + this._focus(terminalId) + 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 }; + } + } -registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager); +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 d43e1f9f..f7272bf8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -7,167 +7,19 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' -import { editToolDesc_toolDescription } from './prompt/prompts.js' import { IVoidFileService } from '../common/voidFileService.js' +import { ITerminalToolService } from './terminalToolService.js' +import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js' // tool use for AI -// we do this using Anthropic's style and convert to OpenAI style later -export type InternalToolInfo = { - name: string, - description: string, - params: { - [paramName: string]: { type: string, description: string | undefined } // name -> type - }, - required: string[], // required paramNames -} -const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } -} 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}`, - params: { - uri: { type: 'string', description: undefined }, - }, - required: ['uri'], - }, - - list_dir: { - name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param - }, - required: ['uri'], - }, - - pathname_search: { - name: 'pathname_search', - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - search: { - name: 'search', - description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - // --- editing (create/delete) --- - - 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'], - }, - - 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_usages - -} satisfies { [name: string]: InternalToolInfo } - -export type ToolName = keyof typeof voidTools -export const toolNames = Object.keys(voidTools) as ToolName[] - -const toolNamesSet = new Set(toolNames) -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} - - -export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) - -type DirectoryItem = { - uri: URI; - name: string; - isDirectory: boolean; - isSymbolicLink: boolean; -} - - -export type ToolCallParams = { - 'read_file': { uri: URI, pageNumber: number }, - 'list_dir': { rootURI: URI, pageNumber: number }, - 'pathname_search': { queryStr: string, pageNumber: number }, - 'search': { queryStr: string, pageNumber: number }, - // --- - 'edit': { uri: URI, changeDescription: string }, - 'create_uri': { uri: URI }, - 'delete_uri': { uri: URI, isRecursive: boolean }, - 'terminal_command': { command: string }, -} - - -export type ToolResultType = { - 'read_file': { fileContents: string, hasNextPage: boolean }, - 'list_dir': { children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'pathname_search': { uris: URI[], hasNextPage: boolean }, - 'search': { uris: URI[], hasNextPage: boolean }, - // --- - 'edit': {}, - 'create_uri': {}, - 'delete_uri': {}, - 'terminal_command': {}, -} - - - -export type ValidateParams = { [T in ToolName]: (p: string) => Promise } -export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } -export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } +type ValidateParams = { [T in ToolName]: (p: string) => Promise } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -193,7 +45,7 @@ const computeDirectoryResult = async ( const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; - const children: DirectoryItem[] = listChildren.map(child => ({ + const children: ToolDirectoryItem[] = listChildren.map(child => ({ name: child.name, uri: child.resource, isDirectory: child.isDirectory, @@ -284,6 +136,12 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { return isRecursive } +const validateProposedTerminalId = (terminalIdUnknown: unknown) => { + const terminalId = terminalIdUnknown + '' + if (!terminalId) return '' + return terminalId +} + export interface IToolsService { readonly _serviceBrand: undefined; validateParams: ValidateParams; @@ -309,7 +167,7 @@ export class ToolsService implements IToolsService { @IInstantiationService instantiationService: IInstantiationService, @IVoidFileService voidFileService: IVoidFileService, @IEditCodeService editCodeService: IEditCodeService, - // @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -380,9 +238,10 @@ export class ToolsService implements IToolsService { terminal_command: async (s: string) => { const o = validateJSON(s) - const { command: commandUnknown } = o + const { command: commandUnknown, terminalId: terminalIdUnknown } = o const command = validateStr('command', commandUnknown) - return { command } + const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) + return { command, proposedTerminalId } }, } @@ -454,10 +313,9 @@ export class ToolsService implements IToolsService { await applyDonePromise return {} }, - terminal_command: async ({ command }) => { - // TODO!!!! - // await // Await user confirmation and then command execution before resolving - return {} + terminal_command: async ({ command, proposedTerminalId }) => { + const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId) + return { terminalId, didCreateTerminal } }, } @@ -490,7 +348,7 @@ export class ToolsService implements IToolsService { return `Change successfully made ${params.uri.fsPath} successfully deleted.` }, terminal_command: (params, result) => { - return `Terminal command "${params.command}" successfully executed.` + return `Terminal command "${params.command}" successfully executed in terminal ${result.terminalId}${result.didCreateTerminal ? `(a newly-created terminal)` : ''}.` }, } diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index f5570488..559c15dc 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -33,6 +33,13 @@ import './media/void.css' import './voidUpdateActions.js' +// tools +import './toolsService.js' +import './terminalToolService.js' + +// register Thread History +import './chatThreadService.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- @@ -52,9 +59,3 @@ import '../common/metricsService.js' // updates import '../common/voidUpdateService.js' -// tools -import './toolsService.js' - -// register Thread History -import './chatThreadService.js' - diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts new file mode 100644 index 00000000..19398801 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -0,0 +1,76 @@ +import { URI } from '../../../../base/common/uri.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { AnthropicReasoning } from './sendLLMMessageTypes.js'; +import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; + +export type ToolMessage = { + role: 'tool'; + name: T; // internal use + paramsStr: string; // internal use + id: string; // apis require this tool use id + content: string; // give this result to LLM + result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user +} +export type ToolRequestApproval = { + role: 'tool_request'; + name: T; // internal use + params: ToolCallParams[T]; // internal use + voidToolId: string; // internal id Void uses +} + +// 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 = + | { + role: 'user'; + content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) + displayContent: string | null; // content displayed to user - allowed to be '', will be ignored + selections: StagingSelectionItem[] | null; // the user's selection + state: { + stagingSelections: StagingSelectionItem[]; + isBeingEdited: boolean; + } + } | { + role: 'assistant'; + content: string; // content received from LLM - allowed to be '', will be replaced with (empty) + reasoning: string; // reasoning from the LLM, used for step-by-step thinking + + anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning + } + | ToolMessage + | ToolRequestApproval + + +// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) +export type CodeSelection = { + type: 'Selection'; + fileURI: URI; + selectionStr: string; + range: IRange; + state: { + isOpened: boolean; + }; +} + +export type FileSelection = { + type: 'File'; + fileURI: URI; + selectionStr: null; + range: null; + state: { + isOpened: boolean; + }; +} + +export type StagingSelectionItem = CodeSelection | FileSelection + + + +export type CodespanLocationLink = { + uri: URI, // we handle serialization for this + selection?: { // store as JSON so dont have to worry about serialization + startLineNumber: number + startColumn: number, + endLineNumber: number + endColumn: number, + } | undefined +} | null diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts similarity index 99% rename from src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts rename to src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts index ee138358..2ec076d3 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { OnText } from '../../common/sendLLMMessageTypes.js' +import { OnText } from '../sendLLMMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts similarity index 98% rename from src/vs/workbench/contrib/void/browser/prompt/prompts.ts rename to src/vs/workbench/contrib/void/common/prompt/prompts.ts index 3199364f..045263b5 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -5,11 +5,11 @@ import { URI } from '../../../../../base/common/uri.js'; -import { filenameToVscodeLanguage } from '../../common/helpers/detectLanguage.js'; -import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; +import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; -import { os } from '../../common/helpers/systemInfo.js'; -import { IVoidFileService } from '../../common/voidFileService.js'; +import { os } from '../helpers/systemInfo.js'; +import { IVoidFileService } from '../voidFileService.js'; +import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; // this is just for ease of readability diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 76d62af9..b27ea20b 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import type { InternalToolInfo, ToolName } from '../browser/toolsService.js' +import { ToolName, InternalToolInfo } from './toolsServiceTypes.js' import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts new file mode 100644 index 00000000..d7972a3d --- /dev/null +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -0,0 +1,162 @@ +import { URI } from '../../../../base/common/uri.js' +import { editToolDesc_toolDescription } from './prompt/prompts.js'; + + + +// we do this using Anthropic's style and convert to OpenAI style later +export type InternalToolInfo = { + name: string, + description: string, + params: { + [paramName: string]: { type: string, description: string | undefined } // name -> type + }, + required: string[], // required paramNames +} + + + + + +export type ToolDirectoryItem = { + uri: URI; + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} + + + + + +const paginationHelper = { + desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } +} 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}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + list_dir: { + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + pathname_search: { + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + search: { + name: 'search', + description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + // --- editing (create/delete) --- + + 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'], + }, + + 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 } // long description here + }, + required: ['uri', 'changeDescription'], + }, + + terminal_command: { + name: 'terminal_command', + 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). ' }, + }, + required: ['command'], + }, + + + // go_to_definition + // go_to_usages + +} satisfies { [name: string]: InternalToolInfo } + +export type ToolName = keyof typeof voidTools +export const toolNames = Object.keys(voidTools) as ToolName[] + +const toolNamesSet = new Set(toolNames) +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + +export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) + +export type ToolCallParams = { + 'read_file': { uri: URI, pageNumber: number }, + 'list_dir': { rootURI: URI, pageNumber: number }, + 'pathname_search': { queryStr: string, pageNumber: number }, + 'search': { queryStr: string, pageNumber: number }, + // --- + 'edit': { uri: URI, changeDescription: string }, + 'create_uri': { uri: URI }, + 'delete_uri': { uri: URI, isRecursive: boolean }, + 'terminal_command': { command: string, proposedTerminalId: string }, +} + + +export type ToolResultType = { + 'read_file': { fileContents: string, hasNextPage: boolean }, + 'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'pathname_search': { uris: URI[], hasNextPage: boolean }, + 'search': { uris: URI[], hasNextPage: boolean }, + // --- + 'edit': {}, + 'create_uri': {}, + 'delete_uri': {}, + 'terminal_command': { terminalId: string, didCreateTerminal: boolean }, +} + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index dd75882e..abb0bc17 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -8,12 +8,12 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; +import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; -import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; +import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; type InternalCommonMessageParams = {