mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
split into common/ and browser/ so terminal works
This commit is contained in:
parent
2300559081
commit
6a55abcd81
16 changed files with 340 additions and 296 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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() }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 ----------
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)` : ''}.`
|
||||
},
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
|||
162
src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
Normal file
162
src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
Normal 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 },
|
||||
}
|
||||
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue