From 4ffdf546f0842b830e6fed55e015fa8c151b44ad Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 3 Nov 2024 22:37:23 -0800 Subject: [PATCH] main thread streaming state --- .../inlineDiffService/sendLLMMessage.ts | 365 ------------------ .../api/browser/mainThreadInlineDiff.ts | 99 +++-- .../contrib/void/browser/void.contribution.ts | 6 +- .../contrib/void/browser/voidViewPane.ts | 2 +- src/vs/workbench/workbench.common.main.ts | 7 + 5 files changed, 60 insertions(+), 419 deletions(-) delete mode 100644 src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts diff --git a/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts b/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts deleted file mode 100644 index 3bf99b11..00000000 --- a/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts +++ /dev/null @@ -1,365 +0,0 @@ -// import Anthropic from '@anthropic-ai/sdk'; -// import OpenAI from 'openai'; -// import { Ollama } from 'ollama' -// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai'; -// import { VoidConfig } from '../webviews/common/contextForConfig' - -// 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; -// } - -// type SendLLMMessageFnTypeInternal = (params: { -// messages: LLMMessage[]; -// onText: OnText; -// onFinalMessage: OnFinalMessage; -// onError: (error: string) => void; -// voidConfig: VoidConfig; -// abortRef: AbortRef; -// }) => void - -// type SendLLMMessageFnTypeExternal = (params: { -// messages: LLMMessage[]; -// onText: OnText; -// onFinalMessage: (fullText: string) => void; -// onError: (error: string) => void; -// voidConfig: VoidConfig | null; -// abortRef: AbortRef; -// }) => 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) -// if (Number.isNaN(int)) -// return undefined -// return int -// } - -// // Anthropic -// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { - -// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] - -// // find system messages and concatenate them -// const systemMessage = messages -// .filter(msg => msg.role === 'system') -// .map(msg => msg.content) -// .join('\n'); - -// // remove system messages for Anthropic -// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[] - -// const stream = anthropic.messages.stream({ -// system: systemMessage, -// messages: anthropicMessages, -// model: voidConfig.anthropic.model, -// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user -// }); - -// let did_abort = false - -// // when receive text -// stream.on('text', (newText, fullText) => { -// if (did_abort) return -// onText(newText, fullText) -// }) - -// // when we get the final message on this stream (or when error/fail) -// stream.on('finalMessage', (claude_response) => { -// if (did_abort) return -// // stringify the response's content -// const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); -// onFinalMessage(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.') -// } -// else { -// onError(error.message) -// } -// }) - -// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either -// const abort = () => { -// did_abort = true -// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error -// } - -// return { abort } -// }; - -// // Gemini -// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - -// let didAbort = false -// let fullText = '' - -// abortRef.current = () => { -// didAbort = true -// } - -// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey); -// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model }); - -// // remove system messages that get sent to Gemini -// // str of all system messages -// const systemMessage = messages -// .filter(msg => msg.role === 'system') -// .map(msg => msg.content) -// .join('\n'); - -// // Convert messages to Gemini format -// const geminiMessages: Content[] = messages -// .filter(msg => msg.role !== 'system') -// .map((msg, i) => ({ -// parts: [{ text: msg.content }], -// role: msg.role === 'assistant' ? 'model' : 'user' -// })) - -// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, }) -// .then(async response => { -// abortRef.current = () => { -// // response.stream.return(fullText) -// didAbort = true; -// } -// for await (const chunk of response.stream) { -// if (didAbort) return; -// const newText = chunk.text(); -// fullText += newText; -// onText(newText, fullText); -// } -// onFinalMessage(fullText); -// }) -// .catch((error) => { -// if (error instanceof GoogleGenerativeAIFetchError) { -// if (error.status === 400) { -// onError('Invalid API key.'); -// } -// else { -// onError(`${error.name}:\n${error.message}`); -// } -// } -// else { -// onError(error); -// } -// }) -// } - -// // OpenAI, OpenRouter, OpenAICompatible -// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - -// let didAbort = false -// let fullText = '' - -// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either -// abortRef.current = () => { -// didAbort = true; -// }; - -// let openai: OpenAI -// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - -// const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) - -// if (voidConfig.default.whichApi === 'openAI') { -// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); -// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens } -// } -// else if (voidConfig.default.whichApi === 'openRouter') { -// openai = new OpenAI({ -// baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, -// defaultHeaders: { -// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. -// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. -// }, -// }); -// options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens } -// } -// else if (voidConfig.default.whichApi === 'openAICompatible') { -// openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) -// options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens } -// } -// else { -// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) -// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`) -// } - -// openai.chat.completions -// .create(options) -// .then(async response => { -// abortRef.current = () => { -// // response.controller.abort() -// didAbort = true; -// } -// // when receive text -// for await (const chunk of response) { -// if (didAbort) return; -// const newText = chunk.choices[0]?.delta?.content || ''; -// fullText += newText; -// onText(newText, 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}`); -// } -// } -// else { -// onError(error); -// } -// }) - -// }; - -// // Ollama -// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - -// let didAbort = false -// let fullText = '' - -// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either -// abortRef.current = () => { -// didAbort = true; -// }; - -// const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) - -// ollama.chat({ -// model: voidConfig.ollama.model, -// messages: messages, -// stream: true, -// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens -// }) -// .then(async stream => { -// abortRef.current = () => { -// // stream.abort() -// didAbort = true -// } -// // iterate through the stream -// for await (const chunk of stream) { -// if (didAbort) return; -// const newText = chunk.message.content; -// fullText += newText; -// onText(newText, fullText); -// } -// onFinalMessage(fullText); - -// }) -// // when error/fail -// .catch(error => { -// onError(error) -// }) - -// }; - -// // Greptile -// // https://docs.greptile.com/api-reference/query -// // https://docs.greptile.com/quickstart#sample-response-streamed - -// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { - -// let didAbort = false -// let fullText = '' - -// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either -// abortRef.current = () => { -// didAbort = true -// } - -// fetch('https://api.greptile.com/v2/query', { -// method: 'POST', -// headers: { -// 'Authorization': `Bearer ${voidConfig.greptile.apikey}`, -// 'X-Github-Token': `${voidConfig.greptile.githubPAT}`, -// 'Content-Type': `application/json`, -// }, -// body: JSON.stringify({ -// messages, -// stream: true, -// repositories: [voidConfig.greptile.repoinfo], -// }), -// }) -// // this is {message}\n{message}\n{message}...\n -// .then(async response => { -// const text = await response.text() -// console.log('got greptile', text) -// return JSON.parse(`[${text.trim().split('\n').join(',')}]`) -// }) -// // TODO make this actually stream, right now it just sends one message at the end -// .then(async responseArr => { -// if (didAbort) -// return - -// for (const response of responseArr) { - -// const type: string = response['type'] -// const message = response['message'] - -// // when receive text -// if (type === 'message') { -// fullText += message -// onText(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) -// } -// // type: 'status' with an empty 'message' means last message -// else if (type === 'status') { -// if (!message) { -// onFinalMessage(fullText) -// } -// } -// } - -// }) -// .catch(e => { -// onError(e) -// }); - -// } - -// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { -// if (!voidConfig) return; - -// // trim message content (Anthropic and other providers give an error if there is trailing whitespace) -// messages = messages.map(m => ({ ...m, content: m.content.trim() })) - -// switch (voidConfig.default.whichApi) { -// case 'anthropic': -// return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'openAI': -// case 'openRouter': -// case 'openAICompatible': -// return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'gemini': -// return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'ollama': -// return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// case 'greptile': -// return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); -// default: -// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) -// } -// } diff --git a/src/vs/workbench/api/browser/mainThreadInlineDiff.ts b/src/vs/workbench/api/browser/mainThreadInlineDiff.ts index 38c43bf6..2d0280b1 100644 --- a/src/vs/workbench/api/browser/mainThreadInlineDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadInlineDiff.ts @@ -9,7 +9,8 @@ import { ICodeEditor } from '../../../editor/browser/editorBrowser.js'; import { IRange } from '../../../editor/common/core/range.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js'; -import { URI } from '../../../base/common/uri.js'; +import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js'; +import { WorkspaceEdit } from '../../../editor/common/languages.js'; // import { IHistoryService } from '../../services/history/common/history.js'; @@ -26,6 +27,7 @@ export class MainThreadInlineDiff extends Disposable implements MainThreadInline @ICodeEditorService private readonly _editorService: ICodeEditorService, // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z + @IBulkEditService private readonly _bulkEditService: IBulkEditService, ) { super(); @@ -34,72 +36,69 @@ export class MainThreadInlineDiff extends Disposable implements MainThreadInline // this._wcHistoryService.addEntry() } - _streamingState: 'streaming' | 'idle' = 'idle' - - startStreaming(editor: ICodeEditor) { - - this._streamingState = 'streaming' - - // count all changes towards the group - - - // const versionId = editor.getModel()?.getVersionId() - - this._register(editor.onDidChangeModelContent((e) => { - - - // user presses undo (and there is something to undo) - if (e.isUndoing) { - // cancel the stream, then undo normally - return - } - // user presses redo (and there is something to redo) - if (e.isRedoing) { - // cancel the stream, then redo normally - return - - } - - // for good measure - if (e.isEolChange) { - // cancel stream and apply change normally - return - } - - // ignore any other kind of change (make it not happen) - if (this._streamingState === 'streaming') { - // completely ignore the change - return - } - - - })); - - // streamChange(){ - - // } + _streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' } + startStreaming(editorId: string) { + const editor = this._getEditor(editorId) + if (!editor) return + const model = editor.getModel() + if (!model) return // all changes made when streaming should be a part of the group so we can undo them all together - const group = new UndoRedoGroup() + this._streamingState = { + type: 'streaming', + editGroup: new UndoRedoGroup() + } + + // TODO probably need to convert this to a stack + const diffsSnapshotBefore = { placeholder: '' } + const diffsSnapshotAfter = { placeholder: '' } const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, - resource: URI.parse('file:///path/to/file.txt'), + resource: model.uri, label: 'Add Diffs', code: 'undoredo.inlineDiff', undo: () => { - // reapply diffareas and diffs here + console.log('reverting diffareas...', diffsSnapshotBefore.placeholder) }, redo: () => { - // reapply diffareas and diffs here + // when done, need to record diffSnapshotAfter + console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder) } } - this._undoRedoService.pushElement(elt, group) + this._undoRedoService.pushElement(elt, this._streamingState.editGroup) + + // ---------- START ---------- + editor.updateOptions({ readOnly: true }) + + + + // ---------- WHEN DONE ---------- + editor.updateOptions({ readOnly: false }) + + + } + + + + + streamChange(editorId: string, edit: WorkspaceEdit) { + const editor = this._getEditor(editorId) + if (!editor) return + + if (this._streamingState.type !== 'streaming') { + console.error('Expected streamChange to be in state \'streaming\'.') + return + } + + // count all changes towards the group + this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, }) + } diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index eff8132e..001625ca 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -3,16 +3,16 @@ import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions, IViewDescriptor -} from '../../../../workbench/common/views.js'; +} from '../../../common/views.js'; import * as nls from '../../../../nls.js'; -import { VoidViewPane } from '../../../../workbench/contrib/void/browser/voidViewPane.js'; +import { VoidViewPane } from './voidViewPane.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js'; +import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; diff --git a/src/vs/workbench/contrib/void/browser/voidViewPane.ts b/src/vs/workbench/contrib/void/browser/voidViewPane.ts index ccca0c32..b21be223 100644 --- a/src/vs/workbench/contrib/void/browser/voidViewPane.ts +++ b/src/vs/workbench/contrib/void/browser/voidViewPane.ts @@ -1,7 +1,7 @@ -import { ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; +import { ViewPane } from '../../../browser/parts/views/viewPane.js'; // import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 285fea2c..48f1b912 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -12,6 +12,13 @@ import './browser/workbench.contribution.js'; //#endregion +//#region --- void +// Void added this: + +import './contrib/void/browser/void.contribution.js'; + +//#endregion + //#region --- workbench actions