diff --git a/package-lock.json b/package-lock.json index 70f4138a..87c05495 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@vscode/sudo-prompt": "9.3.1", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", + "@vscode/webview-ui-toolkit": "^1.4.0", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -38,6 +39,7 @@ "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", "kerberos": "2.1.1", + "lucide-react": "^0.460.0", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", @@ -2065,6 +2067,52 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@microsoft/fast-element": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz", + "integrity": "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==", + "license": "MIT" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.50.0.tgz", + "integrity": "sha512-8mFYG88Xea1jZf2TI9Lm/jzZ6RWR8x29r24mGuLojNYqIR2Bl8+hnswoV6laApKdCbGMPKnsAL/O68Q0sRxeVg==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.14.0", + "@microsoft/fast-web-utilities": "^5.4.1", + "tabbable": "^5.2.0", + "tslib": "^1.13.0" + } + }, + "node_modules/@microsoft/fast-foundation/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.3.25.tgz", + "integrity": "sha512-jKzmk2xJV93RL/jEFXEZgBvXlKIY4N4kXy3qrjmBfFpqNi3VjY+oUTWyMnHRMC5EUhIFxD+Y1VD4u9uIPX3jQw==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.14.0", + "@microsoft/fast-foundation": "^2.50.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "license": "MIT", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4111,6 +4159,21 @@ "node": ">= 0.6" } }, + "node_modules/@vscode/webview-ui-toolkit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz", + "integrity": "sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "^1.12.0", + "@microsoft/fast-foundation": "^2.49.4", + "@microsoft/fast-react-wrapper": "^0.3.22", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@vscode/windows-ca-certs": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.1.tgz", @@ -8022,6 +8085,12 @@ "node": ">=0.8.x" } }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", + "license": "MIT" + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -14202,6 +14271,15 @@ "es5-ext": "~0.10.2" } }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -20392,6 +20470,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", + "license": "MIT" + }, "node_modules/table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", @@ -21271,8 +21355,7 @@ "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tsscmp": { "version": "1.0.6", diff --git a/package.json b/package.json index 0a5189a7..dc1b4e96 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@vscode/sudo-prompt": "9.3.1", "@vscode/tree-sitter-wasm": "^0.0.4", "@vscode/vscode-languagedetection": "1.0.21", + "@vscode/webview-ui-toolkit": "^1.4.0", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -100,6 +101,7 @@ "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", "kerberos": "2.1.1", + "lucide-react": "^0.460.0", "minimist": "^1.2.6", "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 29429516..59e8af61 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -121,6 +121,8 @@ import { normalizeNFC } from '../../base/common/normalization.js'; import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; +import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js'; + /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -148,7 +150,7 @@ export class CodeApplication extends Disposable { @IStateService private readonly stateService: IStateService, @IFileService private readonly fileService: IFileService, @IProductService private readonly productService: IProductService, - @IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService + @IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService, ) { super(); @@ -508,6 +510,16 @@ export class CodeApplication extends Disposable { }); //#endregion + + // //#region Void IPC + // validatedIpcMain.handle('vscode:sendLLMMessage', async (event, data) => { + // try { + // await this.sendLLMMessage(data); + // } catch (error) { + // console.error('Error sending LLM message:', error); + // } + // }); + // //#endregion } private onUnexpectedError(error: Error): void { @@ -1225,6 +1237,11 @@ export class CodeApplication extends Disposable { mainProcessElectronServer.registerChannel('logger', loggerChannel); sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel)); + // Void + // const sendLLMMessageChannel = ProxyChannel.fromService(accessor.get(ISendLLMMessageService), disposables); + const sendLLMMessageChannel = new LLMMessageChannel(); + mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel); + // Extension Host Debug Broadcasting const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService)); mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel); diff --git a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts index 0303e43f..7c2d1324 100644 --- a/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts +++ b/src/vs/editor/browser/services/inlineDiffService/inlineDiffService.ts @@ -12,7 +12,7 @@ export interface IInlineDiffService { removeDiffs(editor: ICodeEditor): void; } -export const IInlineDiffService = createDecorator('inlineDiffService'); +export const IInlineDiffService = createDecorator('inlineDiffServiceOld'); class InlineDiffService extends Disposable implements IInlineDiffService { private readonly _diffDecorations = new Map(); diff --git a/src/vs/platform/void/browser/llmMessageService.ts b/src/vs/platform/void/browser/llmMessageService.ts new file mode 100644 index 00000000..61f51ed8 --- /dev/null +++ b/src/vs/platform/void/browser/llmMessageService.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPLv3 License. + *--------------------------------------------------------------------------------------------*/ + +import { ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, LLMMessageServiceParams, ProxyLLMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js'; +import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; +import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { Event } from '../../../base/common/event.js'; +import { IDisposable } from '../../../base/common/lifecycle.js'; + + +// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE +export const ISendLLMMessageService = createDecorator('sendLLMMessageService'); + +// defines an interface that node/ creates and browser/ uses +export interface ISendLLMMessageService { + readonly _serviceBrand: undefined; + sendLLMMessage: (params: LLMMessageServiceParams) => string; + abort: (requestId: string) => void; +} + + +export class SendLLMMessageService implements ISendLLMMessageService { + + readonly _serviceBrand: undefined; + private readonly channel: IChannel; + + private readonly _disposablesOfRequestId: Record = {} + + constructor( + @IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side) + ) { + + this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage') + // const service = ProxyChannel.toService(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here + } + + _addDisposable(requestId: string, disposable: IDisposable) { + if (!this._disposablesOfRequestId[requestId]) { + this._disposablesOfRequestId[requestId] = [] + } + this._disposablesOfRequestId[requestId].push(disposable) + } + + + + sendLLMMessage(params: LLMMessageServiceParams) { + const requestId_ = generateUuid(); + const { onText, onFinalMessage, onError, ...proxyParams } = params; + + // listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it + + const onTextEvent: Event = this.channel.listen('onText') + this._addDisposable(requestId_, + onTextEvent(e => { + if (requestId_ !== e.requestId) return; + onText(e) + }) + ) + + const onFinalMessageEvent: Event = this.channel.listen('onFinalMessage') + this._addDisposable(requestId_, + onFinalMessageEvent(e => { + if (requestId_ !== e.requestId) return; + onFinalMessage(e) + this._dispose(requestId_) + }) + ) + + const onErrorEvent: Event = this.channel.listen('onError') + this._addDisposable(requestId_, + onErrorEvent(e => { + if (requestId_ !== e.requestId) return; + console.log('event onError', JSON.stringify(e)) + onError(e) + this._dispose(requestId_) + }) + ) + + // params will be stripped of all its functions + this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams); + + return requestId_ + } + + private _dispose(requestId: string) { + if (!(requestId in this._disposablesOfRequestId)) return + for (const disposable of this._disposablesOfRequestId[requestId]) { + disposable.dispose() + } + delete this._disposablesOfRequestId[requestId] + } + + abort(requestId: string) { + this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams); + this._dispose(requestId) + } +} + +registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed); + diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts new file mode 100644 index 00000000..f62e5183 --- /dev/null +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPLv3 License. + *--------------------------------------------------------------------------------------------*/ + +import { VoidConfig } from '../../../workbench/contrib/void/browser/registerConfig.js'; + +// ---------- type definitions ---------- + +export type OnText = (p: { newText: string, fullText: string }) => void + +export type OnFinalMessage = (p: { fullText: string }) => void + +export type OnError = (p: { error: Error | string }) => void + +export type AbortRef = { current: (() => void) | null } + +export type LLMMessage = { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export type LLMMessageOptions = { + stopTokens?: string[], + prefix?: string, + suffix?: string, +} + +export type LLMMessageServiceParams = { + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + + messages: LLMMessage[]; + voidConfig: VoidConfig | null; + + logging: { + loggingName: string, + }; + + options: LLMMessageOptions; +} + +export type SendLLMMMessageParams = { + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + + messages: LLMMessage[]; + voidConfig: VoidConfig | null; + + logging: { + loggingName: string, + }; + options: LLMMessageOptions; + + abortRef: AbortRef; +} + +// can't send functions across a proxy, use listeners instead +export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const +export type ProxyLLMMessageParams = Omit & { requestId: string } + +export type ProxyOnTextPayload = Parameters[0] & { requestId: string } +export type ProxyOnFinalMessagePayload = Parameters[0] & { requestId: string } +export type ProxyOnErrorPayload = Parameters[0] & { requestId: string } + +export type ProxyLLMMessageAbortParams = { requestId: string } diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts new file mode 100644 index 00000000..9fb7224a --- /dev/null +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPLv3 License. + *--------------------------------------------------------------------------------------------*/ + +// this channel is registered in `app.ts` +// code convention is to make a service responsible for this stuff, and not a channel, but this is simpler. +// you could create one instance in electron-main/my-service.ts and one in browser/my-service.ts (and define the interface IMyService in common/my-service.ts), but we just use a channel here +// registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed); + +import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { sendLLMMessage } from '../../../workbench/contrib/void/browser/react/out/util/sendLLMMessage.js'; +import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js'; + +// NODE IMPLEMENTATION OF SENDLLMMESSAGE - calls sendLLMMessage() and returns listeners + +export class LLMMessageChannel implements IServerChannel { + private readonly _onText = new Emitter(); + readonly onText = this._onText.event; + + private readonly _onFinalMessage = new Emitter(); + readonly onFinalMessage = this._onFinalMessage.event; + + private readonly _onError = new Emitter(); + readonly onError = this._onError.event; + + + private readonly _abortRefOfRequestId: Record = {} + + + constructor() { } + + // browser uses this to listen for changes + listen(_: unknown, event: typeof listenerNames[number]): Event { + if (event === 'onText') { + return this.onText; + } + else if (event === 'onFinalMessage') { + return this.onFinalMessage; + } + else if (event === 'onError') { + return this.onError; + } + else { + throw new Error(`Event not found: ${event}`); + } + } + + // browser uses this to call + async call(_: unknown, command: string, params: any): Promise { + + try { + if (command === 'sendLLMMessage') { + this._callSendLLMMessage(params) + } + else if (command === 'abort') { + this._callAbort(params) + } + else { + throw new Error(`Void sendLLM: command "${command}" not recognized.`) + } + } + catch (e) { + console.log('llmMessageChannel: Call Error:', e) + } + } + + // the only place sendLLMMessage is actually called + private _callSendLLMMessage(params: ProxyLLMMessageParams) { + const { requestId } = params; + + if (!(requestId in this._abortRefOfRequestId)) + this._abortRefOfRequestId[requestId] = { current: null } + + const mainThreadParams: SendLLMMMessageParams = { + ...params, + onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); }, + onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); }, + onError: ({ error }) => { this._onError.fire({ requestId, error }); }, + abortRef: this._abortRefOfRequestId[requestId], + } + sendLLMMessage(mainThreadParams); + } + + private _callAbort(params: ProxyLLMMessageAbortParams) { + const { requestId } = params; + if (!(requestId in this._abortRefOfRequestId)) return + this._abortRefOfRequestId[requestId].current?.() + delete this._abortRefOfRequestId[requestId] + } + + +} diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index ae108b99..0b426a16 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -741,8 +741,24 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }); }); + + + // // Void: send from https:// + // this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => { + // // const voidConfig = this.voidConfigStateService.state.voidConfig + // // const whichApi = voidConfig.default['whichApi'] + // const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint + + // if (endpoint && details.url.startsWith(endpoint)) { + // details.requestHeaders['Origin'] = 'https://app.voideditor.com' + // } + // cb({ cancel: false, requestHeaders: details.requestHeaders }); + // }); } + + + private marketplaceHeadersPromise: Promise | undefined; private getMarketplaceHeaders(): Promise { if (!this.marketplaceHeadersPromise) { diff --git a/src/vs/workbench/contrib/void/browser/getCmdKey.ts b/src/vs/workbench/contrib/void/browser/getCmdKey.ts new file mode 100644 index 00000000..76344138 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/getCmdKey.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPLv3 License. + *--------------------------------------------------------------------------------------------*/ + +import { OperatingSystem, OS } from '../../../../base/common/platform.js'; + +export function getCmdKey(): string { + if (OS === OperatingSystem.Macintosh) { + return '⌘'; + } else { + return 'Ctrl'; + } +} + + + + diff --git a/src/vs/workbench/contrib/void/browser/misc/build.js b/src/vs/workbench/contrib/void/browser/misc/build.js deleted file mode 100644 index 60699b7a..00000000 --- a/src/vs/workbench/contrib/void/browser/misc/build.js +++ /dev/null @@ -1,66 +0,0 @@ -// This is from the old repo - -// const tailwindcss = require('tailwindcss') -// const autoprefixer = require('autoprefixer') -// const postcss = require('postcss') -// const fs = require('fs') - -// const convertTailwindToCSS = ({ from, to }) => { -// console.log('converting ', from, ' --> ', to) - -// const original_css_contents = fs.readFileSync(from, 'utf8') - -// return postcss([ -// tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json -// autoprefixer, -// ]) -// .process(original_css_contents, { from, to }) -// .then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) }) -// .catch(error => { -// console.error('Error in build-css:', error) -// }) -// } - - -// const esbuild = require('esbuild') - -// const convertTSXtoJS = async ({ from, to }) => { -// console.log('converting ', from, ' --> ', to) - -// return esbuild.build({ -// entryPoints: [from], -// bundle: true, -// minify: true, -// sourcemap: true, -// outfile: to, -// format: 'iife', // apparently iife is safe for browsers (safer than cjs) -// platform: 'browser', -// external: ['vscode'], -// }).catch(() => process.exit(1)); -// } - -// (async () => { -// // convert tsx to js -// await convertTSXtoJS({ -// from: 'src/webviews/sidebar/index.tsx', -// to: 'dist/webviews/sidebar/index.js', -// }) - -// await convertTSXtoJS({ -// from: 'src/webviews/ctrlk/index.tsx', -// to: 'dist/webviews/ctrlk/index.js', -// }) - -// await convertTSXtoJS({ -// from: 'src/webviews/diffline/index.tsx', -// to: 'dist/webviews/diffline/index.js', -// }) - -// // convert tailwind to css -// await convertTailwindToCSS({ -// from: 'src/webviews/styles.css', -// to: 'dist/webviews/styles.css', -// }) - -// })() - diff --git a/src/vs/workbench/contrib/void/browser/misc/oldpackage.json b/src/vs/workbench/contrib/void/browser/misc/oldpackage.json deleted file mode 100644 index f1eedb08..00000000 --- a/src/vs/workbench/contrib/void/browser/misc/oldpackage.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "name": "void", - "publisher": "void", - "displayName": "Void", - "description": "", - "version": "0.0.1", - "engines": { - "vscode": "*" - }, - "categories": [ - "Other" - ], - "enabledApiProposals": [ - "editorInsets" - ], - "activationEvents": [], - "main": "./out/extension/extension.js", - "contributes": { - "configuration": { - "title": "Void", - "properties": {} - }, - "commands": [ - { - "command": "typeInspector.inspect", - "title": "Inspect Types of All Variables" - }, - { - "command": "void.ctrl+l", - "title": "Show Sidebar" - }, - { - "command": "void.ctrl+k", - "title": "Make Inline Edit" - }, - { - "command": "void.acceptDiff", - "title": "Approve Diff" - }, - { - "command": "void.rejectDiff", - "title": "Discard Diff" - }, - { - "command": "void.startNewThread", - "title": "Start a new chat", - "icon": "$(add)" - }, - { - "command": "void.toggleThreadSelector", - "title": "View past chats", - "icon": "$(history)" - }, - { - "command": "void.toggleSettings", - "title": "Void settings", - "icon": "$(settings-gear)" - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "voidViewContainer", - "title": "Chat", - "icon": "$(hubot)" - } - ] - }, - "views": { - "voidViewContainer": [ - { - "type": "webview", - "id": "void.viewnumberone", - "name": "Void" - } - ] - }, - "keybindings": [ - { - "command": "void.ctrl+l", - "key": "ctrl+l", - "mac": "cmd+l" - }, - { - "command": "void.ctrl+k", - "key": "ctrl+k", - "mac": "cmd+k" - } - ], - "menus": { - "view/title": [ - { - "command": "void.startNewThread", - "when": "view == 'void.viewnumberone'", - "group": "navigation" - }, - { - "command": "void.toggleThreadSelector", - "when": "view == 'void.viewnumberone'", - "group": "navigation" - }, - { - "command": "void.toggleSettings", - "when": "view == 'void.viewnumberone'", - "group": "navigation" - } - ] - } - }, - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "build": "rimraf dist && node build/build.js", - "pretest": "tsc -p ./ && eslint src --ext ts", - "test": "vscode-test" - }, - "devDependencies": { - "@anthropic-ai/sdk": "^0.31.0", - "@eslint/js": "^9.9.1", - "@google/generative-ai": "^0.21.0", - "@monaco-editor/react": "^4.6.0", - "@rrweb/types": "^2.0.0-alpha.17", - "@types/diff": "^5.2.2", - "@types/diff-match-patch": "^1.0.36", - "@types/jest": "^29.5.12", - "@types/lodash": "^4.17.12", - "@types/mocha": "^10.0.8", - "@types/node": "^22.5.1", - "@types/react": "^18.3.4", - "@types/react-dom": "^18.3.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.3.0", - "@typescript-eslint/parser": "^8.3.0", - "@vscode/test-cli": "^0.0.10", - "@vscode/test-electron": "2.4.1", - "autoprefixer": "^10.4.20", - "diff-match-patch": "^1.0.5", - "esbuild": "^0.23.1", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.35.1", - "eslint-plugin-react-hooks": "^4.6.2", - "globals": "^15.9.0", - "lodash": "^4.17.21", - "marked": "^14.1.0", - "ollama": "^0.5.9", - "openai": "^4.70.2", - "postcss": "^8.4.41", - "posthog-js": "^1.176.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.6.1", - "rimraf": "^6.0.1", - "rrweb-snapshot": "^2.0.0-alpha.4", - "tailwindcss": "^3.4.10", - "typescript": "5.5.4", - "typescript-eslint": "^8.3.0", - "uuid": "^10.0.0" - }, - "dependencies": { - "lru-cache": "^11.0.2", - "tree-sitter": "^0.21.1", - "tree-sitter-javascript": "^0.23.1", - "tree-sitter-python": "^0.23.4" - } -} diff --git a/src/vs/workbench/contrib/void/browser/react/README.md b/src/vs/workbench/contrib/void/browser/react/README.md index e6bfee13..9f998f8d 100644 --- a/src/vs/workbench/contrib/void/browser/react/README.md +++ b/src/vs/workbench/contrib/void/browser/react/README.md @@ -5,6 +5,6 @@ A couple things to remember: - Make sure to add .js at the end of any external imports used in here, e.g. ../../../../../my_file.js. If you don't do this, you will get untraceable errors. -- src/ needs to be shallow so the detection of externals works properly (see tsup.config.js). +- src/ needs to be shallow (1 folder deep) so the detection of externals works properly (see tsup.config.js). diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 80befcdb..5383c4a4 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -10,3 +10,4 @@ execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "pref execSync('npx tsup') +console.log('✅ Done building! Press Cmd+Shift+B again.') 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 34375c89..197d2c1f 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 @@ -6,7 +6,6 @@ import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } import { useConfigState, useService, useThreadsState } from '../util/services.js'; -import { sendLLMMessage } from '../util/sendLLMMessage.js'; import { generateDiffInstructions } from '../../../prompt/systemPrompts.js'; import { userInstructionsStr } from '../../../prompt/stringifyFiles.js'; import { CodeSelection, CodeStagingSelection } from '../../../registerThreads.js'; @@ -17,8 +16,10 @@ import { IModelService } from '../../../../../../../editor/common/services/model import { URI } from '../../../../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../../../../editor/common/model.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { ErrorDisplay } from '../util/ErrorDisplay.js'; +import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; - +// import { } from '@vscode/webview-ui-toolkit/react'; // read files from VSCode const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { @@ -174,11 +175,11 @@ export const SidebarChat = () => { // state of chat const [messageStream, setMessageStream] = useState('') const [isLoading, setIsLoading] = useState(false) - const abortFnRef = useRef<(() => void) | null>(null) - - const [latestError, setLatestError] = useState('') + const latestRequestIdRef = useRef(null) + const [latestError, setLatestError] = useState(null) + const sendLLMMessageService = useService('sendLLMMessageService') const isDisabled = !instructions @@ -209,11 +210,12 @@ export const SidebarChat = () => { // send message to LLM - sendLLMMessage({ + + const object: LLMMessageServiceParams = { logging: { loggingName: 'Chat' }, messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })),], - onText: (newText, fullText) => setMessageStream(fullText), - onFinalMessage: (content) => { + onText: ({ newText, fullText }) => setMessageStream(fullText), + onFinalMessage: ({ fullText: content }) => { console.log('chat: running final message') // add assistant's message to chat history, and clear selection @@ -222,8 +224,8 @@ export const SidebarChat = () => { setMessageStream('') setIsLoading(false) }, - onError: (error) => { - console.log('chat: running error') + onError: ({ error }) => { + console.log('chat: running error', error) // add assistant's message to chat history, and clear selection let content = messageStream; // just use the current content @@ -236,22 +238,25 @@ export const SidebarChat = () => { setLatestError(error) }, voidConfig, - abortRef: abortFnRef, options: {}, - }) + } + + const latestRequestId = sendLLMMessageService.sendLLMMessage(object) + latestRequestIdRef.current = latestRequestId setIsLoading(true) setInstructions(''); formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens threadsStateService.setStaging([]) // clear staging - setLatestError('') + setLatestError(null) } const onAbort = () => { - // abort claude - abortFnRef.current?.() + // abort the LLM + if (latestRequestIdRef.current) + sendLLMMessageService.abort(latestRequestIdRef.current) // if messageStream was not empty, add it to the history const llmContent = messageStream || '(null)' @@ -339,9 +344,11 @@ export const SidebarChat = () => { {/* error message */} - {!latestError ? null :
- {latestError} -
} + {latestError === null ? null : + { setLatestError(null) }} + />} } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarSettings.tsx index 29cae994..d0f91037 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarSettings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarSettings.tsx @@ -109,13 +109,6 @@ export const SidebarSettings = () => { ))} })} - - {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */} -
- {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `} - {`type 'Open User Settings (JSON)',`} - {` and look for 'void.'. `} -
) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx new file mode 100644 index 00000000..ab11ff18 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/util/ErrorDisplay.tsx @@ -0,0 +1,161 @@ +import React, { useState } from 'react'; +import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; + +import { getCmdKey } from '../../../getCmdKey.js'; + +// const opaqueMessage = `\ +// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n +// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".` +// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) { +// e = error as any +// e['Void Team'] = opaqueMessage +// } + + +type Details = { + message: string, + name: string, + stack: string | null, + cause: string | null, + code: string | null, + additional: Record +} + +// Get detailed error information +const getErrorDetails = (error: unknown) => { + + let details: Details; + + let e: Error & { [other: string]: undefined | any } + + // If fetch() fails, it gives an opaque message. We add extra details to the error. + if (error instanceof Error) { + e = error + } + // sometimes error is an object but not an Error + else if (typeof error === 'object') { + e = new Error(`The server didn't give a very useful error message. More details below.`, { cause: JSON.stringify(error) }) + + } + else { + e = new Error(String(error)) + } + // console.log('error display', JSON.stringify(e)) + + const message = e.message && e.error ? + (e.message + ':\n' + e.error) + : e.message || e.error || JSON.stringify(error) + + details = { + name: e.name || 'Error', + message: message, + stack: null, // e.stack is ignored because it's ugly and not very useful + cause: e.cause ? String(e.cause) : null, + code: e.code || null, + additional: {} + } + + + // Collect any additional properties from the e + for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop))) + details.additional[prop] = (e as any)[prop] + + return details; +}; + + + +export const ErrorDisplay = ({ + error, + onDismiss = null, + showDismiss = true, + className = '' +}: { + error: Error | object | string, + onDismiss: (() => void) | null, + showDismiss?: boolean, + className?: string +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + const details = getErrorDetails(error); + const hasDetails = details.cause || Object.keys(details.additional).length > 0; + + return ( +
+ {/* Header */} +
+
+ +
+

+ {details.name} +

+

+ {details.message} +

+
+
+ +
+ {hasDetails && ( + + )} + {showDismiss && onDismiss && ( + + )} +
+
+ + {/* Expandable Details */} + {isExpanded && hasDetails && ( +
+ {details.code && ( +
+ Error Code: + {details.code} +
+ )} + + {details.cause && ( +
+ Cause: + {details.cause} +
+ )} + + {Object.keys(details.additional).length > 0 && ( +
+ Additional Information: +
+								{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
+							
+
+ )} + {/* {details.stack && ( +
+ Stack Trace: +
+								{details.stack}
+							
+
+ )} */} +
+ )} +
+ ); +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx index f075e9ac..6ab7a361 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useState } from 'react'; import * as ReactDOM from 'react-dom/client' import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js'; -import { ConfigState } from '../../../registerConfig.js'; -import { ThreadsState } from '../../../registerThreads.js'; import { _registerServices } from './services.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx index bde4bb50..0f50c576 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/sendLLMMessage.tsx @@ -4,54 +4,21 @@ import { Ollama } from 'ollama/browser' import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { posthog } from 'posthog-js' import type { VoidConfig } from '../../../registerConfig.js'; +import type { LLMMessage, OnText, OnError, OnFinalMessage, SendLLMMMessageParams, LLMMessageOptions, } from '../../../../../../../platform/void/common/llmMessageTypes.js'; -export type AbortRef = { current: (() => void) | null } -export type OnText = (newText: string, fullText: string) => void - -export type OnFinalMessage = (input: string) => void - -export type LLMMessageAnthropic = { - role: 'user' | 'assistant'; - content: string; -} - -export type LLMMessage = { - role: 'system' | 'user' | 'assistant'; - content: string; -} - -export type LLMMessageOptions = { - stopTokens?: string[], - prefix?: string, - suffix?: string, -} type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[]; options: LLMMessageOptions; onText: OnText; onFinalMessage: OnFinalMessage; - onError: (error: string) => void; + onError: OnError; voidConfig: VoidConfig; _setAborter: (aborter: () => void) => void; }) => void -type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[]; - options: LLMMessageOptions; - onText: OnText; - onFinalMessage: (fullText: string) => void; - onError: (error: string) => void; - voidConfig: VoidConfig | null; - abortRef: AbortRef; - - logging: { - loggingName: string, - }; -}) => void - const parseMaxTokensStr = (maxTokensStr: string) => { // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) @@ -61,6 +28,10 @@ const parseMaxTokensStr = (maxTokensStr: string) => { } // Anthropic +type LLMMessageAnthropic = { + role: 'user' | 'assistant'; + content: string; +} const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => { const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] @@ -84,23 +55,23 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi // when receive text stream.on('text', (newText, fullText) => { - onText(newText, fullText) + onText({ newText, fullText }) }) // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (claude_response) => { // stringify the response's content const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); - onFinalMessage(content) + onFinalMessage({ fullText: content }) }) stream.on('error', (error) => { // the most common error will be invalid API key (401), so we handle this with a nice message if (error instanceof Anthropic.APIError && error.status === 401) { - onError('Invalid API key.') + onError({ error: 'Invalid API key.' }) } else { - onError(error.message) + onError({ error }) } }) @@ -139,21 +110,16 @@ const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, o for await (const chunk of response.stream) { const newText = chunk.text(); fullText += newText; - onText(newText, fullText); + onText({ newText, fullText }); } - onFinalMessage(fullText); + onFinalMessage({ fullText }); }) .catch((error) => { - if (error instanceof GoogleGenerativeAIFetchError) { - if (error.status === 400) { - onError('Invalid API key.'); - } - else { - onError(`${error.name}:\n${error.message}`); - } + if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) { + onError({ error: 'Invalid API key.' }); } else { - onError(error); + onError({ error }); } }) } @@ -199,22 +165,17 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal for await (const chunk of response) { const newText = chunk.choices[0]?.delta?.content || ''; fullText += newText; - onText(newText, fullText); + onText({ newText, fullText }); } - onFinalMessage(fullText); + onFinalMessage({ fullText }); }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(error => { - if (error instanceof OpenAI.APIError) { - if (error.status === 401) { - onError('Invalid API key.'); - } - else { - onError(`${error.name}:\n${error.message}`); - } + if (error instanceof OpenAI.APIError && error.status === 401) { + onError({ error: 'Invalid API key.' }); } else { - onError(error); + onError({ error }); } }) @@ -233,19 +194,17 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, options, prompt: options.prefix ?? '', suffix: options.suffix ?? '', stream: true, - options: { - num_predict: parseMaxTokensStr(voidConfig.default.maxTokens), - stop: options.stopTokens, - } + options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens), stop: options.stopTokens, } }) .then(async stream => { _setAborter(() => stream.abort()) + // iterate through the stream for await (const chunk of stream) { const newText = chunk.response; fullText += newText; - onText(newText, fullText); + onText({ newText, fullText }); } - onFinalMessage(fullText); + onFinalMessage({ fullText }); }) // when error/fail @@ -259,10 +218,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, options, model: voidConfig.ollama.model, messages: messages, stream: true, - options: { - num_predict: parseMaxTokensStr(voidConfig.default.maxTokens), // this is max_tokens - stop: options.stopTokens, - } + options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens }) .then(async stream => { _setAborter(() => stream.abort()) @@ -270,9 +226,9 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, options, for await (const chunk of stream) { const newText = chunk.message.content; fullText += newText; - onText(newText, fullText); + onText({ newText, fullText }); } - onFinalMessage(fullText); + onFinalMessage({ fullText }); }) // when error/fail @@ -320,24 +276,24 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin // when receive text if (type === 'message') { fullText += message - onText(message, fullText) + onText({ newText: message, fullText }) } else if (type === 'sources') { const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } fullText += filepath - onText(filepath, fullText) + onText({ newText: filepath, fullText }) } // type: 'status' with an empty 'message' means last message else if (type === 'status') { if (!message) { - onFinalMessage(fullText) + onFinalMessage({ fullText }) } } } }) - .catch(e => { - onError(e) + .catch(error => { + onError({ error }) }); } @@ -346,7 +302,8 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ + +export const sendLLMMessage = ({ messages, options, onText: onText_, @@ -354,8 +311,8 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ onError: onError_, abortRef: abortRef_, voidConfig, - logging: { loggingName }, -}) => { + logging: { loggingName } +}: SendLLMMMessageParams) => { if (!voidConfig) return; // set messages appropriately if fill in middle mode @@ -391,22 +348,23 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ let _setAborter = (fn: () => void) => { _aborter = fn } let _didAbort = false - const onText = (newText: string, fullText: string) => { + const onText: OnText = ({ newText, fullText }) => { if (_didAbort) return - onText_(newText, fullText) + onText_({ newText, fullText }) _fullTextSoFar = fullText } - const onFinalMessage = (fullText: string) => { + const onFinalMessage: OnFinalMessage = ({ fullText }) => { if (_didAbort) return captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) - onFinalMessage_(fullText) + onFinalMessage_({ fullText }) } - const onError = (error: string) => { + const onError: OnError = ({ error }) => { + console.error('sendLLMMessage onError:', error) if (_didAbort) return captureChatEvent(`${loggingName} - Error`, { error }) - onError_(error) + onError_({ error }) } const onAbort = () => { @@ -438,14 +396,15 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ sendGreptileMsg({ messages, options, onText, onFinalMessage, onError, voidConfig, _setAborter, }); break; default: - onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) + onError({ error: `Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!` }) break; } } - catch (e) { - onError(`Unexpected Error in sendLLMMessage: ${e}`); - (_aborter as any)?.() + catch (error) { + if (error instanceof Error) { onError({ error }) } + else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); } + ; (_aborter as any)?.() _didAbort = true } @@ -589,7 +548,7 @@ ${suffix} // export type AbortRef = { current: (() => void) } -// export type OnText = (newText: string, fullText: string) => void +// export type LLMMessageOnText = (newText: string, fullText: string) => void // export type OnFinalMessage = (input: string) => void @@ -609,7 +568,7 @@ ${suffix} // mode: 'chat' | 'fim', // messages: LLMMessage[], // options?: LLMMessageOptions, -// onText: OnText, +// onText: LLMMessageOnText, // onFinalMessage: OnFinalMessage, // onError: (error: string) => void, // abortRef: AbortRef, @@ -622,7 +581,7 @@ ${suffix} // | { mode: 'fim', messages?: undefined, fimInfo: FimInfo, } // ) & { // options?: LLMMessageOptions, -// onText: OnText, +// onText: LLMMessageOnText, // onFinalMessage: OnFinalMessage, // onError: (error: string) => void, // abortRef: AbortRef, 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 30b9af44..9cceb8d7 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 @@ -5,12 +5,15 @@ import { ThreadsState } from '../../../registerThreads.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes + let services: ReactServicesType +// even if React hasn't mounted yet, these variables are always updated to the latest state: let sidebarState: VoidSidebarState let configState: ConfigState let threadsState: ThreadsState +// React listens by adding a setState function to these: const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() const configStateListeners: Set<(s: ConfigState) => void> = new Set() const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set() diff --git a/src/vs/workbench/contrib/void/browser/react/tsup.config.js b/src/vs/workbench/contrib/void/browser/react/tsup.config.js index 31bdc100..a3432000 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -15,7 +15,7 @@ export default defineConfig({ // sourcemap: true, clean: true, - platform: 'browser', + platform: 'browser', // 'node' target: 'esnext', injectStyle: true, // bundle css into the output file outExtension: () => ({ js: '.js' }), diff --git a/src/vs/workbench/contrib/void/browser/registerActions.ts b/src/vs/workbench/contrib/void/browser/registerActions.ts index 226bbf39..8e5cc561 100644 --- a/src/vs/workbench/contrib/void/browser/registerActions.ts +++ b/src/vs/workbench/contrib/void/browser/registerActions.ts @@ -147,7 +147,7 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor): Promise { const stateService = accessor.get(IVoidSidebarStateService) - stateService.setState({ isHistoryOpen: false, currentTab: 'settings' }) + stateService.setState({ isHistoryOpen: false, currentTab: stateService.state.currentTab === 'settings' ? 'chat' : 'settings' }) stateService.fireBlurChat() } }) diff --git a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts index a2f7e6b5..fcd90481 100644 --- a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts +++ b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts @@ -8,12 +8,12 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IVoidConfigStateService } from './registerConfig.js'; -import { sendLLMMessage } from './react/out/util/sendLLMMessage.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 { Range } from '../../../../editor/common/core/range.js'; +import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -138,10 +138,10 @@ type Autocompletion = { suffix: string, startTime: number, endTime: number | undefined, - abortRef: { current: () => void }, status: AutocompletionStatus, llmPromise: Promise | undefined, result: string, + requestId: string | null, } const DEBOUNCE_TIME = 500 @@ -516,7 +516,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ this._autocompletionsOfDocument[docUriStr] = new LRUCache( MAX_CACHE_SIZE, (autocompletion: Autocompletion) => { - autocompletion.abortRef.current() + if (autocompletion.requestId) + this._sendLLMMessageService.abort(autocompletion.requestId) } ) } @@ -624,29 +625,29 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ suffix: suffix, startTime: Date.now(), endTime: undefined, - abortRef: { current: () => { } }, status: 'pending', llmPromise: undefined, result: '', + requestId: null, } // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { - sendLLMMessage({ + const requestId = this._sendLLMMessageService.sendLLMMessage({ logging: { loggingName: 'Autocomplete' }, messages: [], options: { prefix, suffix, stopTokens, }, - onText: async (tokenStr: string, completionStr: string) => { + onText: async ({ newText, fullText }) => { - newAutocompletion.result = completionStr + newAutocompletion.result = fullText // if generation doesn't match the prefix for the first few tokens generated, reject it if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { reject('LLM response did not match user\'s text.') } }, - onFinalMessage: (finalMessage: string) => { + onFinalMessage: ({ fullText }) => { // newAutocompletion.prefix = prefix // newAutocompletion.suffix = suffix @@ -655,19 +656,19 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // newAutocompletion.abortRef = { current: () => { } } newAutocompletion.status = 'finished' // newAutocompletion.promise = undefined - newAutocompletion.result = postprocessResult(extractCodeFromResult(finalMessage)) + newAutocompletion.result = postprocessResult(extractCodeFromResult(fullText)) resolve(newAutocompletion.result) }, - onError: (e: any) => { + onError: ({ error }) => { newAutocompletion.endTime = Date.now() newAutocompletion.status = 'error' - reject(e) + reject(error) }, voidConfig: this._voidConfigStateService.state.voidConfig, - abortRef: newAutocompletion.abortRef, }) + newAutocompletion.requestId = requestId // if the request hasnt resolved in TIMEOUT_TIME seconds, reject it setTimeout(() => { @@ -676,9 +677,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } }, TIMEOUT_TIME) - }) + + // add autocompletion to cache this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion) @@ -703,6 +705,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ constructor( @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, @IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService, + @ISendLLMMessageService private readonly _sendLLMMessageService: ISendLLMMessageService, ) { super() diff --git a/src/vs/workbench/contrib/void/browser/registerConfig.ts b/src/vs/workbench/contrib/void/browser/registerConfig.ts index bb54fbee..b1e8708a 100644 --- a/src/vs/workbench/contrib/void/browser/registerConfig.ts +++ b/src/vs/workbench/contrib/void/browser/registerConfig.ts @@ -241,7 +241,7 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi _serviceBrand: undefined; private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; + readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes state: ConfigState; readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts index 72acfee2..186b3f8f 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts @@ -10,7 +10,6 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { sendLLMMessage } from './react/out/util/sendLLMMessage.js'; // import { throttle } from '../../../../base/common/decorators.js'; import { IVoidConfigStateService } from './registerConfig.js'; import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js'; @@ -29,6 +28,10 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; +import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js'; +import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js'; +// import { ISendLLMMessageService } from '../../../../platform/void/common/sendLLMMessage.js'; +// import { sendLLMMessage } from './react/out/util/sendLLMMessage.js'; // gets converted to --vscode-void-greenBG, see void.css @@ -115,7 +118,7 @@ export interface IInlineDiffsService { } -export const IInlineDiffsService = createDecorator('inlineDiffsService'); +export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); class InlineDiffsService extends Disposable implements IInlineDiffsService { _serviceBrand: undefined; @@ -148,6 +151,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z @ILanguageService private readonly _langService: ILanguageService, + @ISendLLMMessageService private readonly _sendLLMMessageService: ISendLLMMessageService, ) { super(); @@ -730,23 +734,22 @@ Please finish writing the new file by applying the diff to the original file. Re // ${suffix} // `; - - - - const abortRef = { current: null } as { current: null | (() => void) } await new Promise((resolve, reject) => { - sendLLMMessage({ + + let streamRequestId: string | null = null + + const object: LLMMessageServiceParams = { logging: { loggingName: 'streamChunk' }, messages: [ { role: 'system', content: writeFileWithDiffInstructions, }, // TODO include more context too { role: 'user', content: promptContent, } ], - onText: (newText: string, fullText: string) => { + onText: ({ newText, fullText }) => { this._writeDiffAreaLLMText(diffArea, fullText) this._refreshDiffsInURI(uri) }, - onFinalMessage: (fullText: string) => { + onFinalMessage: ({ fullText }) => { this._writeText(uri, fullText, { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed ) @@ -757,14 +760,17 @@ Please finish writing the new file by applying the diff to the original file. Re onError: (e: any) => { console.error('Error rewriting file with diff', e); // TODO indicate there was an error - abortRef.current?.() + if (streamRequestId) + this._sendLLMMessageService.abort(streamRequestId) + diffArea._sweepState = { isStreaming: false, line: null } resolve(); }, voidConfig, - abortRef, - options: {}, - }) + options: {} + } + + streamRequestId = this._sendLLMMessageService.sendLLMMessage(object) }) onFinishEdit() diff --git a/src/vs/workbench/contrib/void/browser/registerSidebar.ts b/src/vs/workbench/contrib/void/browser/registerSidebar.ts index f3b53d82..cddd9853 100644 --- a/src/vs/workbench/contrib/void/browser/registerSidebar.ts +++ b/src/vs/workbench/contrib/void/browser/registerSidebar.ts @@ -47,6 +47,9 @@ import { IVoidConfigStateService } from './registerConfig.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInlineDiffsService } from './registerInlineDiffs.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js'; + + // import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; @@ -65,6 +68,7 @@ export type ReactServicesType = { fileService: IFileService; modelService: IModelService; inlineDiffService: IInlineDiffsService; + sendLLMMessageService: ISendLLMMessageService; } // ---------- Define viewpane ---------- @@ -109,6 +113,7 @@ class VoidSidebarViewPane extends ViewPane { fileService: accessor.get(IFileService), modelService: accessor.get(IModelService), inlineDiffService: accessor.get(IInlineDiffsService), + sendLLMMessageService: accessor.get(ISendLLMMessageService), } mountFn(root, services); }); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 24d89f63..da3a7ede 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -328,6 +328,11 @@ export class DesktopMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // // Void + // const sendLLMMessageService = new SendLLMMessageService(); + // serviceCollection.set(ISendLLMMessageService, sendLLMMessageService); + + return { serviceCollection, logService, storageService, configurationService }; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 30e4ecc9..0f33a1d1 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -14,9 +14,10 @@ import './browser/workbench.contribution.js'; -//#region --- void +//#region --- Void // Void added this: import './contrib/void/browser/void.contribution.js'; +import '../platform/void/browser/llmMessageService.js'; //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 57bf9f01..5b0a42b6 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -31,6 +31,12 @@ import './electron-sandbox/parts/dialogs/dialog.contribution.js'; //#endregion +// //#region --- Void +// // Void added this (modeling off of import '.*clipboardservice.js'): +// import './services/void/electron-main/sendLLMMessage.js'; +// //#endregion + + //#region --- workbench services diff --git a/src/vs/workbench/workbench.web.main.internal.ts b/src/vs/workbench/workbench.web.main.internal.ts index 402696a4..f8cfa0e0 100644 --- a/src/vs/workbench/workbench.web.main.internal.ts +++ b/src/vs/workbench/workbench.web.main.internal.ts @@ -32,6 +32,9 @@ import './browser/web.main.js'; //#endregion + + + //#region --- workbench services import './services/integrity/browser/integrityService.js'; diff --git a/test/unit/electron/preload.js b/test/unit/electron/preload.js index 04439d66..4c878e1c 100644 --- a/test/unit/electron/preload.js +++ b/test/unit/electron/preload.js @@ -77,6 +77,18 @@ process.on(type, callback); } }, + + // Void : { + // /** + // * Send a message to the LLM. + // * @param {any} data The data to send to the LLM. + // * @returns {Promise} The response from the LLM. + // */ + // sendLLMMessage: async (data) => { + // // Use ipcRenderer.invoke to send the message to the main process (see app.ts) + // return await ipcRenderer.invoke('vscode:sendLLMMessage', data); + // } + // }, }; if (process.contextIsolated) {