diff --git a/package-lock.json b/package-lock.json index 0e6f871e..270393d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "posthog-node": "^4.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-tooltip": "^5.28.1", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", "vscode-html-languageservice": "^5.3.1", @@ -6621,6 +6622,12 @@ "node": ">=0.10.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -15094,10 +15101,11 @@ } }, "node_modules/nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/nanoid": { @@ -17999,6 +18007,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-tooltip": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.1.tgz", + "integrity": "sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -18807,10 +18829,11 @@ } }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" }, "node_modules/scheduler": { "version": "0.25.0", diff --git a/package.json b/package.json index f1faef9f..e7613ff0 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "posthog-node": "^4.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-tooltip": "^5.28.1", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", "vscode-html-languageservice": "^5.3.1", diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index a92290a2..d9f424af 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// This bootstrap-fork module handles the initialization of a forked process in VS Code. +// It sets up logging, exception handling, and loads the ESM module system. + import * as performance from './vs/base/common/performance.js'; import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js'; import { bootstrapESM } from './bootstrap-esm.js'; diff --git a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts index 520e71d2..931f2dc5 100644 --- a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts @@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution { } this._state = this._state.map(state => state.mov(forward)); const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition())); + + // Void changed this to skip over added whitespace when using smartSelect + // // Store the original selections for comparison + // const originalSelections = selections; + + // // Keep skipping while we're only adding/removing whitespace + // let keepSkipping = true; + // let skipCount = 0; + // const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit + + // while (keepSkipping && skipCount < MAX_SKIPS) { + // keepSkipping = false; // Reset for each iteration + + // // Check if all selections only added/removed whitespace + // if (originalSelections.length === newSelections.length) { + // for (let i = 0; i < originalSelections.length; i++) { + // const oldSel = originalSelections[i]; + // const newSel = newSelections[i]; + + // if (forward) { // For expanding (^+Shift+Right) + // // Skip if only whitespace was added + // const oldText = model.getValueInRange(oldSel).trim(); + // const newText = model.getValueInRange(newSel).trim(); + // const onlyWhitespaceAdded = oldText === newText && oldText.length > 0; + + // if (onlyWhitespaceAdded) { + // console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, { + // reason: 'only whitespace added', + // oldText: model.getValueInRange(oldSel), + // newText: model.getValueInRange(newSel) + // }); + // keepSkipping = true; + // break; + // } + // } else { // For shrinking (^+Shift+Left) + // // Skip if only whitespace was removed + // const oldText = model.getValueInRange(oldSel).trim(); + // const newText = model.getValueInRange(newSel).trim(); + // const onlyWhitespaceRemoved = oldText === newText && newText.length > 0; + + // if (onlyWhitespaceRemoved) { + // console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, { + // reason: 'only whitespace removed', + // oldText: model.getValueInRange(oldSel), + // newText: model.getValueInRange(newSel) + // }); + // keepSkipping = true; + // break; + // } + // } + // } + // } + + // // If we need to skip, move one more time + // if (keepSkipping) { + // skipCount++; + + // // Try to move to the next range + // const prevState = this._state; + // this._state = this._state.map(state => state.mov(forward)); + + // // Check if we've reached the end of available ranges + // const stateUnchanged = this._state.every((state, idx) => + // state.index === prevState[idx].index + // ); + + // if (stateUnchanged) { + // // We can't move any further, so stop skipping + // keepSkipping = false; + // } else { + // // Update selections for the next iteration + // newSelections = this._state.map(state => Selection.fromPositions( + // state.ranges[state.index].getStartPosition(), + // state.ranges[state.index].getEndPosition() + // )); + // } + // } + // } + + // // Print AFTER selection (before actually setting it) + // console.log('SMART SELECT - AFTER:', newSelections.map(s => { + // return { + // range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`, + // text: model.getValueInRange(s) + // }; + // })); + this._ignoreSelection = true; try { this._editor.setSelections(newSelections); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 16d645a8..39492ef3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -612,7 +612,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."), // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. // On Mac, the delay is 1500. - 'default': isMacintosh ? 1500 : 500, + 'default': 300, // Void changed this from isMacintosh ? 1500 : 500, 'minimum': 0 }, 'workbench.reduceMotion': { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 2dea3290..ab30e454 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -648,8 +648,9 @@ const defaultChat = { providerSetting: product.defaultChatAgent?.providerSetting ?? '', }; +// Void commented this out - copilot head // Add next to the command center if command center is disabled -MenuRegistry.appendMenuItem(MenuId.CommandCenter, { +/* MenuRegistry.appendMenuItem(MenuId.CommandCenter, { submenu: MenuId.ChatTitleBarMenu, title: localize('title4', "Copilot"), icon: Codicon.copilot, @@ -672,7 +673,7 @@ MenuRegistry.appendMenuItem(MenuId.TitleBar, { ContextKeyExpr.has('config.window.commandCenter').negate(), ), order: 1 -}); +}); */ registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction { constructor() { diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index b8c6c466..e1832646 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { InlineCompletion, } from '../../../../editor/common/languages.js'; import { Range } from '../../../../editor/common/core/range.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; @@ -633,8 +632,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ async _provideInlineCompletionItems( model: ITextModel, position: Position, - context: InlineCompletionContext, - token: CancellationToken, ): Promise { const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete @@ -852,7 +849,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.status = 'error' reject(message) }, - onAbort: () => { }, + onAbort: () => { reject('Aborted autocomplete') }, }) newAutocompletion.requestId = requestId @@ -897,9 +894,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ ) { super() - this._langFeatureService.inlineCompletionsProvider.register('*', { + this._register(this._langFeatureService.inlineCompletionsProvider.register('*', { provideInlineCompletions: async (model, position, context, token) => { - const items = await this._provideInlineCompletionItems(model, position, context, token) + const items = await this._provideInlineCompletionItems(model, position) // console.log('item: ', items?.[0]?.insertText) return { items: items, } @@ -936,7 +933,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ }); }, - }) + })) } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 4a2257a3..92bf7af0 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,13 +11,13 @@ 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, voidTools } from '../common/prompt/prompts.js'; -import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js'; +import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -37,6 +37,7 @@ 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'; +import { deepClone } from '../../../../base/common/objects.js'; /* @@ -61,28 +62,6 @@ A checkpoint appears before every LLM message, and before every user message (be */ -const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { - const llmChatMessages: LLMChatMessage[] = [] - for (const c of chatMessages) { - if (c.role === 'user') { - llmChatMessages.push({ role: c.role, content: c.content }) - } - else if (c.role === 'assistant') - 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 === 'decorative_canceled_tool') { // pass - } - else if (c.role === 'checkpoint') { // pass - } - else { - throw new Error(`Role ${(c as any).role} not recognized.`) - } - } - return llmChatMessages -} - - type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] const defaultMessageState: UserMessageState = { @@ -139,10 +118,9 @@ export type ThreadStreamState = { // streaming related - when streaming message streamingToken?: string; - messageSoFar?: string; + displayContentSoFar?: string; reasoningSoFar?: string; - toolNameSoFar?: string; - toolParamsSoFar?: string; + toolCallSoFar?: RawToolCallObj; } } @@ -380,9 +358,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { else if (behavior === 'set') { this.streamState[threadId] = state } + else throw new Error(`setStreamState`) } - this._onDidChangeStreamState.fire({ threadId }) } @@ -442,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } return false } - private _updateLatestToolTo = (threadId: string, tool: ChatMessage & { role: 'tool' }) => { + private _updateLatestTool = (threadId: string, tool: ChatMessage & { role: 'tool' }) => { const swapped = this._swapOutLatestStreamingToolWithResult(threadId, tool) if (swapped) return this._addMessageToThread(threadId, tool) @@ -452,33 +430,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) 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' } - if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen - - const instructions = lastUserMessage.displayContent || '' - 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, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + this._runChatAgent({ callThisToolFirst, threadId, ...this._currentModelSelectionProps() }) , threadId ) } @@ -494,29 +454,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else return - const { name, paramsStr, id } = lastMsg + const { name } = lastMsg const errorMessage = this.errMsgs.rejected - this._updateLatestToolTo(threadId, { role: 'tool', type: 'rejected', params: params, name: name, paramsStr: paramsStr, id, content: errorMessage, result: null }) + this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, 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 @@ -531,19 +475,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { 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 displayContentSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - const toolInProgress = this.streamState[threadId]?.toolNameSoFar - console.log('toolInProgress', toolInProgress) + const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } - this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null }) - if (toolInProgress) { - this._addMessageToThread(threadId, { role: 'decorative_canceled_tool', name: toolInProgress }) + if (toolCallSoFar) { + this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) } + + this._addUserCheckpoint({ threadId }) } this._setStreamState(threadId, {}, 'set') @@ -551,18 +496,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { - private _tools = (chatMode: ChatMode) => { - const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined - : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) - : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] - : undefined - - const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) - return tools - } - - - private readonly errMsgs = { rejected: 'Tool call was rejected by the user.', errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` @@ -571,140 +504,162 @@ class ChatThreadService extends Disposable implements IChatThreadService { private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} + + + // system message + private _generateSystemMessage = async (chatMode: ChatMode) => { + const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) + + const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; + const activeURI = this._editorService.activeEditor?.resource?.fsPath; + + const directoryStr = await this._directoryStrService.getAllDirectoriesStr({ + cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...` + : `...Directories string cut off, ask user for more if necessary...` + }) + + const runningTerminalIds = this._terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode }) + return systemMessage + } + + private _generateLLMMessages = async (threadId: string) => { + const thread = this.state.allThreads[threadId] + if (!thread) return [] + + const chatMessages = deepClone(thread.messages) + const llmChatMessages: LLMChatMessage[] = [] + + // merge tools into user message + for (const c of chatMessages) { + if (c.role === 'assistant') { + // if called a tool, re-add its XML to the message + // alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere + let content = c.displayContent + if (c.toolCall) { + content = `${content}\n\n${toolCallXMLStr(c.toolCall)}` + } + llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning }) + } + else if (c.role === 'user' || c.role === 'tool') { + if (c.role === 'tool') + c.content = `<${c.name}_result>\n${c.content}\n` + + if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user') + llmChatMessages.push({ role: 'user', content: c.content }) + else + llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content + + } + else if (c.role === 'interrupted_streaming_tool') { // pass + } + else if (c.role === 'checkpoint') { // pass + } + else { + throw new Error(`Role ${(c as any).role} not recognized.`) + } + } + return llmChatMessages + } + + + // returns true when the tool call is waiting for user approval + private _runToolCall = async ( + threadId: string, + toolName: ToolName, + opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj }, + ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { + + // compute these below + let toolParams: ToolCallParams[ToolName] + let toolResult: Awaited + let toolResultStr: string + + if (!opts.preapproved) { // skip this if pre-approved + // 1. validate tool params + try { + const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams) + toolParams = params + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, 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 toolRequiresApproval = toolNamesThatRequireApproval.has(toolName) + if (toolRequiresApproval) { + 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', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams }) + if (!autoApprove) { + return { awaitingUserApproval: true } + } + } + } + else { + toolParams = opts.validatedParams + } + + // 3. call the tool + this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') + this._updateLatestTool(threadId, { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null }) + + let interrupted = false + try { + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) + this._currentlyRunningToolInterruptor[threadId] = () => { + interrupted = true; + interruptTool?.(); + delete this._currentlyRunningToolInterruptor[threadId]; + } + toolResult = await result // ts is bad... await is needed + } + catch (error) { + if (interrupted) { + // the tool result is added when we stop running + return { interrupted: true } + } + const errorMessage = getErrorMessage(error) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) + return {} + } + + // 4. stringify the result to give to the LLM + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = this.errMsgs.errWhenStringifying(error) + this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) + return {} + } + + // 5. add to history and keep going + this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, }) + + return {} + }; + + + + private async _runChatAgent({ threadId, modelSelection, modelSelectionOptions, - userMessageContent, callThisToolFirst, }: { threadId: string, modelSelection: ModelSelection | null, modelSelectionOptions: ModelSelectionOptions | undefined, - userMessageContent: string, // content of LATEST user message callThisToolFirst?: ToolMessage & { type: 'tool_request' } }) { - const userMessageFullContent = userMessageContent - const getLatestMessages = async () => { - // replace last userMessage with userMessageFullContent (which contains all the files too) - const thread = this.state.allThreads[threadId] - const latestMessages = thread?.messages ?? [] - const messages_ = toLLMChatMessages(latestMessages) - const lastUserMsgIdx = findLastIdx(messages_, m => m.role === 'user') - if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!) - - // system message - const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - - 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, directoryStr, activeURI, runningTerminalIds, chatMode }) - - // all messages so far in the chat history (including tools) - const messages: LLMChatMessage[] = [ - { role: 'system', content: systemMessage, }, - ...messages_.slice(0, lastUserMsgIdx), - { role: 'user', content: userMessageFullContent }, - ...messages_.slice(lastUserMsgIdx + 1, Infinity), - ] - // console.log('MESSAGES!!!', messages) - return messages - } - - - - // returns true when the tool call is waiting for user approval - const handleToolCall = async ( - tool: ToolCallType, - opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { - const toolName: ToolName = tool.name - const toolParamsStr = tool.paramsStr - const toolId = tool.id - - // compute these below - let toolParams: ToolCallParams[ToolName] - let toolResult: ToolResultType[typeof toolName] - let toolResultStr: string - - if (!opts?.preapproved) { // skip this if pre-approved - // 1. validate tool params - try { - const params = await this._toolsService.validateParams[toolName](toolParamsStr) - toolParams = params - } catch (error) { - const errorMessage = getErrorMessage(error) - 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', type: 'tool_request', content: '(never)', result: null, name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) - if (!autoApprove) { - return { awaitingUserApproval: true } - } - } - } - else { - toolParams = opts.toolParams - } - - // 3. call the tool - this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - let interrupted = false - try { - const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) - this._currentlyRunningToolInterruptor[threadId] = () => { - interrupted = true; - interruptTool?.(); - delete this._currentlyRunningToolInterruptor[threadId]; - } - toolResult = await result // ts is bad... await is needed - } - catch (error) { - if (interrupted) { - // the tool result is added when we stop running - return { interrupted: true } - } - const errorMessage = getErrorMessage(error) - this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, }) - return {} - } - - // 4. stringify the result to give to the LLM - try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - } catch (error) { - const errorMessage = this.errMsgs.errWhenStringifying(error) - 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._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, }) - - return {} - }; // above just defines helpers, below starts the actual function const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here - const tools = this._tools(chatMode) // clear any previous error this._setStreamState(threadId, { error: undefined }, 'set') @@ -716,7 +671,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params }) if (interrupted) return } @@ -727,34 +682,39 @@ class ChatThreadService extends Disposable implements IChatThreadService { isRunningWhenEnd = undefined nMessagesSent += 1 - let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge') - const messages = await getLatestMessages() + const systemMessage = await this._generateSystemMessage(chatMode) + const llmMessages = await this._generateLLMMessages(threadId) + const messages: LLMChatMessage[] = [ + { role: 'system', content: systemMessage }, + ...llmMessages + ] + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', + chatMode, messages, - tools: tools, modelSelection, modelSelectionOptions, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, - onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => { - this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge') + onText: ({ fullText, fullReasoning, toolCall }) => { + this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) - // added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge') - // resolve with tool calls - resMessageIsDonePromise(toolCalls) + onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { + this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning }) + this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') + resMessageIsDonePromise(toolCall) // resolve with tool calls }, onError: (error) => { - const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null }) this._setStreamState(threadId, { error }, 'set') resMessageIsDonePromise() }, @@ -774,14 +734,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { break } this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message - const toolCalls = await messageIsDonePromise // wait for message to complete + const toolCall = await messageIsDonePromise // wait for message to complete if (aborted) { return } this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done // call tool if there is one - const tool: ToolCallType | undefined = toolCalls?.[0] + const tool: RawToolCallObj | undefined = toolCall if (tool) { - const { awaitingUserApproval, interrupted } = await handleToolCall(tool) + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams }) // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. // just detect tool interruption which is the same as chat interruption right now @@ -1118,14 +1078,21 @@ We only need to do it for files that were edited since `from`, ie files between if (!thread) return // should never happen + const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread + if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') { + // if about to call the other LLM, just wait for it by stopping right now + return + } + // stop it (this simply resolves the promise to free up space) + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) + + + // 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) const { chatMode } = this._settingsService.state.globalSettings @@ -1141,7 +1108,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({ threadId, userMessageContent, ...this._currentModelSelectionProps(), }), + this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }), threadId, ) } @@ -1245,7 +1212,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, include: null, pageNumber: 0 }) + const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 }) uris = result.uris } catch (e) { return null @@ -1517,6 +1484,10 @@ We only need to do it for files that were edited since `from`, ie files between } } }, true) + + // when change focused message idx, jump + if (messageIdx !== undefined) + this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true }) } // set message.state diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index 4cdb8f00..bbfa4b8e 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -15,18 +15,17 @@ 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'; +import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.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 }> + getDirectoryStrTool(uri: URI): Promise + getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise } export const IDirectoryStrService = createDecorator('voidDirectoryStrService'); @@ -275,18 +274,21 @@ 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); - const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL); + const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_TOOL); - return { - str: `Directory of ${uri.fsPath}:\n${content}`, - wasCutOff, - } + let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL) + c = `Directory of ${uri.fsPath}:\n${content}` + if (wasCutOff) c = `${c}\n...Result was truncated...` + + return c } - async getAllDirectoriesStr() { + async getAllDirectoriesStr({ cutOffMessage }: { cutOffMessage: string }) { let str: string = ''; let cutOff = false; const folders = this.workspaceContextService.getWorkspace().folders; + if (folders.length === 0) + return '(NO WORKSPACE OPEN)'; for (let i = 0; i < folders.length; i += 1) { if (i > 0) str += '\n'; @@ -301,8 +303,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { // 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); + const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length); str += content; if (wasCutOff) { cutOff = true; @@ -310,7 +311,10 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService { } } - return { wasCutOff: cutOff, str }; + if (cutOff) { + return `${str}\n${cutOffMessage}` + } + return str } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2b4d2eae..efd70d7e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -44,7 +44,6 @@ import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApply 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'; @@ -72,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); const numLinesOfStr = (str: string) => str.split('\n').length + +export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => { + let lengthOfTextPx = 0; + for (const char of content) { + if (char === '\t') { + lengthOfTextPx += tabWidth + } else { + lengthOfTextPx += spaceWidth; + } + } + + return lengthOfTextPx +} + + const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -95,16 +109,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth; const tabWidth = numSpacesInTab * spaceWidth; - let paddingLeft = 0; - for (const char of leadingWhitespace) { - if (char === '\t') { - paddingLeft += tabWidth - } else if (char === ' ') { - paddingLeft += spaceWidth; - } - } + const leftWhitespacePx = getLengthOfTextPx({ + tabWidth, + spaceWidth, + content: leadingWhitespace + }); - return paddingLeft; + + return leftWhitespacePx; }; @@ -190,7 +202,6 @@ class EditCodeService extends Disposable implements IEditCodeService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IFileService private readonly _fileService: IFileService, @IVoidModelService private readonly _voidModelService: IVoidModelService, - @ITextFileService private readonly _textFileService: ITextFileService, ) { super(); @@ -720,16 +731,14 @@ class EditCodeService extends Disposable implements IEditCodeService { resource: uri, label: 'Void Agent', code: 'undoredo.editCode', - undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); }, - redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) } + undo: async () => { opts?.onWillUndo?.(); await this._restoreVoidFileSnapshot(uri, beforeSnapshot) }, + redo: async () => { if (afterSnapshot) await this._restoreVoidFileSnapshot(uri, afterSnapshot) } } this._undoRedoService.pushElement(elt) const onFinishEdit = async () => { 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) - }) + await this._voidModelService.saveModel(uri) } return { onFinishEdit } } @@ -1105,6 +1114,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = this._getURIBeforeStartApplying(opts) if (!uri) return await this._voidModelService.initializeModel(uri) + await this._voidModelService.saveModel(uri) // save the URI } @@ -1400,6 +1410,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + chatMode: null, // not chat onText: (params) => { const { fullText: fullText_ } = params const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) @@ -1586,7 +1597,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - const N_RETRIES = 5 + const N_RETRIES = 2 // allowed to throw errors - this is called inside a promise that handles everything const runSearchReplace = async () => { @@ -1617,6 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + chatMode: null, // not chat onText: (params) => { const { fullText } = params // blocks are [done done done ... {writingFinal|writingOriginal}] @@ -1876,6 +1888,8 @@ class EditCodeService extends Disposable implements IEditCodeService { interruptURIStreaming({ uri }: { uri: URI }) { + if (!this._uriIsStreaming(uri)) return + this._undoHistory(uri) // brute force for now is OK for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -1883,7 +1897,6 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!diffArea._streamState.isStreaming) continue this._stopIfStreaming(diffArea) } - this._undoHistory(uri) } diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index 3a420737..ac5ff5f3 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -76,93 +76,107 @@ opacity: 80%; } +/* styles for all containers used by void */ +.void-scope { + --scrollbar-vertical-width: 8px; + --scrollbar-horizontal-height: 6px; +} +/* Target both void-scope and all its descendants with scrollbars */ +.void-scope, +.void-scope * { + scrollbar-width: thin !important; + scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */ +} +.void-scope::-webkit-scrollbar, +.void-scope *::-webkit-scrollbar { + width: var(--scrollbar-vertical-width) !important; + height: var(--scrollbar-horizontal-height) !important; + background-color: var(--void-bg-3) !important; +} +.void-scope::-webkit-scrollbar-thumb, +.void-scope *::-webkit-scrollbar-thumb { + background-color: var(--void-bg-1) !important; + border-radius: 4px !important; + border: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.void-scope::-webkit-scrollbar-thumb:hover, +.void-scope *::-webkit-scrollbar-thumb:hover { + background-color: var(--void-bg-1) !important; + filter: brightness(1.1) !important; +} + +.void-scope::-webkit-scrollbar-thumb:active, +.void-scope *::-webkit-scrollbar-thumb:active { + background-color: var(--void-bg-1) !important; + filter: brightness(1.2) !important; +} + +.void-scope::-webkit-scrollbar-track, +.void-scope *::-webkit-scrollbar-track { + background-color: var(--void-bg-3) !important; + border: none !important; +} + +.void-scope::-webkit-scrollbar-corner, +.void-scope *::-webkit-scrollbar-corner { + background-color: var(--void-bg-3) !important; +} + +/* Add void-scrollable-element styles to match */ +.void-scrollable-element { + background-color: var(--vscode-editor-background); + --scrollbar-vertical-width: 14px; + --scrollbar-horizontal-height: 6px; + overflow: auto; /* Ensure scrollbars are shown when needed */ +} + +.void-scrollable-element, +.void-scrollable-element * { + scrollbar-width: thin !important; /* For Firefox */ + scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */ +} .void-scrollable-element::-webkit-scrollbar, .void-scrollable-element *::-webkit-scrollbar { - width: 14px !important; - height: 4px !important; -} - -.void-scrollable-element::-webkit-scrollbar-track, -.void-scrollable-element *::-webkit-scrollbar-track { - background: transparent !important; + width: var(--scrollbar-vertical-width) !important; + height: var(--scrollbar-horizontal-height) !important; + background-color: var(--void-bg-3) !important; } .void-scrollable-element::-webkit-scrollbar-thumb, .void-scrollable-element *::-webkit-scrollbar-thumb { - background-color: transparent !important; - border-radius: 0px !important; + background-color: var(--void-bg-1) !important; + border-radius: 4px !important; + border: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; } .void-scrollable-element::-webkit-scrollbar-thumb:hover, .void-scrollable-element *::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground) !important; + background-color: var(--void-bg-1) !important; + filter: brightness(1.1) !important; } .void-scrollable-element::-webkit-scrollbar-thumb:active, .void-scrollable-element *::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground) !important; + background-color: var(--void-bg-1) !important; + filter: brightness(1.2) !important; +} + +.void-scrollable-element::-webkit-scrollbar-track, +.void-scrollable-element *::-webkit-scrollbar-track { + background-color: var(--void-bg-3) !important; + border: none !important; } .void-scrollable-element::-webkit-scrollbar-corner, .void-scrollable-element *::-webkit-scrollbar-corner { - background-color: transparent !important; -} - -.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb { - background-color: var(--vscode-scrollbarSlider-background) !important; + background-color: var(--void-bg-3) !important; } diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 26b5bc37..9507aa59 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -74,7 +74,7 @@ function saveStylesFile() { } catch (err) { console.error('[scope-tailwind] Error saving styles.css:', err); } - }, 4000); + }, 6000); } const args = process.argv.slice(2); 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 c2ef204e..9f364ba0 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 @@ -11,6 +11,7 @@ import { URI } from '../../../../../../../base/common/uri.js' 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 { PlacesType, VariantType } from 'react-tooltip' enum CopyButtonText { Idle = 'Copy', @@ -20,30 +21,28 @@ enum CopyButtonText { type IconButtonProps = { - onClick: () => void; Icon: LucideIcon - disabled?: boolean - className?: string } -export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => ( +export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes) => ( @@ -94,13 +93,14 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => { return } -export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { +export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -110,6 +110,8 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + {...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })} + {...props} /> ) return jumpToFileButton @@ -122,7 +124,6 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => { ) } @@ -163,10 +164,11 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u rerender(c => c + 1) console.log('rerendering....') } - }, [applyBoxId, applyBoxId, uri])) + }, [applyBoxId, uri])) const currStreamState = getStreamState() + return { getStreamState, isDisabled, @@ -175,22 +177,61 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u } -export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => { +type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null +export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes) => { + return ( +
+ {title && {title}} +
+
+ ); +}; + +const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({ + 'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip', + 'data-tooltip-place': position as PlacesType, + 'data-tooltip-content': `${tooltipName}`, + 'data-tooltip-offset': offset, +}) + + +export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes) => { + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) - return
-
-
+ const color = ( + currStreamState === 'idle-no-changes' ? 'dark' : + currStreamState === 'streaming' ? 'orange' : + currStreamState === 'idle-has-changes' ? 'green' : + null + ) + + const tooltipName = ( + currStreamState === 'idle-no-changes' ? 'Done' : + currStreamState === 'streaming' ? 'Applying' : + currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad + '' + ) + + const statusIndicatorHTML = + return statusIndicatorHTML } + export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -216,7 +257,10 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? [] // catch any errors by interrupting the stream - applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) }) + applyDonePromise?.catch(e => { + const uri = getUriBeingApplied(applyBoxId) + if (uri) editCodeService.interruptURIStreaming({ uri: uri }) + }) applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined @@ -251,11 +295,22 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co if (currStreamState === 'streaming') { - return + return } if (currStreamState === 'idle-no-changes') { - return + + return } if (currStreamState === 'idle-has-changes') { @@ -267,19 +322,18 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co } } - export const BlockCodeApplyWrapper = ({ children, initValue, @@ -314,7 +368,7 @@ export const BlockCodeApplyWrapper = ({ {/* header */}
- + {name} 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 d541dc43..2a531815 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 @@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; -import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; export const QuickEditChat = ({ @@ -89,8 +88,6 @@ export const QuickEditChat = ({ editCodeService.removeCtrlKZone({ diffareaid }) }, [editCodeService, diffareaid]) - useScrollbarStyles(sizerRef) - const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() const chatAreaRef = useRef(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 bad7832e..bad585f8 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 @@ -15,17 +15,19 @@ import { ErrorDisplay } from './ErrorDisplay.js'; import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; -import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; 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, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; +import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react'; 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 { ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { PlacesType } from 'react-tooltip'; +import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; @@ -350,7 +352,7 @@ export const VoidChatArea: React.FC = ({
-
+
{featureName === 'Chat' && }
@@ -391,6 +393,9 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re ${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'} ${className} `} + // data-tooltip-id='void-tooltip' + // data-tooltip-content={'Send'} + // data-tooltip-place='left' {...props} > @@ -653,6 +658,7 @@ type ToolHeaderParams = { numResults?: number; hasNextPage?: boolean; children?: React.ReactNode; + bottomChildren?: React.ReactNode; onClick?: () => void; isOpen?: boolean, } @@ -680,23 +686,26 @@ const ToolHeaderWrapper = ({ return (
{/* header */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - if (onClick) { onClick(); } - }} - > - {isDropdown && ( - - )} +
{/* left */} -
+
{ + if (isDropdown) { setIsOpen(v => !v); } + if (onClick) { onClick(); } + }} + > + {isDropdown && ()} {title} - {desc1} + {desc1}
{/* right */} @@ -772,7 +781,7 @@ const SimplifiedToolHeader = ({ -const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => { +const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, currCheckpointIdx, _scrollToBottom }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, currCheckpointIdx: number | undefined, isCheckpointGhost: boolean, _scrollToBottom: (() => void) | null }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -923,7 +932,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr } - + const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 return
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -952,25 +961,32 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, _scr
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> + +
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
+
@@ -1023,6 +1039,7 @@ const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { prose-blockquote:pl-2 prose-blockquote:my-2 + prose-code:text-void-fg-3 prose-code:text-[12px] prose-code:before:content-none prose-code:after:content-none @@ -1074,7 +1091,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted const reasoningStr = chatMessage.reasoning?.trim() || null const hasReasoning = !!reasoningStr - const isDoneReasoning = !!chatMessage.content + const isDoneReasoning = !!chatMessage.displayContent const thread = chatThreadsService.getCurrentThread() @@ -1083,7 +1100,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted messageIdx: messageIdx, } - const isEmpty = !chatMessage.content && !chatMessage.reasoning + const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning if (isEmpty) return null return <> @@ -1107,7 +1124,7 @@ const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted
{ +) + +export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( + +) + + + const CommandBarInChat = () => { - const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - const [isExpanded, setIsExpanded] = useState(false) + const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + const numFilesChanged = sortedCommandBarURIs.length const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') const commandService = accessor.get('ICommandService') + const chatThreadsState = useChatThreadsState() + const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) { - return null - } + const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState<'auto-opened' | 'auto-closed' | 'user-opened' | 'user-closed'>('auto-closed'); + const isFileDetailsOpened = fileDetailsOpenedState === 'auto-opened' || fileDetailsOpenedState === 'user-opened'; + + + useEffect(() => { + // close the file details if there are no files + // this converts 'user-closed' to 'auto-closed' + if (numFilesChanged === 0) { + setFileDetailsOpenedState('auto-closed') + } + // open the file details if it hasnt been closed + if (numFilesChanged > 0 && fileDetailsOpenedState !== 'user-closed') { + setFileDetailsOpenedState('auto-opened') + } + }, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged]) + + + const isFinishedMakingThreadChanges = numFilesChanged !== 0 && (chatThreadsStreamState ? !chatThreadsStreamState.isRunning : true) + + // ======== status of agent ======== + // This icon answers the question "is the LLM doing work on this thread?" + // assume it is single threaded for now + // green = Running + // orange = Requires action + // dark = Done + + const threadStatus = ( + chatThreadsStreamState?.isRunning === 'awaiting_user' ? { title: 'Needs Approval', color: 'yellow', } as const + : chatThreadsStreamState?.isRunning ? { title: 'Running', color: 'orange', } as const + : { title: 'Done', color: 'dark', } as const + ) + + + const threadStatusHTML = + + + // ======== info about changes ======== + // num files changed + // acceptall + rejectall + // popup info about each change (each with num changes + acceptall + rejectall of their own) + + const numFilesChangedStr = numFilesChanged === 0 ? 'No files with changes' + : `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? '' : 's'} with changes` + + + + + const acceptRejectAllButtons =
+ { + sortedCommandBarURIs.forEach(uri => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "reject", + _addToHistory: true, + }); + }); + }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Reject all' + /> + + { + sortedCommandBarURIs.forEach(uri => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "accept", + _addToHistory: true, + }); + }); + }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Accept all' + /> + + + +
+ + + // !select-text cursor-auto + const fileDetailsContent =
+ {sortedCommandBarURIs.map((uri, i) => { + const basename = getBasename(uri.fsPath) + + const { sortedDiffIds, isStreaming } = commandBarStateOfURI[uri.fsPath] ?? {} + const isFinishedMakingFileChanges = !isStreaming + + const numDiffs = sortedDiffIds?.length || 0 + + const fileStatus = (isFinishedMakingFileChanges + ? { title: 'Done', color: 'dark', } as const + : { title: 'Running', color: 'orange', } as const + ) + + const fileNameHTML =
commandService.executeCommand('vscode.open', uri, { preview: true })} + > + {/* */} + {basename} +
+ + + + + const detailsContent =
+ {numDiffs} diff{numDiffs !== 1 ? 's' : ''} +
+ + const acceptRejectButtons =
+ + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Reject file' + + /> + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Accept file' + /> + +
+ + const fileStatusHTML = + + return ( + // name, details +
+
+ {fileNameHTML} + {detailsContent} +
+
+ {acceptRejectButtons} + {fileStatusHTML} +
+
+ ) + })} +
+ + const fileDetailsButton = ( + + ) return ( - - {sortedCommandBarURIs.map((uri, i) => ( - { commandService.executeCommand('vscode.open', uri, { preview: true }) }} - /> - ))} - + <> + {/* file details */} +
+
+ {fileDetailsContent} +
+
+ {/* main content */} +
+
+ {fileDetailsButton} +
+
+ {acceptRejectAllButtons} + {threadStatusHTML} +
+
+ ) } @@ -2004,12 +2296,12 @@ export const SidebarChat = () => { const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) const isRunning = currThreadStreamState?.isRunning const latestError = currThreadStreamState?.error - const messageSoFar = currThreadStreamState?.messageSoFar + const displayContentSoFar = currThreadStreamState?.displayContentSoFar + const toolCallSoFar = currThreadStreamState?.toolCallSoFar const reasoningSoFar = currThreadStreamState?.reasoningSoFar - const toolNameSoFar = currThreadStreamState?.toolNameSoFar - const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar - const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit) + // this is just if it's currently being generated, NOT if it's currently running + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2022,8 +2314,6 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - useScrollbarStyles(sidebarRef) - const onSubmit = useCallback(async () => { if (isDisabled) return @@ -2061,11 +2351,10 @@ export const SidebarChat = () => { const threadId = currentThread.id - const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? Infinity // if not exist, treat like checkpoint is last message (infinity) + const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity) const previousMessagesHTML = useMemo(() => { const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') - // tool request shows up as Editing... if in progress return previousMessages.map((message, i) => { return { _scrollToBottom={() => scrollToBottom(scrollContainerRef)} /> }) - }, [previousMessages, isRunning, threadId]) + }, [previousMessages, threadId, currCheckpointIdx, isRunning]) const streamingChatIdx = previousMessagesHTML.length - const currStreamingMessageHTML = reasoningSoFar || messageSoFar || isRunning ? + const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ? { /> : null - const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar - const messagesHTML = { w-full h-full overflow-x-hidden overflow-y-auto - ${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} + ${previousMessagesHTML.length === 0 && !displayContentSoFar ? 'hidden' : ''} `} > {/* previous messages */} {previousMessagesHTML} - - {currStreamingMessageHTML} - {toolIsGenerating ? - Generating} /> + Generating} + /> : null} {isRunning === 'LLM' && !toolIsGenerating ? @@ -2159,33 +2449,40 @@ export const SidebarChat = () => { } }, [onSubmit, onAbort, isRunning]) - const inputForm =
- { textAreaRef.current?.focus() }} + const inputForm =
+
+ {previousMessages.length > 0 && + + } +
+
- { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} - ref={textAreaRef} - fnsRef={textAreaFnsRef} - multiline={true} - /> + { textAreaRef.current?.focus() }} + > + { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} + ref={textAreaRef} + fnsRef={textAreaFnsRef} + multiline={true} + /> - + +
return ( 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 7db16221..96909236 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 @@ -90,7 +90,7 @@ export const SidebarThreadSelector = () => { // secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? ''); // } - const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length; + const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length; return (
  • diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 8e82d750..d8a5bb79 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -106,6 +106,7 @@ export const VoidInputBox2 = forwardRef(fun return (