mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
Merge pull request #319 from voideditor/model-selection
Terminal Service Progress + Fix Import Issue
This commit is contained in:
commit
ccd0db635b
19 changed files with 932 additions and 420 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,18 +10,23 @@ 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 { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
|
|
@ -55,76 +60,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 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
|
||||
|
|
@ -133,6 +77,13 @@ export type ChatThreads = {
|
|||
state: {
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
|
||||
|
||||
linksOfMessageIdx: { // eg. link = linksOfMessageIdx[4]['RangeFunction']
|
||||
[messageIdx: number]: {
|
||||
[codespanName: string]: CodespanLocationLink
|
||||
}
|
||||
}
|
||||
|
||||
isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO
|
||||
}
|
||||
};
|
||||
|
|
@ -140,10 +91,11 @@ export type ChatThreads = {
|
|||
|
||||
type ThreadType = ChatThreads[string]
|
||||
|
||||
const defaultThreadState: ThreadType['state'] = {
|
||||
export const defaultThreadState: ThreadType['state'] = {
|
||||
stagingSelections: [],
|
||||
focusedMessageIdx: undefined,
|
||||
isCheckedOfSelectionId: {}
|
||||
isCheckedOfSelectionId: {},
|
||||
linksOfMessageIdx: {},
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
|
|
@ -199,12 +151,19 @@ export interface IChatThreadService {
|
|||
isFocusingMessage(): boolean;
|
||||
setFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
|
||||
|
||||
getCodespanLink({ codespanStr, messageIdx, threadId }: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined;
|
||||
addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }): void;
|
||||
generateCodespanLink(codespanStr: string): Promise<CodespanLocationLink>
|
||||
|
||||
// exposed getters/setters
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
|
||||
getCurrentThreadState: () => ThreadType['state']
|
||||
setCurrentThreadState: (newState: Partial<ThreadType['state']>) => void
|
||||
|
||||
|
||||
closeStagingSelectionsInCurrentThread(): void;
|
||||
closeStagingSelectionsInMessage(messageIdx: number): void;
|
||||
|
||||
|
|
@ -243,6 +202,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@IToolsService private readonly _toolsService: IToolsService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
|
||||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
|
@ -260,9 +222,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
// !!! this is important for properly restoring URIs from storage
|
||||
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
|
||||
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
|
||||
return JSON.parse(threadsStr, (key, value) => {
|
||||
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
|
||||
if (value && typeof value === 'object' && value.$mid === 1) { // $mid is the MarshalledId. $mid === 1 means it is a URI
|
||||
return URI.from(value);
|
||||
}
|
||||
return value;
|
||||
|
|
@ -418,8 +381,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
|
||||
|
||||
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
|
||||
const terminalIds = this.terminalToolService.listTerminalIds()
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath), chatMode), },
|
||||
{ role: 'system', content: chat_systemMessage(workspaceFolders, terminalIds, chatMode), },
|
||||
...messages_.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages_.slice(lastUserMsgIdx + 1, Infinity),
|
||||
|
|
@ -452,17 +417,17 @@ 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
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
|
@ -480,7 +445,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// TODO!!! test rejection
|
||||
// if (Math.random() > 0) throw new Error('TESTING')
|
||||
const errorMessage = 'Tool call was rejected by the user.'
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
|
@ -492,7 +457,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
|
@ -503,7 +468,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
} catch (error) {
|
||||
const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
|
@ -551,6 +516,206 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// ---------- the rest ----------
|
||||
|
||||
// gets the location of codespan link so the user can click on it
|
||||
async generateCodespanLink(_codespanStr: string): Promise<CodespanLocationLink> {
|
||||
|
||||
// process codespan to understand what we are searching for
|
||||
// TODO account for more complicated patterns eg `ITextEditorService.openEditor()`
|
||||
const functionOrMethodPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; // `fUnCt10n_name`
|
||||
const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )`
|
||||
|
||||
let target = _codespanStr // the string to search for
|
||||
let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable';
|
||||
if (target.includes('.')) {
|
||||
|
||||
codespanType = 'file'
|
||||
target = _codespanStr
|
||||
|
||||
} else if (functionOrMethodPattern.test(target)) {
|
||||
|
||||
codespanType = 'function-or-class'
|
||||
target = _codespanStr
|
||||
|
||||
} else if (functionParensPattern.test(target)) {
|
||||
const match = target.match(functionParensPattern)
|
||||
if (match && match[1]) {
|
||||
|
||||
codespanType = 'function-or-class'
|
||||
target = match[1]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (codespanType === 'unsearchable') {
|
||||
return null
|
||||
}
|
||||
|
||||
// get history of all AI and user added files in conversation + store in reverse order (MRU)
|
||||
const prevUris = this._getAllSelections()
|
||||
.map(s => s.fileURI)
|
||||
.filter((uri, index, array) => array.findIndex(u => u.toString() === uri.toString()) === index) // O(n^2) but this is small
|
||||
.reverse()
|
||||
|
||||
|
||||
if (codespanType === 'file') {
|
||||
|
||||
|
||||
const doesUriMatchTarget = (uri: URI) => uri.path.includes(target)
|
||||
|
||||
// check if any prevFiles are the `codespanSearch`
|
||||
for (const uri of prevUris) {
|
||||
if (doesUriMatchTarget(uri)) return { uri }
|
||||
}
|
||||
|
||||
// else search codebase for file
|
||||
const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 })
|
||||
|
||||
for (const uri of uris) {
|
||||
if (doesUriMatchTarget(uri)) return { uri }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (codespanType === 'function-or-class') {
|
||||
|
||||
|
||||
// check all prevUris for the target
|
||||
for (const uri of prevUris) {
|
||||
|
||||
const modelRef = await this._textModelService.createModelReference(uri);
|
||||
const model = modelRef.object.textEditorModel;
|
||||
|
||||
try {
|
||||
const matches = model.findMatches(
|
||||
target,
|
||||
false, // searchOnlyEditableRange
|
||||
false, // isRegex
|
||||
true, // matchCase
|
||||
' ', // wordSeparators
|
||||
true // captureMatches
|
||||
);
|
||||
|
||||
const firstThree = matches.slice(0, 3);
|
||||
|
||||
// take first 3 occurences, attempt to goto definition on them
|
||||
for (const match of firstThree) {
|
||||
const position = new Position(match.range.startLineNumber, match.range.startColumn);
|
||||
const definitionProviders = this._languageFeaturesService.definitionProvider.ordered(model);
|
||||
|
||||
for (const provider of definitionProviders) {
|
||||
|
||||
const _definitions = await provider.provideDefinition(model, position, CancellationToken.None);
|
||||
|
||||
if (!_definitions) continue;
|
||||
|
||||
const definitions = Array.isArray(_definitions) ? _definitions : [_definitions];
|
||||
|
||||
for (const definition of definitions) {
|
||||
|
||||
return {
|
||||
uri: definition.uri,
|
||||
selection: {
|
||||
startLineNumber: definition.range.startLineNumber,
|
||||
startColumn: definition.range.startColumn,
|
||||
endLineNumber: definition.range.endLineNumber,
|
||||
endColumn: definition.range.endColumn,
|
||||
}
|
||||
};
|
||||
|
||||
// const defModelRef = await this._textModelService.createModelReference(definition.uri);
|
||||
// const defModel = defModelRef.object.textEditorModel;
|
||||
|
||||
// try {
|
||||
// const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel);
|
||||
|
||||
// for (const symbolProvider of symbolProviders) {
|
||||
// const symbols = await symbolProvider.provideDocumentSymbols(
|
||||
// defModel,
|
||||
// CancellationToken.None
|
||||
// );
|
||||
|
||||
// if (symbols) {
|
||||
// const symbol = symbols.find(s => {
|
||||
// const symbolRange = s.range;
|
||||
// return symbolRange.startLineNumber <= definition.range.startLineNumber &&
|
||||
// symbolRange.endLineNumber >= definition.range.endLineNumber &&
|
||||
// (symbolRange.startLineNumber !== definition.range.startLineNumber || symbolRange.startColumn <= definition.range.startColumn) &&
|
||||
// (symbolRange.endLineNumber !== definition.range.endLineNumber || symbolRange.endColumn >= definition.range.endColumn);
|
||||
// });
|
||||
|
||||
// // if we got to a class/function get the full range and return
|
||||
// if (symbol?.kind === SymbolKind.Function || symbol?.kind === SymbolKind.Method || symbol?.kind === SymbolKind.Class) {
|
||||
// return {
|
||||
// uri: definition.uri,
|
||||
// selection: {
|
||||
// startLineNumber: definition.range.startLineNumber,
|
||||
// startColumn: definition.range.startColumn,
|
||||
// endLineNumber: definition.range.endLineNumber,
|
||||
// endColumn: definition.range.endColumn,
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } finally {
|
||||
// defModelRef.dispose();
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
modelRef.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// unlike above do not search codebase (doesnt make sense)
|
||||
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
getCodespanLink({ codespanStr, messageIdx, threadId }: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return undefined;
|
||||
|
||||
const links = thread.state.linksOfMessageIdx?.[messageIdx]
|
||||
if (!links) return undefined;
|
||||
|
||||
const link = links[codespanStr]
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
async addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return
|
||||
|
||||
this._setState({
|
||||
|
||||
allThreads: {
|
||||
...this.state.allThreads,
|
||||
[threadId]: {
|
||||
...thread,
|
||||
state: {
|
||||
...thread.state,
|
||||
linksOfMessageIdx: {
|
||||
...thread.state.linksOfMessageIdx,
|
||||
[messageIdx]: {
|
||||
...thread.state.linksOfMessageIdx?.[messageIdx],
|
||||
[newLinkText]: newLinkLocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
|
||||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
const thread = state.allThreads[state.currentThreadId]
|
||||
|
|
@ -721,7 +886,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
getCurrentThreadState = () => {
|
||||
|
||||
const currentThread = this.getCurrentThread()
|
||||
|
||||
return currentThread.state
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -1527,11 +1527,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
const errHelper = (erroneousOriginal: string) => `All previous SEARCH/REPLACE blocks (if any) have been applied except the latest erroneous one. Please continue outputting SEARCH/REPLACE blocks. The ORIGINAL code with an error was: ${JSON.stringify(erroneousOriginal)}`
|
||||
const errMsgOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string) => {
|
||||
return str === `Not found` ?
|
||||
`The ORIGINAL code provided could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}`
|
||||
`The ORIGINAL code provided could not be found in the file. You should make sure the text in ORIGINAL matches lines of code EXACTLY. ${errHelper(blockOrig)}`
|
||||
: str === `Not unique` ?
|
||||
`The ORIGINAL code provided shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}`
|
||||
`The ORIGINAL code provided shows up multiple times in the file. We recommend making the ORIGINAL portion bigger so we can find a unique match. ${errHelper(blockOrig)}`
|
||||
: ``
|
||||
}
|
||||
|
||||
|
|
@ -1627,6 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
// REVERT
|
||||
// TODO!!!!! don't actually revert - we want to change this so it doesn't revert but isntead gives the current file contents
|
||||
const numLines = this._getNumLines(uri)
|
||||
if (numLines !== null) this._writeText(uri, originalFileCode,
|
||||
{ startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER },
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
|||
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
|
||||
}, [metricsService, clipboardService, codeStr, setCopyButtonText])
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
const isSingleLine = false //!codeStr.includes('\n')
|
||||
|
||||
return <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonText}
|
||||
|
|
@ -101,17 +101,17 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
}, [streamState, applyingUri, editCodeService, metricsService])
|
||||
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
const isSingleLine = false //!codeStr.includes('\n')
|
||||
|
||||
const applyButton = <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
const stopButton = <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
|
||||
onClick={onInterrupt}
|
||||
>
|
||||
Stop
|
||||
|
|
@ -119,7 +119,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
|
||||
const acceptRejectButtons = <>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
|
|
@ -128,7 +128,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
Accept
|
||||
</button>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-2 rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX } from 'react'
|
||||
import React, { JSX, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'
|
||||
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js'
|
||||
import { Range } from '../../../../../../services/search/common/searchExtTypes.js'
|
||||
import { IRange } from '../../../../../../../base/common/range.js'
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
threadId: string;
|
||||
|
|
@ -20,7 +25,81 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) =>
|
|||
return `${threadId}-${messageIdx}-${tokenIdx}`
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
||||
return <code
|
||||
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{text}
|
||||
</code>
|
||||
|
||||
}
|
||||
|
||||
const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string, rawText: string, chatMessageLocation: ChatMessageLocation }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
|
||||
const chatThreadService = accessor.get('IChatThreadService')
|
||||
const commandSerivce = accessor.get('ICommandService')
|
||||
const editorService = accessor.get('ICodeEditorService')
|
||||
|
||||
const { messageIdx, threadId } = chatMessageLocation
|
||||
|
||||
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
|
||||
|
||||
let link = undefined
|
||||
if (rawText.endsWith("`")) { // if codespan was completed
|
||||
|
||||
// get link from cache
|
||||
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
|
||||
|
||||
if (link === undefined) {
|
||||
// generate link and add to cache
|
||||
(chatThreadService.generateCodespanLink(text)
|
||||
.then(link => {
|
||||
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
|
||||
setDidComputeCodespanLink(true) // rerender
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const onClick = () => {
|
||||
|
||||
if (!link) return;
|
||||
const selection = link.selection
|
||||
|
||||
// open the file
|
||||
commandSerivce.executeCommand('vscode.open', link.uri).then(() => {
|
||||
|
||||
// select the text
|
||||
setTimeout(() => {
|
||||
if (!selection) return;
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
if (!editor) return;
|
||||
|
||||
editor.setSelection(selection)
|
||||
editor.revealRange(selection, ScrollType.Immediate)
|
||||
|
||||
}, 50) // needed when document was just opened and needs to initialize
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <Codespan
|
||||
text={text}
|
||||
onClick={onClick}
|
||||
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
|
||||
/>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
|
@ -35,12 +114,14 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
|
||||
if (t.type === "code") {
|
||||
|
||||
const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({
|
||||
threadId: chatMessageLocationForApply.threadId,
|
||||
messageIdx: chatMessageLocationForApply.messageIdx,
|
||||
const applyBoxId = chatMessageLocation ? getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
}) : null
|
||||
|
||||
// TODO user should only be able to apply this when the code has been closed (t.raw ends with "```")
|
||||
|
||||
return <div>
|
||||
<BlockCode
|
||||
initValue={t.text}
|
||||
|
|
@ -132,7 +213,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={t.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
|
@ -148,7 +229,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={item.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -162,6 +243,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
<RenderToken key={index}
|
||||
token={token}
|
||||
tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
@ -221,11 +303,16 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<code className="font-mono font-medium rounded-sm bg-void-bg-1 px-1">
|
||||
{t.text}
|
||||
</code>
|
||||
)
|
||||
|
||||
if (chatMessageLocation) {
|
||||
return <CodespanWithLink
|
||||
text={t.text}
|
||||
rawText={t.raw}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
/>
|
||||
}
|
||||
|
||||
return <Codespan text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
|
|
@ -244,12 +331,12 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: {
|
|||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocationForApply }: { string: string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
|
||||
<RenderToken key={index} token={token} nested={nested} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 { ToolCallParams, 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;
|
||||
|
|
@ -659,11 +659,10 @@ export const SelectedFiles = (
|
|||
interface DropdownComponentProps {
|
||||
title: string;
|
||||
desc1: string;
|
||||
desc2?: string;
|
||||
desc2?: React.ReactNode;
|
||||
numResults?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownComponent = ({
|
||||
|
|
@ -673,7 +672,6 @@ const DropdownComponent = ({
|
|||
numResults,
|
||||
children,
|
||||
onClick,
|
||||
icon,
|
||||
}: DropdownComponentProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
|
|
@ -695,18 +693,21 @@ const DropdownComponent = ({
|
|||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center flex-nowrap whitespace-nowrap gap-x-2">
|
||||
{icon}
|
||||
<span className="text-void-fg-3">{title}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{desc1}</span>
|
||||
{desc2 && <span className="text-void-fg-4 text-xs">
|
||||
{desc2}
|
||||
</span>}
|
||||
{numResults !== undefined && (
|
||||
<span className="text-void-fg-4 text-xs">
|
||||
{`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center justify-between w-full flex-nowrap whitespace-nowrap gap-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<span className="text-void-fg-3">{title}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{desc1}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{desc2 && <span className="text-void-fg-4 text-xs">
|
||||
{desc2}
|
||||
</span>}
|
||||
{numResults !== undefined && (
|
||||
<span className="text-void-fg-4 text-xs ml-auto mr-1">
|
||||
{`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -908,11 +909,8 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
}}
|
||||
/>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -932,23 +930,10 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB
|
|||
messageIdx: messageIdx,
|
||||
}
|
||||
|
||||
const isEmpty = !chatMessage.content && !chatMessage.reasoning
|
||||
const isEmpty = !chatMessage.content && !chatMessage.reasoning // && !(isLast && isLoading) // TODO!!!!
|
||||
if (isEmpty) return null
|
||||
|
||||
return <>
|
||||
|
||||
{/* reasoning token */}
|
||||
{hasReasoning && <DropdownComponent
|
||||
title="Reasoning"
|
||||
desc1=""
|
||||
icon={<Dot className='stroke-blue-500' />}
|
||||
>
|
||||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocationForApply={chatMessageLocation}
|
||||
/>
|
||||
</DropdownComponent>}
|
||||
|
||||
<div
|
||||
className='
|
||||
text-void-fg-2
|
||||
|
|
@ -977,14 +962,25 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB
|
|||
'
|
||||
>
|
||||
|
||||
{/* reasoning token */}
|
||||
{hasReasoning && <DropdownComponent
|
||||
title="Reasoning"
|
||||
desc1=""
|
||||
>
|
||||
<ChatMarkdownRender
|
||||
string={reasoningStr}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
/>
|
||||
</DropdownComponent>}
|
||||
|
||||
{/* assistant message */}
|
||||
<ChatMarkdownRender
|
||||
string={chatMessage.content || ''}
|
||||
chatMessageLocationForApply={chatMessageLocation}
|
||||
chatMessageLocation={chatMessageLocation}
|
||||
/>
|
||||
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm mx-4' />}
|
||||
|
||||
{/* loading indicator */}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
|
@ -992,29 +988,77 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB
|
|||
|
||||
|
||||
|
||||
const ToolError = ({ title, errorMessage }: { title: string, errorMessage: string }) => {
|
||||
const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: string, errorMessage: string }) => {
|
||||
return (
|
||||
<div className='flex gap-2 p-3 bg-void-bg-2-alt bg-opacity-10 border border-void-warning border-opacity-20 rounded-md'>
|
||||
<AlertTriangle className='text-void-warning flex-shrink-0' size={20} />
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-void-fg-1 font-medium mb-1'>{title}</span>
|
||||
<div className='text-void-fg-3 text-sm opacity-90'>{'Error: ' + errorMessage}</div>
|
||||
</div>
|
||||
</div>
|
||||
// px-2 py-1
|
||||
// <div className='flex gap-2 p-3 border border-void-border-3 text-void-fg-3 rounded bg-void-bg-2-alt'>
|
||||
// <AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={20} />
|
||||
// <div className='flex flex-col'>
|
||||
// <span className='mb-1'>{title + ' error'}</span>
|
||||
// <div className='text-sm opacity-90'>{errorMessage}</div>
|
||||
// </div>
|
||||
// </div>
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={desc1}
|
||||
desc2={
|
||||
<span className="flex items-center flex-nowrap gap-1">
|
||||
<AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={12} />
|
||||
Error
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className='text-xs text-wrap whitespace-pre-wrap break-all break-words'>{errorMessage}</div>
|
||||
</DropdownComponent>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const toolNameToTitle: Record<ToolName, string> = {
|
||||
'read_file': 'Read file',
|
||||
'list_dir': 'Inspect folder',
|
||||
'pathname_search': 'Search (path only)',
|
||||
'search': 'Search (file contents)',
|
||||
'list_dir': 'Inspected folder',
|
||||
'pathname_search': 'Searched by file name',
|
||||
'search': 'Searched files',
|
||||
'create_uri': 'Create file',
|
||||
'delete_uri': 'Delete file',
|
||||
'edit': 'Edit file',
|
||||
'terminal_command': 'Ran terminal command'
|
||||
}
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
|
||||
|
||||
if (_toolParams === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (toolName === 'read_file') {
|
||||
const toolParams = _toolParams as ToolCallParams['read_file']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) : '';
|
||||
} else if (toolName === 'list_dir') {
|
||||
const toolParams = _toolParams as ToolCallParams['list_dir']
|
||||
return toolParams ? `${getBasename(toolParams.rootURI.fsPath)}/` : '';
|
||||
} else if (toolName === 'pathname_search') {
|
||||
const toolParams = _toolParams as ToolCallParams['pathname_search']
|
||||
return toolParams ? `"${toolParams.queryStr}"` : '';
|
||||
} else if (toolName === 'search') {
|
||||
const toolParams = _toolParams as ToolCallParams['search']
|
||||
return toolParams ? `"${toolParams.queryStr}"` : '';
|
||||
} else if (toolName === 'create_uri') {
|
||||
const toolParams = _toolParams as ToolCallParams['create_uri']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) : '';
|
||||
} else if (toolName === 'delete_uri') {
|
||||
const toolParams = _toolParams as ToolCallParams['delete_uri']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) + ' (deleted)' : '';
|
||||
} else if (toolName === 'edit') {
|
||||
const toolParams = _toolParams as ToolCallParams['edit']
|
||||
return toolParams ? getBasename(toolParams.uri.fsPath) : '';
|
||||
} else if (toolName === 'terminal_command') {
|
||||
const toolParams = _toolParams as ToolCallParams['terminal_command']
|
||||
return toolParams ? `"${toolParams.command}"` : '';
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1029,24 +1073,31 @@ const ToolRequestAcceptRejectButtons = ({ toolRequest }: { toolRequest: ToolRequ
|
|||
|
||||
const toolNameToComponent: { [T in ToolName]: {
|
||||
requestWrapper: (props: { toolRequest: ToolRequestApproval<T> }) => React.ReactNode,
|
||||
resultWrapper: (props: { toolMessage: ToolMessage<T> & { result: { type: 'success' } } }) => React.ReactNode,
|
||||
resultWrapper: (props: { toolMessage: ToolMessage<T> }) => React.ReactNode,
|
||||
} } = {
|
||||
'read_file': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={getBasename(params.uri.toString())} icon={<Dot className={`stroke-orange-500`} />}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }}
|
||||
/>
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { value, params } = toolMessage.result
|
||||
return <DropdownComponent title={title} desc1={getBasename(params.uri.toString())} icon={<Dot className={`stroke-orange-500`} />}>
|
||||
|
||||
return <DropdownComponent title={title} desc1={desc1}>
|
||||
<div
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
|
|
@ -1054,7 +1105,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
<div className="flex-shrink-0"><svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg></div>
|
||||
{params.uri.fsPath}
|
||||
</div>
|
||||
{value.hasNextPage && (<div className="italic">AI can scroll for more content...</div>)}
|
||||
{toolMessage.result.value.hasNextPage && (<div className="italic">AI can scroll for more content...</div>)}
|
||||
|
||||
</DropdownComponent>
|
||||
},
|
||||
|
|
@ -1062,23 +1113,26 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
'list_dir': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={`${getBasename(params.rootURI.fsPath)}/`} icon={<Dot className={`stroke-orange-500`} />} />
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1} />
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const explorerService = accessor.get('IExplorerService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
// message.result.hasNextPage = true
|
||||
// message.result.itemsRemaining = 400
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { value, params } = toolMessage.result
|
||||
|
||||
return <DropdownComponent
|
||||
title={title}
|
||||
desc1={`${getBasename(params.rootURI.fsPath)}/`}
|
||||
desc1={desc1}
|
||||
numResults={value.children?.length}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
>
|
||||
{value.children?.map((child, i) => (
|
||||
<div
|
||||
|
|
@ -1099,28 +1153,31 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
</div>
|
||||
)}
|
||||
</DropdownComponent>
|
||||
|
||||
}
|
||||
},
|
||||
'pathname_search': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={`"${params.queryStr}"`} icon={<Dot className={`stroke-orange-500`} />} />
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1} />
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { value, params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={`"${params.queryStr}"`}
|
||||
desc1={desc1}
|
||||
numResults={value.uris.length}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
>
|
||||
{value.uris.map((uri, i) => (
|
||||
<div
|
||||
|
|
@ -1133,8 +1190,7 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
<div className="flex-shrink-0"><svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg></div>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
))}
|
||||
{value.hasNextPage && (
|
||||
<div className="italic">
|
||||
More results available...
|
||||
|
|
@ -1147,21 +1203,26 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
'search': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={`"${params.queryStr}"`} icon={<Dot className={`stroke-orange-500`} />} />
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1} />
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { value, params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={`"${params.queryStr}"`}
|
||||
desc1={desc1}
|
||||
numResults={value.uris.length}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
>
|
||||
{value.uris.map((uri, i) => (
|
||||
<div key={i}
|
||||
|
|
@ -1177,24 +1238,29 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
)
|
||||
}
|
||||
},
|
||||
|
||||
'create_uri': {
|
||||
requestWrapper: ({ toolRequest }) => {
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath)} icon={<Dot className={`stroke-orange-500`} />} />
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1} />
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={getBasename(params.uri.fsPath)}
|
||||
desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1204,20 +1270,27 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath) + ' (deleted)'}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }}
|
||||
/>
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={getBasename(params.uri.fsPath) + ' (deleted)'}
|
||||
desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
/>
|
||||
)
|
||||
|
|
@ -1228,25 +1301,30 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={getBasename(params.uri.fsPath)} icon={<Dot className={`stroke-orange-500`} />}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }}
|
||||
>
|
||||
<ChatMarkdownRender string={params.changeDescription} />
|
||||
<ChatMarkdownRender string={toolRequest.params.changeDescription} chatMessageLocation={undefined} />
|
||||
</DropdownComponent>
|
||||
},
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={getBasename(params.uri.fsPath)}
|
||||
desc1={desc1}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1256,8 +1334,8 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolRequest.name]
|
||||
const { params } = toolRequest
|
||||
return <DropdownComponent title={title} desc1={`"${params.command}"`} icon={<Dot className={`stroke-orange-500`} />}
|
||||
const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params)
|
||||
return <DropdownComponent title={title} desc1={desc1}
|
||||
// TODO!!! open the terminal with that ID
|
||||
/>
|
||||
},
|
||||
|
|
@ -1265,26 +1343,30 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name]
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
|
||||
if (toolMessage.result.type === 'error') {
|
||||
return <ToolError title={title} desc1={desc1} errorMessage={toolMessage.result.value} />
|
||||
}
|
||||
|
||||
const { params } = toolMessage.result
|
||||
|
||||
return (
|
||||
<DropdownComponent
|
||||
title={title}
|
||||
desc1={`"${params.command}"`}
|
||||
icon={<Dot className={`stroke-orange-500`} />}
|
||||
desc1={desc1}
|
||||
>
|
||||
<div
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
// TODO!!! open terminal
|
||||
>
|
||||
<div className="flex-shrink-0"><svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg></div>
|
||||
<ChatMarkdownRender string={''} />
|
||||
<ChatMarkdownRender string={''} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</DropdownComponent>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1318,8 +1400,10 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) =>
|
|||
</>
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
|
||||
const title = toolNameToTitle[chatMessage.name]
|
||||
if (chatMessage.result.type === 'error') return <ToolError title={title} errorMessage={chatMessage.result.value} />
|
||||
// if (chatMessage.result.type === 'error') return <ToolError title={title} params={chatMessage.result.params} errorMessage={chatMessage.result.value} />
|
||||
|
||||
const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough...
|
||||
return <ToolResultComponent toolMessage={chatMessage} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
isPasswordField={isPasswordField}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender string={subTextMd} />
|
||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
|
@ -421,11 +421,11 @@ export const FeaturesTab = () => {
|
|||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 prose-ol:list-decimal opacity-80'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} chatMessageLocation={undefined} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,66 +6,130 @@
|
|||
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 { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.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, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, contents: string }>;
|
||||
listTerminalIds(): string[];
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
|
||||
|
||||
const nameOfId = (id: string) => {
|
||||
if (id === '1') return 'Void Agent'
|
||||
return `Void Agent (${id})`
|
||||
}
|
||||
const idOfName = (name: string) => {
|
||||
if (name === 'Void Agent') return '1'
|
||||
|
||||
const match = name.match(/Void Agent \((\d+)\)/)
|
||||
if (!match) return null
|
||||
if (Number.isInteger(match[1]) && Number(match[1]) >= 1) return match[1]
|
||||
return null
|
||||
}
|
||||
|
||||
export class TerminalToolService extends Disposable implements ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private terminalInstances: Record<string, ITerminalInstance> = {}
|
||||
private terminalInstanceOfId: Record<string, ITerminalInstance> = {}
|
||||
|
||||
constructor(
|
||||
@ITerminalService private readonly terminalService: ITerminalService
|
||||
@ITerminalService private readonly terminalService: ITerminalService,
|
||||
) {
|
||||
super();
|
||||
|
||||
// initialize any terminals that are already open
|
||||
|
||||
for (const terminal of terminalService.instances) {
|
||||
const proposedTerminalId = idOfName(terminal.title)
|
||||
if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal
|
||||
}
|
||||
console.log('Initialized terminal instances:', this.terminalInstanceOfId)
|
||||
|
||||
}
|
||||
|
||||
async createNewTerminal() {
|
||||
const terminalId = generateUuid();
|
||||
|
||||
this.terminalService.createTerminal({});
|
||||
listTerminalIds() {
|
||||
return Object.keys(this.terminalInstanceOfId)
|
||||
}
|
||||
|
||||
getValidNewTerminalId(): string {
|
||||
// {1 2 3} # size 3, new=4
|
||||
// {1 3 4} # size 3, new=2
|
||||
// 1 <= newTerminalId <= n + 1
|
||||
const n = Object.keys(this.terminalInstanceOfId).length;
|
||||
if (n === 0) return '1'
|
||||
|
||||
for (let i = 1; i <= n + 1; i++) {
|
||||
const potentialId = i + '';
|
||||
if (!(potentialId in this.terminalInstanceOfId)) return potentialId;
|
||||
}
|
||||
throw new Error('This should never be reached by pigeonhole principle');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async _getOrCreateTerminal(proposedTerminalId: string) {
|
||||
// if terminal ID exists, return it
|
||||
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
|
||||
// create new terminal and return its ID
|
||||
const terminalId = this.getValidNewTerminalId();
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
location: TerminalLocation.Editor,
|
||||
config: { name: `Void Agent (${terminalId})`, }
|
||||
location: TerminalLocation.Panel,
|
||||
config: { name: nameOfId(terminalId), title: nameOfId(terminalId) }
|
||||
});
|
||||
|
||||
this.terminalInstances[terminalId] = terminal
|
||||
return terminalId;
|
||||
this.terminalInstanceOfId[terminalId] = terminal
|
||||
return { terminalId, didCreateTerminal: true }
|
||||
}
|
||||
|
||||
async runCommand(command: string, terminalId?: string) {
|
||||
|
||||
if (!terminalId) {
|
||||
terminalId = await this.createNewTerminal();
|
||||
|
||||
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
|
||||
await this.terminalService.whenConnected;
|
||||
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
|
||||
const terminal = this.terminalInstanceOfId[terminalId];
|
||||
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
|
||||
|
||||
|
||||
if (!waitForCompletion) {
|
||||
console.log('NOT WAITING FOR COMPLETION')
|
||||
await terminal.sendText(command, true);
|
||||
return { terminalId, didCreateTerminal, contents: '(command is running in background...)' };
|
||||
}
|
||||
|
||||
const terminal = this.terminalInstances[terminalId];
|
||||
if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`);
|
||||
// stream
|
||||
|
||||
terminal.sendText(command, true);
|
||||
return;
|
||||
let data = ''
|
||||
const d1 = terminal.onData(newData => { data += newData })
|
||||
|
||||
// terminal.onExit(() => {
|
||||
// console.log('TERMINALEXIT')
|
||||
// })
|
||||
|
||||
await terminal.sendText(command, true);
|
||||
// wait for the command to finish
|
||||
const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection);
|
||||
if (commandDetection) {
|
||||
const d2 = commandDetection.onCommandFinished(() => {
|
||||
console.log('FINISHED', data)
|
||||
d1.dispose()
|
||||
d2.dispose()
|
||||
return { terminalId, didCreateTerminal, contents: data }
|
||||
})
|
||||
}
|
||||
|
||||
console.log('didnot wait', data)
|
||||
d1.dispose()
|
||||
return { terminalId, didCreateTerminal, contents: 'Could not await data...' }
|
||||
}
|
||||
|
||||
async focus(terminalId: string) {
|
||||
const terminal = this.terminalInstances[terminalId];
|
||||
if (!terminal) throw new Error(`That terminal was closed.`);
|
||||
|
||||
|
||||
terminal.focus(true);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager);
|
||||
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,19 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
|||
return isRecursive
|
||||
}
|
||||
|
||||
const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
|
||||
if (!terminalIdUnknown) return '1'
|
||||
const terminalId = terminalIdUnknown + ''
|
||||
return terminalId
|
||||
}
|
||||
|
||||
const validateWaitForCompletion = (b: unknown) => {
|
||||
if (typeof b === 'string') {
|
||||
if (b === 'true') return true
|
||||
if (b === 'false') return false
|
||||
}
|
||||
return true // default is true
|
||||
}
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateParams;
|
||||
|
|
@ -309,7 +174,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 +245,11 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
terminal_command: async (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown } = o
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
const command = validateStr('command', commandUnknown)
|
||||
return { command }
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
},
|
||||
|
||||
}
|
||||
|
|
@ -454,10 +321,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, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { terminalId, didCreateTerminal }
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -490,7 +356,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'; params: ToolCallParams[T] | undefined; 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
|
||||
|
|
@ -23,21 +23,24 @@ Do NOT output the whole file if possible, and try to write as LITTLE as needed t
|
|||
|
||||
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}.
|
||||
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
|
||||
Please assist the user with their query. The user's query is never invalid.
|
||||
|
||||
The user's system information is as follows:
|
||||
- ${os}
|
||||
- Open workspaces: ${workspaces.join(', ')}
|
||||
|
||||
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
|
||||
${(mode === 'agent' || mode === 'gather') && runningTerminalIds.length !== 0 ? `\
|
||||
- Running terminal IDs: ${runningTerminalIds.join(', ')}
|
||||
`: '\n'}
|
||||
${mode === 'agent' || mode === 'gather' /* tool use */ ? `\
|
||||
You will be given tools you can call.
|
||||
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
|
||||
- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, understand the codebase, ${mode === 'agent' ? 'edit files, ' : ''}etc.
|
||||
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results.
|
||||
- Some tools only work if the user has a workspace open.
|
||||
- Some tools only work if the user has a workspace open. ${mode === 'gather' ? '' : `
|
||||
- NEVER modify a file outside one of the the user's workspaces without confirmation from the user.`}
|
||||
\
|
||||
`: `\
|
||||
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
|
||||
|
|
@ -45,8 +48,7 @@ You're allowed to ask for more context. For example, if the user only gives you
|
|||
`}
|
||||
|
||||
${mode === 'agent' /* code blocks */ ? `\
|
||||
Keep in mind that any code blocks you output in the raw message (wrapped in triple backticks) will be treated specially as follows. This does NOT apply to code blocks in tool calls.
|
||||
- Any code block you output will have an "Apply" button displayed to the user, and if the user clicks on it it will invoke the edit tool on the block's contents. As a result, all code blocks should describe relevant changes.
|
||||
If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code.
|
||||
`: `\
|
||||
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
|
||||
- The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created.
|
||||
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
|||
163
src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
Normal file
163
src/vs/workbench/contrib/void/common/toolsServiceTypes.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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.' },
|
||||
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
|
||||
terminalId: { type: 'string', description: 'Optional (if provided, value must be an integer >= 1). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
|
||||
},
|
||||
required: ['command'],
|
||||
},
|
||||
|
||||
|
||||
// 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, waitForCompletion: boolean },
|
||||
}
|
||||
|
||||
|
||||
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