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", diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index e552e6d0..39d243b5 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 = 300 // 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,7 +92,22 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this const newVals = relevantVals() if (!eq(prevVals, newVals)) { - refresh() + + const prevEnabled = prevVals[0] as boolean + const enabled = newVals[0] as boolean + + // if it was just enabled, or there was a change and it wasn't to the enabled state, refresh + if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) { + // if user just clicked enable, refresh + this.refreshModels(providerName, !enabled) + } + else { + // else if user just clicked disable, don't refresh + + // //give cooldown before re-enabling (or at least re-fetching) + // const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT) + // this._setTimeoutId(providerName, timeoutId) + } prevVals = newVals } }) @@ -107,9 +117,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 +132,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 +150,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/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..1b1b2ff7 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: '', }, @@ -110,8 +110,8 @@ export const customProviderSettings = { apiKey: '', }, openAICompatible: { - apiKey: '', endpoint: '', + apiKey: '', }, gemini: { 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,28 +154,48 @@ export type SettingName = keyof SettingsForProvider -export const customSettingNamesOfProvider = (providerName: ProviderName) => { - return Object.keys(customProviderSettings[providerName]) as CustomSettingName[] + +type DisplayInfoForProviderName = { + title: string, } +export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => { + if (providerName === 'anthropic') { + return { + title: 'Anthropic', + } + } + else if (providerName === 'openAI') { + return { + title: 'OpenAI', + } + } + else if (providerName === 'openRouter') { + return { + title: 'OpenRouter', + } + } + else if (providerName === 'ollama') { + return { + title: 'Ollama', - - -export const titleOfProviderName = (providerName: ProviderName) => { - if (providerName === 'anthropic') - return 'Anthropic' - else if (providerName === 'openAI') - return 'OpenAI' - else if (providerName === 'ollama') - return 'Ollama' - else if (providerName === 'openRouter') - return 'OpenRouter' - else if (providerName === 'openAICompatible') - return 'OpenAI-Compatible' - else if (providerName === 'gemini') - return 'Gemini' - else if (providerName === 'groq') - return 'Groq' + } + } + else if (providerName === 'openAICompatible') { + return { + title: 'OpenAI-Compatible', + } + } + else if (providerName === 'gemini') { + return { + title: 'Gemini', + } + } + else if (providerName === 'groq') { + return { + title: 'Groq', + } + } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) } @@ -179,9 +203,7 @@ export const titleOfProviderName = (providerName: ProviderName) => { type DisplayInfo = { title: string, placeholder: string, - - helpfulUrl?: string, - urlPurpose?: string, + subTextMd?: string, } export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { if (settingName === 'apiKey') { @@ -195,32 +217,27 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openAICompatible' ? 'sk-key...' : '(never)', - helpfulUrl: providerName === 'anthropic' ? 'https://console.anthropic.com/settings/keys' : - providerName === 'openAI' ? 'https://platform.openai.com/api-keys' : - providerName === 'openRouter' ? 'https://openrouter.ai/settings/keys' : - providerName === 'gemini' ? 'https://aistudio.google.com/apikey' : - providerName === 'groq' ? 'https://console.groq.com/keys' : + subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : + providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : + providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : + providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : + providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : providerName === 'openAICompatible' ? undefined : undefined, - - urlPurpose: 'to get your API key.', } } 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)', - helpfulUrl: providerName === 'ollama' ? 'https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network' - : providerName === 'openAICompatible' ? undefined - : undefined, - - urlPurpose: 'for more information.', + subTextMd: providerName === 'ollama' ? 'Read about Ollama [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' : + undefined, } } else if (settingName === 'enabled') { @@ -278,42 +295,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 +358,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 +385,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/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 71% rename from src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts rename to src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 157f4292..803029c2 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/systemPrompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -3,37 +3,144 @@ * 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} +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 rewrite the selection following the user's instructions. +Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). -// 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 +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. -// Complete the following: -// \`\`\` -//
${prefix}
-// ${suffix} -// `; +## 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 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 = ` 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/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 0977a18a..c1750d81 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -169,7 +169,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? if (t.type === "link") { return ( - + { window.open(t.href) }} href={t.href} title={t.title ?? undefined}> {t.text} ) 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..75cc5419 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 { 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: generateDiffInstructions } + 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,16 +501,16 @@ 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` + `@@[&_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` } > @@ -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/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css index 2ceabda6..31742153 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css @@ -8,14 +8,18 @@ @tailwind utilities; -@layer components { - .select-ellipsis select { - text-overflow: ellipsis; - white-space: nowrap; - padding-right: 24px; - } +.select-child-restyle select { + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 24px; } +* { + 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 70be6772..6cfcf9a5 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, + size = 'md', + label, + disabled = false, +}: { + value: boolean; + onChange: (value: boolean) => void; + label?: string; + disabled?: boolean; + size?: 'xs' | 'sm' | 'sm+' | '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[]); @@ -104,7 +194,7 @@ export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectB let containerRef = useRef(null); return { containerRef.current = container 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 234f8cd3..2cee2419 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,13 +5,14 @@ import { useState, useEffect } from 'react' import { ThreadsState } from '../../../threadHistoryService.js' -import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' 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 { RefreshableProviderName, RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js' +import { VoidQuickEditState } from '../../../quickEditStateService.js' +import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.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() @@ -51,7 +55,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( @@ -108,6 +120,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/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 66b11f3d..cc00422e 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 @@ -1,11 +1,11 @@ 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 { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName } 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' +import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' @@ -31,12 +31,14 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide const { state } = refreshModelState[providerName] const isRefreshing = state === 'refreshing' - const providerTitle = titleOfProviderName(providerName) + const { title: providerTitle } = displayInfoOfProviderName(providerName) return
- Refresh Default Models for {providerTitle}. + { + justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.` + }
} @@ -66,10 +68,11 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { const [errorString, setErrorString] = useState('') - const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames]) + + const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames]) return <> -
+
{/* model */}
void }) => {
{/* button */} -
+
+ + {!errorString ? null :
+ {errorString} +
} + +
- {!errorString ? null :
- {errorString} -
} } @@ -126,10 +133,13 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { const AddModelMenuFull = () => { const [open, setOpen] = useState(false) - return
+ return
{open ? { setOpen(false) }} /> - : + : }
} @@ -148,24 +158,34 @@ export const ModelDump = () => { modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings.enabled }))) } + // sort by hidden + modelDump.sort((a, b) => { + return Number(b.providerEnabled) - Number(a.providerEnabled) + }) + return
{modelDump.map(m => { const { isHidden, isDefault, modelName, providerName, providerEnabled } = m + const disabled = !providerEnabled + return
{/* left part is width:full */} -
+
{`${modelName} (${providerName})`}
{/* right part is anything that fits */}
{isDefault ? '' : '(custom model)'} - -
+ + { settingsStateService.toggleModelHidden(providerName, modelName) }} + disabled={disabled} + size='sm' + /> + +
{isDefault ? null : }
@@ -180,16 +200,18 @@ export const ModelDump = () => { const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => { - const { title, placeholder, } = displayInfoOfSettingName(providerName, settingName) - const voidSettingsService = useService('settingsStateService') + const { title: providerTitle, } = displayInfoOfProviderName(providerName) + + const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName) + const voidSettingsService = useService('settingsStateService') let weChangedTextRef = false return
{ if (weChangedTextRef) return voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) @@ -211,9 +233,12 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider }, [voidSettingsService, providerName, settingName])} multiline={false} /> + {subTextMd === undefined ? null :
+ +
} +
- } const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { @@ -223,16 +248,31 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = const { enabled } = voidSettingsState.settingsOfProvider[providerName] const settingNames = customSettingNamesOfProvider(providerName) - return <> -
-

{titleOfProviderName(providerName)}

- + const { title: providerTitle } = displayInfoOfProviderName(providerName) + + return
+
+

{providerTitle}

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

{description}

@@ -287,10 +329,10 @@ export const Settings = () => { {/* tabs */}
- -
@@ -303,18 +345,17 @@ export const Settings = () => {
-

Models

+

Providers

+ + + + +

Models

-

Providers

-
- - - -
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 b3bb7368..2a7547a0 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', diff --git a/src/vs/workbench/contrib/void/browser/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts index 576850fa..bbe7cf4b 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarPane.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts @@ -84,7 +84,7 @@ class SidebarViewPane extends ViewPane { }); } - override layoutBody(height: number, width: number): void { + protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width) this.element.style.height = `${height}px` this.element.style.width = `${width}px`