split into common/ and browser/ so terminal works

This commit is contained in:
Andrew Pareles 2025-03-09 18:07:21 -07:00
parent 2300559081
commit 6a55abcd81
16 changed files with 340 additions and 296 deletions

View file

@ -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';

View file

@ -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 = <T>(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<T extends ToolName> = {
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<T extends ToolName> = {
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<ToolName>
| ToolRequestApproval<ToolName>
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

View file

@ -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';

View file

@ -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() }}
>

View file

@ -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 = ({

View file

@ -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<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement|null>;
// UI customization
className?: string;

View file

@ -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 ----------

View file

@ -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<string>;
runCommand(command: string, terminalId?: string): Promise<void>;
focus(terminalId: string): Promise<void>;
runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>;
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
@ -23,49 +20,69 @@ export const ITerminalToolService = createDecorator<ITerminalToolService>('Termi
export class TerminalToolService extends Disposable implements ITerminalToolService {
readonly _serviceBrand: undefined;
private terminalInstances: Record<string, ITerminalInstance> = {}
private terminalInstanceOfId: Record<string, ITerminalInstance> = {}
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);

View file

@ -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<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const toolNamesThatRequireApproval = new Set<ToolName>(['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<ToolCallParams[T]> }
export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<ToolResultType[T]> }
export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<ToolResultType[T]> }
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)` : ''}.`
},
}

View file

@ -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'

View file

@ -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<T extends ToolName> = {
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<T extends ToolName> = {
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<ToolName>
| ToolRequestApproval<ToolName>
// 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

View file

@ -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 {

View file

@ -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

View file

@ -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'

View file

@ -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<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const toolNamesThatRequireApproval = new Set<ToolName>(['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 },
}

View file

@ -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 = {