From d8e9b508906995ff2b1c4435a75edd193912c791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sat, 7 Dec 2024 14:49:28 +0100 Subject: [PATCH 001/321] Advanced builder section added --- CONTRIBUTING.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f0214ba..51e98ed2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,3 +150,31 @@ We keep track of all the files we've changed with Void so it's easy to rebase: ## References For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md). + + +### Advanced Builder shortcuts (if you are here, you RTFM till the end so you deserve it 😉) +1. Install dependencies and build the react components +`npm install && cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../..` + +2. Develop the things you want then : +`npm run watch` +Wait for the watch task to be done +While the watch task is running open a new terminal then build + +NOTE : It's even possible to combine the 1. and 2. commands : +`npm install && cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../.. && npm run watch` +But you allready knew it 🤓. This is just useless because the watch task will need to be done again if you are not recloning the repo and building the react components. + +3. Build +### On Mac +`./scripts/code.sh` +Using ⌘ + ⇥ (tab) to get focus to the editor window or by clic on it +⌘ + R is reloading the window to see changes + +### On Windows +`./scripts/code.bat` +- Press Ctrl+Shift+P and run "Reload Window" inside the new window to see changes + +### On Linux +`./scripts/code.sh` +Press Ctrl+Shift+P and run "Reload Window" inside the new window to see changes From 01efab6a1d5475525e02e23b06a8be5680bc929a Mon Sep 17 00:00:00 2001 From: mp Date: Tue, 17 Dec 2024 19:30:14 -0800 Subject: [PATCH 002/321] cmd+L prompt --- .../void/browser/prompt/systemPrompts.ts | 85 +++++++++++++++++++ .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts index 157f4292..161fd30b 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts @@ -34,7 +34,92 @@ // ${suffix} // `; +export const generateCtrlLInstructions = `\ +You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. +Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). + +Instructions: +1. Output the changes to make to the entire file. +1. Do not re-write the entire file. +3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments. +4. You must give enough context to apply the change in the correct location. + +## EXAMPLE + +FILES +selected file \`math.ts\`: +\`\`\` +const addNumbers = (a, b) => a + b +const subtractNumbers = (a, b) => a - b +const divideNumbers = (a, b) => a / b +\`\`\` + +SELECTION +\`\`\` +const subtractNumbers = (a, b) => a - b +\`\`\` + +INSTRUCTIONS +\`\`\` +add a function that multiplies numbers below this +\`\`\` + +EXPECTED OUTPUT +We can add the following code to the file: +\`\`\` +// existing code... +const subtractNumbers = (a, b) => a - b; +const multiplyNumbers = (a, b) => a * b; +// existing code... +\`\`\` + +## EXAMPLE + +FILES +selected file \`fib.ts\`: +\`\`\` + +const dfs = (root) => { + if (!root) return; + console.log(root.val); + dfs(root.left); + dfs(root.right); +} +const fib = (n) => { + if (n < 1) return 1 + return fib(n - 1) + fib(n - 2) +} +\`\`\` + +SELECTION +\`\`\` + return fib(n - 1) + fib(n - 2) +\`\`\` + +INSTRUCTIONS +\`\`\` +memoize results +\`\`\` + +EXPECTED OUTPUT +To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function: +\`\`\` +// existing code... +const fib = (n, memo = {}) => { + if (n < 1) return 1; + if (memo[n]) return memo[n]; // Check if result is already computed + memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo + return memo[n]; +} +\`\`\` +Explanation: +Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n. +Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result. +Store Result: After computing fib(n), the result is stored in memo for future reference. + +## END EXAMPLES +` export const generateDiffInstructions = ` You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. 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 e5ff34c9..976af32b 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 @@ -7,7 +7,7 @@ import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js'; -import { generateDiffInstructions } from '../../../prompt/systemPrompts.js'; +import { generateCtrlLInstructions, generateDiffInstructions } from '../../../prompt/systemPrompts.js'; import { userInstructionsStr } from '../../../prompt/stringifySelections.js'; import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js'; @@ -325,7 +325,7 @@ export const SidebarChat = () => { // add system message to chat history - const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions } + const systemPromptElt: ChatMessage = { role: 'system', content: generateCtrlLInstructions } threadsStateService.addMessageToCurrentThread(systemPromptElt) // add user's message to chat history From ac8365def4415a3dbd65dd80410b5d0b2e126c1c Mon Sep 17 00:00:00 2001 From: mp Date: Wed, 18 Dec 2024 02:29:11 -0800 Subject: [PATCH 003/321] ctrl+L --- product.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/product.json b/product.json index a35f197c..d892feaf 100644 --- a/product.json +++ b/product.json @@ -31,6 +31,10 @@ "nodejsRepository": "https://nodejs.org", "urlProtocol": "code-oss", "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/", + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, "builtInExtensions": [ { "name": "ms-vscode.js-debug-companion", From 36694018196567760976c44fd45bb92069887ba1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 19 Dec 2024 01:28:43 -0800 Subject: [PATCH 004/321] upgrade to switches --- .../void/common/voidSettingsService.ts | 2 +- .../platform/void/common/voidSettingsTypes.ts | 2 +- .../void/browser/react/src/util/inputs.tsx | 96 ++++++++++++++++++- .../react/src/void-settings-tsx/Settings.tsx | 36 +++++-- 4 files changed, 125 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index 0c0ceb28..ad776239 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../storage/comm import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; -const STORAGE_KEY = 'void.voidSettingsI' +const STORAGE_KEY = 'void.voidSettingsStorage' type SetSettingOfProviderFn = ( providerName: ProviderName, diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 7aa1eb78..5e47fee5 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -110,8 +110,8 @@ export const customProviderSettings = { apiKey: '', }, openAICompatible: { - apiKey: '', endpoint: '', + apiKey: '', }, gemini: { apiKey: '', 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 70be6772..0c7d039f 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 @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import React, { useCallback, useEffect, useRef } from 'react'; -import { useService } from '../util/services.js'; +import { useIsDark, useService } from '../util/services.js'; import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; -import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'; @@ -48,7 +49,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac }) => { const contextViewProvider = useService('contextViewService'); - return [ @@ -93,6 +93,96 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac +export const VoidSwitch = ({ + value, + onChange, + label, + disabled = false, + size = 'md' +}: { + value: boolean; + onChange: (value: boolean) => void; + label?: string; + disabled?: boolean; + size?: 'xs' | 'sm' | 'msm' | 'md'; +}) => { + return ( + + ); +}; + + + + + +export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => { + const divRef = useRef(null) + const instanceRef = useRef(null) + + useEffect(() => { + if (!instanceRef.current) return + instanceRef.current.checked = value + }, [value]) + + + return { + divRef.current = container + return [label, value, defaultCheckboxStyles] as const + }, [label, value])} + onCreateInstance={useCallback((instance: Checkbox) => { + instanceRef.current = instance; + divRef.current?.append(instance.domNode) + const d = instance.onChange(() => onClick(instance.checked)) + return [d] + }, [onClick])} + dispose={useCallback((instance: Checkbox) => { + instance.dispose() + instance.domNode.remove() + }, [])} + + /> + +} + + export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectBoxRef, options }: { onChangeSelection: (value: T) => void; onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]); diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 66b11f3d..1d7d10df 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' -import { VoidInputBox, VoidSelectBox } from '../util/inputs.js' +import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js' import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js' import { X, RefreshCw, Loader2, Check } from 'lucide-react' import { RefreshableProviderName, refreshableProviderNames } from '../../../../../../../platform/void/common/refreshModelService.js' @@ -226,7 +226,29 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = return <>

