From 4a9c0c864f0762f914c02403830e52ae229a0939 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 7 Mar 2025 19:18:47 -0800 Subject: [PATCH 1/6] links draft 1 --- .../contrib/void/browser/chatThreadService.ts | 251 +++++++++++++++++- .../react/src/markdown/ChatMarkdownRender.tsx | 125 ++++++++- .../react/src/sidebar-tsx/SidebarChat.tsx | 76 +++--- .../react/src/void-settings-tsx/Settings.tsx | 12 +- 4 files changed, 405 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index f02f9e2c..205568d4 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -21,7 +21,11 @@ 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 { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { LocationLink, SymbolKind } from '../../../../editor/common/languages.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Position } from '../../../../editor/common/core/position.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -78,6 +82,15 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection +export type CodespanLocationLink = { + uri: URI, // we handle serialization for this + selection?: { // store as JSON so dont have to worry about serialization + startLineNumber: number + startColumn: number, + endLineNumber: number + endColumn: number, + } | undefined +} | null export type ToolMessage = { role: 'tool'; @@ -133,6 +146,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 } }; @@ -143,7 +163,8 @@ type ThreadType = ChatThreads[string] const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, - isCheckedOfSelectionId: {} + isCheckedOfSelectionId: {}, + linksOfMessageIdx: {}, } export type ThreadsState = { @@ -199,12 +220,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 + // exposed getters/setters getCurrentMessageState: (messageIdx: number) => UserMessageState setCurrentMessageState: (messageIdx: number, newState: Partial) => void getCurrentThreadState: () => ThreadType['state'] setCurrentThreadState: (newState: Partial) => void + closeStagingSelectionsInCurrentThread(): void; closeStagingSelectionsInMessage(messageIdx: number): void; @@ -243,6 +271,8 @@ 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, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -260,6 +290,7 @@ 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 @@ -551,6 +582,220 @@ 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 { + + // process codespan to understand what we are searching for + // TODO account for more complicated patterns eg `ITextEditorService.openEditor()` + const filePattern = /^[^\s.]+\.[^\s.]+$/; + const functionPattern = /^[^\s(]+\([^)]*\)$/; + + let target = _codespanStr // the string to search for + let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable'; + if (filePattern.test(target)) { + + codespanType = 'file' + target = _codespanStr + + } else if (functionPattern.test(target)) { + const match = target.match(functionPattern) + if (match && match[0]) { + + codespanType = 'function-or-class' + target = match[0] + + } + } + + 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.split('(')[0].trim(), // remove parameters + false, // searchOnlyEditableRange + false, // isRegex + true, // matchCase + null, // 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 locationLinks: LocationLink[] = []; + + if (Array.isArray(definitions)) { + // Handle Location[] or LocationLink[] + for (const def of definitions) { + if ('uri' in def) { + locationLinks.push({ + uri: def.uri, + range: def.range, + targetSelectionRange: def.range, + originSelectionRange: undefined + }); + } else { + locationLinks.push(def); + } + } + } else { + // Handle single Location + locationLinks.push({ + uri: definitions.uri, + range: definitions.range, + targetSelectionRange: definitions.range, + originSelectionRange: undefined + }); + } + + const definition = locationLinks[0]; + if (!definition) continue; + + // Load definition file model + 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); + }); + + + console.log('@@@ symbol', symbol?.name, symbol?.kind) + + // if we got to a class/function get the full range and return + if (symbol?.kind === SymbolKind.Function || 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 location = links[codespanStr] + if (!location) return undefined; + + return location + } + + 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 +966,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { getCurrentThreadState = () => { + const currentThread = this.getCurrentThread() + return currentThread.state } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 50b8378e..d2b7cc56 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -3,11 +3,17 @@ * 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 { CodespanLocationLink } from '../../../chatThreadService.js' +import { IRange } from '../../../../../../../base/common/range.js' +import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' + export type ChatMessageLocation = { threadId: string; @@ -20,7 +26,87 @@ 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 + {text} + + +} + +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(false) + + console.log('rerender', didComputeCodespanLink ? 1 : 0) + + 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) + + }) + } + + } + + + const onClick = () => { + + if (!link) return; + const selection = link.selection + + // open the file + commandSerivce.executeCommand('vscode.open', link.uri).then(() => { + + console.log('click:', selection, link.uri) + + // select the text + if (!selection) return; + + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + + editor.setSelection(selection) + editor.revealRange(selection, ScrollType.Immediate) + + }) + + } + + + return +} + +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 +121,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
- + } @@ -148,7 +236,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { )} - + ))} @@ -162,6 +250,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { ))} @@ -221,11 +310,19 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { // inline code if (t.type === "codespan") { - return ( - - {t.text} - - ) + + + console.log('chatmessagelocation', chatMessageLocation) + + if (chatMessageLocation) { + return + } + + return } if (t.type === "br") { @@ -244,12 +341,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) => ( - + ))} ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index cc9d779e..6bcf5ae8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -936,19 +936,6 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB if (isEmpty) return null return <> - - {/* reasoning token */} - {hasReasoning && } - > - - } -
+ {/* reasoning token */} + {hasReasoning && } + > + + } + {/* assistant message */} - {isLoading && } - + {/* loading indicator */} + {isLoading && }
@@ -1122,25 +1121,28 @@ const toolNameToComponent: { [T in ToolName]: { numResults={value.uris.length} icon={} > - {value.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > -
- {uri.fsPath.split('/').pop()} -
- )) + { + value.uris.map((uri, i) => ( +
{ + commandService.executeCommand('vscode.open', uri, { preview: true }) + }} + > +
+ {uri.fsPath.split('/').pop()} +
+ )) } - {value.hasNextPage && ( -
- More results available... -
- )} - + { + value.hasNextPage && ( +
+ More results available... +
+ ) + } + ) } }, @@ -1232,8 +1234,8 @@ const toolNameToComponent: { [T in ToolName]: { return } onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - - + + }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1278,7 +1280,7 @@ const toolNameToComponent: { [T in ToolName]: { // TODO!!! open terminal >
- +
) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 10068af2..346d9143 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -291,7 +291,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider isPasswordField={isPasswordField} /> {subTextMd === undefined ? null :
- +
} @@ -421,11 +421,11 @@ export const FeaturesTab = () => { {/*

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

*/}

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

- - - - - + + + + + {/* TODO we should create UI for downloading models without user going into terminal */}
From 2300559081a4b8cf48c5185f6a74b7f8f6d51ac0 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 8 Mar 2025 01:01:16 -0800 Subject: [PATCH 2/6] links to code --- .../contrib/void/browser/chatThreadService.ts | 146 ++++++++---------- .../react/src/markdown/ChatMarkdownRender.tsx | 31 ++-- 2 files changed, 77 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 205568d4..44bfefe0 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -293,7 +293,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 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; @@ -587,22 +587,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { // process codespan to understand what we are searching for // TODO account for more complicated patterns eg `ITextEditorService.openEditor()` - const filePattern = /^[^\s.]+\.[^\s.]+$/; - const functionPattern = /^[^\s(]+\([^)]*\)$/; + 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 (filePattern.test(target)) { + if (target.includes('.')) { codespanType = 'file' target = _codespanStr - } else if (functionPattern.test(target)) { - const match = target.match(functionPattern) - if (match && match[0]) { + } 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[0] + target = match[1] } } @@ -618,7 +623,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { .reverse() - if (codespanType === 'file') { @@ -650,96 +654,79 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { const matches = model.findMatches( - target.split('(')[0].trim(), // remove parameters + target, false, // searchOnlyEditableRange false, // isRegex true, // matchCase - null, // wordSeparators + ' ', // 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 locationLinks: LocationLink[] = []; + const _definitions = await provider.provideDefinition(model, position, CancellationToken.None); - if (Array.isArray(definitions)) { - // Handle Location[] or LocationLink[] - for (const def of definitions) { - if ('uri' in def) { - locationLinks.push({ - uri: def.uri, - range: def.range, - targetSelectionRange: def.range, - originSelectionRange: undefined - }); - } else { - locationLinks.push(def); + 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, } - } - } else { - // Handle single Location - locationLinks.push({ - uri: definitions.uri, - range: definitions.range, - targetSelectionRange: definitions.range, - originSelectionRange: undefined - }); - } + }; - const definition = locationLinks[0]; - if (!definition) continue; + // const defModelRef = await this._textModelService.createModelReference(definition.uri); + // const defModel = defModelRef.object.textEditorModel; - // Load definition file model - const defModelRef = await this._textModelService.createModelReference(definition.uri); - const defModel = defModelRef.object.textEditorModel; + // try { + // const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel); - try { - const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel); + // for (const symbolProvider of symbolProviders) { + // const symbols = await symbolProvider.provideDocumentSymbols( + // defModel, + // CancellationToken.None + // ); - 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 (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); - }); - - - console.log('@@@ symbol', symbol?.name, symbol?.kind) - - // if we got to a class/function get the full range and return - if (symbol?.kind === SymbolKind.Function || 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(); + // // 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(); + // } } } } @@ -763,10 +750,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const links = thread.state.linksOfMessageIdx?.[messageIdx] if (!links) return undefined; - const location = links[codespanStr] - if (!location) return undefined; + const link = links[codespanStr] - return location + return link } async addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }) { diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index d2b7cc56..c5f8ff42 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -50,26 +50,20 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) - console.log('rerender', didComputeCodespanLink ? 1 : 0) - 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) + (chatThreadService.generateCodespanLink(text) .then(link => { - chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) - - setDidComputeCodespanLink(true) - + setDidComputeCodespanLink(true) // rerender }) + ) } } @@ -83,22 +77,22 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string // open the file commandSerivce.executeCommand('vscode.open', link.uri).then(() => { - console.log('click:', selection, link.uri) - // select the text - if (!selection) return; + setTimeout(() => { + if (!selection) return; - const editor = editorService.getActiveCodeEditor() - if (!editor) return; + const editor = editorService.getActiveCodeEditor() + if (!editor) return; - editor.setSelection(selection) - editor.revealRange(selection, ScrollType.Immediate) + editor.setSelection(selection) + editor.revealRange(selection, ScrollType.Immediate) + + }, 50) // needed when document was just opened and needs to initialize }) } - return Date: Sun, 9 Mar 2025 18:07:21 -0700 Subject: [PATCH 3/6] split into common/ and browser/ so terminal works --- .../void/browser/autocompleteService.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 92 ++------- .../contrib/void/browser/editCodeService.ts | 4 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - .../react/src/sidebar-tsx/ErrorDisplay.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../contrib/void/browser/sidebarActions.ts | 3 +- .../void/browser/terminalToolService.ts | 77 +++++--- .../contrib/void/browser/toolsService.ts | 182 ++---------------- .../contrib/void/browser/void.contribution.ts | 13 +- .../void/common/chatThreadServiceTypes.ts | 76 ++++++++ .../helpers/extractCodeFromResult.ts | 2 +- .../{browser => common}/prompt/prompts.ts | 8 +- .../void/common/sendLLMMessageTypes.ts | 2 +- .../contrib/void/common/toolsServiceTypes.ts | 162 ++++++++++++++++ .../llmMessage/sendLLMMessage.impl.ts | 4 +- 16 files changed, 340 insertions(+), 296 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts rename src/vs/workbench/contrib/void/{browser => common}/helpers/extractCodeFromResult.ts (99%) rename src/vs/workbench/contrib/void/{browser => common}/prompt/prompts.ts (98%) create mode 100644 src/vs/workbench/contrib/void/common/toolsServiceTypes.ts diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 8d219f1c..cc9f3a92 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -15,7 +15,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromRegular } from '../common/helpers/extractCodeFromResult.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { isWindows } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 44bfefe0..7ee42b36 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -10,21 +10,21 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; -import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; +import { LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { IToolsService } from './toolsService.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { LocationLink, SymbolKind } from '../../../../editor/common/languages.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -59,85 +59,15 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { } -// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) -export type CodeSelection = { - type: 'Selection'; - fileURI: URI; - selectionStr: string; - range: IRange; - state: { - isOpened: boolean; - }; -} - -export type FileSelection = { - type: 'File'; - fileURI: URI; - selectionStr: null; - range: null; - state: { - isOpened: boolean; - }; -} - -export type StagingSelectionItem = CodeSelection | FileSelection - -export type CodespanLocationLink = { - uri: URI, // we handle serialization for this - selection?: { // store as JSON so dont have to worry about serialization - startLineNumber: number - startColumn: number, - endLineNumber: number - endColumn: number, - } | undefined -} | null - -export type ToolMessage = { - role: 'tool'; - name: T; // internal use - paramsStr: string; // internal use - id: string; // apis require this tool use id - content: string; // give this result to LLM - result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user -} -export type ToolRequestApproval = { - role: 'tool_request'; - name: T; // internal use - params: ToolCallParams[T]; // internal use - voidToolId: string; // internal id Void uses -} - -// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. -export type ChatMessage = - | { - role: 'user'; - content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) - displayContent: string | null; // content displayed to user - allowed to be '', will be ignored - selections: StagingSelectionItem[] | null; // the user's selection - state: { - stagingSelections: StagingSelectionItem[]; - isBeingEdited: boolean; - } - } | { - role: 'assistant'; - content: string; // content received from LLM - allowed to be '', will be replaced with (empty) - reasoning: string; // reasoning from the LLM, used for step-by-step thinking - - anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning - } - | ToolMessage - | ToolRequestApproval - type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] - -export const defaultMessageState: UserMessageState = { +const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false, } // a 'thread' means a chat message history -export type ChatThreads = { +type ChatThreads = { [id: string]: { id: string; // store the id here too createdAt: string; // ISO string @@ -160,7 +90,7 @@ export type ChatThreads = { type ThreadType = ChatThreads[string] -const defaultThreadState: ThreadType['state'] = { +export const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {}, @@ -483,11 +413,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { res_() return } - const toolName = tool.name + const toolName: ToolName = tool.name shouldSendAnotherMessage = true // 1. validate tool params - let toolParams: ToolCallParams[typeof toolName] + let toolParams: ToolCallParams[ToolName] try { const params = await this._toolsService.validateParams[toolName](tool.paramsStr) toolParams = params diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9ef06ede..5c973961 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,12 +25,12 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; import { filenameToVscodeLanguage } from '../common/helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index fe70caa3..c7ecf1ba 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -93,7 +93,6 @@ export const QuickEditChat = ({ onClose={onX} isStreaming={isStreamingRef.current} isDisabled={isDisabled} - featureName="Ctrl+K" className="py-2 w-full" onClickAnywhere={() => { textAreaRef.current?.focus() }} > diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 9aef4b72..e8aec937 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; -import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js'; import { useSettingsState } from '../util/services.js'; +import { errorDetails } from '../../../../common/sendLLMMessageTypes.js'; export const ErrorDisplay = ({ diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 6bcf5ae8..8f26dd49 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -25,11 +25,11 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../chatThreadService.js'; import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; -import { ToolName } from '../../../toolsService.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; +import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; +import { ToolName } from '../../../../common/toolsServiceTypes.js'; @@ -274,7 +274,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization className?: string; diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 23974f7e..799f7777 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,7 +11,6 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; @@ -29,6 +28,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { localize2 } from '../../../../nls.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IVoidUriStateService } from './voidUriStateService.js'; +import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; +import { IChatThreadService } from './chatThreadService.js'; // ---------- Register commands and keybindings ---------- diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 2fe64d11..6940151e 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -6,16 +6,13 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; +import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - createNewTerminal(terminalId: string): Promise; - runCommand(command: string, terminalId?: string): Promise; - focus(terminalId: string): Promise; + runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>; } export const ITerminalToolService = createDecorator('TerminalToolService'); @@ -23,49 +20,69 @@ export const ITerminalToolService = createDecorator('Termi export class TerminalToolService extends Disposable implements ITerminalToolService { readonly _serviceBrand: undefined; - private terminalInstances: Record = {} + private terminalInstanceOfId: Record = {} constructor( - @ITerminalService private readonly terminalService: ITerminalService + @ITerminalService private readonly terminalService: ITerminalService, ) { super(); } - async createNewTerminal() { - const terminalId = generateUuid(); - this.terminalService.createTerminal({}); + + + getValidNewTerminalId(): string { + // {1 2 3} # size 3, new=4 + // {1 3 4} # size 3, new=2 + // 1 <= newTerminalId <= n + 1 + const n = Object.keys(this.terminalInstanceOfId).length; + for (let i = 1; i <= n + 1; i++) { + const potentialId = i + ''; + if (!(potentialId in this.terminalInstanceOfId)) return potentialId; + } + throw new Error('This should never be reached by pigeonhole principle'); + } + + + private async _createNewTerminal() { + const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ - location: TerminalLocation.Editor, + location: TerminalLocation.Panel, config: { name: `Void Agent (${terminalId})`, } }); - - this.terminalInstances[terminalId] = terminal + this.terminalInstanceOfId[terminalId] = terminal return terminalId; } - async runCommand(command: string, terminalId?: string) { - - if (!terminalId) { - terminalId = await this.createNewTerminal(); - } - - const terminal = this.terminalInstances[terminalId]; - if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`); - - terminal.sendText(command, true); - return; + private async _getValidTerminalId(proposedTerminalId: string) { + // if there is no terminal ID provided, create one + if (proposedTerminalId in this.terminalInstanceOfId) + return { terminalId: proposedTerminalId, didCreateTerminal: false } + const terminalId = await this._createNewTerminal() + return { terminalId, didCreateTerminal: true } } - async focus(terminalId: string) { - const terminal = this.terminalInstances[terminalId]; - if (!terminal) throw new Error(`That terminal was closed.`); - - + private async _focus(terminalId: string) { + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) return terminal.focus(true); return; } + + async runCommand(command: string, proposedTerminalId: string) { + await this.terminalService.whenConnected; + const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId) + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`); + this._focus(terminalId) + await terminal.sendText(command, true); + // terminal.onData(data => console.log('DATA!!', data)); + // terminal.onProcessReplayComplete(data => console.log('REPLAY!!', data)); + // terminal.onDidSendText(data => console.log('SEND!!', data)); + return { terminalId, didCreateTerminal }; + } + } -registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager); +registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index d43e1f9f..f7272bf8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -7,167 +7,19 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' -import { editToolDesc_toolDescription } from './prompt/prompts.js' import { IVoidFileService } from '../common/voidFileService.js' +import { ITerminalToolService } from './terminalToolService.js' +import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js' // tool use for AI -// we do this using Anthropic's style and convert to OpenAI style later -export type InternalToolInfo = { - name: string, - description: string, - params: { - [paramName: string]: { type: string, description: string | undefined } // name -> type - }, - required: string[], // required paramNames -} -const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } -} as const - -export const voidTools = { - // --- context-gathering (read/search/list) --- - - read_file: { - name: 'read_file', - description: `Returns file contents of a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - }, - required: ['uri'], - }, - - list_dir: { - name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param - }, - required: ['uri'], - }, - - pathname_search: { - name: 'pathname_search', - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - search: { - name: 'search', - description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - // --- editing (create/delete) --- - - create_uri: { - name: 'create_uri', - description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, - params: { - uri: { type: 'string', description: undefined }, - }, - required: ['uri'], - }, - - delete_uri: { - name: 'delete_uri', - description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } - }, - required: ['uri', 'params'], - }, - - edit: { // APPLY TOOL - name: 'edit', - description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - changeDescription: { type: 'string', description: editToolDesc_toolDescription } - }, - required: ['uri', 'changeDescription'], - }, - - terminal_command: { - name: 'terminal_command', - description: `Executes a terminal command.`, - params: { - command: { type: 'string', description: 'The terminal command to execute.' } - }, - required: ['command'], - }, - - - // go_to_definition - // go_to_usages - -} satisfies { [name: string]: InternalToolInfo } - -export type ToolName = keyof typeof voidTools -export const toolNames = Object.keys(voidTools) as ToolName[] - -const toolNamesSet = new Set(toolNames) -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} - - -export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) - -type DirectoryItem = { - uri: URI; - name: string; - isDirectory: boolean; - isSymbolicLink: boolean; -} - - -export type ToolCallParams = { - 'read_file': { uri: URI, pageNumber: number }, - 'list_dir': { rootURI: URI, pageNumber: number }, - 'pathname_search': { queryStr: string, pageNumber: number }, - 'search': { queryStr: string, pageNumber: number }, - // --- - 'edit': { uri: URI, changeDescription: string }, - 'create_uri': { uri: URI }, - 'delete_uri': { uri: URI, isRecursive: boolean }, - 'terminal_command': { command: string }, -} - - -export type ToolResultType = { - 'read_file': { fileContents: string, hasNextPage: boolean }, - 'list_dir': { children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'pathname_search': { uris: URI[], hasNextPage: boolean }, - 'search': { uris: URI[], hasNextPage: boolean }, - // --- - 'edit': {}, - 'create_uri': {}, - 'delete_uri': {}, - 'terminal_command': {}, -} - - - -export type ValidateParams = { [T in ToolName]: (p: string) => Promise } -export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } -export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } +type ValidateParams = { [T in ToolName]: (p: string) => Promise } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -193,7 +45,7 @@ const computeDirectoryResult = async ( const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; - const children: DirectoryItem[] = listChildren.map(child => ({ + const children: ToolDirectoryItem[] = listChildren.map(child => ({ name: child.name, uri: child.resource, isDirectory: child.isDirectory, @@ -284,6 +136,12 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { return isRecursive } +const validateProposedTerminalId = (terminalIdUnknown: unknown) => { + const terminalId = terminalIdUnknown + '' + if (!terminalId) return '' + return terminalId +} + export interface IToolsService { readonly _serviceBrand: undefined; validateParams: ValidateParams; @@ -309,7 +167,7 @@ export class ToolsService implements IToolsService { @IInstantiationService instantiationService: IInstantiationService, @IVoidFileService voidFileService: IVoidFileService, @IEditCodeService editCodeService: IEditCodeService, - // @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -380,9 +238,10 @@ export class ToolsService implements IToolsService { terminal_command: async (s: string) => { const o = validateJSON(s) - const { command: commandUnknown } = o + const { command: commandUnknown, terminalId: terminalIdUnknown } = o const command = validateStr('command', commandUnknown) - return { command } + const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) + return { command, proposedTerminalId } }, } @@ -454,10 +313,9 @@ export class ToolsService implements IToolsService { await applyDonePromise return {} }, - terminal_command: async ({ command }) => { - // TODO!!!! - // await // Await user confirmation and then command execution before resolving - return {} + terminal_command: async ({ command, proposedTerminalId }) => { + const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId) + return { terminalId, didCreateTerminal } }, } @@ -490,7 +348,7 @@ export class ToolsService implements IToolsService { return `Change successfully made ${params.uri.fsPath} successfully deleted.` }, terminal_command: (params, result) => { - return `Terminal command "${params.command}" successfully executed.` + return `Terminal command "${params.command}" successfully executed in terminal ${result.terminalId}${result.didCreateTerminal ? `(a newly-created terminal)` : ''}.` }, } diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index f5570488..559c15dc 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -33,6 +33,13 @@ import './media/void.css' import './voidUpdateActions.js' +// tools +import './toolsService.js' +import './terminalToolService.js' + +// register Thread History +import './chatThreadService.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- @@ -52,9 +59,3 @@ import '../common/metricsService.js' // updates import '../common/voidUpdateService.js' -// tools -import './toolsService.js' - -// register Thread History -import './chatThreadService.js' - diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts new file mode 100644 index 00000000..19398801 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -0,0 +1,76 @@ +import { URI } from '../../../../base/common/uri.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { AnthropicReasoning } from './sendLLMMessageTypes.js'; +import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; + +export type ToolMessage = { + role: 'tool'; + name: T; // internal use + paramsStr: string; // internal use + id: string; // apis require this tool use id + content: string; // give this result to LLM + result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user +} +export type ToolRequestApproval = { + role: 'tool_request'; + name: T; // internal use + params: ToolCallParams[T]; // internal use + voidToolId: string; // internal id Void uses +} + +// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. +export type ChatMessage = + | { + role: 'user'; + content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) + displayContent: string | null; // content displayed to user - allowed to be '', will be ignored + selections: StagingSelectionItem[] | null; // the user's selection + state: { + stagingSelections: StagingSelectionItem[]; + isBeingEdited: boolean; + } + } | { + role: 'assistant'; + content: string; // content received from LLM - allowed to be '', will be replaced with (empty) + reasoning: string; // reasoning from the LLM, used for step-by-step thinking + + anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning + } + | ToolMessage + | ToolRequestApproval + + +// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) +export type CodeSelection = { + type: 'Selection'; + fileURI: URI; + selectionStr: string; + range: IRange; + state: { + isOpened: boolean; + }; +} + +export type FileSelection = { + type: 'File'; + fileURI: URI; + selectionStr: null; + range: null; + state: { + isOpened: boolean; + }; +} + +export type StagingSelectionItem = CodeSelection | FileSelection + + + +export type CodespanLocationLink = { + uri: URI, // we handle serialization for this + selection?: { // store as JSON so dont have to worry about serialization + startLineNumber: number + startColumn: number, + endLineNumber: number + endColumn: number, + } | undefined +} | null diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts similarity index 99% rename from src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts rename to src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts index ee138358..2ec076d3 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { OnText } from '../../common/sendLLMMessageTypes.js' +import { OnText } from '../sendLLMMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts similarity index 98% rename from src/vs/workbench/contrib/void/browser/prompt/prompts.ts rename to src/vs/workbench/contrib/void/common/prompt/prompts.ts index 3199364f..045263b5 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -5,11 +5,11 @@ import { URI } from '../../../../../base/common/uri.js'; -import { filenameToVscodeLanguage } from '../../common/helpers/detectLanguage.js'; -import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; +import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; -import { os } from '../../common/helpers/systemInfo.js'; -import { IVoidFileService } from '../../common/voidFileService.js'; +import { os } from '../helpers/systemInfo.js'; +import { IVoidFileService } from '../voidFileService.js'; +import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; // this is just for ease of readability diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 76d62af9..b27ea20b 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import type { InternalToolInfo, ToolName } from '../browser/toolsService.js' +import { ToolName, InternalToolInfo } from './toolsServiceTypes.js' import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts new file mode 100644 index 00000000..d7972a3d --- /dev/null +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -0,0 +1,162 @@ +import { URI } from '../../../../base/common/uri.js' +import { editToolDesc_toolDescription } from './prompt/prompts.js'; + + + +// we do this using Anthropic's style and convert to OpenAI style later +export type InternalToolInfo = { + name: string, + description: string, + params: { + [paramName: string]: { type: string, description: string | undefined } // name -> type + }, + required: string[], // required paramNames +} + + + + + +export type ToolDirectoryItem = { + uri: URI; + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} + + + + + +const paginationHelper = { + desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } +} as const + +export const voidTools = { + // --- context-gathering (read/search/list) --- + + read_file: { + name: 'read_file', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + list_dir: { + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + pathname_search: { + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + search: { + name: 'search', + description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + // --- editing (create/delete) --- + + create_uri: { + name: 'create_uri', + description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + params: { + uri: { type: 'string', description: undefined }, + }, + required: ['uri'], + }, + + delete_uri: { + name: 'delete_uri', + description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + }, + required: ['uri', 'params'], + }, + + edit: { // APPLY TOOL + name: 'edit', + description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here + }, + required: ['uri', 'changeDescription'], + }, + + terminal_command: { + name: 'terminal_command', + description: `Executes a terminal command.`, + params: { + command: { type: 'string', description: 'The terminal command to execute.' }, + terminalId: { type: 'string', description: 'Optional. The terminal ID to execute the command in. Must be a number starting at 1. If a terminal does not exist with this ID, a new one will be created (not necessarily with the same ID as the provided one). ' }, + }, + required: ['command'], + }, + + + // go_to_definition + // go_to_usages + +} satisfies { [name: string]: InternalToolInfo } + +export type ToolName = keyof typeof voidTools +export const toolNames = Object.keys(voidTools) as ToolName[] + +const toolNamesSet = new Set(toolNames) +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + +export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) + +export type ToolCallParams = { + 'read_file': { uri: URI, pageNumber: number }, + 'list_dir': { rootURI: URI, pageNumber: number }, + 'pathname_search': { queryStr: string, pageNumber: number }, + 'search': { queryStr: string, pageNumber: number }, + // --- + 'edit': { uri: URI, changeDescription: string }, + 'create_uri': { uri: URI }, + 'delete_uri': { uri: URI, isRecursive: boolean }, + 'terminal_command': { command: string, proposedTerminalId: string }, +} + + +export type ToolResultType = { + 'read_file': { fileContents: string, hasNextPage: boolean }, + 'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'pathname_search': { uris: URI[], hasNextPage: boolean }, + 'search': { uris: URI[], hasNextPage: boolean }, + // --- + 'edit': {}, + 'create_uri': {}, + 'delete_uri': {}, + 'terminal_command': { terminalId: string, didCreateTerminal: boolean }, +} + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index dd75882e..abb0bc17 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -8,12 +8,12 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; +import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; -import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; +import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; type InternalCommonMessageParams = { From 217a33cc2e816ca0b5c5434ba20a47eb82247674 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 10 Mar 2025 00:39:50 -0700 Subject: [PATCH 4/6] tool call improvements (error message + better ux) --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../react/src/markdown/ChatMarkdownRender.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 278 ++++++++++++------ .../void/common/chatThreadServiceTypes.ts | 2 +- 4 files changed, 185 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7ee42b36..62720353 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -423,7 +423,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { 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 } @@ -441,7 +441,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 } @@ -453,7 +453,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 } @@ -464,7 +464,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 } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index c5f8ff42..1b46bef1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -10,7 +10,6 @@ import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage. import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' import { useAccessor, useChatThreadsState } from '../util/services.js' import { Range } from '../../../../../../services/search/common/searchExtTypes.js' -import { CodespanLocationLink } from '../../../chatThreadService.js' import { IRange } from '../../../../../../../base/common/range.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 8f26dd49..19af982f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -29,7 +29,7 @@ import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLangu import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolName } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName } from '../../../../common/toolsServiceTypes.js'; @@ -274,7 +274,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization className?: string; @@ -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' : ''}`} /> )} -
- {icon} - {title} - {desc1} - {desc2 && - {desc2} - } - {numResults !== undefined && ( - - {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} - - )} +
+
+ {title} + {desc1} +
+
+ {desc2 && + {desc2} + } + {numResults !== undefined && ( + + {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} + + )} +
} -
- - } @@ -968,7 +966,6 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB {hasReasoning && } > { +const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: string, errorMessage: string }) => { return ( -
- -
- {title} -
{'Error: ' + errorMessage}
-
-
+ // px-2 py-1 + //
+ // + //
+ // {title + ' error'} + //
{errorMessage}
+ //
+ //
+ + + Error + + } + > +
{errorMessage}
+
+ ) } const toolNameToTitle: Record = { '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 '' + } +} @@ -1028,24 +1073,31 @@ const ToolRequestAcceptRejectButtons = ({ toolRequest }: { toolRequest: ToolRequ const toolNameToComponent: { [T in ToolName]: { requestWrapper: (props: { toolRequest: ToolRequestApproval }) => React.ReactNode, - resultWrapper: (props: { toolMessage: ToolMessage & { result: { type: 'success' } } }) => React.ReactNode, + resultWrapper: (props: { toolMessage: ToolMessage }) => React.ReactNode, } } = { 'read_file': { requestWrapper: ({ toolRequest }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } - onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { 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 + } + const { value, params } = toolMessage.result - return }> + + return
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1053,7 +1105,7 @@ const toolNameToComponent: { [T in ToolName]: {
{params.uri.fsPath}
- {value.hasNextPage && (
AI can scroll for more content...
)} + {toolMessage.result.value.hasNextPage && (
AI can scroll for more content...
)}
}, @@ -1061,23 +1113,26 @@ const toolNameToComponent: { [T in ToolName]: { 'list_dir': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, 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 + } const { value, params } = toolMessage.result + return } > {value.children?.map((child, i) => (
)} - } }, 'pathname_search': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, 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 + } const { value, params } = toolMessage.result + return ( } > - { - value.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > -
- {uri.fsPath.split('/').pop()} -
- )) - } - { - value.hasNextPage && ( -
- More results available... -
- ) - } -
+ {value.uris.map((uri, i) => ( +
{ + commandService.executeCommand('vscode.open', uri, { preview: true }) + }} + > +
+ {uri.fsPath.split('/').pop()} +
+ ))} + {value.hasNextPage && ( +
+ More results available... +
+ )} +
) } }, 'search': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, 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 + } const { value, params } = toolMessage.result + return ( } > {value.uris.map((uri, i) => (
{ const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, 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 + } + const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - icon={} /> ) } @@ -1206,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 { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { 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 + } + const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} /> ) @@ -1230,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 } - onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { 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 + } const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - icon={} /> ) } @@ -1258,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 } + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, @@ -1267,13 +1343,18 @@ 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 + } const { params } = toolMessage.result + return ( } + desc1={desc1} >
} else if (role === 'tool') { + const title = toolNameToTitle[chatMessage.name] - if (chatMessage.result.type === 'error') return + // if (chatMessage.result.type === 'error') return + const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... return } diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 19398801..0819f242 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -9,7 +9,7 @@ export type ToolMessage = { 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 + 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 = { role: 'tool_request'; From 65be47d97052ada9eb0a7c39faf449a7f703011e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 10 Mar 2025 00:59:42 -0700 Subject: [PATCH 5/6] ancilary terminal tool --- .../contrib/void/browser/chatThreadService.ts | 6 +- .../contrib/void/browser/editCodeService.ts | 5 +- .../void/browser/terminalToolService.ts | 97 +++++++++++++------ .../contrib/void/browser/toolsService.ts | 18 +++- .../contrib/void/common/prompt/prompts.ts | 11 ++- .../contrib/void/common/toolsServiceTypes.ts | 5 +- 6 files changed, 100 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 62720353..328f4fe9 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -26,6 +26,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan 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 = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -203,6 +204,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @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 @@ -379,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), diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 5c973961..eba51a9c 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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, 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)}` : `` } diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 6940151e..a9f2e08a 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -6,36 +6,65 @@ 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 { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; -import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalService, ITerminalInstance, ITerminalInstanceService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>; + runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, contents: string }>; + listTerminalIds(): string[]; } export const ITerminalToolService = createDecorator('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 terminalInstanceOfId: Record = {} constructor( - @ITerminalService private readonly terminalService: ITerminalService, + @ITerminalInstanceService private readonly terminalInstanceService: ITerminalInstanceService, ) { 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) + } - + 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; @@ -44,45 +73,59 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } - private async _createNewTerminal() { + + 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.Panel, - config: { name: `Void Agent (${terminalId})`, } + config: { name: nameOfId(terminalId), title: nameOfId(terminalId) } }); this.terminalInstanceOfId[terminalId] = terminal - return terminalId; - } - - private async _getValidTerminalId(proposedTerminalId: string) { - // if there is no terminal ID provided, create one - if (proposedTerminalId in this.terminalInstanceOfId) - return { terminalId: proposedTerminalId, didCreateTerminal: false } - const terminalId = await this._createNewTerminal() return { terminalId, didCreateTerminal: true } } - private async _focus(terminalId: string) { - const terminal = this.terminalInstanceOfId[terminalId]; - if (!terminal) return - terminal.focus(true); - return; - } - async runCommand(command: string, proposedTerminalId: string) { + runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => { await this.terminalService.whenConnected; - const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId) + 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.`); - this._focus(terminalId) + + + if (!waitForCompletion) { + console.log('NOT WAITING FOR COMPLETION') + await terminal.sendText(command, true); + return { terminalId, didCreateTerminal, contents: '(command is running in background...)' }; + } + + // stream + + let data = '' + const d1 = terminal.onData(newData => { data += newData }) + await terminal.sendText(command, true); - // terminal.onData(data => console.log('DATA!!', data)); - // terminal.onProcessReplayComplete(data => console.log('REPLAY!!', data)); - // terminal.onDidSendText(data => console.log('SEND!!', data)); - return { terminalId, didCreateTerminal }; + // 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...' } } + + } registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index f7272bf8..cc851e10 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -137,11 +137,18 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { } const validateProposedTerminalId = (terminalIdUnknown: unknown) => { + if (!terminalIdUnknown) return '1' const terminalId = terminalIdUnknown + '' - if (!terminalId) return '' 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; @@ -238,10 +245,11 @@ export class ToolsService implements IToolsService { terminal_command: async (s: string) => { const o = validateJSON(s) - const { command: commandUnknown, terminalId: terminalIdUnknown } = o + const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o const command = validateStr('command', commandUnknown) const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) - return { command, proposedTerminalId } + const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown) + return { command, proposedTerminalId, waitForCompletion } }, } @@ -313,8 +321,8 @@ export class ToolsService implements IToolsService { await applyDonePromise return {} }, - terminal_command: async ({ command, proposedTerminalId }) => { - const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId) + terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { + const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) return { terminalId, didCreateTerminal } }, } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 045263b5..e61fff3c 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -23,15 +23,17 @@ 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. @@ -45,8 +47,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. diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index d7972a3d..b4d00691 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -112,7 +112,8 @@ export const voidTools = { description: `Executes a terminal command.`, params: { command: { type: 'string', description: 'The terminal command to execute.' }, - terminalId: { type: 'string', description: 'Optional. The terminal ID to execute the command in. Must be a number starting at 1. If a terminal does not exist with this ID, a new one will be created (not necessarily with the same ID as the provided one). ' }, + 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'], }, @@ -144,7 +145,7 @@ export type ToolCallParams = { 'edit': { uri: URI, changeDescription: string }, 'create_uri': { uri: URI }, 'delete_uri': { uri: URI, isRecursive: boolean }, - 'terminal_command': { command: string, proposedTerminalId: string }, + 'terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean }, } From ab8147934077c41dcfaeb43798baa97de6bc2fc8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 10 Mar 2025 15:35:57 -0700 Subject: [PATCH 6/6] terminalService --- .../contrib/void/browser/editCodeService.ts | 1 + .../react/src/markdown/ApplyBlockHoverButtons.tsx | 14 +++++++------- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/browser/terminalToolService.ts | 8 ++++++-- .../contrib/void/common/prompt/prompts.ts | 3 ++- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index eba51a9c..7535dadc 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1628,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 }, diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 832d7eb6..90ec360f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -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 const stopButton =