From f047ef9c5541b569d2a6cd5dd288e40fdd9cc0b5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 30 Mar 2025 20:52:00 -0700 Subject: [PATCH 01/30] add dummy contrib --- .../contrib/void/browser/_dummyContrib.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/vs/workbench/contrib/void/browser/_dummyContrib.ts diff --git a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts new file mode 100644 index 00000000..e19c1182 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts @@ -0,0 +1,59 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; + + +export interface IDummyService { + readonly _serviceBrand: undefined; +} + +export const IDummyService = createDecorator('DummyService'); + + + + +registerAction2(class extends Action2 { + constructor() { + super({ + f1: true, + id: 'void.dummy', + title: localize2('dummy', 'dummy: Init'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.Digit0, + weight: KeybindingWeight.VoidExtension, + } + }); + } + async run(accessor: ServicesAccessor): Promise { + console.log('hi') + const n = accessor.get(IDummyService) + console.log('Hi', n._serviceBrand) + } +}) + +// on mount +class DummyService extends Disposable implements IWorkbenchContribution, IDummyService { + static readonly ID = 'workbench.contrib.void.dummy' + _serviceBrand: undefined; + + constructor( + ) { + super() + + } +} + +registerSingleton(IDummyService, DummyService, InstantiationType.Eager); + +registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); From 878a439acd0c8c235fe1a90333b36e205401aaaf Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 30 Mar 2025 20:53:07 -0700 Subject: [PATCH 02/30] rm tool test thread --- .../contrib/void/browser/chatThreadService.ts | 406 +++++++++--------- 1 file changed, 203 insertions(+), 203 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ca2f6369..65c49a3b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -21,7 +21,7 @@ import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; @@ -310,224 +310,224 @@ class ChatThreadService extends Disposable implements IChatThreadService { } const threads = this._convertThreadDataFromStorage(threadsStr); - threads['abc'] = { - id: 'abc', - createdAt: new Date().toISOString(), - lastModified: new Date().toISOString(), - messages: [ - { - role: 'tool', - name: 'pathname_search', - id: 'tool-1', - paramsStr: '{"query": "hello", "pageNumber": 0}', - content: '/users/andrew/void/Desktop/etc/abc.txt', - result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, - } satisfies ToolMessage<'pathname_search'>, - { - role: 'tool', - name: 'pathname_search', - id: 'tool-1', - paramsStr: '{"query": "hello", "pageNumber": 0}', - content: '/users/andrew/void/Desktop/etc/abc.txt', - result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, - } satisfies ToolMessage<'pathname_search'>, + // threads['abc'] = { + // id: 'abc', + // createdAt: new Date().toISOString(), + // lastModified: new Date().toISOString(), + // messages: [ + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-1', + // paramsStr: '{"query": "hello", "pageNumber": 0}', + // content: '/users/andrew/void/Desktop/etc/abc.txt', + // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, + // } satisfies ToolMessage<'pathname_search'>, + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-1', + // paramsStr: '{"query": "hello", "pageNumber": 0}', + // content: '/users/andrew/void/Desktop/etc/abc.txt', + // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, + // } satisfies ToolMessage<'pathname_search'>, - // { - // role: 'tool_request', - // name: 'pathname_search', - // params: { queryStr: 'hello', pageNumber: 0 }, - // paramsStr: '{"query": "hello", "pageNumber": 0}', - // id: 'request-1', - // } satisfies ToolRequestApproval<'pathname_search'>, + // // { + // // role: 'tool_request', + // // name: 'pathname_search', + // // params: { queryStr: 'hello', pageNumber: 0 }, + // // paramsStr: '{"query": "hello", "pageNumber": 0}', + // // id: 'request-1', + // // } satisfies ToolRequestApproval<'pathname_search'>, - { - role: 'tool', - name: 'list_dir', - id: 'tool-2', - paramsStr: '{"uri": "/Users/username/Documents"}', - content: 'Directory listing of /Users/username/Documents', - result: { - type: 'success', - params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, - value: { - children: [ - { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, - { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } - ], - hasNextPage: true, - hasPrevPage: true, - itemsRemaining: 5, - } - }, - } satisfies ToolMessage<'list_dir'>, + // { + // role: 'tool', + // name: 'list_dir', + // id: 'tool-2', + // paramsStr: '{"uri": "/Users/username/Documents"}', + // content: 'Directory listing of /Users/username/Documents', + // result: { + // type: 'success', + // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, + // value: { + // children: [ + // { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, + // { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } + // ], + // hasNextPage: true, + // hasPrevPage: true, + // itemsRemaining: 5, + // } + // }, + // } satisfies ToolMessage<'list_dir'>, - // { - // role: 'tool_request', - // name: 'list_dir', - // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, - // paramsStr: '{"uri": "/Users/username/Documents"}', - // id: 'request-2', - // } satisfies ToolRequestApproval<'list_dir'>, + // // { + // // role: 'tool_request', + // // name: 'list_dir', + // // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, + // // paramsStr: '{"uri": "/Users/username/Documents"}', + // // id: 'request-2', + // // } satisfies ToolRequestApproval<'list_dir'>, - { - role: 'tool', - name: 'read_file', - id: 'tool-3', - paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - content: 'Content of file1.txt\nThis is a sample file.\nHello world!', - result: { - type: 'success', - params: { uri: URI.file('/src/vs/workbench/hi'), pageNumber: 0 }, - value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } - }, - } satisfies ToolMessage<'read_file'>, + // { + // role: 'tool', + // name: 'read_file', + // id: 'tool-3', + // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + // content: 'Content of file1.txt\nThis is a sample file.\nHello world!', + // result: { + // type: 'success', + // params: { uri: URI.file('/src/vs/workbench/hi'), pageNumber: 0 }, + // value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } + // }, + // } satisfies ToolMessage<'read_file'>, - // { - // role: 'tool_request', - // name: 'read_file', - // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, - // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // id: 'request-3', - // } satisfies ToolRequestApproval<'read_file'>, + // // { + // // role: 'tool_request', + // // name: 'read_file', + // // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, + // // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + // // id: 'request-3', + // // } satisfies ToolRequestApproval<'read_file'>, - { - role: 'tool', - name: 'grep_search', - id: 'tool-4', - paramsStr: '{"query": "function main"}', - content: 'Found matches in 3 files', - result: { - type: 'success', - params: { queryStr: 'function main', pageNumber: 0 }, - value: { - uris: [ - URI.file('/Users/username/Project/main.js'), - URI.file('/Users/username/Project/src/app.js'), - URI.file('/Users/username/Project/test/test.js') - ], - hasNextPage: false - } - }, - } satisfies ToolMessage<'grep_search'>, + // { + // role: 'tool', + // name: 'grep_search', + // id: 'tool-4', + // paramsStr: '{"query": "function main"}', + // content: 'Found matches in 3 files', + // result: { + // type: 'success', + // params: { queryStr: 'function main', pageNumber: 0 }, + // value: { + // uris: [ + // URI.file('/Users/username/Project/main.js'), + // URI.file('/Users/username/Project/src/app.js'), + // URI.file('/Users/username/Project/test/test.js') + // ], + // hasNextPage: false + // } + // }, + // } satisfies ToolMessage<'grep_search'>, - // { - // role: 'tool_request', - // name: 'grep_search', - // params: { queryStr: 'function main', pageNumber: 0 }, - // paramsStr: '{"query": "function main"}', - // id: 'request-4', - // } satisfies ToolRequestApproval<'grep_search'>, + // // { + // // role: 'tool_request', + // // name: 'grep_search', + // // params: { queryStr: 'function main', pageNumber: 0 }, + // // paramsStr: '{"query": "function main"}', + // // id: 'request-4', + // // } satisfies ToolRequestApproval<'grep_search'>, - // --- + // // --- - { - role: 'tool', - name: 'edit', - id: 'tool-5', - paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', - content: 'Successfully edited the file at /Users/username/Project/main.js', - result: { - type: 'success', - params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - value: Promise.resolve() - }, - } satisfies ToolMessage<'edit'>, - { - role: 'tool_request', - name: 'edit', - params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "I think we should do this:```Add console.log statement\n for i in ...\n\t\tdo:\nabc```"}', - id: 'request-5', - } satisfies ToolRequestApproval<'edit'>, + // { + // role: 'tool', + // name: 'edit', + // id: 'tool-5', + // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', + // content: 'Successfully edited the file at /Users/username/Project/main.js', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, + // value: Promise.resolve() + // }, + // } satisfies ToolMessage<'edit'>, + // { + // role: 'tool_request', + // name: 'edit', + // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, + // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "I think we should do this:```Add console.log statement\n for i in ...\n\t\tdo:\nabc```"}', + // id: 'request-5', + // } satisfies ToolRequestApproval<'edit'>, - { - role: 'tool', - name: 'create_uri', - id: 'tool-6', - paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - content: 'Successfully created file at /Users/username/Project/new-file/', - result: { - type: 'success', - params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi/'), isFolder: true }, - value: {} - }, - } satisfies ToolMessage<'create_uri'>, - { - role: 'tool_request', - name: 'create_uri', - params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, - paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - id: 'request-6', - } satisfies ToolRequestApproval<'create_uri'>, + // { + // role: 'tool', + // name: 'create_uri', + // id: 'tool-6', + // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + // content: 'Successfully created file at /Users/username/Project/new-file/', + // result: { + // type: 'success', + // params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi/'), isFolder: true }, + // value: {} + // }, + // } satisfies ToolMessage<'create_uri'>, + // { + // role: 'tool_request', + // name: 'create_uri', + // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, + // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + // id: 'request-6', + // } satisfies ToolRequestApproval<'create_uri'>, - { - role: 'tool', - name: 'delete_uri', - id: 'tool-7', - paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - content: 'Successfully deleted file at /Users/username/Project/old-file.js', - result: { - type: 'success', - params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - value: {} - }, - } satisfies ToolMessage<'delete_uri'>, - { - role: 'tool_request', - name: 'delete_uri', - params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - id: 'request-7', - } satisfies ToolRequestApproval<'delete_uri'>, + // { + // role: 'tool', + // name: 'delete_uri', + // id: 'tool-7', + // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + // content: 'Successfully deleted file at /Users/username/Project/old-file.js', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + // value: {} + // }, + // } satisfies ToolMessage<'delete_uri'>, + // { + // role: 'tool_request', + // name: 'delete_uri', + // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + // id: 'request-7', + // } satisfies ToolRequestApproval<'delete_uri'>, - { - role: 'tool', - name: 'terminal_command', - id: 'tool-8', - paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - content: 'Command executed: npm install\nAdded 123 packages in 3.5s', - result: { - type: 'success', - params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - value: { - terminalId: '1', - didCreateTerminal: false, - result: 'Added 123 packages in 3.5s', - resolveReason: { type: 'done', exitCode: 0 } - } - }, - } satisfies ToolMessage<'terminal_command'>, - { - role: 'tool_request', - name: 'terminal_command', - params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - id: 'request-8', - } satisfies ToolRequestApproval<'terminal_command'>, + // { + // role: 'tool', + // name: 'terminal_command', + // id: 'tool-8', + // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + // content: 'Command executed: npm install\nAdded 123 packages in 3.5s', + // result: { + // type: 'success', + // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + // value: { + // terminalId: '1', + // didCreateTerminal: false, + // result: 'Added 123 packages in 3.5s', + // resolveReason: { type: 'done', exitCode: 0 } + // } + // }, + // } satisfies ToolMessage<'terminal_command'>, + // { + // role: 'tool_request', + // name: 'terminal_command', + // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + // id: 'request-8', + // } satisfies ToolRequestApproval<'terminal_command'>, - // Examples of error and rejected states - { - role: 'tool', - name: 'pathname_search', - id: 'tool-error', - paramsStr: '{"query": "invalid**query"}', - content: 'Error: Invalid search pattern', - result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, - } satisfies ToolMessage<'pathname_search'>, + // // Examples of error and rejected states + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-error', + // paramsStr: '{"query": "invalid**query"}', + // content: 'Error: Invalid search pattern', + // result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, + // } satisfies ToolMessage<'pathname_search'>, - { - role: 'tool', - name: 'pathname_search', - id: 'tool-rejected', - paramsStr: '{"query": "sensitive-data"}', - content: 'Tool call was rejected by the user.', - result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, - } satisfies ToolMessage<'pathname_search'>, - ], - state: defaultThreadState, - } + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-rejected', + // paramsStr: '{"query": "sensitive-data"}', + // content: 'Tool call was rejected by the user.', + // result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, + // } satisfies ToolMessage<'pathname_search'>, + // ], + // state: defaultThreadState, + // } return threads } From 7c0ba713143884855dca23a91439ed0a594666d1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 30 Mar 2025 21:30:01 -0700 Subject: [PATCH 03/30] scaffolding for checkpoints --- .../contrib/void/browser/chatThreadService.ts | 31 ++++++++++++++----- .../void/common/chatThreadServiceTypes.ts | 15 +++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 65c49a3b..dddd7d18 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -76,18 +76,19 @@ type ThreadType = { createdAt: string; // ISO string lastModified: string; // ISO string messages: ChatMessage[]; + currentHistoryIdx: number | null; // index in messages, ALWAYS points to a LLMHistoryEntry or UserHistoryEntry, or -1 if no changes. current code is inclusive of the current index's change + + // this doesn't need to go in a state object, but feels right state: { stagingSelections: StagingSelectionItem[]; - focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none) + focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) linksOfMessageIdx: { // eg. link = linksOfMessageIdx[4]['RangeFunction'] [messageIdx: number]: { [codespanName: string]: CodespanLocationLink } } - - isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO - } + }; } type ChatThreads = { @@ -97,7 +98,6 @@ type ChatThreads = { export const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, - isCheckedOfSelectionId: {}, linksOfMessageIdx: {}, } @@ -130,7 +130,8 @@ const newThreadObject = () => { lastModified: now, messages: [], state: defaultThreadState, - } satisfies ChatThreads[string] + currentHistoryIdx: null, + } satisfies ThreadType } @@ -943,6 +944,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + async callWhenJumpBackToIdx(toIdx: number) { + // TODO!!! + } @@ -1251,7 +1255,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add the current file as a staging selection const model = this._codeEditorService.getActiveCodeEditor()?.getModel() if (model) { - this._setCurrentThreadState({ ...defaultThreadState, stagingSelections: [{ type: 'File', fileURI: model.uri, language: model.getLanguageId(), selectionStr: null, range: null, state: { isOpened: false, wasAddedAsCurrentFile: true } }] }) + this._setCurrentThreadState({ + ...defaultThreadState, + stagingSelections: [{ + type: 'File', + fileURI: model.uri, + language: model.getLanguageId(), + selectionStr: null, + range: null, + state: { + isOpened: false, + wasAddedAsCurrentFile: true + } + }] + }) } return; } diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index c4893d6c..7dfe39e5 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -24,6 +24,18 @@ export type ToolRequestApproval = { id: string; // proposed tool's id } + +// checkpoints +export type LLMHistoryEntry = { // ALWAYS comes right after a {role:'tool', name:'edit'} message + role: 'LLM_changes'; + afterStrOfURI: { [fsPath: string]: string }; +} +export type UserHistoryEntry = { // ALWAYS comes right before a {role:'user'} message, or if it's the last message (w/o a user message yet) + role: 'user_changes'; + afterStrOfURI: { [fsPath: string]: string }; +} + + // 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 = | { @@ -44,6 +56,8 @@ export type ChatMessage = } | ToolMessage | ToolRequestApproval + | LLMHistoryEntry // invisible + | UserHistoryEntry // invisible // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) @@ -75,6 +89,7 @@ export type StagingSelectionItem = CodeSelection | FileSelection +// a link to a symbol (an underlined link to a piece of code) export type CodespanLocationLink = { uri: URI, // we handle serialization for this displayText: string, From 7decc8e14652455390db4cfb2d39e951825e17ea Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 31 Mar 2025 20:35:40 -0700 Subject: [PATCH 04/30] add checkpoints --- .../contrib/void/browser/chatThreadService.ts | 540 ++++++++++-------- .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- .../void/common/chatThreadServiceTypes.ts | 17 +- .../contrib/void/common/voidModelService.ts | 10 +- 4 files changed, 311 insertions(+), 260 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index dddd7d18..5553b32d 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -21,7 +21,7 @@ import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; +import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; @@ -29,16 +29,35 @@ import { shorten } from '../../../../base/common/labels.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { findLastIdx } from '../../../../base/common/arraysFind.js'; -const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { - for (let i = arr.length - 1; i >= 0; i--) { - if (condition(arr[i])) { - return i; - } - } - return -1; -} +type LLMCheckpoint = CheckpointEntry & { type: 'after_tool_edits' } +type UserCheckpoint = CheckpointEntry & { type: 'after_user_edits' } +/* +Checkpoints: +pivots: user | tool (edit) +if there are repeated pivots, a checkpoint goes directly after the last one +checkpoint_modifications always go directly after a checkpoint + +user +-- checkpoint -------- +assistant +tool (edit) + -------- checkpoint - starts here <-- know exact change (file A after) +assistant | +tool (edit) v +-- checkpoint -------- +assistant +tool (not edit) +assistant +user +-- checkpoint -------- user checkpoint (JIT) - compute change from all files to here when need to +-- checkpoint_modifications --------- - these always come DIRECLY after a checkpoint, and reflect the user's modifications on this one checkpoint only. + (only counts when reverting to/from this exact checkpoint, not past it). + Added when user jumps to another checkpoint but made changes here. + +*/ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { @@ -75,11 +94,15 @@ type ThreadType = { id: string; // store the id here too createdAt: string; // ISO string lastModified: string; // ISO string + messages: ChatMessage[]; - currentHistoryIdx: number | null; // index in messages, ALWAYS points to a LLMHistoryEntry or UserHistoryEntry, or -1 if no changes. current code is inclusive of the current index's change + firstStrOfURI: { [fsPath: string]: string | undefined }; // part of checkpointing + // this doesn't need to go in a state object, but feels right state: { + latestCheckpointIdx: number | null; // the latest checkpoint we're standing at or null + stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) @@ -96,6 +119,7 @@ type ChatThreads = { } export const defaultThreadState: ThreadType['state'] = { + latestCheckpointIdx: null, stagingSelections: [], focusedMessageIdx: undefined, linksOfMessageIdx: {}, @@ -130,7 +154,7 @@ const newThreadObject = () => { lastModified: now, messages: [], state: defaultThreadState, - currentHistoryIdx: null, + firstStrOfURI: {}, } satisfies ThreadType } @@ -141,6 +165,8 @@ const newThreadObject = () => { export const THREAD_STORAGE_KEY = 'void.chatThreadStorageI' + + export interface IChatThreadService { readonly _serviceBrand: undefined; @@ -184,8 +210,8 @@ export interface IChatThreadService { addUserMessageAndStreamResponse({ userMessage, threadId }: { userMessage: string, threadId: string }): Promise; // approve/reject - approveTool(threadId: string): void; - rejectTool(threadId: string): void; + approveLatestToolRequest(threadId: string): void; + rejectLatestToolRequest(threadId: string): void; } export const IChatThreadService = createDecorator('voidChatThreadService'); @@ -311,225 +337,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } const threads = this._convertThreadDataFromStorage(threadsStr); - // threads['abc'] = { - // id: 'abc', - // createdAt: new Date().toISOString(), - // lastModified: new Date().toISOString(), - // messages: [ - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-1', - // paramsStr: '{"query": "hello", "pageNumber": 0}', - // content: '/users/andrew/void/Desktop/etc/abc.txt', - // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, - // } satisfies ToolMessage<'pathname_search'>, - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-1', - // paramsStr: '{"query": "hello", "pageNumber": 0}', - // content: '/users/andrew/void/Desktop/etc/abc.txt', - // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, - // } satisfies ToolMessage<'pathname_search'>, - - // // { - // // role: 'tool_request', - // // name: 'pathname_search', - // // params: { queryStr: 'hello', pageNumber: 0 }, - // // paramsStr: '{"query": "hello", "pageNumber": 0}', - // // id: 'request-1', - // // } satisfies ToolRequestApproval<'pathname_search'>, - - // { - // role: 'tool', - // name: 'list_dir', - // id: 'tool-2', - // paramsStr: '{"uri": "/Users/username/Documents"}', - // content: 'Directory listing of /Users/username/Documents', - // result: { - // type: 'success', - // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, - // value: { - // children: [ - // { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, - // { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } - // ], - // hasNextPage: true, - // hasPrevPage: true, - // itemsRemaining: 5, - // } - // }, - // } satisfies ToolMessage<'list_dir'>, - - // // { - // // role: 'tool_request', - // // name: 'list_dir', - // // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, - // // paramsStr: '{"uri": "/Users/username/Documents"}', - // // id: 'request-2', - // // } satisfies ToolRequestApproval<'list_dir'>, - - // { - // role: 'tool', - // name: 'read_file', - // id: 'tool-3', - // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // content: 'Content of file1.txt\nThis is a sample file.\nHello world!', - // result: { - // type: 'success', - // params: { uri: URI.file('/src/vs/workbench/hi'), pageNumber: 0 }, - // value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } - // }, - // } satisfies ToolMessage<'read_file'>, - - // // { - // // role: 'tool_request', - // // name: 'read_file', - // // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, - // // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // // id: 'request-3', - // // } satisfies ToolRequestApproval<'read_file'>, - - // { - // role: 'tool', - // name: 'grep_search', - // id: 'tool-4', - // paramsStr: '{"query": "function main"}', - // content: 'Found matches in 3 files', - // result: { - // type: 'success', - // params: { queryStr: 'function main', pageNumber: 0 }, - // value: { - // uris: [ - // URI.file('/Users/username/Project/main.js'), - // URI.file('/Users/username/Project/src/app.js'), - // URI.file('/Users/username/Project/test/test.js') - // ], - // hasNextPage: false - // } - // }, - // } satisfies ToolMessage<'grep_search'>, - - // // { - // // role: 'tool_request', - // // name: 'grep_search', - // // params: { queryStr: 'function main', pageNumber: 0 }, - // // paramsStr: '{"query": "function main"}', - // // id: 'request-4', - // // } satisfies ToolRequestApproval<'grep_search'>, - - // // --- - - // { - // role: 'tool', - // name: 'edit', - // id: 'tool-5', - // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', - // content: 'Successfully edited the file at /Users/username/Project/main.js', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - // value: Promise.resolve() - // }, - // } satisfies ToolMessage<'edit'>, - // { - // role: 'tool_request', - // name: 'edit', - // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "I think we should do this:```Add console.log statement\n for i in ...\n\t\tdo:\nabc```"}', - // id: 'request-5', - // } satisfies ToolRequestApproval<'edit'>, - - // { - // role: 'tool', - // name: 'create_uri', - // id: 'tool-6', - // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - // content: 'Successfully created file at /Users/username/Project/new-file/', - // result: { - // type: 'success', - // params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi/'), isFolder: true }, - // value: {} - // }, - // } satisfies ToolMessage<'create_uri'>, - // { - // role: 'tool_request', - // name: 'create_uri', - // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, - // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - // id: 'request-6', - // } satisfies ToolRequestApproval<'create_uri'>, - - // { - // role: 'tool', - // name: 'delete_uri', - // id: 'tool-7', - // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - // content: 'Successfully deleted file at /Users/username/Project/old-file.js', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - // value: {} - // }, - // } satisfies ToolMessage<'delete_uri'>, - // { - // role: 'tool_request', - // name: 'delete_uri', - // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - // id: 'request-7', - // } satisfies ToolRequestApproval<'delete_uri'>, - - // { - // role: 'tool', - // name: 'terminal_command', - // id: 'tool-8', - // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - // content: 'Command executed: npm install\nAdded 123 packages in 3.5s', - // result: { - // type: 'success', - // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - // value: { - // terminalId: '1', - // didCreateTerminal: false, - // result: 'Added 123 packages in 3.5s', - // resolveReason: { type: 'done', exitCode: 0 } - // } - // }, - // } satisfies ToolMessage<'terminal_command'>, - // { - // role: 'tool_request', - // name: 'terminal_command', - // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - // id: 'request-8', - // } satisfies ToolRequestApproval<'terminal_command'>, - - - - // // Examples of error and rejected states - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-error', - // paramsStr: '{"query": "invalid**query"}', - // content: 'Error: Invalid search pattern', - // result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, - // } satisfies ToolMessage<'pathname_search'>, - - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-rejected', - // paramsStr: '{"query": "sensitive-data"}', - // content: 'Tool call was rejected by the user.', - // result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, - // } satisfies ToolMessage<'pathname_search'>, - // ], - // state: defaultThreadState, - // } - return threads } @@ -631,7 +438,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - approveTool(threadId: string) { + approveLatestToolRequest(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -639,7 +446,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role !== 'tool_request') return // should never happen - const lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user') + const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user') const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' } if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen @@ -651,7 +458,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) } - rejectTool(threadId: string) { + rejectLatestToolRequest(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -670,7 +477,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const isRunning = this.streamState[threadId]?.isRunning // reject the tool for the user if (isRunning === 'awaiting_user') { - this.rejectTool(threadId) + this.rejectLatestToolRequest(threadId) } // interrupt the tool else if (isRunning === 'tool') { @@ -741,7 +548,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] const latestMessages = thread?.messages ?? [] const messages_ = toLLMChatMessages(latestMessages) - const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') + const lastUserMsgIdx = findLastIdx(messages_, m => m.role === 'user') if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!) // system message @@ -944,11 +751,226 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - async callWhenJumpBackToIdx(toIdx: number) { - // TODO!!! + // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint + // call this right after LLM edits a file + addOrUpdateToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + const { model } = this._voidModelService.getModel(uri) + if (!model) return // should never happen + + const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') + const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') + + const afterStr = model.getValue() // afterStr = the value of the file right after the edit + + let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined + if (prevLLMCheckpointIdx !== -1) { + prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } + this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) + } + const newLLMCheckpoint: LLMCheckpoint = { + role: 'checkpoint', + type: 'after_tool_edits', + afterStrOfURI: { + ...prevLLMCheckpoint?.afterStrOfURI, + [uri.fsPath]: afterStr, + }, + } + console.log('NEW LLM CHECKPOINT', newLLMCheckpoint, JSON.stringify(this.state.allThreads[threadId], null, 2)) + this._addMessageToThread(threadId, newLLMCheckpoint) } + // user checkpoints are always computed JIT + // we assume there are no messages after the checkpoint we're adding here + // call this right before user sends message + addOrUpdateUserMessageCheckpoint({ threadId, }: { threadId: string, }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + + const newUserCheckpoint: UserCheckpoint = { + role: 'checkpoint', + type: 'after_user_edits', // user backup + afterStrOfURI: {}, + } + + // first get the last user checkpoint + const lastNonUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type !== 'after_user_edits') + + // merge all recent user checkpoints + const latestAfterStrOfURI: { [fsPath: string]: string } = {} // helps merge user edits + for (let k = 0; k <= thread.messages.length; k += 1) { + const message = thread.messages[k] + if (message.role !== 'checkpoint') continue + for (const uri in message.afterStrOfURI) + latestAfterStrOfURI[uri] = message.afterStrOfURI[uri] + + // remove any user messages that come after the last LLM checkpoint (we're merging them into one big user message) + if (k > lastNonUserCheckpointIdx) + this._removeMessageFromThread(threadId, k) + } + + // compute afterStr of all files we detected, and if they're different, add them as a user edit + for (const fsPath in latestAfterStrOfURI) { + const uri = URI.file(fsPath) + const { model } = this._voidModelService.getModel(uri) + if (!model) continue + const oldAfterStr = latestAfterStrOfURI[uri.fsPath] + const currentAfterStr = model.getValue() + if (oldAfterStr === currentAfterStr) continue + // if there was a change, add it as a user edit + newUserCheckpoint.afterStrOfURI = { + ...newUserCheckpoint.afterStrOfURI, + [uri.fsPath]: currentAfterStr + } + } + + + this._addMessageToThread(threadId, newUserCheckpoint) + + // update latest checkpoint idx to the one we just added + const newThread = this.state.allThreads[threadId] + if (!newThread) return // should never happen + const latestCheckpointIdx = newThread.messages.length - 1 + this._setThreadState(threadId, { latestCheckpointIdx }) + + + console.log('NEW USER CHECKPOINT', latestCheckpointIdx, newUserCheckpoint, JSON.stringify(this.state.allThreads[threadId], null, 2)) + } + + + private _getCheckpointAfter = ({ threadId, messageIdx: afterIdx }: { threadId: string, messageIdx: number }): [CheckpointEntry, number] | undefined => { + const thread = this.state.allThreads[threadId] + if (!thread) return undefined + for (let i = afterIdx; i < thread.messages.length; i++) { + const message = thread.messages[i] + if (message.role === 'checkpoint') { + return [message, i] + } + } + return undefined + } + + private _getAllChangedCheckpointURIs({ threadId, fromIdx, toIdx }: { threadId: string, fromIdx: number, toIdx: number }) { + const thread = this.state.allThreads[threadId] + if (!thread) return null // should never happen + const fsPaths: Set = new Set() + for (let i = fromIdx; i <= toIdx; i += 1) { + const message = thread.messages[i] + if (message.role !== 'checkpoint') continue + for (const fsPath in message.afterStrOfURI) { + fsPaths.add(fsPath) + } + } + return fsPaths + } + + jumpToCheckpointAfterMessageIdx({ threadId, messageIdx }: { threadId: string, messageIdx: number }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + + const c = this._getCheckpointAfter({ threadId, messageIdx }) + if (c === undefined) return // should never happen + + const fromIdx = thread.state.latestCheckpointIdx + if (fromIdx === null) return // should never happen + + // TODO!!! change toIdx if there's a checkpointModification on the To, and add a checkpoint modification on the from + const [_, toIdx_] = c + const toIdx = toIdx_ + if (toIdx === fromIdx) return + + const writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { + const { model } = this._voidModelService.getModelFromFsPath(fsPath) + if (!model) return // should never happen + model.applyEdits([{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file + text + }]) + } + + /* +if undoing + +A,B,C are all files. +x means a checkpoint where the file changed. + +A B C D E F G H I +x x x x x x x x x +| | | | | | | | | +x | | | | | | | x +---x-|-|-|-x-|-x-|----- <-- to + x | | | | | x + | | x x | + | | | | +-------x-|---x-x------- <-- from + x + +We need to revert anything that happened between to+1 and from. +**We do this by finding the last x from 0...`to` for each file and applying those contents.** +We only need to do it for files that were edited since `to`, ie files between to+1...from. +*/ + if (toIdx < fromIdx) { + const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, toIdx: toIdx + 1, fromIdx }) + for (const fsPath of checkpointURIs ?? []) { + let found = false + + // apply lowest down content for each uri (or original if not found) + + for (let k = toIdx; k >= 0; k -= 1) { + const message = thread.messages[k] + if (message.role !== 'checkpoint') continue + if (fsPath in message.afterStrOfURI) { + found = true + writeFullFile({ fsPath, text: message.afterStrOfURI[fsPath] }) + break + } + } + if (!found) { + const originalStr = thread.firstStrOfURI[fsPath] + if (originalStr === undefined) continue + writeFullFile({ fsPath, text: originalStr }) + } + } + } + + /* +if redoing + +A B C D E F G H I +x x x x x x x x x +| | | | | | | | | +x | | | | | | | x +---x-|-|-|-x-|-x-|----- <-- from + x | | | | | x + | | x x | + | | | | +-------x-|---x-x------- <-- to + x + +We need to apply latest change for anything that happened between from+1 and to. +We only need to do it for files that were edited since `from`, ie files between from+1...to. +*/ + if (toIdx > fromIdx) { + const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, fromIdx: fromIdx + 1, toIdx }) + for (const fsPath of checkpointURIs ?? []) { + // apply lowest down content for each uri + // (do not need to apply original since we're only applying to files that changed) + for (let k = toIdx; k >= fromIdx + 1; k -= 1) { + const message = thread.messages[k] + if (message.role !== 'checkpoint') continue + if (fsPath in message.afterStrOfURI) { + writeFullFile({ fsPath, text: message.afterStrOfURI[fsPath] }) + break + } + } + } + } + + this._setThreadState(threadId, { latestCheckpointIdx: toIdx }) + // TODO!!! add/merge a checkpoint modification if relevant + } async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[], }, threadId: string }) { @@ -970,6 +992,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) + this.addOrUpdateUserMessageCheckpoint({ threadId }) this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) } @@ -1255,7 +1278,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add the current file as a staging selection const model = this._codeEditorService.getActiveCodeEditor()?.getModel() if (model) { - this._setCurrentThreadState({ + this._setThreadState(this.state.currentThreadId, { ...defaultThreadState, stagingSelections: [{ type: 'File', @@ -1286,25 +1309,48 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - _addMessageToThread(threadId: string, message: ChatMessage) { + private _addMessageToThread(threadId: string, message: ChatMessage) { const { allThreads } = this.state - const oldThread = allThreads[threadId] if (!oldThread) return // should never happen - // update state and store it const newThreads = { ...allThreads, [oldThread.id]: { ...oldThread, lastModified: new Date().toISOString(), - messages: [...oldThread.messages, message], + messages: [ + ...oldThread.messages, + message + ], } } this._storeAllThreads(newThreads) this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) } + + private _removeMessageFromThread(threadId: string, messageIdx: number) { + const { allThreads } = this.state + const oldThread = allThreads[threadId] + if (!oldThread) return // should never happen + // update state and store it + const newThreads = { + ...allThreads, + [oldThread.id]: { + ...oldThread, + lastModified: new Date().toISOString(), + messages: [ + ...oldThread.messages.slice(0, messageIdx), + ...oldThread.messages.slice(messageIdx + 1, Infinity), + ], + } + } + this._storeAllThreads(newThreads) + this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) + } + + // sets the currently selected message (must be undefined if no message is selected) setCurrentlyFocusedMessageIdx(messageIdx: number | undefined) { @@ -1354,9 +1400,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // set thread.state - private _setCurrentThreadState(state: Partial): void { - - const threadId = this.state.currentThreadId + private _setThreadState(threadId: string, state: Partial): void { const thread = this.state.allThreads[threadId] if (!thread) return @@ -1409,7 +1453,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { return currentThread.state } setCurrentThreadState = (newState: Partial) => { - this._setCurrentThreadState(newState) + this._setThreadState(this.state.currentThreadId, newState) } // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) 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 00b26d15..8eb3eebb 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 @@ -1248,7 +1248,7 @@ const ToolRequestAcceptRejectButtons = () => { const onAccept = useCallback(() => { try { // this doesn't need to be wrapped in try/catch anymore const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.approveTool(threadId) + chatThreadsService.approveLatestToolRequest(threadId) metricsService.capture('Tool Request Accepted', {}) } catch (e) { console.error('Error while approving message in chat:', e) } }, [chatThreadsService, metricsService]) @@ -1256,7 +1256,7 @@ const ToolRequestAcceptRejectButtons = () => { const onReject = useCallback(() => { try { const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.rejectTool(threadId) + chatThreadsService.rejectLatestToolRequest(threadId) } catch (e) { console.error('Error while approving message in chat:', e) } metricsService.capture('Tool Request Rejected', {}) }, [chatThreadsService, metricsService]) diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 7dfe39e5..b4af2b89 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -26,16 +26,18 @@ export type ToolRequestApproval = { // checkpoints -export type LLMHistoryEntry = { // ALWAYS comes right after a {role:'tool', name:'edit'} message - role: 'LLM_changes'; - afterStrOfURI: { [fsPath: string]: string }; -} -export type UserHistoryEntry = { // ALWAYS comes right before a {role:'user'} message, or if it's the last message (w/o a user message yet) - role: 'user_changes'; +export type CheckpointEntry = { + role: 'checkpoint'; + type: 'after_user_edits' | 'after_tool_edits'; + afterStrOfURI: { [fsPath: string]: string }; +} | { // modifications that only count when undoing/redoing + role: 'checkpoint_modification'; + type: 'user_modifications'; afterStrOfURI: { [fsPath: string]: string }; } + // 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 = | { @@ -56,8 +58,7 @@ export type ChatMessage = } | ToolMessage | ToolRequestApproval - | LLMHistoryEntry // invisible - | UserHistoryEntry // invisible + | CheckpointEntry // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) diff --git a/src/vs/workbench/contrib/void/common/voidModelService.ts b/src/vs/workbench/contrib/void/common/voidModelService.ts index 8cbf4ac9..a463b50a 100644 --- a/src/vs/workbench/contrib/void/common/voidModelService.ts +++ b/src/vs/workbench/contrib/void/common/voidModelService.ts @@ -14,6 +14,7 @@ export interface IVoidModelService { readonly _serviceBrand: undefined; initializeModel(uri: URI): Promise; getModel(uri: URI): VoidModelType; + getModelFromFsPath(fsPath: string): VoidModelType; getModelSafe(uri: URI): Promise; } @@ -37,8 +38,8 @@ class VoidModelService extends Disposable implements IVoidModelService { this._modelRefOfURI[uri.fsPath] = editorModelRef; }; - getModel = (uri: URI): VoidModelType => { - const editorModelRef = this._modelRefOfURI[uri.fsPath]; + getModelFromFsPath = (fsPath: string): VoidModelType => { + const editorModelRef = this._modelRefOfURI[fsPath]; if (!editorModelRef) { return { model: null, editorModel: null }; } @@ -52,6 +53,11 @@ class VoidModelService extends Disposable implements IVoidModelService { return { model, editorModel: editorModelRef.object }; }; + getModel = (uri: URI) => { + return this.getModelFromFsPath(uri.fsPath) + } + + getModelSafe = async (uri: URI): Promise => { if (!(uri.fsPath in this._modelRefOfURI)) await this.initializeModel(uri); return this.getModel(uri); From a94ce5d4748df0d1a0020c7ab383e7d3f6cdc38f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 31 Mar 2025 22:56:47 -0700 Subject: [PATCH 05/30] checkpoints work! need to add user_temp_modifications now and deal with diffzones --- .../contrib/void/browser/chatThreadService.ts | 82 ++++++++++++------- .../contrib/void/browser/editCodeService.ts | 21 ++--- .../react/src/sidebar-tsx/SidebarChat.tsx | 19 +++++ .../contrib/void/browser/toolsService.ts | 1 - 4 files changed, 82 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 5553b32d..8df93ec6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -70,8 +70,11 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning }) else if (c.role === 'tool') llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content }) - else if (c.role === 'tool_request') { - // pass + else if (c.role === 'tool_request') { // pass + } + else if (c.role === 'checkpoint') { // pass + } + else if (c.role === 'checkpoint_modification') { // pass } else { throw new Error(`Role ${(c as any).role} not recognized.`) @@ -212,6 +215,9 @@ export interface IChatThreadService { // approve/reject approveLatestToolRequest(threadId: string): void; rejectLatestToolRequest(threadId: string): void; + + // jump to history + jumpToCheckpointAfterMessageIdx(opts: { threadId: string, messageIdx: number }): void; } export const IChatThreadService = createDecorator('voidChatThreadService'); @@ -613,6 +619,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') let interrupted = false try { + // add the original file if it wasn't seen before in this thread + if (toolName === 'edit') { this._trackOriginalFileInURI({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) this._currentlyRunningToolInterruptor[threadId] = () => { interrupted = true; @@ -620,6 +629,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { delete this._currentlyRunningToolInterruptor[threadId]; } toolResult = await result // ts is bad... await is needed + + if (toolName === 'edit') { this._addOrUpdateToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } } catch (error) { if (interrupted) { @@ -751,9 +762,28 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + private _trackOriginalFileInURI({ threadId, uri }: { threadId: string, uri: URI }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + const { model } = this._voidModelService.getModel(uri) + if (!model) return + if (!(uri.fsPath in thread.firstStrOfURI)) { + thread.firstStrOfURI[uri.fsPath] = model.getValue() + } + } + + private _addCheckpoint(threadId: string, checkpoint: CheckpointEntry) { + this._addMessageToThread(threadId, checkpoint) + // update latest checkpoint idx to the one we just added + const newThread = this.state.allThreads[threadId] + if (!newThread) return // should never happen + const latestCheckpointIdx = newThread.messages.length - 1 + this._setThreadState(threadId, { latestCheckpointIdx }) + } + // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint // call this right after LLM edits a file - addOrUpdateToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { + private _addOrUpdateToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { const thread = this.state.allThreads[threadId] if (!thread) return const { model } = this._voidModelService.getModel(uri) @@ -777,15 +807,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { [uri.fsPath]: afterStr, }, } - console.log('NEW LLM CHECKPOINT', newLLMCheckpoint, JSON.stringify(this.state.allThreads[threadId], null, 2)) - this._addMessageToThread(threadId, newLLMCheckpoint) + this._addCheckpoint(threadId, newLLMCheckpoint) + } // user checkpoints are always computed JIT // we assume there are no messages after the checkpoint we're adding here // call this right before user sends message - addOrUpdateUserMessageCheckpoint({ threadId, }: { threadId: string, }) { + private _addOrUpdateUserMessageCheckpoint({ threadId, }: { threadId: string, }) { const thread = this.state.allThreads[threadId] if (!thread) return @@ -798,9 +828,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // first get the last user checkpoint const lastNonUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type !== 'after_user_edits') - // merge all recent user checkpoints + // merge all recent user checkpoints and delete them const latestAfterStrOfURI: { [fsPath: string]: string } = {} // helps merge user edits - for (let k = 0; k <= thread.messages.length; k += 1) { + for (let k = 0; k < thread.messages.length; k += 1) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue for (const uri in message.afterStrOfURI) @@ -811,32 +841,23 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._removeMessageFromThread(threadId, k) } - // compute afterStr of all files we detected, and if they're different, add them as a user edit - for (const fsPath in latestAfterStrOfURI) { - const uri = URI.file(fsPath) - const { model } = this._voidModelService.getModel(uri) + + // add a change for all the files where we detect a user change + const allURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: 0, hiIdx: thread.messages.length - 1, }) + for (const fsPath of allURIs ?? []) { + const { model } = this._voidModelService.getModelFromFsPath(fsPath) if (!model) continue - const oldAfterStr = latestAfterStrOfURI[uri.fsPath] + const oldAfterStr = latestAfterStrOfURI[fsPath] const currentAfterStr = model.getValue() if (oldAfterStr === currentAfterStr) continue // if there was a change, add it as a user edit newUserCheckpoint.afterStrOfURI = { ...newUserCheckpoint.afterStrOfURI, - [uri.fsPath]: currentAfterStr + [fsPath]: currentAfterStr } } - - this._addMessageToThread(threadId, newUserCheckpoint) - - // update latest checkpoint idx to the one we just added - const newThread = this.state.allThreads[threadId] - if (!newThread) return // should never happen - const latestCheckpointIdx = newThread.messages.length - 1 - this._setThreadState(threadId, { latestCheckpointIdx }) - - - console.log('NEW USER CHECKPOINT', latestCheckpointIdx, newUserCheckpoint, JSON.stringify(this.state.allThreads[threadId], null, 2)) + this._addCheckpoint(threadId, newUserCheckpoint) } @@ -852,11 +873,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { return undefined } - private _getAllChangedCheckpointURIs({ threadId, fromIdx, toIdx }: { threadId: string, fromIdx: number, toIdx: number }) { + private _getAllChangedCheckpointURIs({ threadId, loIdx, hiIdx }: { threadId: string, loIdx: number, hiIdx: number }) { const thread = this.state.allThreads[threadId] if (!thread) return null // should never happen const fsPaths: Set = new Set() - for (let i = fromIdx; i <= toIdx; i += 1) { + for (let i = loIdx; i <= hiIdx; i += 1) { const message = thread.messages[i] if (message.role !== 'checkpoint') continue for (const fsPath in message.afterStrOfURI) { @@ -889,6 +910,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { text }]) } + console.log(`going from ${fromIdx} to ${toIdx}`) /* if undoing @@ -912,7 +934,7 @@ We need to revert anything that happened between to+1 and from. We only need to do it for files that were edited since `to`, ie files between to+1...from. */ if (toIdx < fromIdx) { - const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, toIdx: toIdx + 1, fromIdx }) + const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx }) for (const fsPath of checkpointURIs ?? []) { let found = false @@ -953,7 +975,7 @@ We need to apply latest change for anything that happened between from+1 and to. We only need to do it for files that were edited since `from`, ie files between from+1...to. */ if (toIdx > fromIdx) { - const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, fromIdx: fromIdx + 1, toIdx }) + const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: fromIdx + 1, hiIdx: toIdx }) for (const fsPath of checkpointURIs ?? []) { // apply lowest down content for each uri // (do not need to apply original since we're only applying to files that changed) @@ -991,8 +1013,8 @@ We only need to do it for files that were edited since `from`, ie files between const userMessageContent = await chat_userMessageContent(instructions, currSelns) // user message + names of files (NOT content) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) + this._addOrUpdateUserMessageCheckpoint({ threadId }) - this.addOrUpdateUserMessageCheckpoint({ threadId }) this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 098b073e..a701abec 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1757,14 +1757,15 @@ class EditCodeService extends Disposable implements IEditCodeService { return } - console.log('---------adding-------') - console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - console.log('block', deepClone(block)) - console.log('origBounds', originalBounds) const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) - console.log('start end', startLine, endLine) + + // console.log('---------adding-------') + // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + // console.log('block', deepClone(block)) + // console.log('origBounds', originalBounds) + // console.log('start end', startLine, endLine) // otherwise if no error, add the position as a diffarea const adding: Omit, 'diffareaid'> = { @@ -1821,7 +1822,6 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinalMessage: async (params) => { const { fullText } = params - console.log('DONE - editCode!', { fullText }) // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") @@ -1836,10 +1836,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // IMPORTANT - sort by lineNum addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) - const { model } = this._voidModelService.getModel(uri) - console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum) - console.log('blocks', deepClone(blocks)) + // const { model } = this._voidModelService.getModel(uri) + // console.log('DONE - editCode!', { fullText }) + // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + // console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum) + // console.log('blocks', deepClone(blocks)) for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata 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 8eb3eebb..71934968 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 @@ -1811,6 +1811,18 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { }; +const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => { + const accessor = useAccessor() + const chatThreadService = accessor.get('IChatThreadService') + + return + +} + type ChatBubbleMode = 'display' | 'edit' type ChatBubbleProps = { chatMessage: ChatMessage, @@ -1866,6 +1878,13 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin return null } + else if (role === 'checkpoint') { + return + } + + else if (role === 'checkpoint_modification') { + return + } } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ad773cd0..314bc6ad 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -399,7 +399,6 @@ export class ToolsService implements IToolsService { return `URI ${params.uri.fsPath} successfully deleted.` }, edit: (params, result) => { - console.log('STR OF RESULT', params) return `Change successfully made to ${params.uri.fsPath}.` }, terminal_command: (params, result) => { From 7e8af9c2ef5e7edcbb41d116043a2cc5d01a3e96 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 2 Apr 2025 00:25:23 -0700 Subject: [PATCH 06/30] improve Apply state and misc other improvements for Apply --- .../contrib/void/browser/editCodeService.ts | 61 ++-- .../void/browser/editCodeServiceInterface.ts | 17 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 283 +++++++++--------- .../src/quick-edit-tsx/QuickEditChat.tsx | 7 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 33 +- .../contrib/void/browser/toolsService.ts | 7 +- .../void/browser/voidCommandBarService.ts | 2 +- .../void/common/chatThreadServiceTypes.ts | 2 +- .../contrib/void/common/prompt/prompts.ts | 18 +- 9 files changed, 231 insertions(+), 199 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a701abec..58b3a76c 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -40,7 +40,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; +import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidModelService } from '../common/voidModelService.js'; @@ -252,9 +252,9 @@ class EditCodeService extends Disposable implements IEditCodeService { onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event; // diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const) - private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>(); + private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>(); private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>(); - onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event; + onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event; onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event; // ctrlKZone: [uri], isStreaming // listen on change streaming @@ -994,7 +994,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea?.type !== 'DiffZone') continue // fire changed diffs (this is the only place Diffs are added) if (!diffArea._streamState.isStreaming) { - this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid }) + this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid }) } } } @@ -1160,29 +1160,50 @@ class EditCodeService extends Disposable implements IEditCodeService { + private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) { + // SR + if (opts.from === 'ClickApply') { + const uri = this._uriOfGivenURI(opts.uri) + if (!uri) return + return uri + } + else if (opts.from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone?.type !== 'CtrlKZone') return + const { _URI: uri } = ctrlKZone + return uri + } + return + } + public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) { + const uri = this._getURIBeforeStartApplying(opts) + if (!uri) return + await this._voidModelService.initializeModel(uri) + } // the applyDonePromise this returns can reject, and should be caught with .catch - public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { + public startApplying(opts: StartApplyingOpts): [URI, Promise] | null { let res: [DiffZone, Promise] | undefined = undefined if (opts.from === 'QuickEdit') { - res = await this._initializeWriteoverStream(opts) // rewrite + res = this._initializeWriteoverStream(opts) // rewrite } else if (opts.from === 'ClickApply') { if (this._settingsService.state.globalSettings.enableFastApply) { const numCharsInFile = this._fileLengthOfGivenURI(opts.uri) if (numCharsInFile === null) return null if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files) - res = await this._initializeWriteoverStream(opts) + res = this._initializeWriteoverStream(opts) } else { - res = await this._initializeSearchAndReplaceStream(opts) // fast apply + res = this._initializeSearchAndReplaceStream(opts) // fast apply } } else { - res = await this._initializeWriteoverStream(opts) // rewrite + res = this._initializeWriteoverStream(opts) // rewrite } } @@ -1278,6 +1299,7 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } + console.log('FIRING START STREAMING IN DIFFZONE!!!') const diffZone = this._addDiffArea(adding) this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) @@ -1308,19 +1330,17 @@ class EditCodeService extends Disposable implements IEditCodeService { } - private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { + private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise] | undefined { const { from, } = opts - let uri: URI - let startRange: 'fullFile' | [number, number] + const uri = this._getURIBeforeStartApplying(opts) + if (!uri) return + let startRange: 'fullFile' | [number, number] let ctrlKZoneIfQuickEdit: CtrlKZone | null = null if (from === 'ClickApply') { - const uri_ = this._uriOfGivenURI(opts.uri) - if (!uri_) return - uri = uri_ startRange = 'fullFile' } else if (from === 'QuickEdit') { @@ -1328,15 +1348,13 @@ class EditCodeService extends Disposable implements IEditCodeService { const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone?.type !== 'CtrlKZone') return ctrlKZoneIfQuickEdit = ctrlKZone - const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone - uri = _URI + const { startLine: startLine_, endLine: endLine_ } = ctrlKZone startRange = [startLine_, endLine_] } else { throw new Error(`Void: diff.type not recognized on: ${from}`) } - await this._voidModelService.initializeModel(uri) const { model } = this._voidModelService.getModel(uri) if (!model) return @@ -1530,13 +1548,12 @@ class EditCodeService extends Disposable implements IEditCodeService { } - private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise] | undefined> { - const { from, applyStr, uri: givenURI, } = opts + private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { + const { from, applyStr, } = opts - const uri = this._uriOfGivenURI(givenURI) + const uri = this._getURIBeforeStartApplying(opts) if (!uri) return - await this._voidModelService.initializeModel(uri) const { model } = this._voidModelService.getModel(uri) if (!model) return diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 8173f837..4be6e23c 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -13,7 +13,15 @@ import { Diff, DiffArea } from './editCodeService.js'; export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts' -export type StartApplyingOpts = ({ +export type CallBeforeStartApplyingOpts = { + from: 'QuickEdit'; + diffareaid: number; // id of the CtrlK area (contains text selection) +} | { + from: 'ClickApply'; + uri: 'current' | URI; +} + +export type StartApplyingOpts = { from: 'QuickEdit'; diffareaid: number; // id of the CtrlK area (contains text selection) startBehavior: StartBehavior; @@ -22,7 +30,7 @@ export type StartApplyingOpts = ({ applyStr: string; uri: 'current' | URI; startBehavior: StartBehavior; -}) +} @@ -37,7 +45,8 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; + callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; + startApplying(opts: StartApplyingOpts): [URI, Promise] | null; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; @@ -49,7 +58,7 @@ export interface IEditCodeService { // events onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; - onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much + onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>; onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>; 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 1069b635..054d1fc9 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 @@ -3,10 +3,9 @@ import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsS import { usePromise, useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' -import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react' +import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js' -import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { Idle = 'Copy', @@ -64,9 +63,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro // // ) -const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' +const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!' -const CopyButton = ({ codeStr }: { codeStr: string }) => { +export const CopyButton = ({ codeStr }: { codeStr: string }) => { const accessor = useAccessor() const metricsService = accessor.get('IMetricsService') @@ -94,11 +93,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } -// state persisted for duration of react only -// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]` -const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } - - export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { @@ -113,164 +107,78 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { }} /> ) - return jumpToFileButton } -export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { + + +export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => { + return ( + + ) +} + + +// state persisted for duration of react only +// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]` +const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } + +const getUriBeingApplied = (applyBoxId: string) => { + return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null +} + + +export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => { const settingsState = useSettingsState() const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') const voidCommandBarService = accessor.get('IVoidCommandBarService') - const metricsService = accessor.get('IMetricsService') const [_, rerender] = useState(0) - const getUriBeingApplied = useCallback(() => { - return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null - }, [applyBoxId]) - const getStreamState = useCallback(() => { - const uri = getUriBeingApplied() + const uri = getUriBeingApplied(applyBoxId) + console.log('uri',uri?.fsPath) if (!uri) return 'idle-no-changes' return voidCommandBarService.getStreamState(uri) - }, [voidCommandBarService, getUriBeingApplied]) + }, [voidCommandBarService, applyBoxId]) // listen for stream updates on this box - - useCommandBarURIListener(useCallback((uri_) => { const shouldUpdate = ( - getUriBeingApplied()?.fsPath === uri_.fsPath + getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath || (uri !== 'current' && uri.fsPath === uri_.fsPath) ) - if (!shouldUpdate) return - rerender(c => c + 1) - }, [applyBoxId, editCodeService, getUriBeingApplied, uri]) - ) - - const onClickSubmit = useCallback(async () => { - if (isDisabled) return - if (getStreamState() === 'streaming') return - const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({ - from: 'ClickApply', - applyStr: codeStr, - uri: uri, - startBehavior: 'keep-conflicts', - }) ?? [] - // catch any errors by interrupting the stream - applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) }) - - applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined - - rerender(c => c + 1) - metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService]) - - - const onInterrupt = useCallback(() => { - if (getStreamState() !== 'streaming') return - const uri = getUriBeingApplied() - if (!uri) return - - editCodeService.interruptURIStreaming({ uri }) - metricsService.capture('Stop Apply', {}) - }, [getStreamState, getUriBeingApplied, editCodeService, metricsService]) - - const onAccept = useCallback(() => { - const uri = getUriBeingApplied() - if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) - }, [getUriBeingApplied, editCodeService]) - - const onReject = useCallback(() => { - const uri = getUriBeingApplied() - if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) - }, [getUriBeingApplied, editCodeService]) - - const onReapply = useCallback(() => { - onReject() - onClickSubmit() - }, [onReject, onClickSubmit]) + if (shouldUpdate) { + rerender(c => c + 1) + console.log('rerendering....') + } + }, [applyBoxId, applyBoxId, uri])) const currStreamState = getStreamState() + console.log('curr stream state', currStreamState) - const copyButton = ( - - ) - - const playButton = ( - - ) - - const stopButton = ( - - ) - - const reapplyButton = ( - - ) - - const acceptButton = ( - - ) - - const rejectButton = ( - - ) - - - - let buttonsHTML = <> - - if (currStreamState === 'streaming') { - buttonsHTML = <> - - {copyButton} - {stopButton} - + return { + getStreamState, + isDisabled, + currStreamState, } +} - if (currStreamState === 'idle-no-changes') { - buttonsHTML = <> - - {copyButton} - {playButton} - - } - if (currStreamState === 'idle-has-changes') { - buttonsHTML = <> - - {reapplyButton} - {rejectButton} - {acceptButton} - - } +export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => { + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) - const statusIndicatorHTML =
+ return
+} - return { - statusIndicatorHTML, - buttonsHTML, +export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + const { + currStreamState, + isDisabled, + getStreamState, + } = useApplyButtonState({ applyBoxId, uri }) + + const onClickSubmit = useCallback(async () => { + if (isDisabled) return + if (getStreamState() === 'streaming') return + const opts = { + from: 'ClickApply', + applyStr: codeStr, + uri: uri, + startBehavior: 'reject-conflicts', + } as const + + await editCodeService.callBeforeStartApplying(opts) + const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? [] + + // catch any errors by interrupting the stream + applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) }) + + applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined + + // rerender(c => c + 1) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + }, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService]) + + + const onInterrupt = useCallback(() => { + if (getStreamState() !== 'streaming') return + const uri = getUriBeingApplied(applyBoxId) + if (!uri) return + + editCodeService.interruptURIStreaming({ uri }) + metricsService.capture('Stop Apply', {}) + }, [getStreamState, applyBoxId, editCodeService, metricsService]) + + const onAccept = useCallback(() => { + const uri = getUriBeingApplied(applyBoxId) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + }, [applyBoxId, editCodeService]) + + const onReject = useCallback(() => { + const uri = getUriBeingApplied(applyBoxId) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + }, [applyBoxId, editCodeService]) + + // const onReapply = useCallback(() => { + // onReject() + // onClickSubmit() + // }, [onReject, onClickSubmit]) + + + if (currStreamState === 'streaming') { + return + } + + if (currStreamState === 'idle-no-changes') { + return + } + + if (currStreamState === 'idle-has-changes') { + return <> + {/* */} + + + } } - - - export const BlockCodeApplyWrapper = ({ children, initValue, @@ -305,10 +292,10 @@ export const BlockCodeApplyWrapper = ({ language: string; uri: URI | 'current', }) => { - - const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri }) const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) + const name = uri !== 'current' ?
- {statusIndicatorHTML} + {name}
- {buttonsHTML} + + {currStreamState === 'idle-no-changes' && } +
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 42e9b81b..d541dc43 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 @@ -63,11 +63,14 @@ export const QuickEditChat = ({ if (isStreamingRef.current) return textAreaFnsRef.current?.disable() - const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({ + const opts = { from: 'QuickEdit', diffareaid, startBehavior: 'keep-conflicts', - }) ?? [] + } as const + + await editCodeService.callBeforeStartApplying(opts) + const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? [] // catch any errors by interrupting the stream applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) }) 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 71934968..7e948af7 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 @@ -28,7 +28,7 @@ import { getModelCapabilities, getIsResoningEnabledState } from '../../../../com import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; -import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; +import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -733,7 +733,7 @@ const ToolHeaderWrapper = ({ {/* left */}
{title} - {desc1} + {desc1}
{/* right */} @@ -1197,7 +1197,7 @@ const titleOfToolName = { running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) }, 'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, - 'terminal_command': { done: `Ran terminal command`, proposed: 'Run terminal command', running: loadingTitleWrapper('Running terminal command') } + 'terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') } } as const satisfies Record @@ -1345,13 +1345,6 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
} -const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDescription: string, applyBoxId: string, uri: URI }) => { - const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: changeDescription, applyBoxId, uri }) - return
- {statusIndicatorHTML} - {buttonsHTML} -
-} const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { @@ -1362,6 +1355,15 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript } +const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: string, uri: URI, codeStr: string }) => { + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) + return
+ + + {currStreamState === 'idle-no-changes' && } + +
+} type ToolRequestState = 'awaiting_user' | 'running' @@ -1682,10 +1684,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { messageIdx: messageIdx, tokenIdx: 'N/A', }) - componentParams.desc2 = } @@ -1764,6 +1767,10 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { command } = params const { terminalId, resolveReason, result } = value + componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} + /> + const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) : resolveReason.type === 'bgtask' ? null : resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : @@ -2052,7 +2059,7 @@ export const SidebarChat = () => { const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed const currStreamingToolHTML = toolIsLoading ? - Getting parameters} /> + Generating} /> : null const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 314bc6ad..636a62b8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -353,12 +353,15 @@ export class ToolsService implements IToolsService { if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`) } - const res = await editCodeService.startApplying({ + const opts = { uri, applyStr: changeDescription, from: 'ClickApply', startBehavior: 'keep-conflicts', - }) + } as const + + await editCodeService.callBeforeStartApplying(opts) + const res = editCodeService.startApplying(opts) if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) const [diffZoneURI, applyDonePromise] = res diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 7711068d..529680da 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -173,7 +173,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar } })) - this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => { + this._register(this._editCodeService.onDidChangeDiffsInDiffZoneNotStreaming(e => { for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: no change diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index b4af2b89..aadf06f5 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -14,7 +14,7 @@ export type ToolMessage = { result: | { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; params: ToolCallParams[T] | undefined; value: string } - | { type: 'rejected'; params: ToolCallParams[T] } + | { type: 'rejected'; params: ToolCallParams[T] } // user rejected } export type ToolRequestApproval = { role: 'tool_request'; diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 29ac971a..b3b921df 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -42,10 +42,14 @@ ${tripleTick[1]}` // ======================================================== tools ======================================================== const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`, param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } } as const +const uriParam = (object: string) => ({ + uri: { type: 'string', description: `The FULL path to the ${object}.` } +}) + export const voidTools = { // --- context-gathering (read/search/list) --- @@ -53,16 +57,16 @@ export const voidTools = { name: 'read_file', description: `Returns file contents of a given URI. ${paginationHelper.desc}`, params: { - uri: { type: 'string', description: undefined }, + ...uriParam('file'), ...paginationHelper.param, }, }, list_dir: { name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`, params: { - uri: { type: 'string', description: undefined }, + ...uriParam('folder'), ...paginationHelper.param, }, }, @@ -91,7 +95,7 @@ export const voidTools = { name: 'create_uri', description: `Create 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 }, + ...uriParam('file or folder'), }, }, @@ -99,7 +103,7 @@ export const voidTools = { name: 'delete_uri', description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, params: { - uri: { type: 'string', description: undefined }, + ...uriParam('file or folder'), params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } }, }, @@ -108,7 +112,7 @@ export const voidTools = { name: 'edit', description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, params: { - uri: { type: 'string', description: undefined }, + ...uriParam('file'), changeDescription: { type: 'string', description: `\ - Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. From 3ac9dcf0c0c827dc0cd456bc31957de577fa872c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 2 Apr 2025 19:13:51 -0700 Subject: [PATCH 07/30] checkpoint progress --- .../contrib/void/browser/chatThreadService.ts | 39 ++++++++------- .../react/src/sidebar-tsx/SidebarChat.tsx | 48 +++++++++++-------- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8df93ec6..9331f841 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -104,7 +104,7 @@ type ThreadType = { // this doesn't need to go in a state object, but feels right state: { - latestCheckpointIdx: number | null; // the latest checkpoint we're standing at or null + currCheckpointIdx: number | null; // the latest checkpoint we're standing at or null stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) @@ -122,7 +122,7 @@ type ChatThreads = { } export const defaultThreadState: ThreadType['state'] = { - latestCheckpointIdx: null, + currCheckpointIdx: null, stagingSelections: [], focusedMessageIdx: undefined, linksOfMessageIdx: {}, @@ -629,8 +629,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { delete this._currentlyRunningToolInterruptor[threadId]; } toolResult = await result // ts is bad... await is needed - - if (toolName === 'edit') { this._addOrUpdateToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } } catch (error) { if (interrupted) { @@ -654,6 +652,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + + // 6. add a checkpoint + if (toolName === 'edit') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } return {} }; @@ -778,35 +779,41 @@ class ChatThreadService extends Disposable implements IChatThreadService { const newThread = this.state.allThreads[threadId] if (!newThread) return // should never happen const latestCheckpointIdx = newThread.messages.length - 1 - this._setThreadState(threadId, { latestCheckpointIdx }) + this._setThreadState(threadId, { currCheckpointIdx: latestCheckpointIdx }) } // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint // call this right after LLM edits a file - private _addOrUpdateToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { + private _addToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { const thread = this.state.allThreads[threadId] if (!thread) return const { model } = this._voidModelService.getModel(uri) if (!model) return // should never happen - const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') - const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') const afterStr = model.getValue() // afterStr = the value of the file right after the edit - let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined - if (prevLLMCheckpointIdx !== -1) { - prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } - this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) - } const newLLMCheckpoint: LLMCheckpoint = { role: 'checkpoint', type: 'after_tool_edits', afterStrOfURI: { - ...prevLLMCheckpoint?.afterStrOfURI, [uri.fsPath]: afterStr, }, } + + // remove and merge + // const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') + // const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') + // let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined + // if (prevLLMCheckpointIdx !== -1) { + // prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } + // this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) + // newLLMCheckpoint.afterStrOfURI = { + // ...newLLMCheckpoint.afterStrOfURI, + // ...prevLLMCheckpoint?.afterStrOfURI, + // } + // } + this._addCheckpoint(threadId, newLLMCheckpoint) } @@ -894,7 +901,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const c = this._getCheckpointAfter({ threadId, messageIdx }) if (c === undefined) return // should never happen - const fromIdx = thread.state.latestCheckpointIdx + const fromIdx = thread.state.currCheckpointIdx if (fromIdx === null) return // should never happen // TODO!!! change toIdx if there's a checkpointModification on the To, and add a checkpoint modification on the from @@ -990,7 +997,7 @@ We only need to do it for files that were edited since `from`, ie files between } } - this._setThreadState(threadId, { latestCheckpointIdx: toIdx }) + this._setThreadState(threadId, { currCheckpointIdx: toIdx }) // TODO!!! add/merge a checkpoint modification if relevant } 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 7e948af7..beba18f1 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,7 +25,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; +import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; @@ -988,7 +988,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB - {} + /> @@ -1821,12 +1821,18 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => { const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - - return + const commandBarService = accessor.get('IVoidCommandBarService') + return
{ + // reject all current changes and then jump back + commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }) + chatThreadService.jumpToCheckpointAfterMessageIdx({ threadId, messageIdx }) + }}> +
+
Checkpoint
+
+
} @@ -2021,18 +2027,22 @@ export const SidebarChat = () => { const previousMessagesHTML = useMemo(() => { const threadId = currentThread.id + const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) + return previousMessages.map((message, i) => { const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user') - return scrollToBottom(scrollContainerRef)} - /> + return
+ scrollToBottom(scrollContainerRef)} + /> +
}) }, [previousMessages, isRunning, currentThread, numMessages]) From bde51106a14ecd6d7f9bfa308c231d894007e6a5 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 3 Apr 2025 01:03:03 -0700 Subject: [PATCH 08/30] checkpoints --- .../contrib/void/browser/chatThreadService.ts | 364 +++++++++--------- .../react/src/sidebar-tsx/SidebarChat.tsx | 13 +- .../void/common/chatThreadServiceTypes.ts | 13 +- 3 files changed, 197 insertions(+), 193 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 9331f841..a7387e29 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -32,30 +32,21 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { findLastIdx } from '../../../../base/common/arraysFind.js'; -type LLMCheckpoint = CheckpointEntry & { type: 'after_tool_edits' } -type UserCheckpoint = CheckpointEntry & { type: 'after_user_edits' } /* -Checkpoints: -pivots: user | tool (edit) -if there are repeated pivots, a checkpoint goes directly after the last one -checkpoint_modifications always go directly after a checkpoint -user --- checkpoint -------- -assistant -tool (edit) - -------- checkpoint - starts here <-- know exact change (file A after) -assistant | -tool (edit) v --- checkpoint -------- -assistant -tool (not edit) -assistant -user --- checkpoint -------- user checkpoint (JIT) - compute change from all files to here when need to --- checkpoint_modifications --------- - these always come DIRECLY after a checkpoint, and reflect the user's modifications on this one checkpoint only. - (only counts when reverting to/from this exact checkpoint, not past it). - Added when user jumps to another checkpoint but made changes here. +Store a checkpoint of all "before" files on each x. +x's show up before user messages and LLM edit tool calls. + +x A (edited A -> A') +(... user modified changes ...) +User message + +x A' B C (edited A'->A'', B->B', C->C') +LLM Edit +x +LLM Edit +x +LLM Edit */ @@ -74,8 +65,6 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { } else if (c.role === 'checkpoint') { // pass } - else if (c.role === 'checkpoint_modification') { // pass - } else { throw new Error(`Role ${(c as any).role} not recognized.`) } @@ -99,12 +88,11 @@ type ThreadType = { lastModified: string; // ISO string messages: ChatMessage[]; - firstStrOfURI: { [fsPath: string]: string | undefined }; // part of checkpointing - + filesWithUserChanges: Set; // this doesn't need to go in a state object, but feels right state: { - currCheckpointIdx: number | null; // the latest checkpoint we're standing at or null + currCheckpointIdx: number | null; // the latest checkpoint we're at (always defined unless chat is empty so there are no checkpts) stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) @@ -121,12 +109,6 @@ type ChatThreads = { [id: string]: undefined | ThreadType; } -export const defaultThreadState: ThreadType['state'] = { - currCheckpointIdx: null, - stagingSelections: [], - focusedMessageIdx: undefined, - linksOfMessageIdx: {}, -} export type ThreadsState = { allThreads: ChatThreads; @@ -156,8 +138,13 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], - state: defaultThreadState, - firstStrOfURI: {}, + state: { + currCheckpointIdx: null, + stagingSelections: [], + focusedMessageIdx: undefined, + linksOfMessageIdx: {}, + }, + filesWithUserChanges: new Set() } satisfies ThreadType } @@ -217,7 +204,7 @@ export interface IChatThreadService { rejectLatestToolRequest(threadId: string): void; // jump to history - jumpToCheckpointAfterMessageIdx(opts: { threadId: string, messageIdx: number }): void; + jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void; } export const IChatThreadService = createDecorator('voidChatThreadService'); @@ -234,6 +221,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { readonly streamState: ThreadStreamState = {} state: ThreadsState // allThreads is persisted, currentThread is not + // used in checkpointing + // private readonly _userModifiedFilesToCheckInCheckpoints = new LRUCache(50) + + + constructor( @IStorageService private readonly _storageService: IStorageService, @IVoidModelService private readonly _voidModelService: IVoidModelService, @@ -246,6 +238,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IMetricsService private readonly _metricsService: IMetricsService, @IEditorService private readonly _editorService: IEditorService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + // @IModelService private readonly _modelService: IModelService, + ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -264,6 +258,22 @@ class ChatThreadService extends Disposable implements IChatThreadService { // when the user changes files, automatically add the new file as a stagingSelection this._register(this._editorService.onDidActiveEditorChange(() => this._addCurrentFileAsStagingSelectionDuringFileChange())); + + // keep track of user-modified files + // const disposablesOfModelId: { [modelId: string]: IDisposable[] } = {} + // this._register( + // this._modelService.onModelAdded(e => { + // if (!(e.id in disposablesOfModelId)) disposablesOfModelId[e.id] = [] + // disposablesOfModelId[e.id].push( + // e.onDidChangeContent(() => { this._userModifiedFilesToCheckInCheckpoints.set(e.uri.fsPath, null) }) + // ) + // }) + // ) + // this._register(this._modelService.onModelRemoved(e => { + // if (!(e.id in disposablesOfModelId)) return + // disposablesOfModelId[e.id].forEach(d => d.dispose()) + // })) + } @@ -619,8 +629,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') let interrupted = false try { - // add the original file if it wasn't seen before in this thread - if (toolName === 'edit') { this._trackOriginalFileInURI({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } + if (toolName === 'edit') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) this._currentlyRunningToolInterruptor[threadId] = () => { @@ -653,8 +662,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - // 6. add a checkpoint - if (toolName === 'edit') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } return {} }; @@ -763,23 +770,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - private _trackOriginalFileInURI({ threadId, uri }: { threadId: string, uri: URI }) { - const thread = this.state.allThreads[threadId] - if (!thread) return - const { model } = this._voidModelService.getModel(uri) - if (!model) return - if (!(uri.fsPath in thread.firstStrOfURI)) { - thread.firstStrOfURI[uri.fsPath] = model.getValue() - } - } - private _addCheckpoint(threadId: string, checkpoint: CheckpointEntry) { this._addMessageToThread(threadId, checkpoint) // update latest checkpoint idx to the one we just added const newThread = this.state.allThreads[threadId] if (!newThread) return // should never happen - const latestCheckpointIdx = newThread.messages.length - 1 - this._setThreadState(threadId, { currCheckpointIdx: latestCheckpointIdx }) + const currCheckpointIdx = newThread.messages.length - 1 + this._setThreadState(threadId, { currCheckpointIdx }) } // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint @@ -790,88 +787,112 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { model } = this._voidModelService.getModel(uri) if (!model) return // should never happen + const currValue = model.getValue() // afterStr = the value of the file right after the edit - const afterStr = model.getValue() // afterStr = the value of the file right after the edit - - const newLLMCheckpoint: LLMCheckpoint = { + this._addCheckpoint(threadId, { role: 'checkpoint', - type: 'after_tool_edits', - afterStrOfURI: { - [uri.fsPath]: afterStr, - }, - } - - // remove and merge - // const lastUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type === 'after_user_edits') - // const prevLLMCheckpointIdx = thread.messages.findIndex((m, i) => i > lastUserCheckpointIdx && m.role === 'checkpoint' && m.type === 'after_tool_edits') - // let prevLLMCheckpoint: LLMCheckpoint | undefined = undefined - // if (prevLLMCheckpointIdx !== -1) { - // prevLLMCheckpoint = thread.messages[prevLLMCheckpointIdx] as ChatMessage & { role: 'checkpoint', type: 'after_tool_edits' } - // this._removeMessageFromThread(threadId, prevLLMCheckpointIdx) - // newLLMCheckpoint.afterStrOfURI = { - // ...newLLMCheckpoint.afterStrOfURI, - // ...prevLLMCheckpoint?.afterStrOfURI, - // } - // } - - this._addCheckpoint(threadId, newLLMCheckpoint) + type: 'tool_edit', + beforeStrOfURI: { [uri.fsPath]: currValue, }, + userModifications: { beforeStrOfURI: {} }, + }) } + private _editMessageInThread(threadId: string, messageIdx: number, newMessage: ChatMessage,) { + const { allThreads } = this.state + const oldThread = allThreads[threadId] + if (!oldThread) return // should never happen + // update state and store it + const newThreads = { + ...allThreads, + [oldThread.id]: { + ...oldThread, + lastModified: new Date().toISOString(), + messages: [ + ...oldThread.messages.slice(0, messageIdx), + newMessage, + ...oldThread.messages.slice(messageIdx + 1, Infinity), + ], + } + } + this._storeAllThreads(newThreads) + this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) + } - // user checkpoints are always computed JIT - // we assume there are no messages after the checkpoint we're adding here - // call this right before user sends message - private _addOrUpdateUserMessageCheckpoint({ threadId, }: { threadId: string, }) { + + + private _computeNeededCheckpointChanges({ threadId }: { threadId: string }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + const { currCheckpointIdx } = thread.state + if (currCheckpointIdx === null) return + + const currStrOfFsPath: { [fsPath: string]: string | undefined } = {} + + // add a change for all the URIs in the checkpoint history + const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: currCheckpointIdx, }) ?? {} + for (const fsPath in lastIdxOfURI ?? {}) { + const { model } = this._voidModelService.getModelFromFsPath(fsPath) + if (!model) continue + const checkpoint2 = thread.messages[lastIdxOfURI[fsPath]] || null + if (!checkpoint2) continue + if (checkpoint2.role !== 'checkpoint') continue + const oldStr = this._getBeforeStrAtCheckpoint(checkpoint2, fsPath, { includeUserModifiedChanges: false }) + const currStr = model.getValue() + if (oldStr === currStr) continue + currStrOfFsPath[fsPath] = currStr + } + + // // add a change for all user-edited files (that aren't in the history) + // for (const fsPath of this._userModifiedFilesToCheckInCheckpoints.keys()) { + // if (fsPath in lastIdxOfURI) continue // if already visisted, don't visit again + // const { model } = this._voidModelService.getModelFromFsPath(fsPath) + // if (!model) continue + // currStrOfFsPath[fsPath] = model.getValue() + // } + + return currStrOfFsPath + } + + // call this right before user sends message or reverts + private _addUserCheckpoint({ threadId }: { threadId: string }) { + const changes = this._computeNeededCheckpointChanges({ threadId }) + this._addCheckpoint(threadId, { + role: 'checkpoint', + type: 'user_edit', + beforeStrOfURI: changes ?? {}, + userModifications: { beforeStrOfURI: {} }, + }) + } + private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) { + const changes = this._computeNeededCheckpointChanges({ threadId }) + const res = this._getCurrentCheckpoint(threadId) + if (!res) return + const [checkpoint, checkpointIdx] = res + this._editMessageInThread(threadId, checkpointIdx, { + ...checkpoint, + userModifications: { beforeStrOfURI: changes ?? {} }, + }) + + } + + private _getCurrentCheckpoint(threadId: string): [CheckpointEntry, number] | undefined { const thread = this.state.allThreads[threadId] if (!thread) return - const newUserCheckpoint: UserCheckpoint = { - role: 'checkpoint', - type: 'after_user_edits', // user backup - afterStrOfURI: {}, - } + const { currCheckpointIdx } = thread.state + if (currCheckpointIdx === null) return - // first get the last user checkpoint - const lastNonUserCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint' && m.type !== 'after_user_edits') - - // merge all recent user checkpoints and delete them - const latestAfterStrOfURI: { [fsPath: string]: string } = {} // helps merge user edits - for (let k = 0; k < thread.messages.length; k += 1) { - const message = thread.messages[k] - if (message.role !== 'checkpoint') continue - for (const uri in message.afterStrOfURI) - latestAfterStrOfURI[uri] = message.afterStrOfURI[uri] - - // remove any user messages that come after the last LLM checkpoint (we're merging them into one big user message) - if (k > lastNonUserCheckpointIdx) - this._removeMessageFromThread(threadId, k) - } - - - // add a change for all the files where we detect a user change - const allURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: 0, hiIdx: thread.messages.length - 1, }) - for (const fsPath of allURIs ?? []) { - const { model } = this._voidModelService.getModelFromFsPath(fsPath) - if (!model) continue - const oldAfterStr = latestAfterStrOfURI[fsPath] - const currentAfterStr = model.getValue() - if (oldAfterStr === currentAfterStr) continue - // if there was a change, add it as a user edit - newUserCheckpoint.afterStrOfURI = { - ...newUserCheckpoint.afterStrOfURI, - [fsPath]: currentAfterStr - } - } - - this._addCheckpoint(threadId, newUserCheckpoint) + const checkpoint = thread.messages[currCheckpointIdx] + if (!checkpoint) return + if (checkpoint.role !== 'checkpoint') return + return [checkpoint, currCheckpointIdx] } - - private _getCheckpointAfter = ({ threadId, messageIdx: afterIdx }: { threadId: string, messageIdx: number }): [CheckpointEntry, number] | undefined => { + private _getCheckpointBeforeMessage = ({ threadId, messageIdx }: { threadId: string, messageIdx: number }): [CheckpointEntry, number] | undefined => { const thread = this.state.allThreads[threadId] if (!thread) return undefined - for (let i = afterIdx; i < thread.messages.length; i++) { + for (let i = messageIdx; i >= 0; i--) { const message = thread.messages[i] if (message.role === 'checkpoint') { return [message, i] @@ -880,45 +901,55 @@ class ChatThreadService extends Disposable implements IChatThreadService { return undefined } - private _getAllChangedCheckpointURIs({ threadId, loIdx, hiIdx }: { threadId: string, loIdx: number, hiIdx: number }) { + private _getCheckpointsBetween({ threadId, loIdx, hiIdx }: { threadId: string, loIdx: number, hiIdx: number }) { const thread = this.state.allThreads[threadId] - if (!thread) return null // should never happen - const fsPaths: Set = new Set() + if (!thread) return { lastIdxOfURI: {} } // should never happen + const lastIdxOfURI: { [fsPath: string]: number } = {} for (let i = loIdx; i <= hiIdx; i += 1) { const message = thread.messages[i] if (message.role !== 'checkpoint') continue - for (const fsPath in message.afterStrOfURI) { - fsPaths.add(fsPath) + for (const fsPath in message.beforeStrOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes + lastIdxOfURI[fsPath] = i } } - return fsPaths + return { lastIdxOfURI } } - jumpToCheckpointAfterMessageIdx({ threadId, messageIdx }: { threadId: string, messageIdx: number }) { + private _getBeforeStrAtCheckpoint = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => { + const beforeStr = fsPath in checkpointMessage.beforeStrOfURI ? checkpointMessage.beforeStrOfURI[fsPath] ?? null : null + if (!opts.includeUserModifiedChanges) return beforeStr + const userModifiedBeforeStr = fsPath in checkpointMessage.userModifications.beforeStrOfURI ? checkpointMessage.userModifications.beforeStrOfURI[fsPath] ?? null : null + return userModifiedBeforeStr ?? beforeStr + } + + + private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { + const { model } = this._voidModelService.getModelFromFsPath(fsPath) + if (!model) return // should never happen + model.applyEdits([{ + range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file + text + }]) + } + + jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) { const thread = this.state.allThreads[threadId] if (!thread) return - const c = this._getCheckpointAfter({ threadId, messageIdx }) + const c = this._getCheckpointBeforeMessage({ threadId, messageIdx }) if (c === undefined) return // should never happen const fromIdx = thread.state.currCheckpointIdx if (fromIdx === null) return // should never happen - // TODO!!! change toIdx if there's a checkpointModification on the To, and add a checkpoint modification on the from - const [_, toIdx_] = c - const toIdx = toIdx_ + const [_, toIdx] = c if (toIdx === fromIdx) return - const writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { - const { model } = this._voidModelService.getModelFromFsPath(fsPath) - if (!model) return // should never happen - model.applyEdits([{ - range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file - text - }]) - } console.log(`going from ${fromIdx} to ${toIdx}`) + // update the user's checkpoint + this._addUserModificationsToCurrCheckpoint({ threadId }) + /* if undoing @@ -941,26 +972,18 @@ We need to revert anything that happened between to+1 and from. We only need to do it for files that were edited since `to`, ie files between to+1...from. */ if (toIdx < fromIdx) { - const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx }) - for (const fsPath of checkpointURIs ?? []) { - let found = false - + const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx }) + for (const fsPath in lastIdxOfURI) { // apply lowest down content for each uri (or original if not found) - for (let k = toIdx; k >= 0; k -= 1) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue - if (fsPath in message.afterStrOfURI) { - found = true - writeFullFile({ fsPath, text: message.afterStrOfURI[fsPath] }) + const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) + if (beforeStr !== null) { + this._writeFullFile({ fsPath, text: beforeStr }) break } } - if (!found) { - const originalStr = thread.firstStrOfURI[fsPath] - if (originalStr === undefined) continue - writeFullFile({ fsPath, text: originalStr }) - } } } @@ -982,15 +1005,15 @@ We need to apply latest change for anything that happened between from+1 and to. We only need to do it for files that were edited since `from`, ie files between from+1...to. */ if (toIdx > fromIdx) { - const checkpointURIs = this._getAllChangedCheckpointURIs({ threadId, loIdx: fromIdx + 1, hiIdx: toIdx }) - for (const fsPath of checkpointURIs ?? []) { + const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: fromIdx + 1, hiIdx: toIdx }) + for (const fsPath in lastIdxOfURI) { // apply lowest down content for each uri - // (do not need to apply original since we're only applying to files that changed) for (let k = toIdx; k >= fromIdx + 1; k -= 1) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue - if (fsPath in message.afterStrOfURI) { - writeFullFile({ fsPath, text: message.afterStrOfURI[fsPath] }) + const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) + if (beforeStr !== null) { + this._writeFullFile({ fsPath, text: beforeStr }) break } } @@ -1020,9 +1043,11 @@ We only need to do it for files that were edited since `from`, ie files between const userMessageContent = await chat_userMessageContent(instructions, currSelns) // user message + names of files (NOT content) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._addOrUpdateUserMessageCheckpoint({ threadId }) this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) + .then(() => { + this._addUserCheckpoint({ threadId }) + }) } dismissStreamError(threadId: string): void { @@ -1308,7 +1333,6 @@ We only need to do it for files that were edited since `from`, ie files between const model = this._codeEditorService.getActiveCodeEditor()?.getModel() if (model) { this._setThreadState(this.state.currentThreadId, { - ...defaultThreadState, stagingSelections: [{ type: 'File', fileURI: model.uri, @@ -1358,28 +1382,6 @@ We only need to do it for files that were edited since `from`, ie files between this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) } - - private _removeMessageFromThread(threadId: string, messageIdx: number) { - const { allThreads } = this.state - const oldThread = allThreads[threadId] - if (!oldThread) return // should never happen - // update state and store it - const newThreads = { - ...allThreads, - [oldThread.id]: { - ...oldThread, - lastModified: new Date().toISOString(), - messages: [ - ...oldThread.messages.slice(0, messageIdx), - ...oldThread.messages.slice(messageIdx + 1, Infinity), - ], - } - } - this._storeAllThreads(newThreads) - this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) - } - - // sets the currently selected message (must be undefined if no message is selected) setCurrentlyFocusedMessageIdx(messageIdx: number | undefined) { 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 beba18f1..22242e3a 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 @@ -1826,8 +1826,8 @@ const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: nu className='pointer-events-auto cursor-pointer select-none hover:brightness-125 flex items-center justify-center' onClick={() => { // reject all current changes and then jump back - commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' }) - chatThreadService.jumpToCheckpointAfterMessageIdx({ threadId, messageIdx }) + commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }) + chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) }}>
Checkpoint
@@ -2024,15 +2024,18 @@ export const SidebarChat = () => { }, [isHistoryOpen, currentThread.id]) const numMessages = previousMessages.length + const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') const previousMessagesHTML = useMemo(() => { const threadId = currentThread.id const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) return previousMessages.map((message, i) => { - const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user') - return
- + = { // checkpoints export type CheckpointEntry = { role: 'checkpoint'; - type: 'after_user_edits' | 'after_tool_edits'; - afterStrOfURI: { [fsPath: string]: string }; -} | { // modifications that only count when undoing/redoing - role: 'checkpoint_modification'; - type: 'user_modifications'; - afterStrOfURI: { [fsPath: string]: string }; + type: 'user_edit' | 'tool_edit'; + beforeStrOfURI: { [fsPath: string]: string | undefined }; + userModifications: { + beforeStrOfURI: { [fsPath: string]: string | undefined }; + }; + // diffAreas: null; } - // 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 = | { From a2a9cdba6019eeed75b4ffba982fe3b43e87e394 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 3 Apr 2025 17:12:18 -0700 Subject: [PATCH 09/30] checkpoints revert/restore diffareas!!! --- .../contrib/void/browser/chatThreadService.ts | 92 +++--- .../contrib/void/browser/editCodeService.ts | 283 ++++++------------ .../void/browser/editCodeServiceInterface.ts | 7 +- .../contrib/void/browser/helpers/findDiffs.ts | 27 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 7 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 8 +- .../void/browser/react/src/util/services.tsx | 2 +- .../void/common/chatThreadServiceTypes.ts | 12 +- .../void/common/editCodeServiceTypes.ts | 119 ++++++++ .../contrib/void/common/prompt/prompts.ts | 2 +- 10 files changed, 290 insertions(+), 269 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a7387e29..a02ff9ab 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -30,6 +30,8 @@ import { IVoidModelService } from '../common/voidModelService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { findLastIdx } from '../../../../base/common/arraysFind.js'; +import { IEditCodeService } from './editCodeServiceInterface.js'; +import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; /* @@ -238,6 +240,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IMetricsService private readonly _metricsService: IMetricsService, @IEditorService private readonly _editorService: IEditorService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IEditCodeService private readonly _editCodeService: IEditCodeService, // @IModelService private readonly _modelService: IModelService, ) { @@ -787,13 +790,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { model } = this._voidModelService.getModel(uri) if (!model) return // should never happen - const currValue = model.getValue() // afterStr = the value of the file right after the edit + const diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri) this._addCheckpoint(threadId, { role: 'checkpoint', type: 'tool_edit', - beforeStrOfURI: { [uri.fsPath]: currValue, }, - userModifications: { beforeStrOfURI: {} }, + voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot }, + userModifications: { voidFileSnapshotOfURI: {} }, }) } @@ -821,13 +824,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { - private _computeNeededCheckpointChanges({ threadId }: { threadId: string }) { + private _computeCheckpointInfo({ threadId }: { threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return const { currCheckpointIdx } = thread.state if (currCheckpointIdx === null) return - const currStrOfFsPath: { [fsPath: string]: string | undefined } = {} + const voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined } = {} // add a change for all the URIs in the checkpoint history const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: currCheckpointIdx, }) ?? {} @@ -837,10 +840,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { const checkpoint2 = thread.messages[lastIdxOfURI[fsPath]] || null if (!checkpoint2) continue if (checkpoint2.role !== 'checkpoint') continue - const oldStr = this._getBeforeStrAtCheckpoint(checkpoint2, fsPath, { includeUserModifiedChanges: false }) - const currStr = model.getValue() - if (oldStr === currStr) continue - currStrOfFsPath[fsPath] = currStr + const res = this._getCheckpointInfo(checkpoint2, fsPath, { includeUserModifiedChanges: false }) + if (!res) continue + const { voidFileSnapshot: oldVoidFileSnapshot } = res + + // if there was any change to the str or diffAreaSnapshot, update. rough approximation of equality, oldDiffAreasSnapshot === diffAreasSnapshot is not perfect + const voidFileSnapshot = this._editCodeService.getVoidFileSnapshot(URI.file(fsPath)) + if (oldVoidFileSnapshot === voidFileSnapshot) continue + voidFileSnapshotOfURI[fsPath] = voidFileSnapshot } // // add a change for all user-edited files (that aren't in the history) @@ -851,27 +858,30 @@ class ChatThreadService extends Disposable implements IChatThreadService { // currStrOfFsPath[fsPath] = model.getValue() // } - return currStrOfFsPath + return { voidFileSnapshotOfURI } } // call this right before user sends message or reverts private _addUserCheckpoint({ threadId }: { threadId: string }) { - const changes = this._computeNeededCheckpointChanges({ threadId }) + const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {} this._addCheckpoint(threadId, { role: 'checkpoint', type: 'user_edit', - beforeStrOfURI: changes ?? {}, - userModifications: { beforeStrOfURI: {} }, + voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, + userModifications: { + voidFileSnapshotOfURI: {}, + }, }) } private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) { - const changes = this._computeNeededCheckpointChanges({ threadId }) + const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {} + const res = this._getCurrentCheckpoint(threadId) if (!res) return const [checkpoint, checkpointIdx] = res this._editMessageInThread(threadId, checkpointIdx, { ...checkpoint, - userModifications: { beforeStrOfURI: changes ?? {} }, + userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, }, }) } @@ -908,29 +918,30 @@ class ChatThreadService extends Disposable implements IChatThreadService { for (let i = loIdx; i <= hiIdx; i += 1) { const message = thread.messages[i] if (message.role !== 'checkpoint') continue - for (const fsPath in message.beforeStrOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes + for (const fsPath in message.voidFileSnapshotOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes lastIdxOfURI[fsPath] = i } } return { lastIdxOfURI } } - private _getBeforeStrAtCheckpoint = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => { - const beforeStr = fsPath in checkpointMessage.beforeStrOfURI ? checkpointMessage.beforeStrOfURI[fsPath] ?? null : null - if (!opts.includeUserModifiedChanges) return beforeStr - const userModifiedBeforeStr = fsPath in checkpointMessage.userModifications.beforeStrOfURI ? checkpointMessage.userModifications.beforeStrOfURI[fsPath] ?? null : null - return userModifiedBeforeStr ?? beforeStr + private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => { + const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null + if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } } + + const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null + return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, } } - private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { - const { model } = this._voidModelService.getModelFromFsPath(fsPath) - if (!model) return // should never happen - model.applyEdits([{ - range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file - text - }]) - } + // private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { + // const { model } = this._voidModelService.getModelFromFsPath(fsPath) + // if (!model) return // should never happen + // model.applyEdits([{ + // range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file + // text + // }]) + // } jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) { const thread = this.state.allThreads[threadId] @@ -978,11 +989,12 @@ We only need to do it for files that were edited since `to`, ie files between to for (let k = toIdx; k >= 0; k -= 1) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue - const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) - if (beforeStr !== null) { - this._writeFullFile({ fsPath, text: beforeStr }) - break - } + const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) + if (!res) continue + const { voidFileSnapshot } = res + if (!voidFileSnapshot) continue + this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot) + break } } } @@ -1011,11 +1023,13 @@ We only need to do it for files that were edited since `from`, ie files between for (let k = toIdx; k >= fromIdx + 1; k -= 1) { const message = thread.messages[k] if (message.role !== 'checkpoint') continue - const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) - if (beforeStr !== null) { - this._writeFullFile({ fsPath, text: beforeStr }) - break - } + const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified }) + if (!res) continue + const { voidFileSnapshot } = res + if (!voidFileSnapshot) continue + + this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot) + break } } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 58b3a76c..a8c15870 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; -import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; +import { findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; @@ -40,13 +40,14 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts } from './editCodeServiceInterface.js'; +import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { deepClone } from '../../../../base/common/objects.js'; import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js'; +import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -107,7 +108,6 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number }; - // finds block.orig in fileContents and return its range in file // startingAtLine is 1-indexed and inclusive const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => { @@ -127,108 +127,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num - -// // TODO diffArea should be removed if we just discovered it has no more diffs in it -// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { -// const diffArea = this.diffAreaOfId[diffareaid] -// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) { -// const { onFinishEdit } = this._addToHistory(uri) -// this._deleteDiffArea(diffArea) -// onFinishEdit() -// } -// } - - -export type Diff = { - diffid: number; - diffareaid: number; // the diff area this diff belongs to, "computed" -} & ComputedDiff - - - -// _ means anything we don't include if we clone it -// DiffArea.originalStartLine is the line in originalCode (not the file) - -type CommonZoneProps = { - diffareaid: number; - startLine: number; - endLine: number; - - _URI: URI; // typically we get the URI from model - -} - -type CtrlKZone = { - type: 'CtrlKZone'; - originalCode?: undefined; - - editorId: string; // the editor the input lives on - - _mountInfo: null | { - textAreaRef: { current: HTMLTextAreaElement | null } - dispose: () => void; - refresh: () => void; - } - - _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here - _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles - -} & CommonZoneProps - - -export type DiffZone = { - type: 'DiffZone', - originalCode: string; - _diffOfId: Record; // diffid -> diff in this DiffArea - _streamState: { - isStreaming: true; - streamRequestIdRef: { current: string | null }; - line: number; - } | { - isStreaming: false; - streamRequestIdRef?: undefined; - line?: undefined; - }; - editorId?: undefined; - linkedStreamingDiffZone?: undefined; - _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles -} & CommonZoneProps - - - -type TrackingZone = { - type: 'TrackingZone'; - metadata: T; - originalCode?: undefined; - editorId?: undefined; - _removeStylesFns?: undefined; -} & CommonZoneProps - - -// called DiffArea for historical purposes, we can rename to something like TextRegion if we want -export type DiffArea = CtrlKZone | DiffZone | TrackingZone - -const diffAreaSnapshotKeys = [ - 'type', - 'diffareaid', - 'originalCode', - 'startLine', - 'endLine', - 'editorId', - -] as const satisfies (keyof DiffArea)[] - -type DiffAreaSnapshot = Pick - - - -type HistorySnapshot = { - snapshottedDiffAreaOfId: Record; - entireFileCode: string; -} - - - // line/col is the location, originalCodeStartLine is the start line of the original code being displayed type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number } @@ -259,7 +157,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // ctrlKZone: [uri], isStreaming // listen on change streaming private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>(); - onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event + onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event; constructor( @@ -722,98 +620,98 @@ class EditCodeService extends Disposable implements IEditCodeService { + + + + private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => { + const { model } = this._voidModelService.getModel(uri) + const snapshottedDiffAreaOfId: Record = {} + + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + + if (diffArea._URI.fsPath !== uri.fsPath) continue + + snapshottedDiffAreaOfId[diffareaid] = deepClone( + Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) + ) as DiffAreaSnapshotEntry + } + + const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' + + // this._noLongerNeedModelReference(uri) + return { + snapshottedDiffAreaOfId, + entireFileCode, // the whole file's code + } + } + + + private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => { + // for each diffarea in this uri, stop streaming if currently streaming + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type === 'DiffZone') + this._stopIfStreaming(diffArea) + } + + // delete all diffareas on this uri (clearing their styles) + this._deleteAllDiffAreas(uri) + + const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot + + // restore diffAreaOfId and diffAreasOfModelId + for (const diffareaid in snapshottedDiffAreaOfId) { + + const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid] + + if (snapshottedDiffArea.type === 'DiffZone') { + this.diffAreaOfId[diffareaid] = { + ...snapshottedDiffArea as DiffAreaSnapshotEntry, + type: 'DiffZone', + _diffOfId: {}, + _URI: uri, + _streamState: { isStreaming: false }, // when restoring, we will never be streaming + _removeStylesFns: new Set(), + } + } + else if (snapshottedDiffArea.type === 'CtrlKZone') { + this.diffAreaOfId[diffareaid] = { + ...snapshottedDiffArea as DiffAreaSnapshotEntry, + _URI: uri, + _removeStylesFns: new Set(), + _mountInfo: null, + _linkedStreamingDiffZone: null, // when restoring, we will never be streaming + } + } + this._addOrInitializeDiffAreaAtURI(uri, diffareaid) + } + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + // restore file content + this._writeURIText(uri, entireModelCode, + 'wholeFileRange', + { shouldRealignDiffAreas: false } + ) + // this._noLongerNeedModelReference(uri) + } + private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) { - - const getCurrentSnapshot = (): HistorySnapshot => { - - const { model } = this._voidModelService.getModel(uri) - const snapshottedDiffAreaOfId: Record = {} - - for (const diffareaid in this.diffAreaOfId) { - const diffArea = this.diffAreaOfId[diffareaid] - - if (diffArea._URI.fsPath !== uri.fsPath) continue - - snapshottedDiffAreaOfId[diffareaid] = deepClone( - Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) - ) as DiffAreaSnapshot - } - - const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' - - // this._noLongerNeedModelReference(uri) - return { - snapshottedDiffAreaOfId, - entireFileCode, // the whole file's code - } - } - - const restoreDiffAreas = async (snapshot: HistorySnapshot) => { - - // for each diffarea in this uri, stop streaming if currently streaming - for (const diffareaid in this.diffAreaOfId) { - const diffArea = this.diffAreaOfId[diffareaid] - if (diffArea.type === 'DiffZone') - this._stopIfStreaming(diffArea) - } - - // delete all diffareas on this uri (clearing their styles) - this._deleteAllDiffAreas(uri) - this.diffAreasOfURI[uri.fsPath]?.clear() - - const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot - - // restore diffAreaOfId and diffAreasOfModelId - for (const diffareaid in snapshottedDiffAreaOfId) { - - const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid] - - if (snapshottedDiffArea.type === 'DiffZone') { - this.diffAreaOfId[diffareaid] = { - ...snapshottedDiffArea as DiffAreaSnapshot, - type: 'DiffZone', - _diffOfId: {}, - _URI: uri, - _streamState: { isStreaming: false }, // when restoring, we will never be streaming - _removeStylesFns: new Set(), - } - } - else if (snapshottedDiffArea.type === 'CtrlKZone') { - this.diffAreaOfId[diffareaid] = { - ...snapshottedDiffArea as DiffAreaSnapshot, - _URI: uri, - _removeStylesFns: new Set(), - _mountInfo: null, - _linkedStreamingDiffZone: null, // when restoring, we will never be streaming - } - } - this._addOrInitializeDiffAreaAtURI(uri, diffareaid) - } - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - // restore file content - this._writeURIText(uri, entireModelCode, - 'wholeFileRange', - { shouldRealignDiffAreas: false } - ) - // this._noLongerNeedModelReference(uri) - } - - const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() - let afterSnapshot: HistorySnapshot | null = null + const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri) + let afterSnapshot: VoidFileSnapshot | null = null const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, resource: uri, label: 'Void Agent', code: 'undoredo.editCode', - undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); }, - redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) } + undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); }, + redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) } } this._undoRedoService.pushElement(elt) const onFinishEdit = async () => { - afterSnapshot = getCurrentSnapshot() + afterSnapshot = this._getCurrentVoidFileSnapshot(uri) await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change. skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack) }) @@ -822,6 +720,16 @@ class EditCodeService extends Disposable implements IEditCodeService { } + public getVoidFileSnapshot(uri: URI) { + return this._getCurrentVoidFileSnapshot(uri) + } + + + public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void { + this._restoreVoidFileSnapshot(uri, snapshot) + } + + // delete diffOfId and diffArea._diffOfId private _deleteDiff(diff: Diff) { const diffArea = this.diffAreaOfId[diff.diffareaid] @@ -886,6 +794,7 @@ class EditCodeService extends Disposable implements IEditCodeService { else if (diffArea.type === 'CtrlKZone') this._deleteCtrlKZone(diffArea) }) + this.diffAreasOfURI[uri.fsPath]?.clear() } private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => { diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 4be6e23c..a63fde7e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -7,8 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Diff, DiffArea } from './editCodeService.js'; - +import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts' @@ -32,8 +31,6 @@ export type StartApplyingOpts = { startBehavior: StartBehavior; } - - export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -70,4 +67,6 @@ export interface IEditCodeService { interruptURIStreaming(opts: { uri: URI }): void; // testDiffs(): void; + getVoidFileSnapshot(uri: URI): VoidFileSnapshot; + restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void; } diff --git a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts index c9235c14..703b2775 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts @@ -3,34 +3,9 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { ComputedDiff } from '../../common/editCodeServiceTypes.js'; import { diffLines } from '../react/out/diff/index.js' -export type ComputedDiff = { - type: 'edit'; - originalCode: string; - originalStartLine: number; - originalEndLine: number; - code: string; - startLine: number; // 1-indexed - endLine: number; -} | { - type: 'insertion'; - // originalCode: string; - originalStartLine: number; // insertion starts on column 0 of this - // originalEndLine: number; - code: string; - startLine: number; - endLine: number; -} | { - type: 'deletion'; - originalCode: string; - originalStartLine: number; - originalEndLine: number; - // code: string; - startLine: number; // deletion starts on column 0 of this - // endLine: number; -} - export function findDiffs(oldStr: string, newStr: string) { // this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion) 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 054d1fc9..7dfbf73c 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 @@ -1,3 +1,8 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + import { useState, useEffect, useCallback } from 'react' import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js' import { usePromise, useRefState } from '../util/helpers.js' @@ -144,7 +149,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u const getStreamState = useCallback(() => { const uri = getUriBeingApplied(applyBoxId) - console.log('uri',uri?.fsPath) if (!uri) return 'idle-no-changes' return voidCommandBarService.getStreamState(uri) }, [voidCommandBarService, applyBoxId]) @@ -162,7 +166,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u }, [applyBoxId, applyBoxId, uri])) const currStreamState = getStreamState() - console.log('curr stream state', currStreamState) return { getStreamState, 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 22242e3a..0092cd24 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 @@ -1,7 +1,3 @@ -//!!!! merged - - - /*-------------------------------------------------------------------------------------- * Copyright 2025 Glass Devtools, Inc. All rights reserved. * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. @@ -1821,12 +1817,12 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => { const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - const commandBarService = accessor.get('IVoidCommandBarService') + // const commandBarService = accessor.get('IVoidCommandBarService') return
{ // reject all current changes and then jump back - commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }) + // commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }) chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) }}>
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 493b87bb..6cd180a0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js'; import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { IEditCodeService } from '../../../editCodeServiceInterface.js' import { ISidebarStateService } from '../../../sidebarStateService.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js' @@ -47,6 +46,7 @@ import { IVoidModelService } from '../../../../common/voidModelService.js' import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js' import { IVoidCommandBarService } from '../../../voidCommandBarService.js' import { INativeHostService } from '../../../../../../../platform/native/common/native.js'; +import { IEditCodeService } from '../../../editCodeServiceInterface.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 64d8976d..b95bc67d 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -1,5 +1,11 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { VoidFileSnapshot } from './editCodeServiceTypes.js'; import { AnthropicReasoning } from './sendLLMMessageTypes.js'; import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; @@ -29,11 +35,11 @@ export type ToolRequestApproval = { export type CheckpointEntry = { role: 'checkpoint'; type: 'user_edit' | 'tool_edit'; - beforeStrOfURI: { [fsPath: string]: string | undefined }; + voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined }; + userModifications: { - beforeStrOfURI: { [fsPath: string]: string | undefined }; + voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined }; }; - // diffAreas: null; } diff --git a/src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts b/src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts new file mode 100644 index 00000000..4aa09de3 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts @@ -0,0 +1,119 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; + +export type ComputedDiff = { + type: 'edit'; + originalCode: string; + originalStartLine: number; + originalEndLine: number; + code: string; + startLine: number; // 1-indexed + endLine: number; +} | { + type: 'insertion'; + // originalCode: string; + originalStartLine: number; // insertion starts on column 0 of this + // originalEndLine: number; + code: string; + startLine: number; + endLine: number; +} | { + type: 'deletion'; + originalCode: string; + originalStartLine: number; + originalEndLine: number; + // code: string; + startLine: number; // deletion starts on column 0 of this + // endLine: number; +} + +// ---------- Diff types ---------- + +export type CommonZoneProps = { + diffareaid: number; + startLine: number; + endLine: number; + + _URI: URI; // typically we get the URI from model + +} + + +export type CtrlKZone = { + type: 'CtrlKZone'; + originalCode?: undefined; + + editorId: string; // the editor the input lives on + + // _ means anything we don't include if we clone it + _mountInfo: null | { + textAreaRef: { current: HTMLTextAreaElement | null } + dispose: () => void; + refresh: () => void; + } + _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles +} & CommonZoneProps + + +export type TrackingZone = { + type: 'TrackingZone'; + metadata: T; + originalCode?: undefined; + editorId?: undefined; + _removeStylesFns?: undefined; +} & CommonZoneProps + + +// called DiffArea for historical purposes, we can rename to something like TextRegion if we want +export type DiffArea = CtrlKZone | DiffZone | TrackingZone + + +export type Diff = { + diffid: number; + diffareaid: number; // the diff area this diff belongs to, "computed" +} & ComputedDiff + + +export type DiffZone = { + type: 'DiffZone', + originalCode: string; + _diffOfId: Record; // diffid -> diff in this DiffArea + _streamState: { + isStreaming: true; + streamRequestIdRef: { current: string | null }; + line: number; + } | { + isStreaming: false; + streamRequestIdRef?: undefined; + line?: undefined; + }; + editorId?: undefined; + linkedStreamingDiffZone?: undefined; + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles +} & CommonZoneProps + + +export const diffAreaSnapshotKeys = [ + 'type', + 'diffareaid', + 'originalCode', + 'startLine', + 'endLine', + 'editorId', + +] as const satisfies (keyof DiffArea)[] + + + +export type DiffAreaSnapshotEntry = Pick + +export type VoidFileSnapshot = { + snapshottedDiffAreaOfId: Record; + entireFileCode: string; +} + diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index b3b921df..9ca15419 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -128,7 +128,7 @@ Here's an example of a good description:\n${editToolDescription}.` name: 'terminal_command', description: `Executes a terminal command.`, params: { - command: { type: 'string', description: 'The terminal command to execute.' }, + command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' }, 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 (value must be an integer >= 1, or empty which will go with the default). 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.' }, }, From de703e82cb12df8eefe63dd58b230c3fe7cfc812 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 4 Apr 2025 02:39:11 -0700 Subject: [PATCH 10/30] add new models, more info in prompt, --- .../contrib/void/browser/chatThreadService.ts | 69 +++++- .../void/browser/directoryTreeService.ts | 222 ++++++++++++++++++ .../react/src/sidebar-tsx/SidebarChat.tsx | 5 +- .../contrib/void/browser/sidebarActions.ts | 6 +- .../contrib/void/common/modelCapabilities.ts | 86 +++++-- .../contrib/void/common/prompt/prompts.ts | 14 +- 6 files changed, 360 insertions(+), 42 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/directoryTreeService.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a02ff9ab..56308a40 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -29,9 +29,11 @@ import { shorten } from '../../../../base/common/labels.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { findLastIdx } from '../../../../base/common/arraysFind.js'; +import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js'; import { IEditCodeService } from './editCodeServiceInterface.js'; import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; /* @@ -241,7 +243,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IEditorService private readonly _editorService: IEditorService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IEditCodeService private readonly _editCodeService: IEditCodeService, - // @IModelService private readonly _modelService: IModelService, + @INotificationService private readonly _notificationService: INotificationService, + @IModelService private readonly _modelService: IModelService, ) { super() @@ -475,7 +478,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { const callThisToolFirst: ToolRequestApproval = lastMessage - this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + this._wrapRunAgentToNotify( + this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + , threadId + ) } rejectLatestToolRequest(threadId: string) { const thread = this.state.allThreads[threadId] @@ -572,8 +578,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { // system message const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - const terminalIds = this._terminalToolService.listTerminalIds() - const systemMessage = chat_systemMessage(workspaceFolders, terminalIds, chatMode) + + const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; + const activeURI = this._editorService.activeEditor?.resource?.fsPath; + + const runningTerminalIds = this._terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, chatMode }) // all messages so far in the chat history (including tools) const messages: LLMChatMessage[] = [ @@ -767,9 +777,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if awaiting user approval, keep isRunning true, else end isRunning this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge') + // if successful, add checkpoint + this._addUserCheckpoint({ threadId }) + // capture number of messages sent this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) - } @@ -1039,6 +1051,43 @@ We only need to do it for files that were edited since `from`, ie files between } + private _wrapRunAgentToNotify(p: Promise, threadId: string) { + const notify = (error: string | null) => { + const thread = this.state.allThreads[threadId] + if (!thread) return + const userMsg = findLast(thread.messages, m => m.role === 'user') + if (!userMsg) return + if (userMsg.role !== 'user') return + const messageContent = userMsg.displayContent.substring(0, 50) + + this._notificationService.notify({ + severity: error ? Severity.Warning : Severity.Info, + message: error ? `Error: ${error} ` : `Task Complete!\n${messageContent}...`, + actions: { + secondary: [{ + id: 'void.goToChat', + enabled: true, + label: `View`, + tooltip: '', + class: undefined, + run: () => { + this.switchToThread(threadId) + // TODO!!! scroll to bottom + } + }] + }, + }) + } + + p.then(() => { + notify(null) + + }).catch((e) => { + notify(getErrorMessage(e)) + throw e + }) + } + async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[], }, threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -1058,10 +1107,10 @@ We only need to do it for files that were edited since `from`, ie files between const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) - .then(() => { - this._addUserCheckpoint({ threadId }) - }) + this._wrapRunAgentToNotify( + this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }), + threadId, + ) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/directoryTreeService.ts b/src/vs/workbench/contrib/void/browser/directoryTreeService.ts new file mode 100644 index 00000000..d6e6fd06 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/directoryTreeService.ts @@ -0,0 +1,222 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import * as path from '../../../../base/common/path.js'; +import { URI } from '../../../../base/common/uri.js'; +import { FilesFilter } from '../../files/browser/views/explorerViewer.js'; +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 { IFileService } from '../../../../platform/files/common/files.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IExplorerService } from '../../files/browser/files.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; + +export interface IDirectoryTreeService { + readonly _serviceBrand: undefined; + getDirectoryTreeWithVSCodeIgnores(directoryPath: string): Promise<{ content: string, cutOff: boolean }>; +} + +export const IDirectoryTreeService = createDecorator('voidDirectoryTreeService'); + +class DirectoryTreeService extends Disposable implements IDirectoryTreeService { + _serviceBrand: undefined; + + constructor( + @IFileService private readonly _fileService: IFileService, + @IConfigurationService private readonly _configService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IExplorerService private readonly explorerService: IExplorerService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService + ) { + super(); + } + + /** + * Prints a directory structure in a tree-like format, respecting gitignore patterns + * @param directoryPath The path to the directory to print + * @returns Object containing the formatted tree as a string and whether it was cut off + */ + public async getDirectoryTreeWithVSCodeIgnores(directoryPath: string): Promise<{ content: string, cutOff: boolean }> { + // Create a files filter instance + const filesFilter = new FilesFilter( + this.workspaceContextService, + this._configService, + this.explorerService, + this.editorService, + this.uriIdentityService, + this._fileService + ); + + const isPathIgnored = this.createVSCodeIgnoreCheck( + directoryPath, + filesFilter, + ); + + const MAX_CHARS = 20_000; + const result = await this.printDirectoryTree(this._fileService, directoryPath, '', isPathIgnored, MAX_CHARS); + + return { + content: result.content, + cutOff: result.cutOff + }; + } + + /** + * Prints a directory structure in a tree-like format, respecting gitignore patterns + * @param fileService The file service to use + * @param directoryPath The path to the directory to print + * @param indent Optional indentation for nested calls + * @param isPathIgnored Optional function to check if a path is ignored + * @param maxChars Maximum number of characters before cutting off + * @returns Object containing the formatted tree and cut-off status + */ + private async printDirectoryTree( + fileService: IFileService, + directoryPath: string, + indent: string = '', + isPathIgnored?: (path: string, isDirectory: boolean) => boolean, + maxChars: number = Infinity + ): Promise<{ content: string, cutOff: boolean }> { + let resolve: (result: { content: string, cutOff: boolean }) => void = () => undefined + const p = new Promise<{ content: string, cutOff: boolean }>((res) => { resolve = res }); + + try { + const directoryUri = URI.file(directoryPath); + const stat = await fileService.resolve(directoryUri); + if (!stat.isDirectory) { + resolve({ content: '', cutOff: false }); + return p; + } + + // For root level only + let result = ''; + let cutOff = false; + + if (indent === '') { + const baseName = path.basename(directoryPath); + result += baseName + '\n'; + + if (result.length >= maxChars) { + resolve({ content: result.substring(0, maxChars), cutOff: true }); + return p; + } + } + + // Separate directories and files + const directories: string[] = []; + const files: string[] = []; + + for (const entry of stat.children || []) { + const itemPath = entry.resource.fsPath; + const isDirectory = entry.isDirectory; + + // Skip ignored files/folders if isPathIgnored is provided + if (isPathIgnored && isPathIgnored(itemPath, isDirectory)) { + continue; + } + + if (isDirectory) { + directories.push(entry.name); + } else { + files.push(entry.name); + } + } + + // Process directories first, then files + const sortedItems = [...directories.sort(), ...files.sort()]; + + // Process each visible item + for (let i = 0; i < sortedItems.length; i++) { + // Check if we've reached the character limit + if (result.length >= maxChars) { + cutOff = true; + break; + } + + const item = sortedItems[i]; + const isLast = i === sortedItems.length - 1; + const itemPath = path.join(directoryPath, item); + const isDirectory = directories.includes(item); + + // Add the current item to the result + const itemLine = `${indent}|--${item}\n`; + + // Check if adding this line would exceed the limit + if (result.length + itemLine.length > maxChars) { + result += itemLine.substring(0, maxChars - result.length); + cutOff = true; + break; + } + + result += itemLine; + + // Recursively process directories + if (isDirectory) { + // Next level indentation + const childIndent = `${indent}${isLast ? ' ' : '| '}`; + const childResult = await this.printDirectoryTree( + fileService, + itemPath, + childIndent, + isPathIgnored, + maxChars - result.length + ); + + result += childResult.content; + + if (childResult.cutOff) { + cutOff = true; + break; + } + } + } + + resolve({ content: result, cutOff }); + } catch (error) { + const errorMessage = `Error: ${error.message}\n`; + const cutOff = errorMessage.length > maxChars; + resolve({ + content: cutOff ? errorMessage.substring(0, maxChars) : errorMessage, + cutOff + }); + } + return p; + } + + /** + * Creates a function that checks if a path should be ignored based on VS Code's FilesFilter + * @param directoryPath Root directory path + * @param filesFilter VS Code's FilesFilter instance + * @param fileService VS Code's FileService instance + * @param configService VS Code's ConfigurationService instance + * @param filesConfigService VS Code's FilesConfigurationService instance + * @returns A function that checks if a path is ignored + */ + private createVSCodeIgnoreCheck( + directoryPath: string, + filesFilter: FilesFilter, + ): (path: string, isDirectory: boolean) => boolean { + // Create a workspace folder URI (root explorer item) + const workspaceUri = URI.file(directoryPath); + + return (itemPath: string, isDirectory: boolean): boolean => { + try { + const itemUri = URI.file(itemPath); + + // Use FilesFilter.isIgnored to check if the item should be hidden based on VS Code's excludes + return filesFilter.isIgnored(itemUri, workspaceUri, isDirectory); + } catch (error) { + console.error(`Error checking if path is ignored: ${itemPath}`, error); + return false; + } + }; + } +} + +registerSingleton(IDirectoryTreeService, DirectoryTreeService, InstantiationType.Delayed); 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 0092cd24..d4a15157 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 @@ -1390,7 +1390,10 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - if (value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` + if (value.hasNextPage && params.pageNumber === 1) // first page + componentParams.desc2 = '(more content available)' + else if (params.pageNumber >= 1) // subsequent pages + componentParams.desc2 = `(part ${params.pageNumber})` } else { const { value, params } = toolMessage.result diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 503981c6..b51f8198 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -200,7 +200,11 @@ registerAction2(class extends Action2 { id: 'void.newChatAction', title: 'New Chat', icon: { id: 'add' }, - menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }] + menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }], + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, + weight: KeybindingWeight.VoidExtension, + }, }); } async run(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 88a45939..3eb78259 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -25,11 +25,9 @@ export const defaultModelsOfProvider = { 'grok-3-latest', ], gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini + 'gemini-2.5-pro-exp-03-25', 'gemini-2.0-flash', - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-2.0-flash-thinking-exp', + 'gemini-2.0-flash-lite', ], deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing 'deepseek-chat', @@ -144,6 +142,19 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: 'openai-style', reasoningCapabilities: false, }, + 'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + reasoningCapabilities: false, // built on qwen 2.5 32B instruct + }, + 'phi4': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: false, + reasoningCapabilities: false, + }, + // llama 'llama3': { supportsFIM: false, @@ -201,6 +212,9 @@ const openSourceModelOptions_assumingOAICompat = { const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { + + const lower = modelName.toLowerCase() + const toFallback = (opts: Omit): ModelOptions & { modelName: string } => { return { modelName, @@ -209,15 +223,19 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN cost: { input: 0, output: 0 }, } } - if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) - if (modelName.includes('claude-3-5') || modelName.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) - if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219']) - if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2']) - if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) + if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) + if (lower.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219']) + if (lower.includes('grok')) return toFallback(xAIModelOptions['grok-2']) + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('qwen') && lower.includes('2.5') && lower.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, contextWindow: 128_000, maxOutputTokens: 8_192, }) } + if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25']) + if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, contextWindow: 16_000, maxOutputTokens: 4_096, }) + if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], contextWindow: 128_000, maxOutputTokens: 4_096 }) // max output unclear if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) } @@ -294,12 +312,13 @@ const anthropicSettings: ProviderSettings = { }, modelOptions: anthropicModelOptions, modelOptionsFallback: (modelName) => { + const lower = modelName.toLowerCase() let fallbackName: keyof typeof anthropicModelOptions | null = null - if (modelName.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219' - if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' - if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' - if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229' - if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229' + if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219' + if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022' + if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022' + if (lower.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229' + if (lower.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229' if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] } return { modelName, ...modelOptionsDefaults, maxOutputTokens: 4_096 } }, @@ -359,10 +378,11 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing const openAISettings: ProviderSettings = { modelOptions: openAIModelOptions, modelOptionsFallback: (modelName) => { + const lower = modelName.toLowerCase() let fallbackName: keyof typeof openAIModelOptions | null = null - if (modelName.includes('o1')) { fallbackName = 'o1' } - if (modelName.includes('o3-mini')) { fallbackName = 'o3-mini' } - if (modelName.includes('gpt-4o')) { fallbackName = 'gpt-4o' } + if (lower.includes('o1')) { fallbackName = 'o1' } + if (lower.includes('o3-mini')) { fallbackName = 'o3-mini' } + if (lower.includes('gpt-4o')) { fallbackName = 'gpt-4o' } if (fallbackName) return { modelName: fallbackName, ...openAIModelOptions[fallbackName] } return null } @@ -384,8 +404,9 @@ const xAIModelOptions = { const xAISettings: ProviderSettings = { modelOptions: xAIModelOptions, modelOptionsFallback: (modelName) => { + const lower = modelName.toLowerCase() let fallbackName: keyof typeof xAIModelOptions | null = null - if (modelName.includes('grok-2')) fallbackName = 'grok-2' + if (lower.includes('grok-2')) fallbackName = 'grok-2' if (fallbackName) return { modelName: fallbackName, ...xAIModelOptions[fallbackName] } return null } @@ -394,6 +415,15 @@ const xAISettings: ProviderSettings = { // ---------------- GEMINI ---------------- const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing + 'gemini-2.5-pro-exp-03-25': { + contextWindow: 1_048_576, + maxOutputTokens: null, // 8_192, + cost: { input: 0, output: 0 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini + reasoningCapabilities: false, + }, 'gemini-2.0-flash': { contextWindow: 1_048_576, maxOutputTokens: null, // 8_192, @@ -660,8 +690,10 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting ollama: ollamaSettings, openAICompatible: openaiCompatible, + // TODO!!! // googleVertex: {}, // microsoftAzure: {}, + // openHands: {}, } as const @@ -669,8 +701,16 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting // returns the capabilities and the adjusted modelName if it was a fallback export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => { + const lowercaseModelName = modelName.toLowerCase() const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] - if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false } + + // search model options object directly first + for (const modelName_ in modelOptions) { + const lowercaseModelName_ = modelName_.toLowerCase() + if (lowercaseModelName === lowercaseModelName_) + return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false } + } + const result = modelOptionsFallback(modelName) if (result) return { ...result, isUnrecognizedModel: false } return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 9ca15419..a7166763 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -3,7 +3,6 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ - import { URI } from '../../../../../base/common/uri.js'; import { os } from '../helpers/systemInfo.js'; import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; @@ -12,7 +11,6 @@ import { IVoidModelService } from '../voidModelService.js'; import { EndOfLinePreference } from '../../../../../editor/common/model.js'; import { InternalToolInfo } from '../toolsServiceTypes.js'; - // this is just for ease of readability export const tripleTick = ['```', '```'] @@ -148,7 +146,7 @@ Here's an example of a good description:\n${editToolDescription}.` -export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ +export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, chatMode: mode }: { workspaceFolders: string[], openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.` : mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.` @@ -159,10 +157,12 @@ Please assist the user with their query. The user's query is never invalid. ${/* system info */''} The user's system information is as follows: - ${os} -- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} -${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\ -- Existing terminal IDs: ${runningTerminalIds.join(', ')} -`: '\n'} +- Open workspace(s): ${workspaceFolders.join(', ') || 'NO WORKSPACE OPEN'} +- Open tab(s): ${openedURIs.join(', ') || 'NO OPENED EDITORS'} +- Active tab: ${activeURI} +${(mode === 'agent') && runningTerminalIds.length !== 0 ? ` +- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''} + ${/* tool use */ mode === 'agent' || mode === 'gather' ? `\ You will be given tools you can call. ${mode === 'agent' ? `\ From c1ffca04a7e54892a85213e63645e51b717b25ff Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 00:26:20 -0700 Subject: [PATCH 11/30] recursive folder tool call and context --- .../contrib/void/browser/chatThreadService.ts | 29 +- .../void/browser/directoryStrService.ts | 318 ++++++++++++++++++ .../void/browser/directoryTreeService.ts | 222 ------------ .../react/src/sidebar-tsx/SidebarChat.tsx | 44 ++- .../contrib/void/browser/toolsService.ts | 91 ++--- .../contrib/void/common/directoryStrTypes.ts | 10 + .../contrib/void/common/prompt/prompts.ts | 21 +- .../contrib/void/common/toolsServiceTypes.ts | 16 +- 8 files changed, 440 insertions(+), 311 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/directoryStrService.ts delete mode 100644 src/vs/workbench/contrib/void/browser/directoryTreeService.ts create mode 100644 src/vs/workbench/contrib/void/common/directoryStrTypes.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 56308a40..e0032dae 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -34,6 +34,8 @@ import { IEditCodeService } from './editCodeServiceInterface.js'; import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { IDirectoryStrService } from './directoryStrService.js'; +import { truncate } from '../../../../base/common/strings.js'; /* @@ -245,7 +247,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IEditCodeService private readonly _editCodeService: IEditCodeService, @INotificationService private readonly _notificationService: INotificationService, @IModelService private readonly _modelService: IModelService, - + @IDirectoryStrService private readonly _directoryStrService: IDirectoryStrService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -582,8 +584,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; const activeURI = this._editorService.activeEditor?.resource?.fsPath; + const { wasCutOff, str: directoryStr_ } = await this._directoryStrService.getAllDirectoriesStr() + + const directoryStr = wasCutOff ? ( + chatMode === 'agent' || chatMode === 'gather' ? `${directoryStr_}\nString cut off, use tools to read more.` + : `${directoryStr_}\nString cut off, ask user for more if necessary.` + ) : directoryStr_ + const runningTerminalIds = this._terminalToolService.listTerminalIds() - const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, chatMode }) + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode }) // all messages so far in the chat history (including tools) const messages: LLMChatMessage[] = [ @@ -1052,22 +1061,23 @@ We only need to do it for files that were edited since `from`, ie files between private _wrapRunAgentToNotify(p: Promise, threadId: string) { - const notify = (error: string | null) => { + const notify = ({ error }: { error: string | null }) => { const thread = this.state.allThreads[threadId] if (!thread) return const userMsg = findLast(thread.messages, m => m.role === 'user') if (!userMsg) return if (userMsg.role !== 'user') return - const messageContent = userMsg.displayContent.substring(0, 50) + const messageContent = truncate(userMsg.displayContent, 50, '...') this._notificationService.notify({ severity: error ? Severity.Warning : Severity.Info, - message: error ? `Error: ${error} ` : `Task Complete!\n${messageContent}...`, + message: error ? `Error: ${error} ` : `A new Chat result is ready.`, + source: messageContent, actions: { - secondary: [{ + primary: [{ id: 'void.goToChat', enabled: true, - label: `View`, + label: `Jump to Chat`, tooltip: '', class: undefined, run: () => { @@ -1080,10 +1090,9 @@ We only need to do it for files that were edited since `from`, ie files between } p.then(() => { - notify(null) - + if (threadId !== this.state.currentThreadId) notify({ error: null }) }).catch((e) => { - notify(getErrorMessage(e)) + if (threadId !== this.state.currentThreadId) notify({ error: getErrorMessage(e) }) throw e }) } diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts new file mode 100644 index 00000000..d1423b04 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -0,0 +1,318 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +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 { IFileService } from '../../../../platform/files/common/files.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'; +import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js'; +import { IExplorerService } from '../../files/browser/files.js'; +import { SortOrder } from '../../files/common/files.js'; +import { ExplorerItem } from '../../files/common/explorerModel.js'; +import { VoidDirectoryItem } from '../common/directoryStrTypes.js'; + + +const MAX_CHARS_TOTAL_BEGINNING = 20_000 +const MAX_CHARS_TOTAL_TOOL = 20_000 +// const MAX_FILES_TOTAL = 200 + + +export interface IDirectoryStrService { + readonly _serviceBrand: undefined; + + getDirectoryStrTool(uri: URI): Promise<{ wasCutOff: boolean, str: string }> + getAllDirectoriesStr(): Promise<{ wasCutOff: boolean, str: string }> + +} +export const IDirectoryStrService = createDecorator('voidDirectoryStrService'); + + + + +// Check if it's a known filtered type like .git +const shouldExcludeDirectory = (item: ExplorerItem) => { + if (item.name === '.git' || + item.name === 'node_modules' || + item.name.startsWith('.') || + item.name === 'dist' || + item.name === 'build' || + item.name === 'out' || + item.name === 'bin' || + item.name === 'coverage' || + item.name === '__pycache__' || + item.name === 'env' || + item.name === 'venv' || + item.name === 'tmp' || + item.name === 'temp' || + item.name === 'artifacts' || + item.name === 'target' || + item.name === 'obj' || + item.name === 'vendor' || + item.name === 'logs' || + item.name === 'cache' + + ) { + return true; + } + return false; +} + +// ---------- ONE LAYER DEEP ---------- + +export const computeDirectoryTree1Deep = async ( + fileService: IFileService, + rootURI: URI, + pageNumber: number = 1, +): Promise => { + const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); + if (!stat.isDirectory) { + return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; + } + + const nChildren = stat.children?.length ?? 0; + + const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1); + const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE + const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1); + + const children: ShallowDirectoryItem[] = listChildren?.map(child => ({ + name: child.name, + uri: child.resource, + isDirectory: child.isDirectory, + isSymbolicLink: child.isSymbolicLink + })) ?? []; + + const hasNextPage = (nChildren - 1) > toChildIdx; + const hasPrevPage = pageNumber > 1; + const itemsRemaining = Math.max(0, nChildren - (toChildIdx + 1)); + + return { + children, + hasNextPage, + hasPrevPage, + itemsRemaining + }; +}; + +export const stringifyDirectoryTree1Deep = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => { + if (!result.children) { + return `Error: ${params.rootURI} is not a directory`; + } + + let output = ''; + const entries = result.children; + + if (!result.hasPrevPage) { // is first page + output += `${params.rootURI.fsPath}\n`; + } + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + const isLast = i === entries.length - 1 && !result.hasNextPage; + const prefix = isLast ? '└── ' : '├── '; + + output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`; + } + + if (result.hasNextPage) { + output += `└── (${result.itemsRemaining} results remaining...)\n`; + } + + return output; +}; + + +// ---------- IN GENERAL ---------- + + +// if the filter exists use it to filter out files and folders when creating the tree +const computeDirectoryTree = async ( + eItem: ExplorerItem, + explorerService: IExplorerService +): Promise => { + // Fetch children with default sort order + const eChildren = await eItem.fetchChildren(SortOrder.FilesFirst); + + const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem) + + // Process children recursively + const children = !isGitIgnoredDirectory ? await Promise.all( + eChildren.map(async c => await computeDirectoryTree(c, explorerService)) + ) : null + + // Create our directory item + const item: VoidDirectoryItem = { + uri: eItem.resource, + name: eItem.name, + isDirectory: eItem.isDirectory, + isSymbolicLink: eItem.isSymbolicLink, + children, + isGitIgnoredDirectory: isGitIgnoredDirectory && { numChildren: eItem.children.size }, + }; + + return item; +}; + + +const stringifyDirectoryTree = ( + node: VoidDirectoryItem, + MAX_CHARS: number, +): { content: string, wasCutOff: boolean } => { + let content = ''; + let wasCutOff = false; + + // If we're already exceeding the max characters, return immediately + if (MAX_CHARS <= 0) { + return { content, wasCutOff: true }; + } + + // Add the root node first (without tree characters) + const nodeLine = `${node.name}${node.isDirectory ? '/' : ''}${node.isSymbolicLink ? ' (symbolic link)' : ''}\n`; + + if (nodeLine.length > MAX_CHARS) { + return { content: '', wasCutOff: true }; + } + + content += nodeLine; + let remainingChars = MAX_CHARS - nodeLine.length; + + // Then recursively add all children with proper tree formatting + if (node.children && node.children.length > 0) { + const { childrenContent, childrenCutOff } = renderChildren( + node.children, + remainingChars, + '' + ); + content += childrenContent; + wasCutOff = childrenCutOff; + } + return { content, wasCutOff }; +}; + +// Helper function to render children with proper tree formatting +const renderChildren = ( + children: VoidDirectoryItem[], + maxChars: number, + parentPrefix: string +): { childrenContent: string, childrenCutOff: boolean } => { + let childrenContent = ''; + let childrenCutOff = false; + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const isLast = i === children.length - 1; + + // Create the tree branch symbols + const branchSymbol = isLast ? '└── ' : '├── '; + const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`; + + // Check if adding this line would exceed the limit + if (childrenContent.length + childLine.length > maxChars) { + childrenCutOff = true; + break; + } + childrenContent += childLine; + + const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ '); + + + // if gitignored, just say the number of children + if (child.isDirectory && child.isGitIgnoredDirectory && child.isGitIgnoredDirectory.numChildren > 0) { + childrenContent += `${nextLevelPrefix}└── ... (${child.isGitIgnoredDirectory.numChildren} children) ...\n` + } + + // Create the prefix for the next level (continuation line or space) + else if (child.children && child.children.length > 0) { + + const { + childrenContent: grandChildrenContent, + childrenCutOff: grandChildrenCutOff + } = renderChildren( + child.children, + maxChars, + nextLevelPrefix + ); + + // If adding grandchildren content would exceed the limit + if (childrenContent.length + grandChildrenContent.length > maxChars) { + childrenCutOff = true; + break; + } + + childrenContent += grandChildrenContent; + + if (grandChildrenCutOff) { + childrenCutOff = true; + break; + } + } + } + + return { childrenContent, childrenCutOff }; +}; + + +// --------------------------------------------------- + + +class DirectoryStrService extends Disposable implements IDirectoryStrService { + _serviceBrand: undefined; + + constructor( + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IExplorerService private readonly explorerService: IExplorerService, + ) { + super(); + } + + async getDirectoryStrTool(uri: URI) { + const eRoot = this.explorerService.findClosest(uri) + if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`) + + const dirTree = await computeDirectoryTree(eRoot, this.explorerService); + console.log('dirtree', dirTree) + const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL); + + return { + str: `Directory of ${uri.fsPath}:\n${content}`, + wasCutOff, + } + } + + async getAllDirectoriesStr() { + let str: string = ''; + let cutOff = false; + const folders = this.workspaceContextService.getWorkspace().folders; + + for (let i = 0; i < folders.length; i += 1) { + if (i > 0) str += '\n'; + + // this prioritizes filling 1st workspace before any other, etc + const f = folders[i]; + str += `Directory of ${f.uri.fsPath}:\n`; + const rootURI = f.uri; + + const eRoot = this.explorerService.findClosestRoot(rootURI); + if (!eRoot) continue; + + // Use our new approach with direct explorer service + const dirTree = await computeDirectoryTree(eRoot, this.explorerService); + console.log('dirtree', dirTree) + const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_BEGINNING - str.length); + str += content; + if (wasCutOff) { + cutOff = true; + break; + } + } + + return { wasCutOff: cutOff, str }; + } +} + +registerSingleton(IDirectoryStrService, DirectoryStrService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/directoryTreeService.ts b/src/vs/workbench/contrib/void/browser/directoryTreeService.ts deleted file mode 100644 index d6e6fd06..00000000 --- a/src/vs/workbench/contrib/void/browser/directoryTreeService.ts +++ /dev/null @@ -1,222 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import * as path from '../../../../base/common/path.js'; -import { URI } from '../../../../base/common/uri.js'; -import { FilesFilter } from '../../files/browser/views/explorerViewer.js'; -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 { IFileService } from '../../../../platform/files/common/files.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IExplorerService } from '../../files/browser/files.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; - -export interface IDirectoryTreeService { - readonly _serviceBrand: undefined; - getDirectoryTreeWithVSCodeIgnores(directoryPath: string): Promise<{ content: string, cutOff: boolean }>; -} - -export const IDirectoryTreeService = createDecorator('voidDirectoryTreeService'); - -class DirectoryTreeService extends Disposable implements IDirectoryTreeService { - _serviceBrand: undefined; - - constructor( - @IFileService private readonly _fileService: IFileService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IEditorService private readonly editorService: IEditorService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IExplorerService private readonly explorerService: IExplorerService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService - ) { - super(); - } - - /** - * Prints a directory structure in a tree-like format, respecting gitignore patterns - * @param directoryPath The path to the directory to print - * @returns Object containing the formatted tree as a string and whether it was cut off - */ - public async getDirectoryTreeWithVSCodeIgnores(directoryPath: string): Promise<{ content: string, cutOff: boolean }> { - // Create a files filter instance - const filesFilter = new FilesFilter( - this.workspaceContextService, - this._configService, - this.explorerService, - this.editorService, - this.uriIdentityService, - this._fileService - ); - - const isPathIgnored = this.createVSCodeIgnoreCheck( - directoryPath, - filesFilter, - ); - - const MAX_CHARS = 20_000; - const result = await this.printDirectoryTree(this._fileService, directoryPath, '', isPathIgnored, MAX_CHARS); - - return { - content: result.content, - cutOff: result.cutOff - }; - } - - /** - * Prints a directory structure in a tree-like format, respecting gitignore patterns - * @param fileService The file service to use - * @param directoryPath The path to the directory to print - * @param indent Optional indentation for nested calls - * @param isPathIgnored Optional function to check if a path is ignored - * @param maxChars Maximum number of characters before cutting off - * @returns Object containing the formatted tree and cut-off status - */ - private async printDirectoryTree( - fileService: IFileService, - directoryPath: string, - indent: string = '', - isPathIgnored?: (path: string, isDirectory: boolean) => boolean, - maxChars: number = Infinity - ): Promise<{ content: string, cutOff: boolean }> { - let resolve: (result: { content: string, cutOff: boolean }) => void = () => undefined - const p = new Promise<{ content: string, cutOff: boolean }>((res) => { resolve = res }); - - try { - const directoryUri = URI.file(directoryPath); - const stat = await fileService.resolve(directoryUri); - if (!stat.isDirectory) { - resolve({ content: '', cutOff: false }); - return p; - } - - // For root level only - let result = ''; - let cutOff = false; - - if (indent === '') { - const baseName = path.basename(directoryPath); - result += baseName + '\n'; - - if (result.length >= maxChars) { - resolve({ content: result.substring(0, maxChars), cutOff: true }); - return p; - } - } - - // Separate directories and files - const directories: string[] = []; - const files: string[] = []; - - for (const entry of stat.children || []) { - const itemPath = entry.resource.fsPath; - const isDirectory = entry.isDirectory; - - // Skip ignored files/folders if isPathIgnored is provided - if (isPathIgnored && isPathIgnored(itemPath, isDirectory)) { - continue; - } - - if (isDirectory) { - directories.push(entry.name); - } else { - files.push(entry.name); - } - } - - // Process directories first, then files - const sortedItems = [...directories.sort(), ...files.sort()]; - - // Process each visible item - for (let i = 0; i < sortedItems.length; i++) { - // Check if we've reached the character limit - if (result.length >= maxChars) { - cutOff = true; - break; - } - - const item = sortedItems[i]; - const isLast = i === sortedItems.length - 1; - const itemPath = path.join(directoryPath, item); - const isDirectory = directories.includes(item); - - // Add the current item to the result - const itemLine = `${indent}|--${item}\n`; - - // Check if adding this line would exceed the limit - if (result.length + itemLine.length > maxChars) { - result += itemLine.substring(0, maxChars - result.length); - cutOff = true; - break; - } - - result += itemLine; - - // Recursively process directories - if (isDirectory) { - // Next level indentation - const childIndent = `${indent}${isLast ? ' ' : '| '}`; - const childResult = await this.printDirectoryTree( - fileService, - itemPath, - childIndent, - isPathIgnored, - maxChars - result.length - ); - - result += childResult.content; - - if (childResult.cutOff) { - cutOff = true; - break; - } - } - } - - resolve({ content: result, cutOff }); - } catch (error) { - const errorMessage = `Error: ${error.message}\n`; - const cutOff = errorMessage.length > maxChars; - resolve({ - content: cutOff ? errorMessage.substring(0, maxChars) : errorMessage, - cutOff - }); - } - return p; - } - - /** - * Creates a function that checks if a path should be ignored based on VS Code's FilesFilter - * @param directoryPath Root directory path - * @param filesFilter VS Code's FilesFilter instance - * @param fileService VS Code's FileService instance - * @param configService VS Code's ConfigurationService instance - * @param filesConfigService VS Code's FilesConfigurationService instance - * @returns A function that checks if a path is ignored - */ - private createVSCodeIgnoreCheck( - directoryPath: string, - filesFilter: FilesFilter, - ): (path: string, isDirectory: boolean) => boolean { - // Create a workspace folder URI (root explorer item) - const workspaceUri = URI.file(directoryPath); - - return (itemPath: string, isDirectory: boolean): boolean => { - try { - const itemUri = URI.file(itemPath); - - // Use FilesFilter.isIgnored to check if the item should be hidden based on VS Code's excludes - return filesFilter.isIgnored(itemUri, workspaceUri, isDirectory); - } catch (error) { - console.error(`Error checking if path is ignored: ${itemPath}`, error); - return false; - } - }; - } -} - -registerSingleton(IDirectoryTreeService, DirectoryTreeService, InstantiationType.Delayed); 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 d4a15157..5083a79c 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 @@ -1180,6 +1180,7 @@ const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, + 'list_dir_recursive': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, 'grep_search': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, 'create_uri': { @@ -1392,7 +1393,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (value.hasNextPage && params.pageNumber === 1) // first page componentParams.desc2 = '(more content available)' - else if (params.pageNumber >= 1) // subsequent pages + else if (params.pageNumber > 1) // subsequent pages componentParams.desc2 = `(part ${params.pageNumber})` } else { @@ -1408,6 +1409,47 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return }, }, + 'list_dir_recursive': { + requestWrapper: null, + resultWrapper: ({ toolMessage }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const icon = null + + if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + + const isError = toolMessage.result.type === 'error' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + + if (toolMessage.result.type === 'success') { + const { value, params } = toolMessage.result + componentParams.children = + + + + + } + else { + const { value, params } = toolMessage.result + componentParams.children = + + {value} + + + } + + return + + } + }, 'list_dir': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 636a62b8..dd30ed71 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -8,11 +8,12 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' import { ITerminalToolService } from './terminalToolService.js' -import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js' +import { ToolCallParams, ToolName, ToolResultType } from '../common/toolsServiceTypes.js' import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' import { basename } from '../../../../base/common/path.js' import { IVoidCommandBarService } from './voidCommandBarService.js' +import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' // tool use for AI @@ -28,77 +29,14 @@ type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Tool // pagination info -const MAX_FILE_CHARS_PAGE = 50_000 -const MAX_CHILDREN_URIs_PAGE = 500 +export const MAX_FILE_CHARS_PAGE = 50_000 +export const MAX_CHILDREN_URIs_PAGE = 500 export const MAX_TERMINAL_CHARS_PAGE = 20_000 export const TERMINAL_TIMEOUT_TIME = 15 export const TERMINAL_BG_WAIT_TIME = 1 -const computeDirectoryResult = async ( - fileService: IFileService, - rootURI: URI, - pageNumber: number = 1 -): Promise => { - const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); - if (!stat.isDirectory) { - return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; - } - - const originalChildrenLength = stat.children?.length ?? 0; - const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1); - const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE - const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; - - const children: ToolDirectoryItem[] = listChildren.map(child => ({ - name: child.name, - uri: child.resource, - isDirectory: child.isDirectory, - isSymbolicLink: child.isSymbolicLink - })); - - const hasNextPage = (originalChildrenLength - 1) > toChildIdx; - const hasPrevPage = pageNumber > 1; - const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1)); - - return { - children, - hasNextPage, - hasPrevPage, - itemsRemaining - }; -}; - -const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => { - if (!result.children) { - return `Error: ${params.rootURI} is not a directory`; - } - - let output = ''; - const entries = result.children; - - if (!result.hasPrevPage) { // is first page - output += `${params.rootURI.fsPath}\n`; - } - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - const isLast = i === entries.length - 1 && !result.hasNextPage; - const prefix = isLast ? '└── ' : '├── '; - - output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`; - } - - if (result.hasNextPage) { - output += `└── (${result.itemsRemaining} results remaining...)\n`; - } - - return output; -}; - - - const validateJSON = (s: string): { [s: string]: unknown } => { @@ -195,6 +133,7 @@ export class ToolsService implements IToolsService { @IEditCodeService editCodeService: IEditCodeService, @ITerminalToolService private readonly terminalToolService: ITerminalToolService, @IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService, + @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -217,6 +156,12 @@ export class ToolsService implements IToolsService { const pageNumber = validatePageNum(pageNumberUnknown) return { rootURI: uri, pageNumber } }, + list_dir_recursive: async (params: string) => { + const o = validateJSON(params) + const { uri: uriStr, } = o + const uri = validateURI(uriStr) + return { rootURI: uri } + }, pathname_search: async (params: string) => { const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -294,10 +239,17 @@ export class ToolsService implements IToolsService { }, list_dir: async ({ rootURI, pageNumber }) => { - const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber) + const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber) return { result: dirResult } }, + list_dir_recursive: async ({ rootURI }) => { + const result = await this.directoryStrService.getDirectoryStrTool(rootURI) + let str = result.str + if (result.wasCutOff) str += '\n(Result was truncated)' + return { result: { str } } + }, + pathname_search: async ({ queryStr, pageNumber }) => { const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, @@ -385,9 +337,12 @@ export class ToolsService implements IToolsService { return result.fileContents + nextPageStr(result.hasNextPage) }, list_dir: (params, result) => { - const dirTreeStr = directoryResultToString(params, result) + const dirTreeStr = stringifyDirectoryTree1Deep(params, result) return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining }, + list_dir_recursive: (params, result) => { + return result.str + }, pathname_search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, diff --git a/src/vs/workbench/contrib/void/common/directoryStrTypes.ts b/src/vs/workbench/contrib/void/common/directoryStrTypes.ts new file mode 100644 index 00000000..c17c22b6 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/directoryStrTypes.ts @@ -0,0 +1,10 @@ +import { URI } from '../../../../base/common/uri.js'; + +export type VoidDirectoryItem = { + uri: URI; + name: string; + isSymbolicLink: boolean; + children: VoidDirectoryItem[] | null; + isDirectory: boolean; + isGitIgnoredDirectory: false | { numChildren: number }; // if directory is gitignored, we ignore children +} diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index a7166763..10613934 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -69,6 +69,14 @@ export const voidTools = { }, }, + list_dir_recursive: { + name: 'list_dir_recursive', + description: `Returns a tree diagram of all the files and folders in the URI. If results are large, the given string will be truncated (this will be indicated). If truncated, you should use this tool on a more specific folder, or just use list_dir which supports pagination but is not recursive.`, + params: { + ...uriParam('folder') + } + }, + pathname_search: { name: 'pathname_search', description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, @@ -146,7 +154,7 @@ Here's an example of a good description:\n${editToolDescription}.` -export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, chatMode: mode }: { workspaceFolders: string[], openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\ +export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.` : mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.` @@ -193,14 +201,21 @@ If you think it's appropriate to suggest an edit to a file, then you must descri - The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. - NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible. - Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise. -Here's an example of a good code block:\n${fileNameEdit}.\ +Here's an example of a good code block:\n${fileNameEdit}. + +If you write a code block that's related to a specific file, please use the same format as above: +- The first line of the code block must be the FULL PATH of the related file if known. +- The remaining contents of the file should proceed as usual. +\ `} ${/* misc */''} Misc: - Do not make things up. - Do not be lazy. - NEVER re-write the entire file. -- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ +- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}. +The user's codebase is structured as follows:\n${directoryStr} +\ ` // agent mode doesn't know about 1st line paths yet // - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path. diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 90d34311..bb746eed 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -2,18 +2,18 @@ import { URI } from '../../../../base/common/uri.js' import { voidTools } from './prompt/prompts.js'; -export type ToolDirectoryItem = { + + +export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } + +// Partial of IFileStat +export type ShallowDirectoryItem = { uri: URI; name: string; isDirectory: boolean; isSymbolicLink: boolean; } - -export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } - - - // we do this using Anthropic's style and convert to OpenAI style later export type InternalToolInfo = { name: string, @@ -43,6 +43,7 @@ export const toolNamesThatRequireApproval = new Set(toolNamesWithAppro export type ToolCallParams = { 'read_file': { uri: URI, pageNumber: number }, 'list_dir': { rootURI: URI, pageNumber: number }, + 'list_dir_recursive': { rootURI: URI }, 'pathname_search': { queryStr: string, pageNumber: number }, 'grep_search': { queryStr: string, pageNumber: number }, // --- @@ -55,7 +56,8 @@ export type ToolCallParams = { export type ToolResultType = { 'read_file': { fileContents: string, hasNextPage: boolean }, - 'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'list_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'list_dir_recursive': { str: string, }, 'pathname_search': { uris: URI[], hasNextPage: boolean }, 'grep_search': { uris: URI[], hasNextPage: boolean }, // --- From 2aae01f3b6accd56556b2db0116c3047263fc703 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 15:59:43 -0700 Subject: [PATCH 12/30] rename tools --- .../contrib/void/browser/aiRegexService.ts | 4 +- .../contrib/void/browser/chatThreadService.ts | 4 +- .../void/browser/directoryStrService.ts | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 68 +++++++++---------- .../contrib/void/browser/toolsService.ts | 54 +++++++-------- .../contrib/void/common/prompt/prompts.ts | 40 +++++------ .../contrib/void/common/toolsServiceTypes.ts | 38 +++++------ 7 files changed, 106 insertions(+), 106 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index 4e37f99c..53ed7e4e 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -34,7 +34,7 @@ // // const result = await new Promise((res, rej) => { // // sendLLMMessage({ // // messages, -// // tools: ['grep_search'], +// // tools: ['search_files'], // // onFinalMessage: ({ result: r, }) => { // // res(r) // // }, @@ -73,7 +73,7 @@ // // const result = new Promise((res, rej) => { // // sendLLMMessage({ // // messages, -// // tools: ['grep_search'], +// // tools: ['search_files'], // // onResult: (r) => { // // res(r) // // } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index e0032dae..55480022 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -651,7 +651,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') let interrupted = false try { - if (toolName === 'edit') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit']).uri }) } + if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) this._currentlyRunningToolInterruptor[threadId] = () => { @@ -1198,7 +1198,7 @@ We only need to do it for files that were edited since `from`, ie files between // else search codebase for `target` let uris: URI[] = [] try { - const { result } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, pageNumber: 0 }) uris = result.uris } catch (e) { return null diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index d1423b04..892f1d73 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -68,7 +68,7 @@ export const computeDirectoryTree1Deep = async ( fileService: IFileService, rootURI: URI, pageNumber: number = 1, -): Promise => { +): Promise => { const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); if (!stat.isDirectory) { return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; @@ -99,7 +99,7 @@ export const computeDirectoryTree1Deep = async ( }; }; -export const stringifyDirectoryTree1Deep = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => { +export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => { if (!result.children) { return `Error: ${params.rootURI} is not a directory`; } 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 5083a79c..ffdb1cd1 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 @@ -1178,23 +1178,23 @@ const loadingTitleWrapper = (item: React.ReactNode) => { } const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const titleOfToolName = { - 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, - 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, - 'list_dir_recursive': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, - 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, - 'grep_search': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, - 'create_uri': { + 'view_file_contents': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, + 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, + 'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, + 'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, + 'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, + 'create_file_or_folder': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`) }, - 'delete_uri': { + 'delete_file_or_folder': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) }, - 'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, - 'terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') } + 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, + 'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') } } as const satisfies Record @@ -1205,29 +1205,29 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName return ''; } - if (toolName === 'read_file') { - const toolParams = _toolParams as ToolCallParams['read_file'] + if (toolName === 'view_file_contents') { + const toolParams = _toolParams as ToolCallParams['view_file_contents'] return getBasename(toolParams.uri.fsPath); - } else if (toolName === 'list_dir') { - const toolParams = _toolParams as ToolCallParams['list_dir'] + } else if (toolName === 'ls_dir') { + const toolParams = _toolParams as ToolCallParams['ls_dir'] return `${getFolderName(toolParams.rootURI.fsPath)}`; - } else if (toolName === 'pathname_search') { - const toolParams = _toolParams as ToolCallParams['pathname_search'] + } else if (toolName === 'search_pathnames_only') { + const toolParams = _toolParams as ToolCallParams['search_pathnames_only'] return `"${toolParams.queryStr}"`; - } else if (toolName === 'grep_search') { - const toolParams = _toolParams as ToolCallParams['grep_search'] + } else if (toolName === 'search_files') { + const toolParams = _toolParams as ToolCallParams['search_files'] return `"${toolParams.queryStr}"`; - } else if (toolName === 'create_uri') { - const toolParams = _toolParams as ToolCallParams['create_uri'] + } else if (toolName === 'create_file_or_folder') { + const toolParams = _toolParams as ToolCallParams['create_file_or_folder'] return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); - } else if (toolName === 'delete_uri') { - const toolParams = _toolParams as ToolCallParams['delete_uri'] + } else if (toolName === 'delete_file_or_folder') { + const toolParams = _toolParams as ToolCallParams['delete_file_or_folder'] return toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) : getBasename(toolParams.uri.fsPath); - } else if (toolName === 'edit') { - const toolParams = _toolParams as ToolCallParams['edit'] + } else if (toolName === 'edit_file') { + const toolParams = _toolParams as ToolCallParams['edit_file'] return getBasename(toolParams.uri.fsPath); - } else if (toolName === 'terminal_command') { - const toolParams = _toolParams as ToolCallParams['terminal_command'] + } else if (toolName === 'run_terminal_command') { + const toolParams = _toolParams as ToolCallParams['run_terminal_command'] return `"${toolParams.command}"`; } else { return '' @@ -1373,7 +1373,7 @@ type ToolComponent = { } const toolNameToComponent: { [T in ToolName]: ToolComponent } = { - 'read_file': { + 'view_file_contents': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1409,7 +1409,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return }, }, - 'list_dir_recursive': { + 'get_dir_structure': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1450,7 +1450,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } }, - 'list_dir': { + 'ls_dir': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1497,7 +1497,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return } }, - 'pathname_search': { + 'search_pathnames_only': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1540,7 +1540,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return } }, - 'grep_search': { + 'search_files': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1585,7 +1585,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { // --- - 'create_uri': { + 'create_file_or_folder': { requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1633,7 +1633,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return } }, - 'delete_uri': { + 'delete_file_or_folder': { requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1683,7 +1683,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return } }, - 'edit': { + 'edit_file': { requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { const accessor = useAccessor() const isError = false @@ -1771,7 +1771,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { return } }, - 'terminal_command': { + 'run_terminal_command': { requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index dd30ed71..90103369 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -139,7 +139,7 @@ export class ToolsService implements IToolsService { const queryBuilder = instantiationService.createInstance(QueryBuilder); this.validateParams = { - read_file: async (params: string) => { + view_file_contents: async (params: string) => { const o = validateJSON(params) const { uri: uriStr, pageNumber: pageNumberUnknown } = o @@ -148,7 +148,7 @@ export class ToolsService implements IToolsService { return { uri, pageNumber } }, - list_dir: async (params: string) => { + ls_dir: async (params: string) => { const o = validateJSON(params) const { uri: uriStr, pageNumber: pageNumberUnknown } = o @@ -156,13 +156,13 @@ export class ToolsService implements IToolsService { const pageNumber = validatePageNum(pageNumberUnknown) return { rootURI: uri, pageNumber } }, - list_dir_recursive: async (params: string) => { + get_dir_structure: async (params: string) => { const o = validateJSON(params) const { uri: uriStr, } = o const uri = validateURI(uriStr) return { rootURI: uri } }, - pathname_search: async (params: string) => { + search_pathnames_only: async (params: string) => { const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -172,7 +172,7 @@ export class ToolsService implements IToolsService { return { queryStr, pageNumber } }, - grep_search: async (params: string) => { + search_files: async (params: string) => { const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -184,7 +184,7 @@ export class ToolsService implements IToolsService { // --- - create_uri: async (params: string) => { + create_file_or_folder: async (params: string) => { const o = validateJSON(params) const { uri: uriUnknown } = o const uri = validateURI(uriUnknown) @@ -193,7 +193,7 @@ export class ToolsService implements IToolsService { return { uri, isFolder } }, - delete_uri: async (params: string) => { + delete_file_or_folder: async (params: string) => { const o = validateJSON(params) const { uri: uriUnknown, params: paramsStr } = o const uri = validateURI(uriUnknown) @@ -203,7 +203,7 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, - edit: async (params: string) => { + edit_file: async (params: string) => { const o = validateJSON(params) const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o const uri = validateURI(uriStr) @@ -211,7 +211,7 @@ export class ToolsService implements IToolsService { return { uri, changeDescription } }, - terminal_command: async (s: string) => { + run_terminal_command: async (s: string) => { const o = validateJSON(s) const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o const command = validateStr('command', commandUnknown) @@ -224,7 +224,7 @@ export class ToolsService implements IToolsService { this.callTool = { - read_file: async ({ uri, pageNumber }) => { + view_file_contents: async ({ uri, pageNumber }) => { await voidModelService.initializeModel(uri) const { model } = await voidModelService.getModelSafe(uri) if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) } @@ -238,19 +238,19 @@ export class ToolsService implements IToolsService { return { result: { fileContents, hasNextPage } } }, - list_dir: async ({ rootURI, pageNumber }) => { + ls_dir: async ({ rootURI, pageNumber }) => { const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber) return { result: dirResult } }, - list_dir_recursive: async ({ rootURI }) => { + get_dir_structure: async ({ rootURI }) => { const result = await this.directoryStrService.getDirectoryStrTool(rootURI) let str = result.str if (result.wasCutOff) str += '\n(Result was truncated)' return { result: { str } } }, - pathname_search: async ({ queryStr, pageNumber }) => { + search_pathnames_only: async ({ queryStr, pageNumber }) => { const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) @@ -266,7 +266,7 @@ export class ToolsService implements IToolsService { return { result: { uris, hasNextPage } } }, - grep_search: async ({ queryStr, pageNumber }) => { + search_files: async ({ queryStr, pageNumber }) => { const query = queryBuilder.text({ pattern: queryStr, isRegExp: true, @@ -286,7 +286,7 @@ export class ToolsService implements IToolsService { // --- - create_uri: async ({ uri, isFolder }) => { + create_file_or_folder: async ({ uri, isFolder }) => { if (isFolder) await fileService.createFolder(uri) else { @@ -295,12 +295,12 @@ export class ToolsService implements IToolsService { return { result: {} } }, - delete_uri: async ({ uri, isRecursive }) => { + delete_file_or_folder: async ({ uri, isRecursive }) => { await fileService.del(uri, { recursive: isRecursive }) return { result: {} } }, - edit: async ({ uri, changeDescription }) => { + edit_file: async ({ uri, changeDescription }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`) @@ -322,7 +322,7 @@ export class ToolsService implements IToolsService { } return { result: applyDonePromise, interruptTool } }, - terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { + run_terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) return { result: { terminalId, didCreateTerminal, result, resolveReason } } }, @@ -333,33 +333,33 @@ export class ToolsService implements IToolsService { // given to the LLM after the call this.stringOfResult = { - read_file: (params, result) => { + view_file_contents: (params, result) => { return result.fileContents + nextPageStr(result.hasNextPage) }, - list_dir: (params, result) => { + ls_dir: (params, result) => { const dirTreeStr = stringifyDirectoryTree1Deep(params, result) return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining }, - list_dir_recursive: (params, result) => { + get_dir_structure: (params, result) => { return result.str }, - pathname_search: (params, result) => { + search_pathnames_only: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, - grep_search: (params, result) => { + search_files: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, // --- - create_uri: (params, result) => { + create_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully created.` }, - delete_uri: (params, result) => { + delete_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully deleted.` }, - edit: (params, result) => { + edit_file: (params, result) => { return `Change successfully made to ${params.uri.fsPath}.` }, - terminal_command: (params, result) => { + run_terminal_command: (params, result) => { const { terminalId, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 10613934..5af30dff 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -51,8 +51,8 @@ const uriParam = (object: string) => ({ export const voidTools = { // --- context-gathering (read/search/list) --- - read_file: { - name: 'read_file', + view_file_contents: { + name: 'view_file_contents', description: `Returns file contents of a given URI. ${paginationHelper.desc}`, params: { ...uriParam('file'), @@ -60,8 +60,8 @@ export const voidTools = { }, }, - list_dir: { - name: 'list_dir', + ls_dir: { + name: 'ls_dir', description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`, params: { ...uriParam('folder'), @@ -69,16 +69,16 @@ export const voidTools = { }, }, - list_dir_recursive: { - name: 'list_dir_recursive', - description: `Returns a tree diagram of all the files and folders in the URI. If results are large, the given string will be truncated (this will be indicated). If truncated, you should use this tool on a more specific folder, or just use list_dir which supports pagination but is not recursive.`, + get_dir_structure: { + name: 'get_dir_structure', + description: `Returns a tree diagram of all the files and folders in the URI. If results are large, the given string will be truncated (this will be indicated). If truncated, you should use this tool on a more specific folder, or just use ls_dir which supports pagination but is not recursive.`, params: { ...uriParam('folder') } }, - pathname_search: { - name: 'pathname_search', + search_pathnames_only: { + name: 'search_pathnames_only', description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, @@ -86,9 +86,9 @@ export const voidTools = { }, }, - grep_search: { - name: 'grep_search', - description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`, + search_files: { + name: 'search_files', + description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`view_file_contents\` tool to view the full file contents of results. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, ...paginationHelper.param, @@ -97,16 +97,16 @@ export const voidTools = { // --- editing (create/delete) --- - create_uri: { - name: 'create_uri', + create_file_or_folder: { + name: 'create_file_or_folder', description: `Create 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: { ...uriParam('file or folder'), }, }, - delete_uri: { - name: 'delete_uri', + delete_file_or_folder: { + name: 'delete_file_or_folder', description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, params: { ...uriParam('file or folder'), @@ -114,8 +114,8 @@ export const voidTools = { }, }, - edit: { // APPLY TOOL - name: 'edit', + edit_file: { // APPLY TOOL + name: 'edit_file', description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, params: { ...uriParam('file'), @@ -130,8 +130,8 @@ Here's an example of a good description:\n${editToolDescription}.` }, }, - terminal_command: { - name: 'terminal_command', + run_terminal_command: { + name: 'run_terminal_command', description: `Executes a terminal command.`, params: { command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' }, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index bb746eed..bb70a40e 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -36,34 +36,34 @@ export const isAToolName = (toolName: string): toolName is ToolName => { } -const toolNamesWithApproval = ['create_uri', 'delete_uri', 'edit', 'terminal_command'] as const satisfies readonly ToolName[] +const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[] export type ToolNameWithApproval = typeof toolNamesWithApproval[number] export const toolNamesThatRequireApproval = new Set(toolNamesWithApproval) export type ToolCallParams = { - 'read_file': { uri: URI, pageNumber: number }, - 'list_dir': { rootURI: URI, pageNumber: number }, - 'list_dir_recursive': { rootURI: URI }, - 'pathname_search': { queryStr: string, pageNumber: number }, - 'grep_search': { queryStr: string, pageNumber: number }, + 'view_file_contents': { uri: URI, pageNumber: number }, + 'ls_dir': { rootURI: URI, pageNumber: number }, + 'get_dir_structure': { rootURI: URI }, + 'search_pathnames_only': { queryStr: string, pageNumber: number }, + 'search_files': { queryStr: string, pageNumber: number }, // --- - 'edit': { uri: URI, changeDescription: string }, - 'create_uri': { uri: URI, isFolder: boolean }, - 'delete_uri': { uri: URI, isRecursive: boolean, isFolder: boolean }, - 'terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean }, + 'edit_file': { uri: URI, changeDescription: string }, + 'create_file_or_folder': { uri: URI, isFolder: boolean }, + 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, + 'run_terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean }, } export type ToolResultType = { - 'read_file': { fileContents: string, hasNextPage: boolean }, - 'list_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'list_dir_recursive': { str: string, }, - 'pathname_search': { uris: URI[], hasNextPage: boolean }, - 'grep_search': { uris: URI[], hasNextPage: boolean }, + 'view_file_contents': { fileContents: string, hasNextPage: boolean }, + 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'get_dir_structure': { str: string, }, + 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, + 'search_files': { uris: URI[], hasNextPage: boolean }, // --- - 'edit': Promise, - 'create_uri': {}, - 'delete_uri': {}, - 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, + 'edit_file': Promise, + 'create_file_or_folder': {}, + 'delete_file_or_folder': {}, + 'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, } From b440c8732f9c6034b7982375f649b5487fcffbee Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 16:59:03 -0700 Subject: [PATCH 13/30] search tools now support regex, reading line numbers, etc --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 10 +- .../contrib/void/browser/toolsService.ts | 103 ++++++++++++++---- .../contrib/void/common/prompt/prompts.ts | 23 +++- .../contrib/void/common/toolsServiceTypes.ts | 10 +- 5 files changed, 111 insertions(+), 37 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 55480022..92a8c25a 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -1198,7 +1198,7 @@ We only need to do it for files that were edited since `from`, ie files between // else search codebase for `target` let uris: URI[] = [] try { - const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, pageNumber: 0 }) + const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, include: null, pageNumber: 0 }) uris = result.uris } catch (e) { return null 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 ffdb1cd1..204aa9e7 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 @@ -541,7 +541,7 @@ export const SelectedFiles = ( } return ( -
+
{allSelections.map((selection, i) => { @@ -1178,7 +1178,7 @@ const loadingTitleWrapper = (item: React.ReactNode) => { } const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const titleOfToolName = { - 'view_file_contents': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, + 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, @@ -1205,8 +1205,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName return ''; } - if (toolName === 'view_file_contents') { - const toolParams = _toolParams as ToolCallParams['view_file_contents'] + if (toolName === 'read_file') { + const toolParams = _toolParams as ToolCallParams['read_file'] return getBasename(toolParams.uri.fsPath); } else if (toolName === 'ls_dir') { const toolParams = _toolParams as ToolCallParams['ls_dir'] @@ -1373,7 +1373,7 @@ type ToolComponent = { } const toolNameToComponent: { [T in ToolName]: ToolComponent } = { - 'view_file_contents': { + 'read_file': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 90103369..255c9112 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -55,7 +55,9 @@ const validateJSON = (s: string): { [s: string]: unknown } => { } } - +const isFalsy = (u: unknown) => { + return !u || u === 'null' || u === 'undefined' +} const validateStr = (argName: string, value: unknown) => { if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`) @@ -64,13 +66,24 @@ const validateStr = (argName: string, value: unknown) => { // We are NOT checking to make sure in workspace +// TODO!!!! check to make sure folder/file exists const validateURI = (uriStr: unknown) => { if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.') - const uri = URI.file(uriStr) return uri } +const validateOptionalURI = (uriStr: unknown) => { + if (isFalsy(uriStr)) return null + return validateURI(uriStr) +} + +const validateOptionalStr = (argName: string, str: unknown) => { + if (isFalsy(str)) return null + return validateStr(argName, str) +} + + const validatePageNum = (pageNumberUnknown: unknown) => { if (!pageNumberUnknown) return 1 const parsedInt = Number.parseInt(pageNumberUnknown + '') @@ -79,6 +92,20 @@ const validatePageNum = (pageNumberUnknown: unknown) => { return parsedInt } +const validateNumber = (numStr: unknown, opts: { default: number | null }) => { + if (typeof numStr === 'number') + return numStr + if (isFalsy(numStr)) return opts.default + + if (typeof numStr === 'string') { + const parsedInt = Number.parseInt(numStr + '') + if (!Number.isInteger(parsedInt)) return opts.default + return parsedInt + } + + return opts.default +} + const validateRecursiveParamStr = (paramsUnknown: unknown) => { if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.') const params = paramsUnknown @@ -92,12 +119,15 @@ const validateProposedTerminalId = (terminalIdUnknown: unknown) => { return terminalId } -const validateWaitForCompletion = (b: unknown) => { +const validateBoolean = (b: unknown, opts: { default: boolean }) => { if (typeof b === 'string') { if (b === 'true') return true if (b === 'false') return false } - return true // default is true + if (typeof b === 'boolean') { + return b + } + return opts.default } @@ -139,14 +169,17 @@ export class ToolsService implements IToolsService { const queryBuilder = instantiationService.createInstance(QueryBuilder); this.validateParams = { - view_file_contents: async (params: string) => { + read_file: async (params: string) => { const o = validateJSON(params) - const { uri: uriStr, pageNumber: pageNumberUnknown } = o + const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - return { uri, pageNumber } + const startLine = validateNumber(startLineUnknown, { default: null }) + const endLine = validateNumber(endLineUnknown, { default: null }) + + return { uri, startLine, endLine, pageNumber } }, ls_dir: async (params: string) => { const o = validateJSON(params) @@ -164,22 +197,35 @@ export class ToolsService implements IToolsService { }, search_pathnames_only: async (params: string) => { const o = validateJSON(params) - const { query: queryUnknown, pageNumber: pageNumberUnknown } = o + const { + query: queryUnknown, + include: includeUnknown, + pageNumber: pageNumberUnknown + } = o const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) + const include = validateOptionalStr('include', includeUnknown) - return { queryStr, pageNumber } + return { queryStr, include, pageNumber } }, search_files: async (params: string) => { const o = validateJSON(params) - const { query: queryUnknown, pageNumber: pageNumberUnknown } = o + const { + query: queryUnknown, + searchInFolder: searchInFolderUnknown, + isRegex: isRegexUnknown, + pageNumber: pageNumberUnknown + } = o const queryStr = validateStr('query', queryUnknown) const pageNumber = validatePageNum(pageNumberUnknown) - return { queryStr, pageNumber } + const searchInFolder = validateOptionalURI(searchInFolderUnknown) + const isRegex = validateBoolean(isRegexUnknown, { default: false }) + + return { queryStr, searchInFolder, isRegex, pageNumber } }, // --- @@ -216,7 +262,7 @@ export class ToolsService implements IToolsService { const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o const command = validateStr('command', commandUnknown) const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) - const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown) + const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true }) return { command, proposedTerminalId, waitForCompletion } }, @@ -224,16 +270,25 @@ export class ToolsService implements IToolsService { this.callTool = { - view_file_contents: async ({ uri, pageNumber }) => { + read_file: async ({ uri, startLine, endLine, pageNumber }) => { await voidModelService.initializeModel(uri) const { model } = await voidModelService.getModelSafe(uri) if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) } - const readFileContents = model.getValue(EndOfLinePreference.LF) + + let contents: string + if (startLine === null && endLine === null) { + contents = model.getValue(EndOfLinePreference.LF) + } + else { + const startLineNumber = startLine === null ? 1 : startLine + const endLineNumber = endLine === null ? model.getLineCount() : endLine + contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF) + } const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate - const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 + const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate + const hasNextPage = (contents.length - 1) - toIdx >= 1 return { result: { fileContents, hasNextPage } } }, @@ -250,9 +305,11 @@ export class ToolsService implements IToolsService { return { result: { str } } }, - search_pathnames_only: async ({ queryStr, pageNumber }) => { + search_pathnames_only: async ({ queryStr, include, pageNumber }) => { + const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, + includePattern: include ?? undefined, }) const data = await searchService.fileSearch(query, CancellationToken.None) @@ -266,11 +323,15 @@ export class ToolsService implements IToolsService { return { result: { uris, hasNextPage } } }, - search_files: async ({ queryStr, pageNumber }) => { + search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => { + const searchFolders = searchInFolder === null ? + workspaceContextService.getWorkspace().folders.map(f => f.uri) + : [searchInFolder] + const query = queryBuilder.text({ pattern: queryStr, - isRegExp: true, - }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) + isRegExp: isRegex, + }, searchFolders) const data = await searchService.textSearch(query, CancellationToken.None) @@ -333,7 +394,7 @@ export class ToolsService implements IToolsService { // given to the LLM after the call this.stringOfResult = { - view_file_contents: (params, result) => { + read_file: (params, result) => { return result.fileContents + nextPageStr(result.hasNextPage) }, ls_dir: (params, result) => { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 5af30dff..8724c51b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -48,14 +48,23 @@ const uriParam = (object: string) => ({ uri: { type: 'string', description: `The FULL path to the ${object}.` } }) + +const searchParams = { + searchInFolder: { type: 'string', description: 'Only search files in this given folder. Leave as empty to search all available files.' }, + isRegex: { type: 'string', description: 'Whether to treat the query as a regular expression. Default is "false".' }, +} as const + + export const voidTools = { // --- context-gathering (read/search/list) --- - view_file_contents: { - name: 'view_file_contents', + read_file: { + name: 'read_file', description: `Returns file contents of a given URI. ${paginationHelper.desc}`, params: { ...uriParam('file'), + startLine: { type: 'string', description: 'Line to start reading from. Default is "null", treated as 1.' }, + endLine: { type: 'string', description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' }, ...paginationHelper.param, }, }, @@ -71,7 +80,7 @@ export const voidTools = { get_dir_structure: { name: 'get_dir_structure', - description: `Returns a tree diagram of all the files and folders in the URI. If results are large, the given string will be truncated (this will be indicated). If truncated, you should use this tool on a more specific folder, or just use ls_dir which supports pagination but is not recursive.`, + description: `Returns a tree diagram of all the files and folders in the given folder URI. Call this to learn more about a folder. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`, params: { ...uriParam('folder') } @@ -79,18 +88,20 @@ export const voidTools = { search_pathnames_only: { name: 'search_pathnames_only', - description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, + description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, + ...searchParams, ...paginationHelper.param, }, }, search_files: { name: 'search_files', - description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`view_file_contents\` tool to view the full file contents of results. ${paginationHelper.desc}`, + description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, + ...searchParams, ...paginationHelper.param, }, }, @@ -110,7 +121,7 @@ export const voidTools = { description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, params: { ...uriParam('file or folder'), - params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + params: { type: 'string', description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' } }, }, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index bb70a40e..54e88d51 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -40,12 +40,13 @@ const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', export type ToolNameWithApproval = typeof toolNamesWithApproval[number] export const toolNamesThatRequireApproval = new Set(toolNamesWithApproval) +// PARAMS OF TOOL CALL export type ToolCallParams = { - 'view_file_contents': { uri: URI, pageNumber: number }, + 'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number }, 'ls_dir': { rootURI: URI, pageNumber: number }, 'get_dir_structure': { rootURI: URI }, - 'search_pathnames_only': { queryStr: string, pageNumber: number }, - 'search_files': { queryStr: string, pageNumber: number }, + 'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number }, + 'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number }, // --- 'edit_file': { uri: URI, changeDescription: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, @@ -54,8 +55,9 @@ export type ToolCallParams = { } +// RESULT OF TOOL CALL export type ToolResultType = { - 'view_file_contents': { fileContents: string, hasNextPage: boolean }, + 'read_file': { fileContents: string, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'get_dir_structure': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, From 1d9e0faaa312263731cc91f505a0c29067519f72 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 20:40:59 -0700 Subject: [PATCH 14/30] make chats by reference --- .../contrib/void/browser/chatThreadService.ts | 69 ++++--- .../react/src/sidebar-tsx/SidebarChat.tsx | 96 +++------- .../contrib/void/browser/sidebarActions.ts | 88 +++++---- .../void/common/chatThreadServiceTypes.ts | 40 ++-- .../contrib/void/common/prompt/prompts.ts | 174 ++++++++++-------- 5 files changed, 220 insertions(+), 247 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 92a8c25a..87ba8fff 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,7 +11,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString, voidTools } from '../common/prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, voidTools } from '../common/prompt/prompts.js'; import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -186,9 +186,9 @@ export interface IChatThreadService { getCurrentFocusedMessageIdx(): number | undefined; isCurrentlyFocusingMessage(): boolean; setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void; - // current thread's staging selections - closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void; - closeCurrentStagingSelectionsInThread(): void; + // // current thread's staging selections + // closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void; + // closeCurrentStagingSelectionsInThread(): void; // codespan links (link to symbols in the markdown) getCodespanLink(opts: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined; @@ -294,11 +294,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const newStagingSelection: StagingSelectionItem = { type: 'File', - fileURI: newModel.uri, + uri: newModel.uri, language: newModel.getLanguageId(), - selectionStr: null, - range: null, - state: { isOpened: false, wasAddedAsCurrentFile: true } + state: { wasAddedAsCurrentFile: true } } const focusedMessageIdx = this.getCurrentFocusedMessageIdx(); @@ -312,7 +310,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); // add the new file if it doesn't exist - const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.fsPath === newStagingSelection.fileURI.fsPath) + const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) if (!fileIsAdded) { newStagingSelections.push(newStagingSelection) } @@ -549,8 +547,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { private async _runChatAgent({ threadId, - prevSelns, - currSelns, modelSelection, modelSelectionOptions, userMessageContent, @@ -565,11 +561,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { callThisToolFirst?: ToolRequestApproval }) { - - // define helper functions so we can tell what's going on - // for now, do not recompute selections as we run (it seems to confuse tool-use models) - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped - const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files + const userMessageFullContent = userMessageContent const getLatestMessages = async () => { // replace last userMessage with userMessageFullContent (which contains all the files too) const thread = this.state.allThreads[threadId] @@ -1112,7 +1104,11 @@ We only need to do it for files that were edited since `from`, ie files between // add user's message to chat history const instructions = userMessage - const userMessageContent = await chat_userMessageContent(instructions, currSelns) // user message + names of files (NOT content) + const { chatMode } = this._settingsService.state.globalSettings + + const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const + + const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) @@ -1166,7 +1162,7 @@ We only need to do it for files that were edited since `from`, ie files between // get history of all AI and user added files in conversation + store in reverse order (MRU) const prevUris = this._getAllSelections(threadId) - .map(s => s.fileURI) + .map(s => s.uri) .filter((uri, index, array) => array.findIndex(u => u.fsPath === uri.fsPath) === index) // O(n^2) but this is small .reverse() @@ -1407,12 +1403,9 @@ We only need to do it for files that were edited since `from`, ie files between this._setThreadState(this.state.currentThreadId, { stagingSelections: [{ type: 'File', - fileURI: model.uri, + uri: model.uri, language: model.getLanguageId(), - selectionStr: null, - range: null, state: { - isOpened: false, wasAddedAsCurrentFile: true } }] @@ -1523,31 +1516,31 @@ We only need to do it for files that were edited since `from`, ie files between } - closeCurrentStagingSelectionsInThread = () => { - const currThread = this.getCurrentThreadState() + // closeCurrentStagingSelectionsInThread = () => { + // const currThread = this.getCurrentThreadState() - // close all stagingSelections - const closedStagingSelections = currThread.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) + // // close all stagingSelections + // const closedStagingSelections = currThread.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) - const newThread = currThread - newThread.stagingSelections = closedStagingSelections + // const newThread = currThread + // newThread.stagingSelections = closedStagingSelections - this.setCurrentThreadState(newThread) + // this.setCurrentThreadState(newThread) - } + // } - closeCurrentStagingSelectionsInMessage: IChatThreadService['closeCurrentStagingSelectionsInMessage'] = ({ messageIdx }) => { - const currMessage = this.getCurrentMessageState(messageIdx) + // closeCurrentStagingSelectionsInMessage: IChatThreadService['closeCurrentStagingSelectionsInMessage'] = ({ messageIdx }) => { + // const currMessage = this.getCurrentMessageState(messageIdx) - // close all stagingSelections - const closedStagingSelections = currMessage.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) + // // close all stagingSelections + // const closedStagingSelections = currMessage.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) - const newMessage = currMessage - newMessage.stagingSelections = closedStagingSelections + // const newMessage = currMessage + // newMessage.stagingSelections = closedStagingSelections - this.setCurrentMessageState(messageIdx, newMessage) + // this.setCurrentMessageState(messageIdx, newMessage) - } + // } 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 204aa9e7..be8e7e57 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 @@ -507,18 +507,16 @@ export const SelectedFiles = ( useEffect(() => { const computeRecents = async () => { const prospectiveURIs = recentUris - .filter(uri => !selections.find(s => s.type === 'File' && s.fileURI.fsPath === uri.fsPath)) + .filter(uri => !selections.find(s => s.type === 'File' && s.uri.fsPath === uri.fsPath)) .slice(0, maxProspectiveFiles) const answer: StagingSelectionItem[] = [] for (const uri of prospectiveURIs) { answer.push({ type: 'File', - fileURI: uri, + uri: uri, language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext', - selectionStr: null, - range: null, - state: { isOpened: false, wasAddedAsCurrentFile: false }, + state: { wasAddedAsCurrentFile: false }, }) } return answer @@ -545,19 +543,13 @@ export const SelectedFiles = ( {allSelections.map((selection, i) => { - const isThisSelectionOpened = (!!selection.selectionStr && selection.state.isOpened && type === 'staging') - const isThisSelectionAFile = selection.selectionStr === null const isThisSelectionProspective = i > selections.length - 1 - const isThisSelectionAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` return
{/* summarybox */}
{ - // const newS = [...s] - // newS[i] = !newS[i] - // return newS - // }); - + } + else if (selection.type === 'CodeSelection') { + commandService.executeCommand('vscode.open', selection.uri, { + preview: true, + // TODO!!! open in range + }); + } + else if (selection.type === 'Folder') { + // TODO!!! reveal in tree } }} > { // file name and range - getBasename(selection.fileURI.fsPath) - + (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`) + getBasename(selection.uri.fsPath) + + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') } - {isThisSelectionAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.fileURI.fsPath && + {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? {`(Current File)`} + : null } {type === 'staging' && !isThisSelectionProspective ? // X button @@ -642,27 +627,6 @@ export const SelectedFiles = ( : <> }
- - {/* code box */} - {isThisSelectionOpened ? -
{ - e.stopPropagation(); // don't focus input box - }} - > - -
- : <> - }
})} @@ -840,13 +804,13 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB const canInitialize = mode === 'edit' && textAreaRefState const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { - setStagingSelections((chatMessage.selections || []) - .map(s => { // quick hack so we dont have to do anything more - const sNew = s - sNew.state.wasAddedAsCurrentFile = false // wipe all "current file" info when the user first edits a message - return sNew + setStagingSelections( + (chatMessage.selections || []).map(s => { // quick hack so we dont have to do anything more + if (s.type === 'File') return { ...s, state: { ...s.state, wasAddedAsCurrentFile: false, } } + else return s }) ) + if (textAreaFnsRef.current) textAreaFnsRef.current.setValue(chatMessage.displayContent || '') @@ -896,7 +860,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB // update state setIsBeingEdited(false) chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) - chatThreadsService.closeCurrentStagingSelectionsInMessage({ messageIdx }) // stream the edit const userMessage = textAreaRefState.value; @@ -2033,9 +1996,6 @@ export const SidebarChat = () => { const threadId = chatThreadsService.state.currentThreadId - // update state - chatThreadsService.closeCurrentStagingSelectionsInThread() // close all selections - // send message to LLM const userMessage = textAreaRef.current?.value ?? '' diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index b51f8198..ad453853 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -14,7 +14,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { ITextModel } from '../../../../editor/common/model.js'; import { VOID_VIEW_ID } from './sidebarPane.js'; import { IMetricsService } from '../common/metricsService.js'; import { ISidebarStateService } from './sidebarStateService.js'; @@ -53,23 +52,41 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e return newRange } -const getContentInRange = (model: ITextModel, range: IRange | null) => { - if (!range) - return null - const content = model.getValueInRange(range) - const trimmedContent = content - .replace(/^\s*\n/g, '') // trim pure whitespace lines from start - .replace(/\n\s*$/g, '') // trim pure whitespace lines from end - return trimmedContent -} +// const getContentInRange = (model: ITextModel, range: IRange | null) => { +// if (!range) +// return null +// const content = model.getValueInRange(range) +// const trimmedContent = content +// .replace(/^\s*\n/g, '') // trim pure whitespace lines from start +// .replace(/\n\s*$/g, '') // trim pure whitespace lines from end +// return trimmedContent +// } -const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => { - return currentSelections?.findIndex(s => - s.fileURI.fsPath === newSelection.fileURI.fsPath - && s.range?.startLineNumber === newSelection.range?.startLineNumber - && s.range?.endLineNumber === newSelection.range?.endLineNumber - ) +const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => { + if (!currentSelections) return null + + for (let i = 0; i < currentSelections.length; i += 1) { + const s = currentSelections[i] + + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + + if (s.type === 'File' && newSelection.type === 'File') { + return [i, s] as const + } + if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') { + if (s.uri.fsPath !== newSelection.uri.fsPath) continue + // if there's any collision return true + const [oldStart, oldEnd] = s.range + const [newStart, newEnd] = newSelection.range + if (oldStart !== newStart || oldEnd !== newEnd) continue + return [i, s] as const + } + if (s.type === 'Folder' && newSelection.type === 'Folder') { + return [i, s] as const + } + } + return null } const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open' @@ -114,22 +131,18 @@ registerAction2(class extends Action2 { editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) } - const selectionStr = getContentInRange(model, selectionRange) - const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { + const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { type: 'File', - fileURI: model.uri, + uri: model.uri, language: model.getLanguageId(), - selectionStr: null, - range: null, - state: { isOpened: false, wasAddedAsCurrentFile: false } + state: { wasAddedAsCurrentFile: false } } : { - type: 'Selection', - fileURI: model.uri, + type: 'CodeSelection', + uri: model.uri, language: model.getLanguageId(), - selectionStr: selectionStr, - range: selectionRange, - state: { isOpened: true, wasAddedAsCurrentFile: false } + range: [selectionRange.startLineNumber, selectionRange.endLineNumber], + state: { wasAddedAsCurrentFile: false } } // update the staging selections @@ -149,17 +162,18 @@ registerAction2(class extends Action2 { setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) } - // close all selections besides the new one - selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } })) - // if matches with existing selection, overwrite (since text may change) - const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection) - if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) { - setSelections([ - ...selections!.slice(0, matchingStagingEltIdx), - selection, - ...selections!.slice(matchingStagingEltIdx + 1, Infinity) - ]) + const replaceRes = findStagingItemToReplace(selections, selection) + if (replaceRes) { + const [idx, newSel] = replaceRes + + if (idx !== undefined && idx !== -1) { + setSelections([ + ...selections!.slice(0, idx), + newSel, + ...selections!.slice(idx + 1, Infinity) + ]) + } } // if no match, add it else { diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index b95bc67d..0c9cbebb 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------*/ import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { VoidFileSnapshot } from './editCodeServiceTypes.js'; import { AnthropicReasoning } from './sendLLMMessageTypes.js'; import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; @@ -66,34 +65,25 @@ export type ChatMessage = | CheckpointEntry -// 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; - language: string; - selectionStr: string; - range: IRange; - state: { - isOpened: boolean; - wasAddedAsCurrentFile: boolean; - }; -} - -export type FileSelection = { +// one of the square items that indicates a selection in a chat bubble +export type StagingSelectionItem = { type: 'File'; - fileURI: URI; + uri: URI; language: string; - selectionStr: null; - range: null; - state: { - isOpened: boolean; - wasAddedAsCurrentFile: boolean; - }; + state: { wasAddedAsCurrentFile: boolean; }; +} | { + type: 'CodeSelection'; + range: [number, number]; + uri: URI; + language: string; + state: { wasAddedAsCurrentFile: boolean; }; +} | { + type: 'Folder'; + uri: URI; + language?: undefined; + state?: undefined; } -export type StagingSelectionItem = CodeSelection | FileSelection - - // a link to a symbol (an underlined link to a piece of code) export type CodespanLocationLink = { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 8724c51b..1cfa7c4e 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -3,13 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { URI } from '../../../../../base/common/uri.js'; import { os } from '../helpers/systemInfo.js'; -import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; +import { StagingSelectionItem } from '../chatThreadServiceTypes.js'; import { ChatMode } from '../voidSettingsTypes.js'; +import { InternalToolInfo } from '../toolsServiceTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { EndOfLinePreference } from '../../../../../editor/common/model.js'; -import { InternalToolInfo } from '../toolsServiceTypes.js'; // this is just for ease of readability export const tripleTick = ['```', '```'] @@ -232,96 +231,113 @@ The user's codebase is structured as follows:\n${directoryStr} // - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path. -type FileSelnLocal = { fileURI: URI, language: string, content: string } -const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => { - return `\ -${fileURI.fsPath} -${tripleTick[0]}${language} -${content} -${tripleTick[1]} -` -} -const stringifyCodeSelection = ({ fileURI, language, selectionStr, range }: CodeSelection) => { - return `\ -${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber}) -${tripleTick[0]}${language} -${selectionStr} -${tripleTick[1]} -` -} +// type FileSelnLocal = { fileURI: URI, language: string, content: string } +// const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => { +// return `\ +// ${fileURI.fsPath} +// ${tripleTick[0]}${language} +// ${content} +// ${tripleTick[1]} +// ` +// } +// const stringifyCodeSelection = ({ uri, language, range }: StagingSelectionItem & { type: 'CodeSelection' }) => { +// return `\ -const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' -const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => { - if (fileSelections.length === 0) return null - const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { - const { model } = await voidModelService.getModelSafe(sel.fileURI) - const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr - return { ...sel, content } - })) - return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') -} +// ${tripleTick[0]}${language} +// ${selectionStr} +// ${tripleTick[1]} +// ` +// } + +// const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' +// const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => { +// if (fileSelections.length === 0) return null +// const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { +// const { model } = await voidModelService.getModelSafe(sel.fileURI) +// const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr +// return { ...sel, content } +// })) +// return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') +// } -const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { - return codeSelections.map(sel => { - stringifyCodeSelection(sel) - }).join('\n') || null -} - -const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { - if (!currSelns) return '' - return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') -} -export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => { +// export const chat_selectionsString = async ( +// prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, +// voidModelService: IVoidModelService, +// ) => { - const selnsStr = stringifySelectionNames(currSelns) +// // ADD IN FILES AT TOP +// const allSelections = [...currSelns || [], ...prevSelns || []] - let str = '' - if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` } - str += `\nINSTRUCTIONS\n${instructions}` - return str; -}; +// if (allSelections.length === 0) return null -export const chat_selectionsString = async ( - prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, - voidModelService: IVoidModelService, +// for (const selection of allSelections) { +// if (selection.type === 'Selection') { +// codeSelections.push(selection) +// } +// else if (selection.type === 'File') { +// const fileSelection = selection +// const path = fileSelection.fileURI.fsPath +// if (!filesURIs.has(path)) { +// filesURIs.add(path) +// fileSelections.push(fileSelection) +// } +// } +// } + +// const filesStr = await stringifyFileSelections(fileSelections, voidModelService) +// const selnsStr = stringifyCodeSelections(codeSelections) + +// const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n') +// return fileContents || null +// } + +// export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => { +// if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}` +// else return userMessage +// } + +export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null, + opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService } ) => { - // ADD IN FILES AT TOP - const allSelections = [...currSelns || [], ...prevSelns || []] + const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})` + let selnsStrs: string[] = [] + if (opts.type === 'references') { + selnsStrs = currSelns?.map((s) => { + if (s.type === 'File') return `${s.uri.fsPath}` + if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}` + if (s.type === 'Folder') return `${s.uri.fsPath}/` + return '' + }) ?? [] + } + if (opts.type === 'fullCode') { + selnsStrs = await Promise.all(currSelns?.map(async (s) => { + if (s.type === 'File' || s.type === 'CodeSelection') { + const voidModelService = opts.voidModelService + const { model } = await voidModelService.getModelSafe(s.uri) + if (!model) return '' + const val = model.getValue(EndOfLinePreference.LF) - if (allSelections.length === 0) return null - - const codeSelections: CodeSelection[] = [] - const fileSelections: FileSelection[] = [] - const filesURIs = new Set() - - for (const selection of allSelections) { - if (selection.type === 'Selection') { - codeSelections.push(selection) - } - else if (selection.type === 'File') { - const fileSelection = selection - const path = fileSelection.fileURI.fsPath - if (!filesURIs.has(path)) { - filesURIs.add(path) - fileSelections.push(fileSelection) + const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : '' + const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}` + return str } - } + if (s.type === 'Folder') { + // TODO + return '' + } + return '' + }) ?? []) } - const filesStr = await stringifyFileSelections(fileSelections, voidModelService) - const selnsStr = stringifyCodeSelections(codeSelections) - - const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n') - return fileContents || null -} - -export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => { - if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}` - else return userMessage + const selnsStr = selnsStrs.join('\n') ?? '' + let str = '' + str += `${instructions}` + if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}` + return str; } From 3c93e0a414d7adf8af71824769b494d2eb96442c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 20:56:08 -0700 Subject: [PATCH 15/30] separate out thinking by feature --- .../void/browser/autocompleteService.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 7 ++--- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 ++-- .../contrib/void/common/storageKeys.ts | 19 ++++++++++++ .../void/common/voidSettingsService.ts | 29 +++++++++---------- .../contrib/void/common/voidSettingsTypes.ts | 9 +++++- 6 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/storageKeys.ts diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 34c7025c..b8c6c466 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -793,7 +793,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const featureName: FeatureName = 'Autocomplete' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined // set parameters of `newAutocompletion` appropriately diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 87ba8fff..224a57c6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -36,6 +36,7 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { IModelService } from '../../../../editor/common/services/model.js'; import { IDirectoryStrService } from './directoryStrService.js'; import { truncate } from '../../../../base/common/strings.js'; +import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; /* @@ -155,10 +156,6 @@ const newThreadObject = () => { } -// past values: -// 'void.chatThreadStorage' - -export const THREAD_STORAGE_KEY = 'void.chatThreadStorageI' @@ -455,7 +452,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) const featureName: FeatureName = 'Chat' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined return { modelSelection, modelSelectionOptions } } 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 be8e7e57..2499ae26 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 @@ -157,7 +157,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const { reasoningCapabilities } = getModelCapabilities(providerName, modelName) const { canTurnOffReasoning, reasoningBudgetSlider } = reasoningCapabilities || {} - const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[providerName]?.[modelName] + const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions) if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) return null // unused right now @@ -174,7 +174,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => if (reasoningBudgetSlider?.type === 'slider') { // if it's a slider const { min: min_, max, default: defaultVal } = reasoningBudgetSlider - const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + const value = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters const stepSize = Math.round((max - min_) / nSteps) @@ -191,7 +191,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => value={value} onChange={(newVal) => { const disabled = newVal === min && canTurnOffReasoning - voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !disabled, reasoningBudget: newVal }) + voidSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !disabled, reasoningBudget: newVal }) }} /> {isReasoningEnabled ? `${value} tokens` : 'Thinking disabled'} diff --git a/src/vs/workbench/contrib/void/common/storageKeys.ts b/src/vs/workbench/contrib/void/common/storageKeys.ts new file mode 100644 index 00000000..476c05b1 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/storageKeys.ts @@ -0,0 +1,19 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +// past values: +// 'void.settingsServiceStorage' +// 'void.settingsServiceStorageI' // 1.0.2 + +// 1.0.3 +export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII' + + +// past values: +// 'void.chatThreadStorage' +// 'void.chatThreadStorageI' // 1.0.2 + +// 1.0.3 +export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII' diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index e631a8aa..47558e71 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -12,13 +12,9 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; import { getModelCapabilities } from './modelCapabilities.js'; +import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js'; -// past values: -// 'void.settingsServiceStorage' - -const STORAGE_KEY = 'void.settingsServiceStorageI' - // name is the name in the dropdown export type ModelOption = { name: string, selection: ModelSelection } @@ -38,7 +34,7 @@ type SetModelSelectionOfFeatureFn = ( type SetGlobalSettingFn = (settingName: T, newVal: GlobalSettings[T]) => void; -type SetOptionsOfModelSelection = (providerName: ProviderName, modelName: string, newVal: Partial) => void +type SetOptionsOfModelSelection = (featureName: FeatureName, providerName: ProviderName, modelName: string, newVal: Partial) => void export type VoidSettingsState = { @@ -177,7 +173,7 @@ const defaultState = () => { settingsOfProvider: deepClone(defaultSettingsOfProvider), modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null }, globalSettings: deepClone(defaultGlobalSettings), - optionsOfModelSelection: {}, + optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} }, _modelOptions: [], // computed later } return d @@ -227,7 +223,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { private async _readState(): Promise { - const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION) + const encryptedState = this._storageService.get(VOID_SETTINGS_STORAGE_KEY, StorageScope.APPLICATION) if (!encryptedState) return defaultState() @@ -240,7 +236,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { private async _storeState() { const state = this.state const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state)) - this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER); + this._storageService.store(VOID_SETTINGS_STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER); } setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => { @@ -318,16 +314,19 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - setOptionsOfModelSelection = async (providerName: ProviderName, modelName: string, newVal: Partial) => { + setOptionsOfModelSelection = async (featureName: FeatureName, providerName: ProviderName, modelName: string, newVal: Partial) => { const newState: VoidSettingsState = { ...this.state, optionsOfModelSelection: { ...this.state.optionsOfModelSelection, - [providerName]: { - ...this.state.optionsOfModelSelection[providerName], - [modelName]: { - ...this.state.optionsOfModelSelection[providerName]?.[modelName], - ...newVal + [featureName]: { + ...this.state.optionsOfModelSelection[featureName], + [providerName]: { + ...this.state.optionsOfModelSelection[featureName][providerName], + [modelName]: { + ...this.state.optionsOfModelSelection[featureName][providerName]?.[modelName], + ...newVal + } } } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index acfe57f0..bbb274a4 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -420,4 +420,11 @@ export type ModelSelectionOptions = { reasoningBudget?: number; } -export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }> +export type OptionsOfModelSelection = { + [featureName in FeatureName]: Partial<{ + [providerName in ProviderName]: { + [modelName: string]: + ModelSelectionOptions | undefined + } + }> +} From 67a253164eb66c56a137c11888ff7e4229bbc997 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 20:56:28 -0700 Subject: [PATCH 16/30] + --- src/vs/workbench/contrib/void/browser/editCodeService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a8c15870..ef4ccdc9 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1361,7 +1361,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined // allowed to throw errors - this is called inside a promise that handles everything const runWriteover = async () => { @@ -1569,7 +1569,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const featureName: FeatureName = 'Apply' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined const N_RETRIES = 5 From a5ac79c6a43e43d81c75321e0df1b053e1b02c45 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 21:06:27 -0700 Subject: [PATCH 17/30] gemma --- src/vs/workbench/contrib/void/common/modelCapabilities.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 3eb78259..b1316474 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -155,6 +155,13 @@ const openSourceModelOptions_assumingOAICompat = { reasoningCapabilities: false, }, + 'gemma': { // https://news.ycombinator.com/item?id=43451406 + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: false, + reasoningCapabilities: false, + }, + // llama 'llama3': { supportsFIM: false, @@ -235,6 +242,7 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, contextWindow: 128_000, maxOutputTokens: 8_192, }) } if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25']) if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, contextWindow: 16_000, maxOutputTokens: 4_096, }) + if (lower.includes('gemma')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.gemma, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], contextWindow: 128_000, maxOutputTokens: 4_096 }) // max output unclear if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) From bd0db41f6700a0adc88b29a491b0ff3b37303af8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 5 Apr 2025 21:13:03 -0700 Subject: [PATCH 18/30] compare by ignoring whitespace --- .../contrib/void/browser/editCodeService.ts | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ef4ccdc9..2b4d2eae 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -108,14 +108,31 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number }; +// Helper function to remove whitespace except newlines +const removeWhitespaceExceptNewlines = (str: string): string => { + return str.replace(/[^\S\n]+/g, ''); +} + + + // finds block.orig in fileContents and return its range in file // startingAtLine is 1-indexed and inclusive -const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => { - const idx = fileContents.indexOf(text, - startingAtLine !== undefined ? - fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine - : 0 - ) +const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => { + + const startLineIdx = (fileContents: string) => startingAtLine !== undefined ? + fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine + : 0 + + // idx = starting index in fileContents + let idx = fileContents.indexOf(text, startLineIdx(fileContents)) + + // try to find it ignoring all whitespace this time + if (idx === -1 && canFallbackToRemoveWhitespace) { + text = removeWhitespaceExceptNewlines(text) + fileContents = removeWhitespaceExceptNewlines(fileContents) + idx = fileContents.indexOf(text, startLineIdx(fileContents)); + } + if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const @@ -141,10 +158,8 @@ class EditCodeService extends Disposable implements IEditCodeService { diffAreaOfId: Record = {}; // diffareaId -> diffArea diffOfId: Record = {}; // diffid -> diff (redundant with diffArea._diffOfId) - // events - // uri: diffZones // listen on change diffZones private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event; @@ -1617,7 +1632,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // update stream state to the first line of original if some portion of original has been written if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine) + const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) if (typeof originalRange !== 'string') { const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) diffZone._streamState.line = startLine @@ -1644,7 +1659,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!(blockNum in addedTrackingZoneOfBlockNum)) { - const originalBounds = findTextInCode(block.orig, originalFileCode) + const originalBounds = findTextInCode(block.orig, originalFileCode, true) // if error if (typeof originalBounds === 'string') { console.log('--------------Error finding text in code:') From 033de587f2c936d50851dba3f0c876d66aef5d9c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 03:29:45 -0700 Subject: [PATCH 19/30] improve checkpoint logic + UI --- .../contrib/void/browser/chatThreadService.ts | 158 ++++++------ .../src/markdown/ApplyBlockHoverButtons.tsx | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 243 ++++++++++-------- .../src/sidebar-tsx/SidebarThreadSelector.tsx | 2 +- .../contrib/void/common/prompt/prompts.ts | 3 +- 5 files changed, 231 insertions(+), 181 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 224a57c6..16cf18e9 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -55,6 +55,9 @@ LLM Edit x LLM Edit + +INVARIANT: +A checkpoint appears before every LLM message, and before every user message (before user really means directly after LLM is done). */ @@ -99,7 +102,7 @@ type ThreadType = { // this doesn't need to go in a state object, but feels right state: { - currCheckpointIdx: number | null; // the latest checkpoint we're at (always defined unless chat is empty so there are no checkpts) + currCheckpointIdx: number | null; // the latest checkpoint we're at (null if not at a particular checkpoint, like if the chat is streaming, or chat just finished and we haven't clicked on a checkpt) stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the user message that is being edited (undefined if none) @@ -775,8 +778,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if awaiting user approval, keep isRunning true, else end isRunning this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge') - // if successful, add checkpoint - this._addUserCheckpoint({ threadId }) + // add checkpoint before the next user message + if (!isRunningWhenEnd) + this._addUserCheckpoint({ threadId }) // capture number of messages sent this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) @@ -785,31 +789,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { private _addCheckpoint(threadId: string, checkpoint: CheckpointEntry) { this._addMessageToThread(threadId, checkpoint) - // update latest checkpoint idx to the one we just added - const newThread = this.state.allThreads[threadId] - if (!newThread) return // should never happen - const currCheckpointIdx = newThread.messages.length - 1 - this._setThreadState(threadId, { currCheckpointIdx }) + // // update latest checkpoint idx to the one we just added + // const newThread = this.state.allThreads[threadId] + // if (!newThread) return // should never happen + // const currCheckpointIdx = newThread.messages.length - 1 + // this._setThreadState(threadId, { currCheckpointIdx: currCheckpointIdx }) } - // merge any LLM checkpoint before this one (and after a user checkpoint if one exists), and add the checkpoint - // call this right after LLM edits a file - private _addToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { - const thread = this.state.allThreads[threadId] - if (!thread) return - const { model } = this._voidModelService.getModel(uri) - if (!model) return // should never happen - const diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri) - - this._addCheckpoint(threadId, { - role: 'checkpoint', - type: 'tool_edit', - voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot }, - userModifications: { voidFileSnapshotOfURI: {} }, - }) - - } private _editMessageInThread(threadId: string, messageIdx: number, newMessage: ChatMessage,) { const { allThreads } = this.state @@ -833,17 +820,25 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => { + const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null + if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } } - private _computeCheckpointInfo({ threadId }: { threadId: string }) { + const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null + return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, } + } + + private _computeNewCheckpointInfo({ threadId }: { threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return - const { currCheckpointIdx } = thread.state - if (currCheckpointIdx === null) return + + const lastCheckpointIdx = findLastIdx(thread.messages, (m) => m.role === 'checkpoint') ?? -1 + if (lastCheckpointIdx === -1) return const voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined } = {} // add a change for all the URIs in the checkpoint history - const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: currCheckpointIdx, }) ?? {} + const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: lastCheckpointIdx, }) ?? {} for (const fsPath in lastIdxOfURI ?? {}) { const { model } = this._voidModelService.getModelFromFsPath(fsPath) if (!model) continue @@ -871,44 +866,32 @@ class ChatThreadService extends Disposable implements IChatThreadService { return { voidFileSnapshotOfURI } } - // call this right before user sends message or reverts + private _addUserCheckpoint({ threadId }: { threadId: string }) { - const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {} + const { voidFileSnapshotOfURI } = this._computeNewCheckpointInfo({ threadId }) ?? {} this._addCheckpoint(threadId, { role: 'checkpoint', type: 'user_edit', voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, - userModifications: { - voidFileSnapshotOfURI: {}, - }, + userModifications: { voidFileSnapshotOfURI: {}, }, }) } - private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) { - const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {} - - const res = this._getCurrentCheckpoint(threadId) - if (!res) return - const [checkpoint, checkpointIdx] = res - this._editMessageInThread(threadId, checkpointIdx, { - ...checkpoint, - userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, }, - }) - - } - - private _getCurrentCheckpoint(threadId: string): [CheckpointEntry, number] | undefined { + // call this right after LLM edits a file + private _addToolEditCheckpoint({ threadId, uri, }: { threadId: string, uri: URI }) { const thread = this.state.allThreads[threadId] if (!thread) return - - const { currCheckpointIdx } = thread.state - if (currCheckpointIdx === null) return - - const checkpoint = thread.messages[currCheckpointIdx] - if (!checkpoint) return - if (checkpoint.role !== 'checkpoint') return - return [checkpoint, currCheckpointIdx] + const { model } = this._voidModelService.getModel(uri) + if (!model) return // should never happen + const diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri) + this._addCheckpoint(threadId, { + role: 'checkpoint', + type: 'tool_edit', + voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot }, + userModifications: { voidFileSnapshotOfURI: {} }, + }) } + private _getCheckpointBeforeMessage = ({ threadId, messageIdx }: { threadId: string, messageIdx: number }): [CheckpointEntry, number] | undefined => { const thread = this.state.allThreads[threadId] if (!thread) return undefined @@ -927,7 +910,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const lastIdxOfURI: { [fsPath: string]: number } = {} for (let i = loIdx; i <= hiIdx; i += 1) { const message = thread.messages[i] - if (message.role !== 'checkpoint') continue + if (message?.role !== 'checkpoint') continue for (const fsPath in message.voidFileSnapshotOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes lastIdxOfURI[fsPath] = i } @@ -935,27 +918,49 @@ class ChatThreadService extends Disposable implements IChatThreadService { return { lastIdxOfURI } } - private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => { - const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null - if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } } + private _readCurrentCheckpoint(threadId: string): [CheckpointEntry, number] | undefined { + const thread = this.state.allThreads[threadId] + if (!thread) return - const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null - return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, } + const { currCheckpointIdx } = thread.state + if (currCheckpointIdx === null) return + + const checkpoint = thread.messages[currCheckpointIdx] + if (!checkpoint) return + if (checkpoint.role !== 'checkpoint') return + return [checkpoint, currCheckpointIdx] + } + private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) { + const { voidFileSnapshotOfURI } = this._computeNewCheckpointInfo({ threadId }) ?? {} + const res = this._readCurrentCheckpoint(threadId) + if (!res) return + const [checkpoint, checkpointIdx] = res + this._editMessageInThread(threadId, checkpointIdx, { + ...checkpoint, + userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, }, + }) } - // private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => { - // const { model } = this._voidModelService.getModelFromFsPath(fsPath) - // if (!model) return // should never happen - // model.applyEdits([{ - // range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file - // text - // }]) - // } - - jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) { + private _makeUsStandOnCheckpoint({ threadId }: { threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return + if (thread.state.currCheckpointIdx === null) { + const lastMsg = thread.messages[thread.messages.length - 1] + if (lastMsg?.role !== 'checkpoint') + this._addUserCheckpoint({ threadId }) + this._setThreadState(threadId, { currCheckpointIdx: thread.messages.length - 1 }) + } + } + + jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) { + + // if null, add a new temp checkpoint so user can jump forward again + this._makeUsStandOnCheckpoint({ threadId }) + + const thread = this.state.allThreads[threadId] + if (!thread) return + if (this.streamState[threadId]?.isRunning) return const c = this._getCheckpointBeforeMessage({ threadId, messageIdx }) if (c === undefined) return // should never happen @@ -1045,7 +1050,6 @@ We only need to do it for files that were edited since `from`, ie files between } this._setThreadState(threadId, { currCheckpointIdx: toIdx }) - // TODO!!! add/merge a checkpoint modification if relevant } @@ -1090,6 +1094,12 @@ We only need to do it for files that were edited since `from`, ie files between const thread = this.state.allThreads[threadId] if (!thread) return // should never happen + + // add dummy before this message to keep checkpoint before user message idea consistent + if (thread.messages.length === 0) { + this._addUserCheckpoint({ threadId }) + } + // if the current thread is already streaming, stop it (this simply resolves the promise to free up space) const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) @@ -1109,6 +1119,8 @@ We only need to do it for files that were edited since `from`, ie files between const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) + this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming + this._wrapRunAgentToNotify( this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }), threadId, 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 7dfbf73c..c2ef204e 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 @@ -191,7 +191,7 @@ export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, u
} -export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { +export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') @@ -255,7 +255,7 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string } if (currStreamState === 'idle-no-changes') { - return + return } if (currStreamState === 'idle-has-changes') { @@ -322,7 +322,7 @@ export const BlockCodeApplyWrapper = ({
{currStreamState === 'idle-no-changes' && } - +
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 2499ae26..9c31e142 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 @@ -22,7 +22,7 @@ import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; -import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; +import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -769,7 +769,7 @@ const SimplifiedToolHeader = ({ -const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, _scrollToBottom: (() => void) | null }) => { +const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -886,7 +886,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB } } - if (!chatMessage.content && isCommitted) { // don't show if empty and not loading (if loading, want to show). + if (!chatMessage.content) { // don't show if empty and not loading (if loading, want to show). return null } @@ -929,6 +929,8 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted, _scrollToB ${mode === 'edit' ? 'w-full max-w-full' : mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre } + + ${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''} `} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -1062,12 +1064,11 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => { {children}
} -const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, isToolBeingWritten }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType, isToolBeingWritten: boolean }) => { +const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const reasoningStr = chatMessage.reasoning?.trim() || null const hasReasoning = !!reasoningStr const isDoneReasoning = !!chatMessage.content @@ -1080,34 +1081,36 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas } const isEmpty = !chatMessage.content && !chatMessage.reasoning - const isLoading = !isCommitted && !isToolBeingWritten && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') - const isLastAndLoading = isLast && isLoading - if (isEmpty && !isLastAndLoading) return null + if (isEmpty) return null return <> {/* reasoning token */} - {hasReasoning && - - - - } + {hasReasoning && +
+ + + + + +
+ } {/* assistant message */} - - - {/* loading indicator */} - {isLoading && } - +
+ + + +
} @@ -1321,7 +1324,7 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin {currStreamState === 'idle-no-changes' && } - +
} @@ -1822,20 +1825,26 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { }; -const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => { +const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIsRunning }: { message: CheckpointEntry, threadId: string; messageIdx: number, isCheckpointGhost: boolean, threadIsRunning: boolean }) => { const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - // const commandBarService = accessor.get('IVoidCommandBarService') + return
{ - // reject all current changes and then jump back - // commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' }) - chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) - }}> -
-
Checkpoint
-
+ className={` + flex items-center justify-center + px-2 text-xs text-void-fg-3 + ${isCheckpointGhost ? 'opacity-50' : ''} + `} + > +
{ + if (threadIsRunning) return + chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) + }} + > + Checkpoint +
} @@ -1845,45 +1854,57 @@ type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isCommitted: boolean, - isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message) + canAcceptReject: boolean, chatIsRunning: IsRunningType, threadId: string, - isToolBeingWritten: boolean, + currCheckpointIdx: number, _scrollToBottom: (() => void) | null, } -const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId, isToolBeingWritten, _scrollToBottom }: ChatBubbleProps) => { +const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, canAcceptReject, chatIsRunning, threadId, _scrollToBottom }: ChatBubbleProps) => { const role = chatMessage.role + const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts) + if (role === 'user') { return } else if (role === 'assistant') { return } else if (role === 'tool_request') { const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper - const toolRequestType = ( + const toolRequestState = ( chatIsRunning === 'awaiting_user' ? 'awaiting_user' : chatIsRunning === 'tool' ? 'running' - : null + : chatIsRunning === 'message' ? null + : null ) - if (ToolRequestWrapper && isLast) { // if it's the last message + if (ToolRequestWrapper && canAcceptReject) { // if it's the last message return <> - {toolRequestType !== null && } - {chatIsRunning === 'awaiting_user' && } + {toolRequestState !== null && +
+ +
} + {chatIsRunning === 'awaiting_user' && +
+ +
} } return null @@ -1891,17 +1912,26 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin else if (role === 'tool') { const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper if (ToolResultWrapper) - return + return
+ +
return null } else if (role === 'checkpoint') { - return + return } - else if (role === 'checkpoint_modification') { - return - } } @@ -1974,7 +2004,7 @@ export const SidebarChat = () => { const toolNameSoFar = currThreadStreamState?.toolNameSoFar const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar - const toolIsLoading = !!toolNameSoFar && toolNameSoFar === 'edit' // show loading for slow tools (right now just edit) + const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2024,36 +2054,37 @@ export const SidebarChat = () => { scrollContainerRef.current?.scrollTo({ top: 0, left: 0 }) }, [isHistoryOpen, currentThread.id]) - const numMessages = previousMessages.length - const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') - - const previousMessagesHTML = useMemo(() => { - const threadId = currentThread.id - const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) - - return previousMessages.map((message, i) => { - const isLast = i === lastMessageIdx && (isRunning === 'tool' || isRunning === 'awaiting_user') - return
- scrollToBottom(scrollContainerRef)} - /> -
- }) - }, [previousMessages, isRunning, currentThread, numMessages]) const threadId = currentThread.id + const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) + + const previousMessagesHTML = useMemo(() => { + const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') + + console.log('PREVMSGS', previousMessages) + + return previousMessages.map((message, i) => { + const canAcceptReject = i === lastMessageIdx && message.role === 'tool_request' + + return scrollToBottom(scrollContainerRef)} + /> + }) + }, [previousMessages, isRunning, threadId]) + const streamingChatIdx = previousMessagesHTML.length - const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ? - { anthropicReasoning: null, }} messageIdx={streamingChatIdx} - isCommitted={!isRunning} + isCommitted={false} chatIsRunning={isRunning} - isLast={true} + canAcceptReject={false} + threadId={threadId} - isToolBeingWritten={toolIsLoading} _scrollToBottom={null} /> : null - const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar - const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed - const currStreamingToolHTML = toolIsLoading ? - Generating} /> - : null - - const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] - - const threadSelector =
- -
+ const proposedToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar + const generatingToolTitle = typeof proposedToolTitle === 'function' ? proposedToolTitle(null) : proposedToolTitle const messagesHTML = { `} > {/* previous messages */} - {allMessagesHTML} + {previousMessagesHTML} + + + {currStreamingMessageHTML} + + + {toolIsGenerating ? + Generating} /> + : null} + + {isRunning === 'message' && !toolIsGenerating ? + {/* loading indicator */} + {} + : null} {/* error message */} @@ -2158,7 +2191,11 @@ export const SidebarChat = () => { return (
- {threadSelector} + {/* History selector */} +
+ +
+
{messagesHTML} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 2e0b4bd2..7db16221 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -72,7 +72,7 @@ export const SidebarThreadSelector = () => { let firstMsg = null; // let secondMsg = null; - const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request'); + const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user'); if (firstUserMsgIdx !== -1) { // firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? ''); diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 1cfa7c4e..34410b97 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -165,7 +165,7 @@ Here's an example of a good description:\n${editToolDescription}.` export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\ -You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.` : mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.` : mode === 'normal' ? `to assist the user with their coding tasks.` @@ -224,6 +224,7 @@ Misc: - Do not be lazy. - NEVER re-write the entire file. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}. +- Today's date is ${new Date().toDateString()} The user's codebase is structured as follows:\n${directoryStr} \ ` From f6358e319bd1c3d6e839026dcfe06da5914c261a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 17:31:11 -0700 Subject: [PATCH 20/30] model capabilities progress --- .../contrib/void/common/modelCapabilities.ts | 96 ++++++++++++++++--- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index b1316474..a5c488e3 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -123,36 +123,53 @@ const modelOptionsDefaults: ModelOptions = { } + +// TODO!!! double check all context sizes below +// TODO!!! add openrouter common models +// TODO!!! allow user to modify capabilities and tell them if autodetected model or falling back + const openSourceModelOptions_assumingOAICompat = { 'deepseekR1': { supportsFIM: false, supportsSystemMessage: false, supportsTools: false, reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, + contextWindow: 32_000, maxOutputTokens: 4_096, + }, + 'deepseekCoderV3': { + supportsFIM: false, + supportsSystemMessage: false, // unstable + supportsTools: false, + reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'deepseekCoderV2': { supportsFIM: false, supportsSystemMessage: false, // unstable supportsTools: false, reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'codestral': { supportsFIM: true, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, // built on qwen 2.5 32B instruct + contextWindow: 128_000, maxOutputTokens: 4_096 }, 'phi4': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: false, reasoningCapabilities: false, + contextWindow: 16_000, maxOutputTokens: 4_096, }, 'gemma': { // https://news.ycombinator.com/item?id=43451406 @@ -160,32 +177,52 @@ const openSourceModelOptions_assumingOAICompat = { supportsSystemMessage: 'system-role', supportsTools: false, reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, + }, + // llama 4 https://ai.meta.com/blog/llama-4-multimodal-intelligence/ + 'llama4-scout': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + reasoningCapabilities: false, + contextWindow: 10_000_000, maxOutputTokens: 4_096, + }, + 'llama4-maverick': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + reasoningCapabilities: false, + contextWindow: 10_000_000, maxOutputTokens: 4_096, }, - // llama + // llama 3 'llama3': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'llama3.1': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'llama3.2': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'llama3.3': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, // qwen 'qwen2.5coder': { @@ -193,12 +230,14 @@ const openSourceModelOptions_assumingOAICompat = { supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: false, + contextWindow: 32_000, maxOutputTokens: 4_096, }, 'qwq': { supportsFIM: false, // no FIM, yes reasoning supportsSystemMessage: 'system-role', supportsTools: 'openai-style', reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, + contextWindow: 128_000, maxOutputTokens: 8_192, }, // FIM only 'starcoder2': { @@ -206,14 +245,18 @@ const openSourceModelOptions_assumingOAICompat = { supportsSystemMessage: false, supportsTools: false, reasoningCapabilities: false, + contextWindow: 128_000, maxOutputTokens: 8_192, + }, 'codegemma:2b': { supportsFIM: true, supportsSystemMessage: false, supportsTools: false, reasoningCapabilities: false, + contextWindow: 128_000, maxOutputTokens: 8_192, + }, -} as const satisfies { [s: string]: Partial } +} as const satisfies { [s: string]: Omit } @@ -230,21 +273,46 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN cost: { input: 0, output: 0 }, } } - if (lower.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) + if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower)) + return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat]) + + if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25']) + if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) if (lower.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219']) + if (lower.includes('grok')) return toFallback(xAIModelOptions['grok-2']) - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('qwen') && lower.includes('2.5') && lower.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, contextWindow: 128_000, maxOutputTokens: 8_192, }) } - if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25']) - if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, contextWindow: 16_000, maxOutputTokens: 4_096, }) - if (lower.includes('gemma')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.gemma, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], contextWindow: 128_000, maxOutputTokens: 4_096 }) // max output unclear - if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) + + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1 }) + if (lower.includes('deepseek') && lower.includes('v2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2 }) + if (lower.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV3 }) + + if (lower.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, }) + if (lower.includes('llama3.1')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.1'], }) + if (lower.includes('llama3.2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.2'], }) + if (lower.includes('llama3.3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.3'], }) + if (lower.includes('llama') || lower.includes('scout')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] }) + if (lower.includes('llama') || lower.includes('maverick')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] }) + if (lower.includes('llama')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] }) + + if (lower.includes('qwen') && lower.includes('2.5') && lower.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'] }) + if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, }) } + if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, }) + if (lower.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral }) + + if (lower.includes('gemma')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.gemma, }) + + if (lower.includes('starcoder2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.starcoder2, }) + + if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], }) // max output unclear + + if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini']) + if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o']) + if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini']) + if (lower.includes('o1')) return toFallback(openAIModelOptions['o1']) + if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini']) + // if (lower.includes('o3')) return toFallback(openAIModelOptions['o3']) + return toFallback(modelOptionsDefaults) } From 5f76fa8d7dca5243259eecb7a9c61c293911979f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 17:49:32 -0700 Subject: [PATCH 21/30] turn off reasoning for everything except chat by default --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 9 +++++---- .../contrib/void/common/modelCapabilities.ts | 14 +++++++++----- .../llmMessage/sendLLMMessage.impl.ts | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) 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 9c31e142..efc3bd45 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 @@ -20,7 +20,7 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; +import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; @@ -158,7 +158,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const { canTurnOffReasoning, reasoningBudgetSlider } = reasoningCapabilities || {} const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] - const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions) + const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) return null // unused right now // return
@@ -174,12 +174,13 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => if (reasoningBudgetSlider?.type === 'slider') { // if it's a slider const { min: min_, max, default: defaultVal } = reasoningBudgetSlider - const value = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal - const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters const stepSize = Math.round((max - min_) / nSteps) const min = canTurnOffReasoning ? min_ - stepSize : min_ + const value = isReasoningEnabled ? min_ - stepSize + : voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + return
Thinking { - const { supportsReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {} + const { supportsReasoning, canTurnOffReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {} if (!supportsReasoning) return false - const defaultEnabledVal = true // if can't toggle reasoning, then this must be true. just true as default + // default to enabled if can't turn off, or if the featureName is Chat. + const defaultEnabledVal = featureName === 'Chat' || !canTurnOffReasoning + const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal return isReasoningEnabled } @@ -823,6 +826,7 @@ export const getIsResoningEnabledState = ( // used to force reasoning state (complex) into something simple we can just read from when sending a message export const getSendableReasoningInfo = ( + featureName: FeatureName, providerName: ProviderName, modelName: string, modelSelectionOptions: ModelSelectionOptions | undefined, @@ -830,7 +834,7 @@ export const getSendableReasoningInfo = ( const { canIOReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {} if (!canIOReasoning) return null - const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions) + const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) if (!isReasoningEnabled) return null // check for reasoning budget 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 1d61af41..a4bce5b9 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 @@ -165,7 +165,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage // reasoning const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {} - const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} // tools @@ -325,7 +325,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM const { providerReasoningIOSettings } = getProviderCapabilities(providerName) // reasoning - const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} // tools From 5b0d8f44184609450700cbe7491c0dc7454bb497 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 22:08:00 -0700 Subject: [PATCH 22/30] tool use + checkpoint state updates --- .../contrib/void/browser/chatThreadService.ts | 124 +++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 499 +++++++++--------- .../void/common/chatThreadServiceTypes.ts | 40 +- .../contrib/void/common/prompt/prompts.ts | 2 +- 4 files changed, 365 insertions(+), 300 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 16cf18e9..3a70a52e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -21,7 +21,7 @@ import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; +import { ChatMessage, CheckpointEntry, CodespanLocationLink, StagingSelectionItem, ToolMessage } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; @@ -71,7 +71,7 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning }) else if (c.role === 'tool') llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content }) - else if (c.role === 'tool_request') { // pass + else if (c.role === 'decorative_canceled_tool') { // pass } else if (c.role === 'checkpoint') { // pass } @@ -125,7 +125,12 @@ export type ThreadsState = { currentThreadId: string; // intended for internal use only } -export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user' +export type IsRunningType = + | 'LLM' // the LLM is currently streaming + | 'tool' // whether a tool is currently running + | 'awaiting_user' // awaiting user call + | undefined + export type ThreadStreamState = { [threadId: string]: undefined | { // state related to streaming (not just when streaming) @@ -460,13 +465,33 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + + private _swapOutLatestStreamingToolWithResult = (threadId: string, tool: ChatMessage & { role: 'tool' }) => { + const messages = this.state.allThreads[threadId]?.messages + if (!messages) return false + const lastMsg = messages[messages.length - 1] + if (!lastMsg) return false + if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) { + this._editMessageInThread(threadId, messages.length - 1, tool) + return true + } + return false + } + private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => { + const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool) + if (swapped) return + this._addMessageToThread(threadId, tool) + } + approveLatestToolRequest(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const lastMessage = thread.messages[thread.messages.length - 1] - if (lastMessage.role !== 'tool_request') return // should never happen + const lastMsg = thread.messages[thread.messages.length - 1] + if (!( + lastMsg.role === 'tool' && (lastMsg.type === 'tool_request') + )) return // should never happen const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user') const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' } @@ -476,7 +501,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { const prevSelns: StagingSelectionItem[] = this._getAllSelections(threadId) const currSelns: StagingSelectionItem[] = [] - const callThisToolFirst: ToolRequestApproval = lastMessage + const callThisToolFirst: ToolMessage = lastMsg + + this._updateLatestToolTo(threadId, { + role: 'tool', + type: 'running_now', + name: lastMsg.name, + paramsStr: lastMsg.paramsStr, + id: lastMsg.id, + params: lastMsg.params, + content: '(value not received yet...)', // this typically shouldn't ever get read + result: null + }) this._wrapRunAgentToNotify( this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) @@ -487,37 +523,64 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const lastMessage = thread.messages[thread.messages.length - 1] - if (lastMessage.role !== 'tool_request') return // should never happen - const { name, params, paramsStr, id } = lastMessage + const lastMsg = thread.messages[thread.messages.length - 1] + + let params: ToolCallParams[ToolName] + if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) { + params = lastMsg.params + } + else return + + const { name, paramsStr, id } = lastMsg const errorMessage = this.errMsgs.rejected - this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null }) this._setStreamState(threadId, {}, 'set') } + + // private _rejectLatestStreamingTool(threadId: string) { + // const thread = this.state.allThreads[threadId] + // if (!thread) return // should never happen + + // const lastMessage = thread.messages[thread.messages.length - 1] + // if (lastMessage.role !== 'tool') return + // const { name, paramsStr, id, result } = lastMessage + // if (result.type !== 'running_now') return + // const { params } = result + + // const errorMessage = this.errMsgs.rejected + // this._swapOutLatestStreamingToolWithResult(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, }) + // this._setStreamState(threadId, {}, 'set') + + // } + stopRunning(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const isRunning = this.streamState[threadId]?.isRunning - // reject the tool for the user - if (isRunning === 'awaiting_user') { - this.rejectLatestToolRequest(threadId) - } - // interrupt the tool - else if (isRunning === 'tool') { - this._currentlyRunningToolInterruptor[threadId]?.() - } + // reject the tool for the user if relevant + this.rejectLatestToolRequest(threadId) + + // interrupt the tool if relevant + this._currentlyRunningToolInterruptor[threadId]?.() + // interrupt assistant message - else if (isRunning === 'message') { + const isRunning = this.streamState[threadId]?.isRunning + if (isRunning === 'LLM') { // abort the stream first so it doesn't change any state const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + const toolInProgress = this.streamState[threadId]?.toolNameSoFar + console.log('toolInProgress', toolInProgress) const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + + if (toolInProgress) { + this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress }) + } } this._setStreamState(threadId, {}, 'set') @@ -559,7 +622,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { modelSelectionOptions: ModelSelectionOptions | undefined, userMessageContent: string, // content of LATEST user message - callThisToolFirst?: ToolRequestApproval + callThisToolFirst?: ToolMessage & { type: 'tool_request' } }) { const userMessageFullContent = userMessageContent const getLatestMessages = async () => { @@ -620,16 +683,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) return {} } + // once validated, add checkpoint for edit + if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval const requiresApproval = toolNamesThatRequireApproval.has(toolName) if (requiresApproval) { const autoApprove = this._settingsService.state.globalSettings.autoApprove // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) - this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) + this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) if (!autoApprove) { return { awaitingUserApproval: true } } @@ -643,8 +708,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') let interrupted = false try { - if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } - const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) this._currentlyRunningToolInterruptor[threadId] = () => { interrupted = true; @@ -655,12 +718,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { if (interrupted) { - // ideally this should have same implementation as abort - addMessage should get called in stopRunning - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: this.errMsgs.rejected, result: { type: 'rejected', params: toolParams }, }) + // the tool result is added when we stop running return { interrupted: true } } const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) return {} } @@ -669,12 +731,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) return {} } // 5. add to history and keep going - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, }) return {} }; @@ -708,7 +770,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message - this._setStreamState(threadId, { isRunning: 'message' }, 'merge') + this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') const messages = await getLatestMessages() const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', 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 efc3bd45..93c11eea 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 @@ -22,7 +22,7 @@ import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; -import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; +import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -176,10 +176,12 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters const stepSize = Math.round((max - min_) / nSteps) - const min = canTurnOffReasoning ? min_ - stepSize : min_ - const value = isReasoningEnabled ? min_ - stepSize - : voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + const valueIfOff = min_ - stepSize + const min = canTurnOffReasoning ? valueIfOff : min_ + const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + : valueIfOff + return
Thinking @@ -1137,33 +1139,33 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". -const loadingTitleWrapper = (item: React.ReactNode) => { +const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { return {item} } -const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, 'search_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, - 'create_file_or_folder': { - done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`, - running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`) - }, - 'delete_file_or_folder': { - done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`, - running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) - }, + 'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) }, + 'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) }, 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'run_terminal_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') } } as const satisfies Record +const getTitle = (toolMessage: Pick): React.ReactNode => { + const t = toolMessage + if (!toolNames.includes(t.name as ToolName)) return t.name // good measure + + const toolName = t.name as ToolName + if (t.type === 'success') return titleOfToolName[toolName].done + if (t.type === 'running_now') return titleOfToolName[toolName].running + return titleOfToolName[toolName].proposed +} const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1329,46 +1331,63 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
} -type ToolRequestState = 'awaiting_user' | 'running' -type RequestWrapper = (props: { toolRequest: ToolRequestApproval, messageIdx: number, toolRequestState: ToolRequestState, threadId: string }) => React.ReactNode -type ResultWrapper = (props: { toolMessage: ToolMessage, messageIdx: number, threadId: string }) => React.ReactNode -type ToolComponent = { - requestWrapper: T extends ToolNameWithApproval ? RequestWrapper : null, - resultWrapper: ResultWrapper, +const InvalidTool = ({ toolName }: { toolName: string }) => { + const accessor = useAccessor() + const title = getTitle({ name: toolName, type: 'invalid_params' }) + const desc1 = 'Invalid parameters' + const icon = null + const isError = true + const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + return } -const toolNameToComponent: { [T in ToolName]: ToolComponent } = { +const CanceledTool = ({ toolName }: { toolName: string }) => { + const accessor = useAccessor() + const title = getTitle({ name: toolName, type: 'rejected' }) + const desc1 = '' + const icon = null + const isRejected = true + const componentParams: ToolHeaderParams = { title, desc1, icon, isRejected } + return +} + + +type ResultWrapper = (props: { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode +const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } = { 'read_file': { - requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const { uri } = toolMessage.result.params ?? {} + + const title = getTitle(toolMessage) + + const { uri } = toolMessage.params ?? {} const desc1 = uri ? getBasename(uri.fsPath) : ''; const icon = null - if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.result.type === 'error' + const isError = toolMessage.type === 'tool_error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result + if (toolMessage.type === 'success') { + const { params, result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - if (value.hasNextPage && params.pageNumber === 1) // first page + if (result.hasNextPage && params.pageNumber === 1) // first page componentParams.desc2 = '(more content available)' else if (params.pageNumber > 1) // subsequent pages componentParams.desc2 = `(part ${params.pageNumber})` } - else { - const { value, params } = toolMessage.result + else if (toolMessage.type === 'tool_error') { + const { params, result } = toolMessage if (params) componentParams.desc2 = componentParams.children = - {value} + {result} } @@ -1377,26 +1396,27 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { }, }, 'get_dir_structure': { - requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null - if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.result.type === 'error' + const isError = toolMessage.type === 'tool_error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result + if (toolMessage.type === 'success') { + const { params, result } = toolMessage componentParams.children = } = { } else { - const { value, params } = toolMessage.result + const { params, result } = toolMessage componentParams.children = - {value} + {result} } @@ -1418,27 +1438,28 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } }, 'ls_dir': { - requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null - if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.result.type === 'error' + const isError = toolMessage.type === 'tool_error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result - componentParams.numResults = value.children?.length - componentParams.hasNextPage = value.hasNextPage - componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined + if (toolMessage.type === 'success') { + const { params, result } = toolMessage + componentParams.numResults = result.children?.length + componentParams.hasNextPage = result.hasNextPage + componentParams.children = !result.children || (result.children.length ?? 0) === 0 ? undefined : - {value.children.map((child, i) => ( ( { @@ -1447,16 +1468,16 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { // explorerService.select(child.uri, true); }} />))} - {value.hasNextPage && - + {result.hasNextPage && + } } else { - const { value, params } = toolMessage.result + const { params, result } = toolMessage componentParams.children = - {value} + {result} } @@ -1465,41 +1486,42 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } }, 'search_pathnames_only': { - requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isError = toolMessage.type === 'tool_error' + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null - if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result - componentParams.numResults = value.uris.length - componentParams.hasNextPage = value.hasNextPage - componentParams.children = value.uris.length === 0 ? undefined + if (toolMessage.type === 'success') { + const { params, result } = toolMessage + componentParams.numResults = result.uris.length + componentParams.hasNextPage = result.hasNextPage + componentParams.children = result.uris.length === 0 ? undefined : - {value.uris.map((uri, i) => ( ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} - {value.hasNextPage && + {result.hasNextPage && } } else { - const { value, params } = toolMessage.result + const { params, result } = toolMessage componentParams.children = - {value} + {result} } @@ -1508,41 +1530,42 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } }, 'search_files': { - requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isError = toolMessage.type === 'tool_error' + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null - if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'tool_request') return null + if (toolMessage.type === 'rejected') return null // will never happen, not rejectable + if (toolMessage.type === 'running_now') return null // do not show running const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result - componentParams.numResults = value.uris.length - componentParams.hasNextPage = value.hasNextPage - componentParams.children = value.uris.length === 0 ? undefined + if (toolMessage.type === 'success') { + const { params, result } = toolMessage + componentParams.numResults = result.uris.length + componentParams.hasNextPage = result.hasNextPage + componentParams.children = result.uris.length === 0 ? undefined : - {value.uris.map((uri, i) => ( ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} - {value.hasNextPage && + {result.hasNextPage && } } else { - const { value, params } = toolMessage.result + const { params, result } = toolMessage componentParams.children = - {value} + {result} } @@ -1553,137 +1576,111 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { // --- 'create_file_or_folder': { - requestWrapper: ({ toolRequest, toolRequestState }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const explorerService = accessor.get('IExplorerService') - const isError = false - const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - const icon = null - - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - - return - }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' - const isFolder = toolMessage.result.params?.isFolder ?? false - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success') { - const { params, value } = toolMessage.result + if (toolMessage.type === 'success') { + const { params, result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else if (toolMessage.result.type === 'rejected') { - const { params } = toolMessage.result + else if (toolMessage.type === 'rejected') { + const { params } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else if (toolMessage.result.type === 'error') { - const { params, value } = toolMessage.result + else if (toolMessage.type === 'tool_error') { + const { params, result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - {value} + {result} } + else if (toolMessage.type === 'running_now') { + // nothing more is needed + } + else if (toolMessage.type === 'tool_request') { + // nothing more is needed + } return } }, 'delete_file_or_folder': { - requestWrapper: ({ toolRequest, toolRequestState }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const isError = false - const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - const icon = null - - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - - const { params } = toolRequest - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - - return - }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isFolder = toolMessage.result.params?.isFolder ?? false - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isFolder = toolMessage.params?.isFolder ?? false + const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success') { - const { params, value } = toolMessage.result + if (toolMessage.type === 'success') { + const { params, result } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else if (toolMessage.result.type === 'rejected') { - const { params } = toolMessage.result + else if (toolMessage.type === 'rejected') { + const { params } = toolMessage componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else if (toolMessage.result.type === 'error') { - const { params, value } = toolMessage.result + else if (toolMessage.type === 'tool_error') { + const { params, result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - {value} + {result} } + else if (toolMessage.type === 'running_now') { + const { params, result } = toolMessage + componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + } + else if (toolMessage.type === 'tool_request') { + const { params, result } = toolMessage + componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + } return } }, 'edit_file': { - requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { - const accessor = useAccessor() - const isError = false - const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - const icon = null - - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - - const { params } = toolRequest - componentParams.children = - - - - componentParams.desc2 = - - return - }, resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' + + const title = getTitle(toolMessage) + + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { - const { params } = toolMessage.result + if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { + const { params } = toolMessage + componentParams.children = + + + componentParams.desc2 = + } + else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { + const { params } = toolMessage // add apply box if (params) { @@ -1701,8 +1698,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } // add children - if (toolMessage.result.type !== 'error') { - const { params } = toolMessage.result + if (toolMessage.type !== 'tool_error') { + const { params } = toolMessage componentParams.children = } = { } else { // error - const { params, value } = toolMessage.result + const { params, result } = toolMessage if (params) { componentParams.children = {/* error */} - {value} + {result} {/* content */} @@ -1729,7 +1726,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } else { componentParams.children = - {value} + {result} } } @@ -1739,47 +1736,28 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } }, 'run_terminal_command': { - requestWrapper: ({ toolRequest, toolRequestState }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const terminalToolsService = accessor.get('ITerminalToolService') - const isError = false - const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - const icon = null - - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - - const { proposedTerminalId, waitForCompletion } = toolRequest.params - if (terminalToolsService.terminalExists(proposedTerminalId)) - componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - if (!waitForCompletion) - componentParams.desc2 = '(background task)' - - return - }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed - const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + const isError = toolMessage.type === 'tool_error' + const title = getTitle(toolMessage) + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.params) const icon = null - const isRejected = toolMessage.result.type === 'rejected' + const isRejected = toolMessage.type === 'rejected' const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success') { - const { value, params } = toolMessage.result + if (toolMessage.type === 'success') { + const { params, result } = toolMessage const { command } = params - const { terminalId, resolveReason, result } = value + const { terminalId, resolveReason, result: terminalResult } = result componentParams.desc2 = { terminalToolsService.openTerminal(terminalId) }} /> - const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) + const additionalDetailsStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) : resolveReason.type === 'bgtask' ? null : resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : resolveReason.type === 'toofull' ? `\n(truncated)` @@ -1795,8 +1773,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = {
{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} {`Result: `} - {result} - {resultStr} + {terminalResult} + {additionalDetailsStr}
@@ -1805,8 +1783,8 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' } - else if (toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { - const { params } = toolMessage.result + else if (toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { + const { params } = toolMessage if (params) { const { proposedTerminalId, waitForCompletion } = params if (terminalToolsService.terminalExists(proposedTerminalId)) @@ -1814,11 +1792,18 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { if (!waitForCompletion) componentParams.desc2 = '(background task)' } - if (toolMessage.result.type === 'error') { - const { value } = toolMessage.result - componentParams.children = {value} + if (toolMessage.type === 'tool_error') { + const { result } = toolMessage + componentParams.children = {result} } } + else if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { + const { proposedTerminalId, waitForCompletion } = toolMessage.params + if (terminalToolsService.terminalExists(proposedTerminalId)) + componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) + if (!waitForCompletion) + componentParams.desc2 = '(background task)' + } return } @@ -1833,7 +1818,7 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs return
@@ -1855,14 +1840,13 @@ type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isCommitted: boolean, - canAcceptReject: boolean, chatIsRunning: IsRunningType, threadId: string, currCheckpointIdx: number, _scrollToBottom: (() => void) | null, } -const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, canAcceptReject, chatIsRunning, threadId, _scrollToBottom }: ChatBubbleProps) => { +const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => { const role = chatMessage.role const isCheckpointGhost = messageIdx > currCheckpointIdx && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts) @@ -1883,46 +1867,61 @@ const ChatBubble = ({ chatMessage, currCheckpointIdx, isCommitted, messageIdx, c isCommitted={isCommitted} /> } - else if (role === 'tool_request') { - const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper - const toolRequestState = ( - chatIsRunning === 'awaiting_user' ? 'awaiting_user' - : chatIsRunning === 'tool' ? 'running' - : chatIsRunning === 'message' ? null - : null - ) - if (ToolRequestWrapper && canAcceptReject) { // if it's the last message - return <> - {toolRequestState !== null && -
- -
} - {chatIsRunning === 'awaiting_user' && -
- -
} - - } - return null - } + // else if (role === 'tool_request') { + // const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper + // const toolRequestState = ( + // chatIsRunning === 'awaiting_user' ? 'awaiting_user' + // : chatIsRunning === 'tool' ? 'running' + // : chatIsRunning === 'message' ? null + // : null + // ) + // if (ToolRequestWrapper && canAcceptReject) { // if it's the last message + // return <> + // {toolRequestState !== null && + //
+ // + //
} + // {chatIsRunning === 'awaiting_user' && + //
+ // + //
} + // + // } + // return null + // } else if (role === 'tool') { + + if (chatMessage.type === 'invalid_params') { + return + } + const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper if (ToolResultWrapper) - return
- -
+ return <> +
+ +
+ {chatMessage.type === 'tool_request' ? +
+ +
: null} + return null } + else if (role === 'decorative_canceled_tool') { + return + } + else if (role === 'checkpoint') { return { const previousMessagesHTML = useMemo(() => { const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') - console.log('PREVMSGS', previousMessages) - + // tool request shows up as Editing... if in progress return previousMessages.map((message, i) => { - const canAcceptReject = i === lastMessageIdx && message.role === 'tool_request' - return { messageIdx={i} isCommitted={true} chatIsRunning={isRunning} - canAcceptReject={canAcceptReject} threadId={threadId} _scrollToBottom={() => scrollToBottom(scrollContainerRef)} /> @@ -2095,15 +2090,13 @@ export const SidebarChat = () => { messageIdx={streamingChatIdx} isCommitted={false} chatIsRunning={isRunning} - canAcceptReject={false} threadId={threadId} _scrollToBottom={null} /> : null - const proposedToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar - const generatingToolTitle = typeof proposedToolTitle === 'function' ? proposedToolTitle(null) : proposedToolTitle + const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar const messagesHTML = { Generating} /> : null} - {isRunning === 'message' && !toolIsGenerating ? + {isRunning === 'LLM' && !toolIsGenerating ? {/* loading indicator */} {} : null} diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 0c9cbebb..915a3e7d 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -10,25 +10,35 @@ 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 + content: string; // give this result to LLM (string of value) +} & ( + // in order of events: + | { type: 'invalid_params', result: null, params: null, name: string } - // if rejected, don't show in chat - result: - | { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } - | { type: 'error'; params: ToolCallParams[T] | undefined; value: string } - | { type: 'rejected'; params: ToolCallParams[T] } // user rejected -} -export type ToolRequestApproval = { - role: 'tool_request'; - name: T; // internal use - params: ToolCallParams[T]; // internal use - paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) - id: string; // proposed tool's id + | { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user + + | { type: 'running_now', result: null, name: T, params: ToolCallParams[T], } + + | { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running + | { type: 'success', result: ToolResultType[T], name: T, params: ToolCallParams[T], } + | { type: 'rejected', result: null, name: T, params: ToolCallParams[T], } + ) // user rejected + +export type DecorativeCanceledTool = { + role: 'decorative_canceled_tool'; + name: string; } +// export type ToolRequestApproval = { +// role: 'tool_request'; +// name: T; // internal use +// params: ToolCallParams[T]; // internal use +// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) +// id: string; // proposed tool's id +// } + // checkpoints export type CheckpointEntry = { @@ -61,7 +71,7 @@ export type ChatMessage = anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning } | ToolMessage - | ToolRequestApproval + | DecorativeCanceledTool | CheckpointEntry diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 34410b97..c80a0a3b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -79,7 +79,7 @@ export const voidTools = { get_dir_structure: { name: 'get_dir_structure', - description: `Returns a tree diagram of all the files and folders in the given folder URI. Call this to learn more about a folder. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`, + description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`, params: { ...uriParam('folder') } From 3fc0fd0bc0c9a517a122d7a411dc52a4a4252e32 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 22:16:32 -0700 Subject: [PATCH 23/30] ghost --- .../react/src/sidebar-tsx/SidebarChat.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) 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 93c11eea..bad7832e 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 @@ -1816,14 +1816,15 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs const chatThreadService = accessor.get('IChatThreadService') return
{ if (threadIsRunning) return chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) @@ -1897,7 +1898,9 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes else if (role === 'tool') { if (chatMessage.type === 'invalid_params') { - return + return
+ +
} const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper @@ -1919,7 +1922,9 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes } else if (role === 'decorative_canceled_tool') { - return + return
+ +
} else if (role === 'checkpoint') { From 76125cc83e57e6221e0961b43a1256c2bb39ad9a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 22:46:30 -0700 Subject: [PATCH 24/30] feed lint errors --- .../contrib/void/browser/toolsService.ts | 24 +++++++++++++++---- .../contrib/void/common/toolsServiceTypes.ts | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 255c9112..2dbd1b55 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -14,6 +14,8 @@ import { EndOfLinePreference } from '../../../../editor/common/model.js' import { basename } from '../../../../base/common/path.js' import { IVoidCommandBarService } from './voidCommandBarService.js' import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' +import { IMarkerService } from '../../../../platform/markers/common/markers.js' +import { timeout } from '../../../../base/common/async.js' // tool use for AI @@ -23,7 +25,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree type ValidateParams = { [T in ToolName]: (p: string) => Promise } type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> } -type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } +type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited) => string } @@ -164,6 +166,7 @@ export class ToolsService implements IToolsService { @ITerminalToolService private readonly terminalToolService: ITerminalToolService, @IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService, @IDirectoryStrService private readonly directoryStrService: IDirectoryStrService, + @IMarkerService private readonly markerService: IMarkerService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -364,7 +367,7 @@ export class ToolsService implements IToolsService { edit_file: async ({ uri, changeDescription }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { - throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`) + throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and resume later.`) } const opts = { uri, @@ -381,7 +384,19 @@ export class ToolsService implements IToolsService { const interruptTool = () => { // must reject the applyPromiseDone promise editCodeService.interruptURIStreaming({ uri: diffZoneURI }) } - return { result: applyDonePromise, interruptTool } + + const lintErrorsPromise = applyDonePromise.then(async () => { + await timeout(500) + const lintErrorsStr = this.markerService + .read({ resource: uri }) + .map(l => l.message) + .join('\n') + + if (!lintErrorsStr) return { lintErrorsStr: null } + return { lintErrorsStr } + }) + + return { result: lintErrorsPromise, interruptTool } }, run_terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) @@ -418,7 +433,8 @@ export class ToolsService implements IToolsService { return `URI ${params.uri.fsPath} successfully deleted.` }, edit_file: (params, result) => { - return `Change successfully made to ${params.uri.fsPath}.` + const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\nIf this is related to a change you made, you should eventually fix this error.` : `No lint errors found.` + return `Change successfully made to ${params.uri.fsPath}. ${additionalStr}` }, run_terminal_command: (params, result) => { diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 54e88d51..980ea587 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -63,7 +63,7 @@ export type ToolResultType = { 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 'search_files': { uris: URI[], hasNextPage: boolean }, // --- - 'edit_file': Promise, + 'edit_file': Promise<{ lintErrorsStr: string | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, 'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, From 7c4e92a030e41e8572804efd1a24d61092df4bcf Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 6 Apr 2025 22:47:57 -0700 Subject: [PATCH 25/30] pass lint errors!! --- src/vs/workbench/contrib/void/browser/toolsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 2dbd1b55..439bed62 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -433,7 +433,7 @@ export class ToolsService implements IToolsService { return `URI ${params.uri.fsPath} successfully deleted.` }, edit_file: (params, result) => { - const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\nIf this is related to a change you made, you should eventually fix this error.` : `No lint errors found.` + const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.` return `Change successfully made to ${params.uri.fsPath}. ${additionalStr}` }, run_terminal_command: (params, result) => { From 7aa5f1e4ece6304fadcae057167bfe792257988a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 7 Apr 2025 01:46:07 -0700 Subject: [PATCH 26/30] context trimming --- .../contrib/void/browser/toolsService.ts | 1 - .../contrib/void/common/modelCapabilities.ts | 30 ++--- .../llmMessage/preprocessLLMMessages.ts | 110 ++++++++++++++++-- .../llmMessage/sendLLMMessage.impl.ts | 10 +- 4 files changed, 120 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 439bed62..1936be08 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -437,7 +437,6 @@ export class ToolsService implements IToolsService { return `Change successfully made to ${params.uri.fsPath}. ${additionalStr}` }, run_terminal_command: (params, result) => { - const { terminalId, didCreateTerminal, diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 5417649c..ad76ebec 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -67,8 +67,8 @@ export const defaultModelsOfProvider = { type ModelOptions = { - contextWindow: number; // input tokens // <-- UNUSED - maxOutputTokens: number | null; // output tokens // <-- UNUSED + contextWindow: number; // input tokens + maxOutputTokens: number | null; // output tokens, defaults to 4092 cost: { // <-- UNUSED input: number; output: number; @@ -113,9 +113,9 @@ type ProviderSettings = { const modelOptionsDefaults: ModelOptions = { - contextWindow: 32_000, // unused - maxOutputTokens: null, // unused - cost: { input: 0, output: 0 }, // unused + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0, output: 0 }, supportsSystemMessage: false, supportsTools: false, supportsFIM: false, @@ -493,7 +493,7 @@ const xAISettings: ProviderSettings = { const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing 'gemini-2.5-pro-exp-03-25': { contextWindow: 1_048_576, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, cost: { input: 0, output: 0 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -502,7 +502,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-2.0-flash': { contextWindow: 1_048_576, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, // 8_192, cost: { input: 0.10, output: 0.40 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -511,7 +511,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-2.0-flash-lite-preview-02-05': { contextWindow: 1_048_576, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, // 8_192, cost: { input: 0.075, output: 0.30 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -520,7 +520,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-flash': { contextWindow: 1_048_576, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, // 8_192, cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -529,7 +529,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-pro': { contextWindow: 2_097_152, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -538,7 +538,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, 'gemini-1.5-flash-8b': { contextWindow: 1_048_576, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now supportsFIM: false, supportsSystemMessage: 'system-role', @@ -559,13 +559,13 @@ const deepseekModelOptions = { 'deepseek-chat': { ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing - maxOutputTokens: null, // 8_000, + maxOutputTokens: 8_000, // 8_000, cost: { cache_read: .07, input: .27, output: 1.10, }, }, 'deepseek-reasoner': { ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 64_000, - maxOutputTokens: null, // 8_000, + maxOutputTokens: 8_000, // 8_000, cost: { cache_read: .14, input: .55, output: 2.19, }, }, } as const satisfies { [s: string]: ModelOptions } @@ -584,7 +584,7 @@ const deepseekSettings: ProviderSettings = { const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/ 'llama-3.3-70b-versatile': { contextWindow: 128_000, - maxOutputTokens: null, // 32_768, + maxOutputTokens: 32_768, // 32_768, cost: { input: 0.59, output: 0.79 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -593,7 +593,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq }, 'llama-3.1-8b-instant': { contextWindow: 128_000, - maxOutputTokens: null, // 8_192, + maxOutputTokens: 8_192, cost: { input: 0.05, output: 0.08 }, supportsFIM: false, supportsSystemMessage: 'system-role', diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index ab9991c1..21995562 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -40,17 +40,10 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM const newMessages: LLMChatMessage[] = [] if (messages.length >= 0) newMessages.push(messages[0]) - // remove duplicate roles + // remove duplicate roles - we used to do this, but we don't anymore for (let i = 1; i < messages.length; i += 1) { - const curr = messages[i] - // const prev = messages[i - 1] - // // if found a repeated role, put the current content in the prev - // if ((curr.role === 'assistant' && prev.role === 'assistant')) { - // prev.content += '\n' + curr.content - // continue - // } - // add the message - newMessages.push(curr) + const m = messages[i] + newMessages.push(m) } const finalMessages = newMessages.map(m => ({ ...m, content: m.content.trim() })) return { messages: finalMessages } @@ -61,6 +54,94 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM +const CHARS_PER_TOKEN = 4 +const TRIM_TO_LEN = 60 + +const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputTokens }: { messages: LLMChatMessage[], contextWindow: number, maxOutputTokens: number }): { messages: LLMChatMessage[] } => { + + // the higher the weight, the higher the desire to truncate + const alreadyTrimmedIdxes = new Set() + const weight = (message: LLMChatMessage, messages: LLMChatMessage[], idx: number) => { + const base = message.content.length + + let multiplier: number + if (message.role === 'system') + return 0 // never erase system message + + if (message.role === 'user') { + multiplier = 4 + } + else { + multiplier = 8 + } + + // last 3 msgs are very important + if (idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { + multiplier *= .05 + } + + return base * multiplier + + } + const _findLargestByWeight = (messages: LLMChatMessage[]) => { + let largestIndex = -1 + let largestWeight = -Infinity + for (let i = 0; i < messages.length; i += 1) { + const m = messages[i] + const w = weight(m, messages, i) + if (w > largestWeight) { + largestWeight = w + largestIndex = i + } + } + return largestIndex + } + + let totalLen = 0 + for (const m of messages) { totalLen += m.content.length } + const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN + if (charsNeedToTrim <= 0) return { messages } + + // <-----------------------------------------> + // 0 | | | + // | contextWindow | + // contextWindow - maxOut|putTokens + // | + // totalLen + + + // TRIM HIGHEST WEIGHT MESSAGES + let remainingCharsToTrim = charsNeedToTrim + let i = 0 + + while (remainingCharsToTrim > 0) { + i += 1 + if (i > 100) break + + const trimIdx = _findLargestByWeight(messages) + const m = messages[trimIdx] + + // if can finish here, do + const numCharsWillTrim = m.content.length - TRIM_TO_LEN + if (numCharsWillTrim > remainingCharsToTrim) { + m.content = m.content.slice(0, m.content.length - remainingCharsToTrim) + break + } + + remainingCharsToTrim -= numCharsWillTrim + m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...' + alreadyTrimmedIdxes.add(trimIdx) + } + + return { messages } + +} + + + + + + // no matter whether the model supports a system message or not (or what format it supports), add it in some way const prepareMessages_systemMessage = ({ @@ -378,14 +459,21 @@ export const prepareMessages = ({ supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature, + contextWindow, + maxOutputTokens, }: { messages: LLMChatMessage[], aiInstructions: string, supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', supportsTools: false | 'anthropic-style' | 'openai-style', supportsAnthropicReasoningSignature: boolean, + contextWindow: number, + maxOutputTokens: number | null | undefined, }) => { - const { messages: messages1 } = prepareMessages_normalize({ messages }) + maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 + + const { messages: messages0 } = prepareMessages_normalize({ messages }) + const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens }) const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature }) const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage }) const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools }) 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 a4bce5b9..05610f6a 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 @@ -157,7 +157,8 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage modelName, supportsSystemMessage, supportsTools, - // maxOutputTokens, + contextWindow, + maxOutputTokens, reasoningCapabilities, } = getModelCapabilities(providerName, modelName_) @@ -173,10 +174,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} // max tokens - // const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens + const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens // instance - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens }) const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, @@ -316,6 +317,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM const { modelName, supportsSystemMessage, + contextWindow, supportsTools, maxOutputTokens, reasoningCapabilities, @@ -339,7 +341,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens // instance - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true }) + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens }) const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true From 71dd603c3dee9baa0d8cf1f93a94d99598877748 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 7 Apr 2025 01:59:45 -0700 Subject: [PATCH 27/30] context weights --- .../electron-main/llmMessage/preprocessLLMMessages.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 21995562..f14fb285 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -68,15 +68,16 @@ const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputToke if (message.role === 'system') return 0 // never erase system message + multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases if (message.role === 'user') { - multiplier = 4 + multiplier *= 1 } else { - multiplier = 8 + multiplier *= 10 // llm tokens are far less valuable than user tokens } - // last 3 msgs are very important - if (idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { + // 1st message, last 3 msgs, any already modified message should be low in weight + if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { multiplier *= .05 } From 707fe9e0a32a5a7e3be153cbd9715e399d300857 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 7 Apr 2025 02:40:27 -0700 Subject: [PATCH 28/30] add links for any file the LLM has read --- .../contrib/void/browser/chatThreadService.ts | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3a70a52e..80364d33 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -388,17 +388,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._onDidChangeCurrentThread.fire() } - private _getAllSelections(threadId: string) { - const thread = this.state.allThreads[threadId] - if (!thread) return [] - return thread.messages.flatMap(m => m.role === 'user' && m.selections || []) - } - - private _getSelectionsUpToMessageIdx(messageIdx: number) { - const thread = this.getCurrentThread() - const prevMessages = thread.messages.slice(0, messageIdx) - return prevMessages.flatMap(m => m.role === 'user' && m.selections || []) - } private _setStreamState(threadId: string, state: Partial>, behavior: 'set' | 'merge') { if (state === undefined) @@ -435,7 +424,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // get prev and curr selections before clearing the message - const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) // selections for previous messages const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message // clear messages up to the index @@ -451,7 +439,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // re-add the message and stream it - this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: { prevSelns, currSelns }, threadId }) + this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId }) } @@ -498,8 +486,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen const instructions = lastUserMessage.displayContent || '' - const prevSelns: StagingSelectionItem[] = this._getAllSelections(threadId) - const currSelns: StagingSelectionItem[] = [] const callThisToolFirst: ToolMessage = lastMsg @@ -515,7 +501,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { }) this._wrapRunAgentToNotify( - this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + this._runChatAgent({ callThisToolFirst, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) , threadId ) } @@ -616,8 +602,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { callThisToolFirst, }: { threadId: string, - prevSelns: StagingSelectionItem[], - currSelns: StagingSelectionItem[], modelSelection: ModelSelection | null, modelSelectionOptions: ModelSelectionOptions | undefined, userMessageContent: string, // content of LATEST user message @@ -1152,7 +1136,7 @@ We only need to do it for files that were edited since `from`, ie files between }) } - async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[], }, threadId: string }) { + async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen @@ -1166,15 +1150,11 @@ We only need to do it for files that were edited since `from`, ie files between const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - // selections in all past chats, then in current chat (can have many duplicates here) - const prevSelns: StagingSelectionItem[] = _chatSelections?.prevSelns ?? this._getAllSelections(threadId) - const currSelns: StagingSelectionItem[] = _chatSelections?.currSelns ?? thread.state.stagingSelections + const { chatMode } = this._settingsService.state.globalSettings // add user's message to chat history const instructions = userMessage - - const { chatMode } = this._settingsService.state.globalSettings - + const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content) @@ -1184,7 +1164,7 @@ We only need to do it for files that were edited since `from`, ie files between this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming this._wrapRunAgentToNotify( - this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }), + this._runChatAgent({ threadId, userMessageContent, ...this._currentModelSelectionProps(), }), threadId, ) } @@ -1197,6 +1177,35 @@ We only need to do it for files that were edited since `from`, ie files between // ---------- the rest ---------- + private _getAllSeenFileURIs(threadId: string) { + const thread = this.state.allThreads[threadId] + if (!thread) return [] + + const fsPathsSet = new Set() + const uris: URI[] = [] + const addURI = (uri: URI) => { + if (!fsPathsSet.has(uri.fsPath)) uris.push(uri) + fsPathsSet.add(uri.fsPath) + uris.push(uri) + } + + for (const m of thread.messages) { + // URIs of user selections + if (m.role === 'user') { + for (const sel of m.selections ?? []) { + addURI(sel.uri) + } + } + // URIs of files that have been read + else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') { + const params = m.params as ToolCallParams['read_file'] + addURI(params.uri) + } + } + return uris + } + + // gets the location of codespan link so the user can click on it generateCodespanLink: IChatThreadService['generateCodespanLink'] = async ({ codespanStr: _codespanStr, threadId }) => { @@ -1206,7 +1215,7 @@ We only need to do it for files that were edited since `from`, ie files between const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )` let target = _codespanStr // the string to search for - let codespanType: 'file-or-folder' | 'function-or-class' | 'unsearchable' = 'unsearchable'; + let codespanType: 'file-or-folder' | 'function-or-class' if (target.includes('.') || target.includes('/')) { codespanType = 'file-or-folder' @@ -1225,22 +1234,16 @@ We only need to do it for files that were edited since `from`, ie files between target = match[1] } + else { return null } } - - if (codespanType === 'unsearchable') { + else { return null } // get history of all AI and user added files in conversation + store in reverse order (MRU) - const prevUris = this._getAllSelections(threadId) - .map(s => s.uri) - .filter((uri, index, array) => array.findIndex(u => u.fsPath === uri.fsPath) === index) // O(n^2) but this is small - .reverse() - + const prevUris = this._getAllSeenFileURIs(threadId).reverse() if (codespanType === 'file-or-folder') { - - const doesUriMatchTarget = (uri: URI) => uri.path.includes(target) // check if any prevFiles are the `target` From c699577183802a185d04e25d0580025f1a4bf09a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 7 Apr 2025 02:51:46 -0700 Subject: [PATCH 29/30] only add current file if no message was sent --- .../contrib/void/browser/chatThreadService.ts | 59 ++++++------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 80364d33..4a2257a3 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -187,6 +187,7 @@ export interface IChatThreadService { setCurrentMessageState: (messageIdx: number, newState: Partial) => void getCurrentThreadState: () => ThreadType['state'] setCurrentThreadState: (newState: Partial) => void + // you can edit multiple messages - the one you're currently editing is "focused", and we add items to that one when you press cmd+L. getCurrentFocusedMessageIdx(): number | undefined; isCurrentlyFocusingMessage(): boolean; @@ -290,12 +291,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + // add the current file to the thread being edited private _addCurrentFileAsStagingSelectionDuringFileChange() { - - - // add the current file to the thread being edited const newModel = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null - if (!newModel) { return; } + if (!newModel) { return } + + const isCurrentlyFocusing = this.isCurrentlyFocusingMessage() + if (isCurrentlyFocusing) return + + // only add if the user hasn't sent a message yet + if (this.getCurrentThread().messages.length !== 0) return const newStagingSelection: StagingSelectionItem = { type: 'File', @@ -304,44 +309,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { state: { wasAddedAsCurrentFile: true } } - const focusedMessageIdx = this.getCurrentFocusedMessageIdx(); - - // add the selection - if (focusedMessageIdx === undefined) { // user is in the default thread - - const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; - - // remove all old selectons that are marked as `wasAddedAsCurrentFile` - const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); - - // add the new file if it doesn't exist - const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) - if (!fileIsAdded) { - newStagingSelections.push(newStagingSelection) - } - - // update thread state with new selections - this.setCurrentThreadState({ stagingSelections: newStagingSelections }); - - - - } else { // user is editing a message - - // do nothing. I don't think it feels good to auto-add the current file when you're editing a message. - - // const oldStagingSelections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections || []; - // const newStagingSelections = [...filteredStagingSelections, newSelection]; - // this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: newSelections }); - - // // if the file already exists, do nothing - // const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.fsPath === newSelection.fileURI.fsPath) - // if (alreadyHasFile) { return; } - - // const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedDuringFileChange); // remove all old selectons that were added during a file change - - - } + const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; + const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath) + if (fileIsAlreadyHere) return + // remove all old selectons that are marked as `wasAddedAsCurrentFile`, and add new selection + const newStagingSelections: StagingSelectionItem[] = [ + ...oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile), + newStagingSelection + ] + this.setCurrentThreadState({ stagingSelections: newStagingSelections }); } From 2912d5cd89ae3bc46a9547ce52ba91e663976c91 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 7 Apr 2025 03:37:43 -0700 Subject: [PATCH 30/30] rm logs --- src/vs/workbench/contrib/void/browser/directoryStrService.ts | 1 - .../void/browser/react/src/void-settings-tsx/Settings.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index 892f1d73..4cdb8f00 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -275,7 +275,6 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`) const dirTree = await computeDirectoryTree(eRoot, this.explorerService); - console.log('dirtree', dirTree) const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL); return { 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 80ccd6e7..6fe499e7 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 @@ -262,7 +262,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider const settingsState = useSettingsState() const settingValue = settingsState.settingsOfProvider[providerName][settingName] as string // this should always be a string in this component - console.log(`providerName:${providerName} settingName: ${settingName}, settingValue: ${settingValue}`) if (typeof settingValue !== 'string') { console.log('Error: Provider setting had a non-string value.') return