{titleOfProviderName(providerName)}

- + { + const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled + voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef) + }, [voidSettingsService, providerName])} + size='xs' + disabled={false} + label='' + /> + {/* { + const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled + voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef) + }, [voidSettingsService, providerName])} + /> */} + + {/* */}
{/* settings besides models (e.g. api key) */} {settingNames.map((settingName, i) => { @@ -254,10 +276,12 @@ export const VoidFeatureFlagSettings = () => { const value = voidSettingsState.featureFlagSettings[flagName] const { description } = displayInfoOfFeatureFlag(flagName) return
-
- +
+ { voidSettingsService.setFeatureFlag(flagName, !value) }} + />

{description}

From d6074e7ab586d51c219125f61de17d4500f8c5e7 Mon Sep 17 00:00:00 2001 From: mp Date: Thu, 19 Dec 2024 02:14:26 -0800 Subject: [PATCH 005/321] ctrlk draft --- .../browser/helpers/reactServicesHelper.ts | 3 + .../void/browser/inlineDiffsService.ts | 2 +- .../prompt/{systemPrompts.ts => prompts.ts} | 78 +++++++---- .../browser/prompt/stringifySelections.ts | 32 ----- .../contrib/void/browser/quickEditActions.ts | 130 ++++++++++++++++-- .../void/browser/quickEditStateService.ts | 82 +++++++++++ .../browser/react/src/ctrl-k-tsx/CtrlK.tsx | 18 +++ .../react/src/ctrl-k-tsx/CtrlKChat.tsx | 83 +++++++++++ .../browser/react/src/ctrl-k-tsx/index.tsx | 8 ++ .../react/src/sidebar-tsx/SidebarChat.tsx | 66 +++++---- .../react/src/util/mountFnGenerator.tsx | 5 +- .../void/browser/react/src/util/services.tsx | 24 +++- .../contrib/void/browser/react/tsup.config.js | 1 + 13 files changed, 431 insertions(+), 101 deletions(-) rename src/vs/workbench/contrib/void/browser/prompt/{systemPrompts.ts => prompts.ts} (88%) delete mode 100644 src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts create mode 100644 src/vs/workbench/contrib/void/browser/quickEditStateService.ts create mode 100644 src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx create mode 100644 src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx diff --git a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts index 3c043344..08ad3fdb 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts @@ -9,10 +9,12 @@ import { ILLMMessageService } from '../../../../../platform/void/common/llmMessa import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js'; import { IInlineDiffsService } from '../inlineDiffsService.js'; +import { IQuickEditStateService } from '../quickEditStateService.js'; import { ISidebarStateService } from '../sidebarStateService.js'; import { IThreadHistoryService } from '../threadHistoryService.js'; export type ReactServicesType = { + quickEditStateService: IQuickEditStateService; sidebarStateService: ISidebarStateService; settingsStateService: IVoidSettingsService; threadsStateService: IThreadHistoryService; @@ -33,6 +35,7 @@ export type ReactServicesType = { export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => { return { + quickEditStateService: accessor.get(IQuickEditStateService), settingsStateService: accessor.get(IVoidSettingsService), sidebarStateService: accessor.get(ISidebarStateService), threadsStateService: accessor.get(IThreadHistoryService), diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 5d6c0c49..e33c9991 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; -import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js'; +import { writeFileWithDiffInstructions } from './prompt/prompts.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; diff --git a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts similarity index 88% rename from src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts rename to src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 161fd30b..803029c2 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -3,38 +3,36 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -// // used for ctrl+l -// const partialGenerationInstructions = `` + +import { CodeSelection } from '../threadHistoryService.js'; + +const stringifySelections = (selections: CodeSelection[]) => { + + return selections.map(({ fileURI, content, selectionStr }) => + `\ +File: ${fileURI.fsPath} +\`\`\` +${content // this was the enite file which is foolish + } +\`\`\`${selectionStr === null ? '' : ` +Selection: ${selectionStr}`} +`).join('\n') +} -// // used for ctrl+k, autocomplete -// const fimInstructions = `` +export const generateCtrlLPrompt = (instructions: string, selections: CodeSelection[] | null) => { + let str = ''; + if (selections && selections.length > 0) { + str += stringifySelections(selections); + str += `Please edit the selected code following these instructions:\n` + } + str += `${instructions}`; + return str; +}; -// CTRL+K prompt: -// const promptContent = `Here is the user's original selection: -// \`\`\` -// ${selection} -// \`\`\` -// The user wants to apply the following instructions to the selection: -// ${instructions} - -// Please rewrite the selection following the user's instructions. - -// Instructions to follow: -// 1. Follow the user's instructions -// 2. You may ONLY CHANGE the selection, and nothing else in the file -// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection -// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake - -// Complete the following: -// \`\`\` -//
${prefix}
-// ${suffix} -// `; - -export const generateCtrlLInstructions = `\ +export const ctrlLSystem = `\ You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). @@ -118,9 +116,33 @@ Memoization Object: A memo object is used to store the results of Fibonacci calc Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result. Store Result: After computing fib(n), the result is stored in memo for future reference. -## END EXAMPLES +## END EXAMPLES\ ` +export const generateCtrlKPrompt = ({ selection, prefix, suffix, instructions, }: { selection: string, prefix: string, suffix: string, instructions: string, }) => `\ +Here is the user's original selection: +\`\`\` +${selection} +\`\`\` + +The user wants to apply the following instructions to the selection: +${instructions} + +Please rewrite the selection following the user's instructions. + +Instructions to follow: +1. Follow the user's instructions +2. You may ONLY CHANGE the selection, and nothing else in the file +3. Make sure all brackets in the new selection are balanced the same was as in the original selection +3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake + +Complete the following: +\`\`\` +
${prefix}
+${suffix} +`; + + export const generateDiffInstructions = ` You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. diff --git a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts b/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts deleted file mode 100644 index e23ac6f4..00000000 --- a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { CodeSelection } from '../threadHistoryService.js'; - -export const stringifySelections = (selections: CodeSelection[]) => { - - - - return selections.map(({ fileURI, content, selectionStr }) => - `\ -File: ${fileURI.fsPath} -\`\`\` -${content // this was the enite file which is foolish - } -\`\`\`${selectionStr === null ? '' : ` -Selection: ${selectionStr}`} -`).join('\n') -} - - -export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => { - let str = ''; - if (selections && selections.length > 0) { - str += stringifySelections(selections); - str += `Please edit the selected code following these instructions:\n` - } - str += `${instructions}`; - return str; -}; diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index fcef7270..784011d2 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -1,9 +1,111 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { createDecorator, IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +// import { IInlineDiffService } from '../../../../editor/browser/services/inlineDiffService/inlineDiffService.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { mountCtrlK } from './react/out/ctrl-k-tsx/index.js'; +import { getReactServices } from './helpers/reactServicesHelper.js'; +import { URI } from '../../../../base/common/uri.js'; + + +type InitialZone = { uri: URI, startLine: number, selectedText: string, } + +export type QuickEditPropsType = { + quickEditId: number, +} + +export type QuickEdit = { + startLine: number, // 0-indexed + beforeCode: string, + afterCode?: string, + instructions?: string, + responseText?: string, // model can produce a text response too +} + + +export interface IQuickEditService { + readonly _serviceBrand: undefined; + readonly onDidChangeState: Event; + addZone(zone: InitialZone): void; +} + +export const IQuickEditService = createDecorator('voidQuickEditService'); +class VoidQuickEditService extends Disposable implements IQuickEditService { + _serviceBrand: undefined; + + quickEditId: number = 0 + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + // state + // state: {} + + constructor( + // @IInlineDiffService private readonly _inlineDiffService: IInlineDiffService, + @ICodeEditorService private readonly _editorService: ICodeEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + } + + addZone(zone: InitialZone) { + + const addZoneToEditor = (editor: ICodeEditor) => { + + const model = editor.getModel() + if (!model) return + + editor.changeViewZones(accessor => { + + const domNode = document.createElement('div'); + domNode.style.zIndex = '1' + + // domNode.className = 'void-redBG' + const viewZone: IViewZone = { + // afterLineNumber: computedDiff.startLine - 1, + afterLineNumber: 1, + heightInPx: 100, + // heightInLines: 1, + // minWidthInPx: 200, + domNode: domNode, + // marginDomNode: document.createElement('div'), // displayed to left + suppressMouseDown: false, + }; + + // const zoneId = + accessor.addZone(viewZone) + + this._instantiationService.invokeFunction(accessor => { + const services = getReactServices(accessor) + + const props: QuickEditPropsType = { + quickEditId: this.quickEditId++, + } + mountCtrlK(domNode, services, props) + }) + + // disposeInThisEditorFns.push(() => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) }) + }) + } + + + const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === zone.uri.fsPath) + for (const editor of editors) { + addZoneToEditor(editor) + } + } + +} + +registerSingleton(IQuickEditService, VoidQuickEditService, InstantiationType.Eager); + export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' @@ -12,17 +114,25 @@ registerAction2(class extends Action2 { super({ id: VOID_CTRL_K_ACTION_ID, title: 'Void: Quick Edit', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, weight: KeybindingWeight.BuiltinExtension } }); } async run(accessor: ServicesAccessor): Promise { - console.log('hello111!') - const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() - if (!model) - return - - console.log('hello!') + const quickEditService = accessor.get(IQuickEditService) + const editorService = accessor.get(ICodeEditorService) const metricsService = accessor.get(IMetricsService) - metricsService.capture('User Action', { type: 'Ctrl+K' }) + metricsService.capture('User Action', { type: 'Open Ctrl+K' }) + + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + const model = editor.getModel() + if (!model) return; + const selection = editor.getSelection() + if (!selection) return; + + const uri = model.uri + const startLine = selection.startLineNumber + const selectedText = model.getValueInRange(selection) + + quickEditService.addZone({ uri, startLine, selectedText, }) - console.log('bye!') } }); diff --git a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts new file mode 100644 index 00000000..355b52f1 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts @@ -0,0 +1,82 @@ +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { QuickEdit } from './quickEditActions.js'; + + + +// service that manages state +export type VoidQuickEditState = { + quickEditsOfDocument: { [uri: string]: QuickEdit } +} + +export interface IQuickEditStateService { + readonly _serviceBrand: undefined; + + readonly state: VoidQuickEditState; // readonly to the user + setState(newState: Partial): void; + onDidChangeState: Event; + + onDidFocusChat: Event; + onDidBlurChat: Event; + fireFocusChat(): void; + fireBlurChat(): void; + +} + +export const IQuickEditStateService = createDecorator('voidQuickEditStateService'); +class VoidQuickEditStateService extends Disposable implements IQuickEditStateService { + _serviceBrand: undefined; + + static readonly ID = 'voidQuickEditStateService'; + + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; + + private readonly _onFocusChat = new Emitter(); + readonly onDidFocusChat: Event = this._onFocusChat.event; + + private readonly _onBlurChat = new Emitter(); + readonly onDidBlurChat: Event = this._onBlurChat.event; + + + // state + state: VoidQuickEditState + + constructor( + // @IViewsService private readonly _viewsService: IViewsService, + ) { + super() + + // initial state + this.state = { quickEditsOfDocument: {} } + } + + + setState(newState: Partial) { + // make sure view is open if the tab changes + // if ('currentTab' in newState) { + // this.addQuickEdit() + // } + + this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + fireFocusChat() { + this._onFocusChat.fire() + } + + fireBlurChat() { + this._onBlurChat.fire() + } + + // addQuickEdit() { + // this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID); + // this._viewsService.openView(VOID_VIEW_ID); + // } + +} + +registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx new file mode 100644 index 00000000..e57acf4a --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react' +import { useIsDark, useSidebarState } from '../util/services.js' +import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' +import { CtrlKChat } from './CtrlKChat.js' +import { QuickEditPropsType } from '../../../quickEditActions.js' + +export const CtrlK = (props: QuickEditPropsType) => { + + const isDark = useIsDark() + + return
+ + + +
+ + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx new file mode 100644 index 00000000..edfb3664 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx @@ -0,0 +1,83 @@ + +import React, { FormEvent, useCallback, useRef, useState } from 'react'; +import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useService } from '../util/services.js'; +import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js'; +import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; +import { getCmdKey } from '../../../helpers/getCmdKey.js'; +import { VoidInputBox } from '../util/inputs.js'; +import { QuickEditPropsType } from '../../../quickEditActions.js'; + +export const CtrlKChat = (props: QuickEditPropsType) => { + + const inputBoxRef: React.MutableRefObject = useRef(null); + + // -- imported state -- + // const threadsStateService = useService('service') + // const sidebarState = useSidebarState() + + const quickEditState = useQuickEditState() + + + // -- local state -- + // state of chat + const [messageStream, setMessageStream] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const latestRequestIdRef = useRef(null) + const [latestError, setLatestError] = useState[0] | null>(null) + + + // state of current message + const [instructions, setInstructions] = useState('') // the user's instructions + const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions]) + const isDisabled = !instructions.trim() + + const onSubmit = useCallback((e: FormEvent) => { + // TODO + }, []) + + return
{ + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit(e) + } + }} + onSubmit={(e) => { + console.log('submit!') + onSubmit(e) + }} + onClick={(e) => { + if (e.currentTarget === e.target) { + inputBoxRef.current?.focus() + } + }} + > +
+ + {/* text input */} + +
+ + +
+ + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx new file mode 100644 index 00000000..3b4882d5 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx @@ -0,0 +1,8 @@ + +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { CtrlK } from './CtrlK.js' + + +export const mountCtrlK = mountFnGenerator(CtrlK) + + 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 976af32b..78419e49 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 @@ -3,12 +3,10 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js'; -import { generateCtrlLInstructions, generateDiffInstructions } from '../../../prompt/systemPrompts.js'; -import { userInstructionsStr } from '../../../prompt/stringifySelections.js'; import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js'; import { BlockCode } from '../markdown/BlockCode.js'; @@ -23,6 +21,7 @@ import { getCmdKey } from '../../../helpers/getCmdKey.js' import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; import { VoidInputBox } from '../util/inputs.js'; import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; +import { ctrlLSystem, generateCtrlLPrompt } from '../../../prompt/prompts.js'; const IconX = ({ size, className = '' }: { size: number, className?: string }) => { @@ -85,6 +84,33 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string ); }; +type ButtonProps = ButtonHTMLAttributes +export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { + return +} + +export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { + + return +} + const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => { const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom @@ -277,6 +303,8 @@ export const SidebarChat = () => { const threadsState = useThreadsState() const threadsStateService = useService('threadsStateService') + const llmMessageService = useService('llmMessageService') + // ----- SIDEBAR CHAT state (local) ----- // state of chat @@ -286,7 +314,6 @@ export const SidebarChat = () => { const [latestError, setLatestError] = useState[0] | null>(null) - const llmMessageService = useService('llmMessageService') // state of current message const [instructions, setInstructions] = useState('') // the user's instructions @@ -325,11 +352,11 @@ export const SidebarChat = () => { // add system message to chat history - const systemPromptElt: ChatMessage = { role: 'system', content: generateCtrlLInstructions } + const systemPromptElt: ChatMessage = { role: 'system', content: ctrlLSystem } threadsStateService.addMessageToCurrentThread(systemPromptElt) // add user's message to chat history - const userHistoryElt: ChatMessage = { role: 'user', content: userInstructionsStr(instructions, selections), displayContent: instructions, selections: selections } + const userHistoryElt: ChatMessage = { role: 'user', content: generateCtrlLPrompt(instructions, selections), displayContent: instructions, selections: selections } threadsStateService.addMessageToCurrentThread(userHistoryElt) const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state @@ -474,14 +501,14 @@ export const SidebarChat = () => { {/* middle row */}
`@@[&_textarea]:!void-${style}`) // apply styles to ancestor input and textarea elements + // .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements // .join(' ') + // ` outline-none` // .split(' ') - // .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements + // .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // .join(' '); `@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px]@@[&_div.monaco-inputbox]:!void- @@[&_div.monaco-inputbox]:!void-outline-none` } @@ -508,27 +535,14 @@ export const SidebarChat = () => { {/* submit / stop button */} {isLoading ? // stop button - + /> : // submit button (up arrow) - + /> }
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 d67932dd..b674e7d5 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 @@ -8,8 +8,7 @@ import * as ReactDOM from 'react-dom/client' import { _registerServices } from './services.js'; import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'; - -export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => { +export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType, props?: any) => { if (typeof document === 'undefined') { console.error('index.tsx error: document was undefined') return @@ -19,7 +18,7 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => const root = ReactDOM.createRoot(rootElement) - root.render(); // tailwind dark theme indicator + root.render(); // tailwind dark theme indicator return disposables } 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 250363ce..09053f28 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 @@ -12,6 +12,7 @@ import { ReactServicesType } from '../../../helpers/reactServicesHelper.js' import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' +import { VoidQuickEditState } from '../../../quickEditStateService.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -20,6 +21,9 @@ let services: ReactServicesType // even if React hasn't mounted yet, the variables are always updated to the latest state. // React listens by adding a setState function to these listeners. +let quickEditState: VoidQuickEditState +const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set() + let sidebarState: VoidSidebarState const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() @@ -50,7 +54,15 @@ export const _registerServices = (services_: ReactServicesType) => { wasCalled = true services = services_ - const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services + const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = services + + quickEditState = quickEditStateService.state + disposables.push( + quickEditStateService.onDidChangeState(() => { + quickEditState = quickEditStateService.state + quickEditStateListeners.forEach(l => l(quickEditState)) + }) + ) sidebarState = sidebarStateService.state disposables.push( @@ -106,6 +118,16 @@ export const useService = (serviceName: T): // -- state of services -- +export const useQuickEditState = () => { + const [s, ss] = useState(quickEditState) + useEffect(() => { + ss(quickEditState) + quickEditStateListeners.add(ss) + return () => { quickEditStateListeners.delete(ss) } + }, [ss]) + return s +} + export const useSidebarState = () => { const [s, ss] = useState(sidebarState) useEffect(() => { 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 5e0df9c0..b66bb97d 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -9,6 +9,7 @@ export default defineConfig({ entry: [ './src2/sidebar-tsx/index.tsx', './src2/void-settings-tsx/index.tsx', + './src2/ctrl-k-tsx/index.tsx', './src2/diff/index.tsx', ], outDir: './out', From f457f006fb4c9e1720027e9a4e0aba463e68480b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 19 Dec 2024 04:05:31 -0800 Subject: [PATCH 006/321] settings page styles --- .../void/common/refreshModelService.ts | 51 +++++---- .../platform/void/common/voidSettingsTypes.ts | 49 +++++---- .../void/electron-main/llmMessage/ollama.ts | 6 + .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/browser/react/src/styles.css | 5 + .../void/browser/react/src/util/inputs.tsx | 12 +- .../void/browser/react/src/util/services.tsx | 4 +- .../react/src/void-settings-tsx/Settings.tsx | 103 +++++++++--------- 8 files changed, 129 insertions(+), 103 deletions(-) diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index e552e6d0..945ca4cb 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -9,25 +9,22 @@ import { IVoidSettingsService } from './voidSettingsService.js'; import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; -import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'; +import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js'; import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; -export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] - -export type RefreshableProviderName = typeof refreshableProviderNames[number] -type RefreshableState = { +type RefreshableState = ({ state: 'init', timeoutId: null, } | { state: 'refreshing', - timeoutId: NodeJS.Timeout | null, + timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels } | { state: 'success', timeoutId: null, -} +}) export type RefreshModelStateOfProvider = Record @@ -38,7 +35,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide ollama: ['enabled', 'endpoint'], openAICompatible: ['enabled', 'endpoint', 'apiKey'], } -const REFRESH_INTERVAL = 5000 +const REFRESH_INTERVAL = 5_000 +const COOLDOWN_TIMEOUT = 1_000 // element-wise equals function eq(a: T[], b: T[]): boolean { @@ -64,6 +62,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidAutoEnable = new Emitter(); + constructor( @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, @ILLMMessageService private readonly llmMessageService: ILLMMessageService, @@ -73,8 +73,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ const disposables: Set = new Set() - - const startRefreshing = () => { + const initializePollingAndOnChange = () => { this._clearAllTimeouts() disposables.forEach(d => d.dispose()) disposables.clear() @@ -83,12 +82,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ for (const providerName of refreshableProviderNames) { - const refresh = () => { - // const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] - this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success - } - - refresh() + const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + this.refreshModels(providerName, !enabled) // every time providerName.enabled changes, refresh models too, like a useEffect let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) @@ -97,8 +92,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this const newVals = relevantVals() if (!eq(prevVals, newVals)) { - refresh() prevVals = newVals + + const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + if (enabled) { + // if user just clicked enable, refresh + this.refreshModels(providerName, !enabled) + } + else { + // else if user just clicked disable, give 5 seconds cooldown before re-enabling (or at least re-fetching) + const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT) + this._setTimeoutId(providerName, timeoutId) + } } }) ) @@ -107,9 +112,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models voidSettingsService.waitForInitState.then(() => { - startRefreshing() + initializePollingAndOnChange() this._register( - voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() }) + voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() }) ) }) @@ -122,7 +127,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // start listening for models (and don't stop until success) - async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) { + async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) { this._clearProviderTimeout(providerName) // start loading models @@ -140,15 +145,17 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ else throw new Error('refreshMode fn: unknown provider', providerName) })) - if (options?.enableProviderOnSuccess) + if (enableProviderOnSuccess) { this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true) + this._onDidAutoEnable.fire(providerName) + } this._setRefreshState(providerName, 'success') }, onError: ({ error }) => { // poll console.log('retrying list models:', providerName, error) - const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL) + const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL) this._setTimeoutId(providerName, timeoutId) } }) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 5e47fee5..a02d27fb 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -96,7 +96,7 @@ type UnionOfKeys = T extends T ? keyof T : never; -export const customProviderSettings = { +export const defaultProviderSettings = { anthropic: { apiKey: '', }, @@ -122,15 +122,19 @@ export const customProviderSettings = { } as const -export type ProviderName = keyof typeof customProviderSettings -export const providerNames = Object.keys(customProviderSettings) as ProviderName[] +export type ProviderName = keyof typeof defaultProviderSettings +export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] -type CustomSettingName = UnionOfKeys +type CustomSettingName = UnionOfKeys type CustomProviderSettings = { - [k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined + [k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined } +export const customSettingNamesOfProvider = (providerName: ProviderName) => { + return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[] +} + type CommonProviderSettings = { enabled: boolean | undefined, // undefined initially @@ -150,11 +154,6 @@ export type SettingName = keyof SettingsForProvider -export const customSettingNamesOfProvider = (providerName: ProviderName) => { - return Object.keys(customProviderSettings[providerName]) as CustomSettingName[] -} - - export const titleOfProviderName = (providerName: ProviderName) => { @@ -208,11 +207,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName } else if (settingName === 'endpoint') { return { - title: providerName === 'ollama' ? 'Your Ollama endpoint' : + title: providerName === 'ollama' ? 'Endpoint' : providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) : '(never)', - placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint + placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : '(never)', @@ -278,42 +277,42 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { enabled: undefined, ...defaultCustomSettings, - ...customProviderSettings.anthropic, + ...defaultProviderSettings.anthropic, ...voidInitModelOptions.anthropic, }, openAI: { enabled: undefined, ...defaultCustomSettings, - ...customProviderSettings.openAI, + ...defaultProviderSettings.openAI, ...voidInitModelOptions.openAI, }, gemini: { ...defaultCustomSettings, - ...customProviderSettings.gemini, + ...defaultProviderSettings.gemini, ...voidInitModelOptions.gemini, enabled: undefined, }, groq: { ...defaultCustomSettings, - ...customProviderSettings.groq, + ...defaultProviderSettings.groq, ...voidInitModelOptions.groq, enabled: undefined, }, ollama: { ...defaultCustomSettings, - ...customProviderSettings.ollama, + ...defaultProviderSettings.ollama, ...voidInitModelOptions.ollama, enabled: undefined, }, openRouter: { ...defaultCustomSettings, - ...customProviderSettings.openRouter, + ...defaultProviderSettings.openRouter, ...voidInitModelOptions.openRouter, enabled: undefined, }, openAICompatible: { ...defaultCustomSettings, - ...customProviderSettings.openAICompatible, + ...defaultProviderSettings.openAICompatible, ...voidInitModelOptions.openAICompatible, enabled: undefined, }, @@ -341,11 +340,19 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const +// the models of these can be refreshed (in theory all can, but not all should) +export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] +export type RefreshableProviderName = typeof refreshableProviderNames[number] + + + + + export type FeatureFlagSettings = { - autoRefreshModels: boolean; // automatically scan for local models and enable when found + autoRefreshModels: boolean; } export const defaultFeatureFlagSettings: FeatureFlagSettings = { autoRefreshModels: true, @@ -360,7 +367,7 @@ type FeatureFlagDisplayInfo = { export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => { if (featureFlag === 'autoRefreshModels') { return { - description: 'Automatically scan for and enable local models.', + description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`} } } throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index d9184157..48b5fd25 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -5,6 +5,7 @@ import { Ollama } from 'ollama'; import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -18,6 +19,9 @@ export const ollamaList: _InternalModelListFnType = async ( try { const thisConfig = settingsOfProvider.ollama + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`) + const ollama = new Ollama({ host: thisConfig.endpoint }) ollama.list() .then((response) => { @@ -38,6 +42,8 @@ export const ollamaList: _InternalModelListFnType = async ( export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.ollama + // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in + if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) let fullText = '' 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 e5ff34c9..d2e2125a 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 @@ -483,7 +483,7 @@ export const SidebarChat = () => { // .split(' ') // .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements // .join(' '); - `@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px]@@[&_div.monaco-inputbox]:!void- @@[&_div.monaco-inputbox]:!void-outline-none` + `@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px] @@[&_div.monaco-inputbox]:!void-outline-none` } > diff --git a/src/vs/workbench/contrib/void/browser/react/src/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css index 2ceabda6..ad8b42e3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css @@ -16,6 +16,11 @@ } } +* { + outline: none !important; +} + + /* html { font-size: var(--vscode-font-size); 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 0c7d039f..3aba6510 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 @@ -96,15 +96,15 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac export const VoidSwitch = ({ value, onChange, + size = 'md', label, disabled = false, - size = 'md' }: { value: boolean; onChange: (value: boolean) => void; label?: string; disabled?: boolean; - size?: 'xs' | 'sm' | 'msm' | 'md'; + size?: 'xs' | 'sm' | 'sm+' | 'md'; }) => { return (