diff --git a/.eslintrc.json b/.eslintrc.json index 1edb79cf..16884e64 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,6 +10,7 @@ "jsdoc", "header", "local" + // "react" // Void ], "rules": { "constructor-super": "warn", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 144b7130..b6ba189a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ There are a few ways to contribute: - 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs). - ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension. -Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`. +Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`. @@ -75,6 +75,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing - Make sure you follow the prerequisite steps. - Make sure you have the same NodeJS version as `.nvmrc`. - If you make any React changes, you must re-run `npm run buildreact` and re-build. +- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`. - If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. @@ -101,7 +102,7 @@ We don't usually recommend bundling. Instead, you should probably just build. If # Guidelines -We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs). +We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs). ## Submitting a Pull Request diff --git a/src/vs/platform/void/browser/void.contribution.ts b/src/vs/platform/void/browser/void.contribution.ts new file mode 100644 index 00000000..2d261ea3 --- /dev/null +++ b/src/vs/platform/void/browser/void.contribution.ts @@ -0,0 +1,15 @@ + + +// ---------- common ---------- + +// llmMessage +import '../common/llmMessageService.js' + +// voidSettings +import '../common/voidSettingsService.js' + +// refreshModel +import '../common/refreshModelService.js' + +// metrics +import '../common/metricsService.js' diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index 967b5a9f..e0ec2b12 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -11,7 +11,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { IVoidConfigStateService } from './voidConfigService.js'; +import { IVoidSettingsService } from './voidSettingsService.js'; // import { INotificationService } from '../../notification/common/notification.js'; // calls channel to implement features @@ -42,7 +42,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService constructor( @IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side) - @IVoidConfigStateService private readonly voidConfigStateService: IVoidConfigStateService, + @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, // @INotificationService private readonly notificationService: INotificationService, ) { super() @@ -79,7 +79,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService const { featureName } = proxyParams // end early if no provider - const modelSelection = this.voidConfigStateService.state.modelSelectionOfFeature[featureName] + const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName] if (modelSelection === null) { onError({ message: 'Please add a Provider in Settings!', fullError: null }) return null @@ -92,7 +92,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.onFinalMessageHooks_llm[requestId_] = onFinalMessage this.onErrorHooks_llm[requestId_] = onError - const { settingsOfProvider } = this.voidConfigStateService.state + const { settingsOfProvider } = this.voidSettingsService.state // params will be stripped of all its functions over the IPC channel this.channel.call('sendLLMMessage', { @@ -116,7 +116,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService ollamaList = (params: ServiceOllamaListParams) => { const { onSuccess, onError, ...proxyParams } = params - const { settingsOfProvider } = this.voidConfigStateService.state + const { settingsOfProvider } = this.voidSettingsService.state // add state for request id const requestId_ = generateUuid(); diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 3db5d595..b4e3c54b 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IRange } from '../../../editor/common/core/range' -import { ProviderName, SettingsOfProvider } from './voidConfigTypes' +import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' export type OnText = (p: { newText: string, fullText: string }) => void diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index a724d8ce..1f6b08f3 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -5,7 +5,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; -import { IVoidConfigStateService } from './voidConfigService.js'; +import { IVoidSettingsService } from './voidSettingsService.js'; import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; @@ -38,7 +38,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes constructor( - @IVoidConfigStateService private readonly voidConfigStateService: IVoidConfigStateService, + @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, @ILLMMessageService private readonly llmMessageService: ILLMMessageService, ) { super() @@ -46,11 +46,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // on mount, refresh ollama models this.refreshOllamaModels() - // every time ollama.enabled changes, refresh ollama models - let relevantVals = () => [this.voidConfigStateService.state.settingsOfProvider.ollama.enabled, this.voidConfigStateService.state.settingsOfProvider.ollama.endpoint] + // every time ollama.enabled changes, refresh ollama models, like useEffect + let relevantVals = () => [this.voidSettingsService.state.settingsOfProvider.ollama.enabled, this.voidSettingsService.state.settingsOfProvider.ollama.endpoint] let prevVals = relevantVals() this._register( - this.voidConfigStateService.onDidChangeState(() => { // we might want to debounce this + this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this const newVals = relevantVals() if (!eq(prevVals, newVals)) { this.refreshOllamaModels() @@ -75,7 +75,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this._cancelTimeout() // if ollama is disabled, obivously done - if (this.voidConfigStateService.state.settingsOfProvider.ollama.enabled !== 'true') { + if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) { this._setState('done') return } @@ -85,7 +85,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this.llmMessageService.ollamaList({ onSuccess: ({ models }) => { - this.voidConfigStateService.setSettingOfProvider('ollama', 'models', models.map(model => model.name)) + this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name)) this._setState('done') }, onError: ({ error }) => { diff --git a/src/vs/platform/void/common/void.contribution.ts b/src/vs/platform/void/common/void.contribution.ts deleted file mode 100644 index 2e12fb9c..00000000 --- a/src/vs/platform/void/common/void.contribution.ts +++ /dev/null @@ -1,11 +0,0 @@ -// llmMessage -import './llmMessageService.js' - -// voidConfig -import './voidConfigService.js' - -// refreshModel -import './refreshModelService.js' - -// metrics -import './metricsService.js' diff --git a/src/vs/platform/void/common/voidConfigModelDefaults.ts b/src/vs/platform/void/common/voidConfigModelDefaults.ts deleted file mode 100644 index 8d2fc0f2..00000000 --- a/src/vs/platform/void/common/voidConfigModelDefaults.ts +++ /dev/null @@ -1,58 +0,0 @@ - - -export const defaultAnthropicModels = [ - 'claude-3-5-sonnet-20240620', - // 'claude-3-opus-20240229', - // 'claude-3-sonnet-20240229', - // 'claude-3-haiku-20240307' -] - - -export const defaultOpenAIModels = [ - 'o1-preview', - 'o1-mini', - 'gpt-4o', - 'gpt-4o-mini', - // 'gpt-4o-2024-05-13', - // 'gpt-4o-2024-08-06', - // 'gpt-4o-mini-2024-07-18', - // 'gpt-4-turbo', - // 'gpt-4-turbo-2024-04-09', - // 'gpt-4-turbo-preview', - // 'gpt-4-0125-preview', - // 'gpt-4-1106-preview', - // 'gpt-4', - // 'gpt-4-0613', - // 'gpt-3.5-turbo-0125', - // 'gpt-3.5-turbo', - // 'gpt-3.5-turbo-1106', -] - - - -export const defaultGroqModels = [ - "mixtral-8x7b-32768", - "llama2-70b-4096", - "gemma-7b-it" -] - - -export const defaultGeminiModels = [ - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-1.0-pro' -] - - - - - -export const dummyModelData = { - anthropic: ['claude 3.5'], - openAI: ['gpt 4o'], - ollama: ['llama 3.2', 'codestral'], - openRouter: ['qwen 2.5'], -} - - diff --git a/src/vs/platform/void/common/voidConfigService.ts b/src/vs/platform/void/common/voidConfigService.ts deleted file mode 100644 index 4b065f70..00000000 --- a/src/vs/platform/void/common/voidConfigService.ts +++ /dev/null @@ -1,137 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; -import { deepClone } from '../../../base/common/objects.js'; -import { IEncryptionService } from '../../encryption/common/encryptionService.js'; -import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; -import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { defaultVoidProviderState, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName } from './voidConfigTypes.js'; - - -const STORAGE_KEY = 'void.voidConfigStateII' - -type SetSettingOfProviderFn = ( - providerName: ProviderName, - settingName: S, - newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never], -) => Promise; - -type SetModelSelectionOfFeature = ( - featureName: K, - newVal: ModelSelectionOfFeature[K], -) => Promise; - - -type VoidConfigState = { - readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider - readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature -} - -export interface IVoidConfigStateService { - readonly _serviceBrand: undefined; - readonly state: VoidConfigState; - onDidChangeState: Event; - setSettingOfProvider: SetSettingOfProviderFn; - setModelSelectionOfFeature: SetModelSelectionOfFeature; -} - - -const defaultState = () => { - const d: VoidConfigState = { - settingsOfProvider: deepClone(defaultVoidProviderState), - modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null } - } - return d -} - - -export const IVoidConfigStateService = createDecorator('VoidConfigStateService'); -class VoidConfigService extends Disposable implements IVoidConfigStateService { - _serviceBrand: undefined; - - 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 - - state: VoidConfigState; - - constructor( - @IStorageService private readonly _storageService: IStorageService, - @IEncryptionService private readonly _encryptionService: IEncryptionService, - // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) - // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, - ) { - super() - - // at the start, we haven't read the partial config yet, but we need to set state to something - this.state = defaultState() - - // read and update the actual state immediately - this._readVoidConfigState().then(voidConfigState => { - this._setState(voidConfigState) - }) - - } - - private async _readVoidConfigState(): Promise { - const encryptedPartialConfig = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION) - - if (!encryptedPartialConfig) - return defaultState() - - const voidConfigStateStr = await this._encryptionService.decrypt(encryptedPartialConfig) - return JSON.parse(voidConfigStateStr) - } - - - private async _storeVoidConfigState(voidConfigState: VoidConfigState) { - const encryptedVoidConfigStr = await this._encryptionService.encrypt(JSON.stringify(voidConfigState)) - this._storageService.store(STORAGE_KEY, encryptedVoidConfigStr, StorageScope.APPLICATION, StorageTarget.USER); - } - - setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => { - const newState: VoidConfigState = { - ...this.state, - settingsOfProvider: { - ...this.state.settingsOfProvider, - [providerName]: { - ...this.state.settingsOfProvider[providerName], - [settingName]: newVal, - } - }, - } - // console.log('NEW STATE I', JSON.stringify(newState, null, 2)) - - await this._storeVoidConfigState(newState) - this._setState(newState) - } - - setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal) => { - const newState: VoidConfigState = { - ...this.state, - modelSelectionOfFeature: { - ...this.state.modelSelectionOfFeature, - [featureName]: newVal - } - } - // console.log('NEW STATE II', JSON.stringify(newState, null, 2)) - - await this._storeVoidConfigState(newState) - this._setState(newState) - } - - - - // internal function to update state, should be called every time state changes - private async _setState(voidConfigState: VoidConfigState) { - this.state = voidConfigState - this._onDidChangeState.fire() - } - -} - -registerSingleton(IVoidConfigStateService, VoidConfigService, InstantiationType.Eager); diff --git a/src/vs/platform/void/common/voidConfigTypes.ts b/src/vs/platform/void/common/voidConfigTypes.ts deleted file mode 100644 index cdd3e18f..00000000 --- a/src/vs/platform/void/common/voidConfigTypes.ts +++ /dev/null @@ -1,232 +0,0 @@ - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { defaultAnthropicModels, defaultGeminiModels, defaultGroqModels, defaultOpenAIModels } from './voidConfigModelDefaults.js' - - - -export const voidProviderDefaults = { - anthropic: { - apiKey: '', - }, - openAI: { - apiKey: '', - }, - ollama: { - endpoint: 'http://127.0.0.1:11434', - }, - openRouter: { - apiKey: '', - }, - openAICompatible: { - apiKey: '', - endpoint: '', - }, - gemini: { - apiKey: '', - }, - groq: { - apiKey: '' - } -} as const - - -export const voidInitModelOptions = { - anthropic: { - models: defaultAnthropicModels, - }, - openAI: { - models: defaultOpenAIModels, - }, - ollama: { - models: [], - }, - openRouter: { - models: [], // any string - }, - openAICompatible: { - models: [], - }, - gemini: { - models: defaultGeminiModels, - }, - groq: { - models: defaultGroqModels, - }, -} - - - -export type ProviderName = keyof typeof voidProviderDefaults -export const providerNames = Object.keys(voidProviderDefaults) as ProviderName[] - - - -// state -export type SettingsOfProvider = { - [providerName in ProviderName]: ( - { - [optionName in keyof typeof voidProviderDefaults[providerName]]: string - } - & - { - enabled: string, // 'true' | 'false' - maxTokens: string, - - models: string[], // if null, user can type in any string as a model - }) -} - - -type UnionOfKeys = T extends T ? keyof T : never; - -export type SettingName = UnionOfKeys - - - -type DisplayInfo = { - title: string, - type: string, - placeholder: string, -} - -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' - - throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) -} - -export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { - if (settingName === 'apiKey') { - return { - title: 'API Key', - type: 'string', - placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key - providerName === 'openAI' ? 'sk-proj-key...' : - providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key - providerName === 'gemini' ? 'key...' : - providerName === 'groq' ? 'gsk_key...' : - providerName === 'openAICompatible' ? 'sk-key...' : - '(never)', - } - } - else if (settingName === 'endpoint') { - return { - title: providerName === 'ollama' ? 'Your Ollama endpoint' : - providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) - : '(never)', - type: 'string', - placeholder: providerName === 'ollama' ? voidProviderDefaults.ollama.endpoint - : providerName === 'openAICompatible' ? 'https://my-website.com/v1' - : '(never)', - } - } - else if (settingName === 'maxTokens') { - return { - title: 'Max Tokens', - type: 'number', - placeholder: '1024', - } - } - else if (settingName === 'enabled') { - return { - title: 'Enabled?', - type: 'boolean', - placeholder: '(never)', - } - } - else if (settingName === 'models') { - return { - title: 'Available Models', - type: '(never)', - placeholder: '(never)', - } - } - - throw new Error(`displayInfo: Unknown setting name: "${settingName}"`) - -} - - -// used when waiting and for a type reference -export const defaultVoidProviderState: SettingsOfProvider = { - anthropic: { - ...voidProviderDefaults.anthropic, - ...voidInitModelOptions.anthropic, - enabled: 'false', - maxTokens: '', - }, - openAI: { - ...voidProviderDefaults.openAI, - ...voidInitModelOptions.openAI, - enabled: 'false', - maxTokens: '', - }, - ollama: { - ...voidProviderDefaults.ollama, - ...voidInitModelOptions.ollama, - enabled: 'false', - maxTokens: '', - }, - openRouter: { - ...voidProviderDefaults.openRouter, - ...voidInitModelOptions.openRouter, - enabled: 'false', - maxTokens: '', - }, - openAICompatible: { - ...voidProviderDefaults.openAICompatible, - ...voidInitModelOptions.openAICompatible, - enabled: 'false', - maxTokens: '', - }, - gemini: { - ...voidProviderDefaults.gemini, - ...voidInitModelOptions.gemini, - enabled: 'false', - maxTokens: '', - }, - groq: { - ...voidProviderDefaults.groq, - ...voidInitModelOptions.groq, - enabled: 'false', - maxTokens: '', - } -} - - - -// this is a state -export type ModelSelectionOfFeature = { - 'Ctrl+L': { - providerName: ProviderName, - modelName: string, - } | null, - 'Ctrl+K': { - providerName: ProviderName, - modelName: string, - } | null, - 'Autocomplete': { - providerName: ProviderName, - modelName: string, - } | null, -} -export type FeatureName = keyof ModelSelectionOfFeature -export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const - diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts new file mode 100644 index 00000000..ab9d28f8 --- /dev/null +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { deepClone } from '../../../base/common/objects.js'; +import { IEncryptionService } from '../../encryption/common/encryptionService.js'; +import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js'; + + +const STORAGE_KEY = 'void.voidSettingsI' + +type SetSettingOfProviderFn = ( + providerName: ProviderName, + settingName: S, + newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never], +) => Promise; + +type SetModelSelectionOfFeature = ( + featureName: K, + newVal: ModelSelectionOfFeature[K], + options?: { doNotApplyEffects?: true } +) => Promise; + + + +export type ModelOption = { text: string, value: ModelSelection } + + + +export type VoidSettingsState = { + readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider + readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature + + readonly _modelOptions: ModelOption[] // computed based on the two above items +} + + + +export interface IVoidSettingsService { + readonly _serviceBrand: undefined; + readonly state: VoidSettingsState; + onDidChangeState: Event; + setSettingOfProvider: SetSettingOfProviderFn; + setModelSelectionOfFeature: SetModelSelectionOfFeature; + + setDefaultModels(providerName: ProviderName, modelNames: string[]): void; + toggleModelHidden(providerName: ProviderName, modelName: string): void; + addModel(providerName: ProviderName, modelName: string): void; + deleteModel(providerName: ProviderName, modelName: string): boolean; +} + + +let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => { + let modelOptions: ModelOption[] = [] + for (const providerName of providerNames) { + const providerConfig = settingsOfProvider[providerName] + if (!providerConfig.enabled) continue // if disabled, don't display model options + for (const { modelName, isHidden } of providerConfig.models) { + if (isHidden) continue + modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } }) + } + } + return modelOptions +} + + +const defaultState = () => { + const d: VoidSettingsState = { + settingsOfProvider: deepClone(defaultSettingsOfProvider), + modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null }, + _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed + } + return d +} + + +export const IVoidSettingsService = createDecorator('VoidSettingsService'); +class VoidSettingsService extends Disposable implements IVoidSettingsService { + _serviceBrand: undefined; + + 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 + + state: VoidSettingsState; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IEncryptionService private readonly _encryptionService: IEncryptionService, + // could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER) + // @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, + ) { + super() + + // at the start, we haven't read the partial config yet, but we need to set state to something + this.state = defaultState() + + // read and update the actual state immediately + this._readState().then(s => { + this.state = s + this._onDidChangeState.fire() + }) + } + + private async _readState(): Promise { + const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION) + + if (!encryptedState) + return defaultState() + + const stateStr = await this._encryptionService.decrypt(encryptedState) + return JSON.parse(stateStr) + } + + + private async _storeState() { + const state = this.state + const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state)) + this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER); + } + + setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => { + + const newModelSelectionOfFeature = this.state.modelSelectionOfFeature + + const newSettingsOfProvider = { + ...this.state.settingsOfProvider, + [providerName]: { + ...this.state.settingsOfProvider[providerName], + [settingName]: newVal, + } + } + + // if changed models or enabled a provider, recompute models list + const modelsListChanged = settingName === 'models' || settingName === 'enabled' + const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions + + const newState: VoidSettingsState = { + modelSelectionOfFeature: newModelSelectionOfFeature, + settingsOfProvider: newSettingsOfProvider, + _modelOptions: newModelsList, + } + + // this must go above this.setanythingelse() + this.state = newState + + // if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null) + if (modelsListChanged) { + for (const featureName of featureNames) { + + const currentSelection = newModelSelectionOfFeature[featureName] + const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection)) + + if (selnIdx === -1) { + if (newModelsList.length !== 0) + this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true }) + else + this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true }) + } + } + } + + await this._storeState() + this._onDidChangeState.fire() + } + + + setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => { + const newState: VoidSettingsState = { + ...this.state, + modelSelectionOfFeature: { + ...this.state.modelSelectionOfFeature, + [featureName]: newVal + } + } + + this.state = newState + + if (options?.doNotApplyEffects) + return + + await this._storeState() + this._onDidChangeState.fire() + } + + + + setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) { + const { models } = this.state.settingsOfProvider[providerName] + const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames) + const newModels = [ + ...newDefaultModels, + ...models.filter(m => !m.isDefault), // keep any non-default models + ] + this.setSettingOfProvider(providerName, 'models', newModels) + } + toggleModelHidden(providerName: ProviderName, modelName: string) { + const { models } = this.state.settingsOfProvider[providerName] + const modelIdx = models.findIndex(m => m.modelName === modelName) + if (modelIdx === -1) return + const newModels: ModelInfo[] = [ + ...models.slice(0, modelIdx), + { ...models[modelIdx], isHidden: !models[modelIdx].isHidden }, + ...models.slice(modelIdx + 1, Infinity) + ] + this.setSettingOfProvider(providerName, 'models', newModels) + } + addModel(providerName: ProviderName, modelName: string) { + const { models } = this.state.settingsOfProvider[providerName] + const existingIdx = models.findIndex(m => m.modelName === modelName) + if (existingIdx !== -1) return // if exists, do nothing + const newModels = [ + ...models, + { modelName, isDefault: false, isHidden: false } + ] + this.setSettingOfProvider(providerName, 'models', newModels) + } + deleteModel(providerName: ProviderName, modelName: string): boolean { + const { models } = this.state.settingsOfProvider[providerName] + const delIdx = models.findIndex(m => m.modelName === modelName) + if (delIdx === -1) return false + const newModels = [ + ...models.slice(0, delIdx), // delete the idx + ...models.slice(delIdx + 1, Infinity) + ] + this.setSettingOfProvider(providerName, 'models', newModels) + return true + } + +} + + +registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager); diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts new file mode 100644 index 00000000..6de2f47f --- /dev/null +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -0,0 +1,308 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + + + + +export type ModelInfo = { + modelName: string, + isDefault: boolean, // whether or not it's a default for its provider + isHidden: boolean, // whether or not the user is hiding it +} + + +export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => { + const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually + return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden })) +} + +// https://docs.anthropic.com/en/docs/about-claude/models +export const defaultAnthropicModels = modelInfoOfDefaultNames([ + 'claude-3-5-sonnet-20241022', + 'claude-3-5-haiku-20241022', + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + // 'claude-3-haiku-20240307', +]) + + +// https://platform.openai.com/docs/models/gp +export const defaultOpenAIModels = modelInfoOfDefaultNames([ + 'o1-preview', + 'o1-mini', + 'gpt-4o', + 'gpt-4o-mini', + // 'gpt-4o-2024-05-13', + // 'gpt-4o-2024-08-06', + // 'gpt-4o-mini-2024-07-18', + // 'gpt-4-turbo', + // 'gpt-4-turbo-2024-04-09', + // 'gpt-4-turbo-preview', + // 'gpt-4-0125-preview', + // 'gpt-4-1106-preview', + // 'gpt-4', + // 'gpt-4-0613', + // 'gpt-3.5-turbo-0125', + // 'gpt-3.5-turbo', + // 'gpt-3.5-turbo-1106', +]) + + + +// https://console.groq.com/docs/models +export const defaultGroqModels = modelInfoOfDefaultNames([ + "mixtral-8x7b-32768", + "llama2-70b-4096", + "gemma-7b-it" +]) + + +export const defaultGeminiModels = modelInfoOfDefaultNames([ + 'gemini-1.5-flash', + 'gemini-1.5-pro', + 'gemini-1.5-flash-8b', + 'gemini-1.0-pro' +]) + + + +// export const parseMaxTokensStr = (maxTokensStr: string) => { +// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN +// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) +// if (Number.isNaN(int)) +// return undefined +// return int +// } + + + + +export const anthropicMaxPossibleTokens = (modelName: string) => { + if (modelName === 'claude-3-5-sonnet-20241022' + || modelName === 'claude-3-5-haiku-20241022') + return 8192 + if (modelName === 'claude-3-opus-20240229' + || modelName === 'claude-3-sonnet-20240229' + || modelName === 'claude-3-haiku-20240307') + return 4096 + return 1024 // return a reasonably small number if they're using a different model +} + + +type UnionOfKeys = T extends T ? keyof T : never; + + + +export const customProviderSettingsDefaults = { + anthropic: { + apiKey: '', + }, + openAI: { + apiKey: '', + }, + ollama: { + endpoint: 'http://127.0.0.1:11434', + }, + openRouter: { + apiKey: '', + }, + openAICompatible: { + apiKey: '', + endpoint: '', + }, + gemini: { + apiKey: '', + }, + groq: { + apiKey: '' + } +} as const + +export type ProviderName = keyof typeof customProviderSettingsDefaults +export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[] + + +type CustomSettingName = UnionOfKeys + +type CustomProviderSettings = { + [k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined +} + +type CommonProviderSettings = { + enabled: boolean, + models: ModelInfo[], // if null, user can type in any string as a model +} + +type SettingsForProvider = CustomProviderSettings & CommonProviderSettings + +// part of state +export type SettingsOfProvider = { + [providerName in ProviderName]: SettingsForProvider +} + + +export type SettingName = keyof SettingsForProvider + + + +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' + + throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) +} + +type DisplayInfo = { + title: string, + placeholder: string, +} +export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { + if (settingName === 'apiKey') { + return { + title: 'API Key', + placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key + providerName === 'openAI' ? 'sk-proj-key...' : + providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key + providerName === 'gemini' ? 'key...' : + providerName === 'groq' ? 'gsk_key...' : + providerName === 'openAICompatible' ? 'sk-key...' : + '(never)', + } + } + else if (settingName === 'endpoint') { + return { + title: providerName === 'ollama' ? 'Your Ollama endpoint' : + providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) + : '(never)', + placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint + : providerName === 'openAICompatible' ? 'https://my-website.com/v1' + : '(never)', + } + } + else if (settingName === 'enabled') { + return { + title: '(never)', + placeholder: '(never)', + } + } + else if (settingName === 'models') { + return { + title: '(never)', + placeholder: '(never)', + } + } + + throw new Error(`displayInfo: Unknown setting name: "${settingName}"`) + +} + + + + +const defaultCustomSettings: Record = { + apiKey: undefined, + endpoint: undefined, +} + +export const voidInitModelOptions = { + anthropic: { + models: defaultAnthropicModels, + }, + openAI: { + models: defaultOpenAIModels, + }, + ollama: { + models: [], + }, + openRouter: { + models: [], // any string + }, + openAICompatible: { + models: [], + }, + gemini: { + models: defaultGeminiModels, + }, + groq: { + models: defaultGroqModels, + }, +} + + +// used when waiting and for a type reference +export const defaultSettingsOfProvider: SettingsOfProvider = { + anthropic: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.anthropic, + ...voidInitModelOptions.anthropic, + enabled: false, + }, + openAI: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.openAI, + ...voidInitModelOptions.openAI, + enabled: false, + }, + gemini: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.gemini, + ...voidInitModelOptions.gemini, + enabled: false, + }, + groq: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.groq, + ...voidInitModelOptions.groq, + enabled: false, + }, + ollama: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.ollama, + ...voidInitModelOptions.ollama, + enabled: false, + }, + openRouter: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.openRouter, + ...voidInitModelOptions.openRouter, + enabled: false, + }, + openAICompatible: { + ...defaultCustomSettings, + ...customProviderSettingsDefaults.openAICompatible, + ...voidInitModelOptions.openAICompatible, + enabled: false, + }, +} + + +export type ModelSelection = { providerName: ProviderName, modelName: string } + +export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => { + return m1.modelName === m2.modelName && m1.providerName === m2.providerName +} + +// this is a state +export type ModelSelectionOfFeature = { + 'Ctrl+L': ModelSelection | null, + 'Ctrl+K': ModelSelection | null, + 'Autocomplete': ModelSelection | null, +} +export type FeatureName = keyof ModelSelectionOfFeature +export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const + diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 86282282..19a4ddef 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; -import { parseMaxTokensStr } from './util.js'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; -import { displayInfoOfSettingName } from '../../common/voidConfigTypes.js'; +import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; // Anthropic type LLMMessageAnthropic = { @@ -17,9 +16,9 @@ export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onTe const thisConfig = settingsOfProvider.anthropic - const maxTokens = parseMaxTokensStr(thisConfig.maxTokens) + const maxTokens = anthropicMaxPossibleTokens(modelName) if (maxTokens === undefined) { - onError({ message: `Please set a value for ${displayInfoOfSettingName('anthropic', 'maxTokens').title}.`, fullError: null }) + onError({ message: `Please set a value for Max Tokens.`, fullError: null }) return } diff --git a/src/vs/platform/void/electron-main/llmMessage/groq.ts b/src/vs/platform/void/electron-main/llmMessage/groq.ts index 5ab59211..1b5918a7 100644 --- a/src/vs/platform/void/electron-main/llmMessage/groq.ts +++ b/src/vs/platform/void/electron-main/llmMessage/groq.ts @@ -5,7 +5,6 @@ import Groq from 'groq-sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; -import { parseMaxTokensStr } from './util.js'; // Groq export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { @@ -24,7 +23,7 @@ export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onT model: modelName, stream: true, temperature: 0.7, - max_tokens: parseMaxTokensStr(thisConfig.maxTokens), + // max_tokens: parseMaxTokensStr(thisConfig.maxTokens), }) .then(async response => { _setAborter(() => response.controller.abort()) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index ba04e117..03cf29dc 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -5,7 +5,6 @@ import { Ollama } from 'ollama'; import { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js'; -import { parseMaxTokensStr } from './util.js'; export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -49,7 +48,7 @@ export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, model: modelName, messages: messages, stream: true, - options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens + // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens }) .then(async stream => { _setAborter(() => stream.abort()) diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index 3b8c3645..03eb7761 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -5,7 +5,7 @@ import OpenAI from 'openai'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; -import { parseMaxTokensStr } from './util.js'; +// import { parseMaxTokensStr } from './util.js'; // OpenAI, OpenRouter, OpenAICompatible @@ -20,7 +20,7 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, if (providerName === 'openAI') { const thisConfig = settingsOfProvider.openAI openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) } + options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } } else if (providerName === 'openRouter') { const thisConfig = settingsOfProvider.openRouter @@ -31,12 +31,12 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); - options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) } + options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } } else if (providerName === 'openAICompatible') { const thisConfig = settingsOfProvider.openAICompatible openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) - options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) } + options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } } else { console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`) diff --git a/src/vs/platform/void/electron-main/llmMessage/util.ts b/src/vs/platform/void/electron-main/llmMessage/util.ts deleted file mode 100644 index 988e4706..00000000 --- a/src/vs/platform/void/electron-main/llmMessage/util.ts +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -export const parseMaxTokensStr = (maxTokensStr: string) => { - // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN - const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) - if (Number.isNaN(int)) - return undefined - return int -} - - diff --git a/src/vs/workbench/contrib/void/browser/.gitignore b/src/vs/workbench/contrib/void/browser/.gitignore deleted file mode 100644 index 897c5fc6..00000000 --- a/src/vs/workbench/contrib/void/browser/.gitignore +++ /dev/null @@ -1 +0,0 @@ -void-imports/ diff --git a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts similarity index 100% rename from src/vs/workbench/contrib/void/browser/registerAutocomplete.ts rename to src/vs/workbench/contrib/void/browser/autocompleteService.ts diff --git a/src/vs/workbench/contrib/void/browser/findDiffs.ts b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts similarity index 99% rename from src/vs/workbench/contrib/void/browser/findDiffs.ts rename to src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts index 32b893e4..8586a3ca 100644 --- a/src/vs/workbench/contrib/void/browser/findDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts @@ -3,7 +3,7 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -import { diffLines } from './react/out/util/diffLines.js' +import { diffLines } from '../react/out/diff/index.js' export type ComputedDiff = { type: 'edit'; diff --git a/src/vs/workbench/contrib/void/browser/getCmdKey.ts b/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts similarity index 88% rename from src/vs/workbench/contrib/void/browser/getCmdKey.ts rename to src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts index d2154068..fe520e42 100644 --- a/src/vs/workbench/contrib/void/browser/getCmdKey.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts @@ -3,7 +3,7 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -import { isMacintosh } from '../../../../base/common/platform.js'; +import { isMacintosh } from '../../../../../base/common/platform.js'; // import { OperatingSystem, OS } from '../../../../base/common/platform.js'; // OS === OperatingSystem.Macintosh diff --git a/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts new file mode 100644 index 00000000..3c043344 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/reactServicesHelper.ts @@ -0,0 +1,51 @@ +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; +import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; +import { IContextViewService, IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { ILLMMessageService } from '../../../../../platform/void/common/llmMessageService.js'; +import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js'; +import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js'; +import { IInlineDiffsService } from '../inlineDiffsService.js'; +import { ISidebarStateService } from '../sidebarStateService.js'; +import { IThreadHistoryService } from '../threadHistoryService.js'; + +export type ReactServicesType = { + sidebarStateService: ISidebarStateService; + settingsStateService: IVoidSettingsService; + threadsStateService: IThreadHistoryService; + fileService: IFileService; + modelService: IModelService; + inlineDiffService: IInlineDiffsService; + llmMessageService: ILLMMessageService; + clipboardService: IClipboardService; + refreshModelService: IRefreshModelService; + + themeService: IThemeService, + hoverService: IHoverService, + + contextViewService: IContextViewService; + contextMenuService: IContextMenuService; +} + + +export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => { + return { + settingsStateService: accessor.get(IVoidSettingsService), + sidebarStateService: accessor.get(ISidebarStateService), + threadsStateService: accessor.get(IThreadHistoryService), + fileService: accessor.get(IFileService), + modelService: accessor.get(IModelService), + inlineDiffService: accessor.get(IInlineDiffsService), + llmMessageService: accessor.get(ILLMMessageService), + clipboardService: accessor.get(IClipboardService), + themeService: accessor.get(IThemeService), + hoverService: accessor.get(IHoverService), + refreshModelService: accessor.get(IRefreshModelService), + contextViewService: accessor.get(IContextViewService), + contextMenuService: accessor.get(IContextMenuService), + } +} + diff --git a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts similarity index 99% rename from src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts rename to src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index cb4ae858..5d6c0c49 100644 --- a/src/vs/workbench/contrib/void/browser/registerInlineDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -11,9 +11,8 @@ 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 { IVoidConfigStateService } from './registerConfig.js'; import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js'; -import { ComputedDiff, findDiffs } from './findDiffs.js'; +import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; @@ -144,7 +143,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right - // @IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService, @ICodeEditorService private readonly _editorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z diff --git a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts b/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts index 17372ec9..e23ac6f4 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/stringifySelections.ts @@ -3,7 +3,7 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -import { CodeSelection } from '../registerThreads.js'; +import { CodeSelection } from '../threadHistoryService.js'; export const stringifySelections = (selections: CodeSelection[]) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/diffLines.tsx b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx similarity index 100% rename from src/vs/workbench/contrib/void/browser/react/src/util/diffLines.tsx rename to src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx deleted file mode 100644 index c7f61fdf..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { useCallback, useEffect, useRef, useState } from 'react' -import { FeatureName, featureNames, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js' -import { dummyModelData } from '../../../../../../../platform/void/common/voidConfigModelDefaults.js' -import { useConfigState, useRefreshModelState, useService } from '../util/services.js' -import { VoidSelectBox } from './inputs.js' -import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js' - - -export const ModelSelectionOfFeature = ({ featureName }: { featureName: FeatureName }) => { - - const voidConfigService = useService('configStateService') - const voidConfigState = useConfigState() - - const modelOptions: { text: string, value: [string, string] }[] = [] - - for (const providerName of providerNames) { - const providerConfig = voidConfigState[providerName] - if (providerConfig.enabled !== 'true') continue - providerConfig.models?.forEach(model => { - modelOptions.push({ text: `${model} (${providerName})`, value: [providerName, model] }) - }) - } - - - const isDummy = modelOptions.length === 0 - if (isDummy) { - for (const [providerName, models] of Object.entries(dummyModelData)) { - for (let model of models) { - modelOptions.push({ text: `${model} (${providerName})`, value: ['dummy', 'dummy'] }) - } - } - } - - let weChangedText = false - - return <> -

{featureName}

- { - { - if (isDummy) return // don't set state to the dummy value - if (weChangedText) return - - voidConfigService.setModelSelectionOfFeature(featureName, { providerName: newVal[0] as ProviderName, modelName: newVal[1] }) - }, [voidConfigService, featureName, isDummy])} - // we are responsible for setting the initial state here. always sync instance when state changes. - onCreateInstance={useCallback((instance: SelectBox) => { - const syncInstance = () => { - const settingsAtProvider = voidConfigService.state.modelSelectionOfFeature[featureName] - const index = modelOptions.findIndex(v => v.value[0] === settingsAtProvider?.providerName && v.value[1] === settingsAtProvider?.modelName) - if (index !== -1) { - weChangedText = true - instance.select(index) - weChangedText = false - } - } - syncInstance() - const disposable = voidConfigService.onDidChangeState(syncInstance) - return [disposable] - }, [voidConfigService, modelOptions, featureName])} - />} - - -} - -const RefreshModels = () => { - const refreshModelState = useRefreshModelState() - const refreshModelService = useService('refreshModelService') - - return <> - - {refreshModelState === 'loading' ? 'loading...' : '✅'} - -} - -export const ModelSelectionSettings = () => { - return <> - {featureNames.map(featureName => )} - - - -} - diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx index d605e2d8..55fa83b7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx @@ -8,24 +8,22 @@ import { mountFnGenerator } from '../util/mountFnGenerator.js' // import { SidebarSettings } from './SidebarSettings.js'; -import { useSidebarState } from '../util/services.js'; +import { useIsDark, useSidebarState } from '../util/services.js'; // import { SidebarThreadSelector } from './SidebarThreadSelector.js'; // import { SidebarChat } from './SidebarChat.js'; import '../styles.css' import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { SidebarChat } from './SidebarChat.js'; -import { ModelSelectionSettings } from './ModelSelectionSettings.js'; -import { VoidProviderSettings } from './VoidProviderSettings.js'; import ErrorBoundary from './ErrorBoundary.js'; -const Sidebar = () => { +export const Sidebar = ({ className }: { className: string }) => { const sidebarState = useSidebarState() const { isHistoryOpen, currentTab: tab } = sidebarState - // className='@@void-scope' - return
-
+ const isDark = useIsDark() + return
+
{/* { const tabs = ['chat', 'settings', 'threadSelector'] @@ -33,27 +31,27 @@ const Sidebar = () => { sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any }) }}>clickme {tab} */} -
+
-
+
- + {/* - + */}
-
+ {/*
-
+
*/}
@@ -61,7 +59,3 @@ const Sidebar = () => { } - -const mountFn = mountFnGenerator(Sidebar) -export default mountFn - 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 f45c0e9c..e5ff34c9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -6,10 +6,10 @@ import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react'; -import { useConfigState, useService, useSidebarState, useThreadsState } from '../util/services.js'; +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 '../../../registerThreads.js'; +import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; @@ -19,10 +19,10 @@ import { EndOfLinePreference } from '../../../../../../../editor/common/model.js import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js'; -import { getCmdKey } from '../../../getCmdKey.js' +import { getCmdKey } from '../../../helpers/getCmdKey.js' import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; -import { VoidInputBox } from './inputs.js'; -import { ModelSelectionOfFeature } from './ModelSelectionSettings.js'; +import { VoidInputBox } from '../util/inputs.js'; +import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; const IconX = ({ size, className = '' }: { size: number, className?: string }) => { @@ -58,8 +58,8 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin > @@ -86,6 +86,53 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string }; +const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => { + const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom + const divRef = useRef(null); + + const scrollToBottom = () => { + if (divRef.current) { + divRef.current.scrollTop = divRef.current.scrollHeight; + } + }; + + const onScroll = () => { + const div = divRef.current; + if (!div) return; + + const isBottom = Math.abs( + div.scrollHeight - div.clientHeight - div.scrollTop + ) < 4; + + setIsAtBottom(isBottom); + }; + + // When children change (new messages added) + useEffect(() => { + if (isAtBottom) { + scrollToBottom(); + } + }, [children, isAtBottom]); // Dependency on children to detect new messages + + // Initial scroll to bottom + useEffect(() => { + scrollToBottom(); + }, []); + + return ( +
+ {children} +
+ ); +}; + + // read files from VSCode const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { const model = modelService.getModel(uri) @@ -112,68 +159,66 @@ export const SelectedFiles = ( return ( !!selections && selections.length !== 0 && ( -
- {selections.map((selection, i) => ( - - {/* selected file summary */} -
+ {selections.map((selection, i) => { + + const showSelectionText = selection.selectionStr && selectionIsOpened[i] + + return ( +
+ {/* selection summary */} +
{ - setSelectionIsOpened(s => { - const newS = [...s] - newS[i] = !newS[i] - return newS - }); - }} - > - - - {/* file name */} - {getBasename(selection.fileURI.fsPath)} - {/* selection range */} - {selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} - - - {/* type of selection */} - {selection.selectionStr !== null ? 'Selection' : 'File'} - - {/* X button */} - {type === 'staging' && // hoveredIdx === i - { - if (type !== 'staging') return; - setStaging([...selections.slice(0, i), ...selections.slice(i + 1)]) - }} - > - + onClick={() => { + setSelectionIsOpened(s => { + const newS = [...s] + newS[i] = !newS[i] + return newS + }); + }} + > + + {/* file name */} + {getBasename(selection.fileURI.fsPath)} + {/* selection range */} + {selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} + + {/* type of selection */} + {selection.selectionStr !== null ? 'Selection' : 'File'} + + {/* X button */} + {type === 'staging' && // hoveredIdx === i + { + e.stopPropagation(); + if (type !== 'staging') return; + setStaging([...selections.slice(0, i), ...selections.slice(i + 1)]) + setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)]) + }} + > + + + } +
+ {/* selection text */} + {showSelectionText && +
+ +
}
- {/* selection full text */} - {selection.selectionStr && selectionIsOpened[i] && - { // clear the selection string but keep the file - // // setStaging([...selections.slice(0, i), { ...selection, selectionStr: null }, ...selections.slice(i + 1, Infinity)]) - // // }} - // onClick={() => { - // if (type !== 'staging') return - // setStaging([...selections.slice(0, i), ...selections.slice(i + 1, Infinity)]) - // }} - // className="btn btn-secondary btn-sm border border-vscode-input-border rounded" - // >Remove - // )} - /> - } - - )) - } + ) + })}
) ) @@ -202,7 +247,7 @@ const ChatBubble = ({ chatMessage }: { } return
-
+
{chatbubbleContents}
@@ -228,9 +273,6 @@ export const SidebarChat = () => { return () => disposables.forEach(d => d.dispose()) }, [sidebarStateService, inputBoxRef]) - // config state - const voidConfigState = useConfigState() - // threads state const threadsState = useThreadsState() const threadsStateService = useService('threadsStateService') @@ -248,9 +290,10 @@ export const SidebarChat = () => { // 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 formRef = useRef(null) + const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead + const [sidebarHeight, setSidebarHeight] = useState(0) + const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions]) const onSubmit = async (e: FormEvent) => { @@ -351,39 +394,50 @@ export const SidebarChat = () => { } - const currentThread = threadsStateService.getCurrentThread(threadsState) const selections = threadsState._currentStagingSelections const previousMessages = currentThread?.messages ?? [] - return <> -
+ // const [_test_messages, _set_test_messages] = useState([]) + + return
{ if (ref) { setSidebarHeight(ref.clientHeight); } }} + className={`w-full h-full`} + > + {/* previous messages */} - {previousMessages.map((message, i) => - - )} + {previousMessages.map((message, i) => )} {/* message stream */} -
+ + {/* {_test_messages.map((_, i) =>
div {i}
)} +
{`totalHeight: ${sidebarHeight - formHeight - 30}`}
+
{`sidebarHeight: ${sidebarHeight}`}
+
{`formHeight: ${formHeight}`}
+ */} + + {/* input box */}
0 ? 'absolute bottom-0' : ''}`} >
{ if (ref) { setFormHeight(ref.clientHeight); } }} + className={` + flex flex-col gap-2 p-2 relative input text-left shrink-0 + transition-all duration-200 + rounded-md + bg-vscode-input-bg + border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border + `} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { onSubmit(e) @@ -393,9 +447,14 @@ export const SidebarChat = () => { console.log('submit!') onSubmit(e) }} + onClick={(e) => { + if (e.currentTarget === e.target) { + inputBoxRef.current?.focus() + } + }} > {/* top row */} -
+ <> {/* selections */} {(selections && selections.length !== 0) && @@ -410,10 +469,24 @@ export const SidebarChat = () => { showDismiss={true} /> } -
+ {/* middle row */} -
+
`@@[&_textarea]:!void-${style}`) // apply styles to ancestor input and textarea elements + // .join(' ') + + // ` outline-none` + // .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` + } + > + {/* text input */} {
{/* bottom row */} -
+
{/* submit options */} -
- +
+
{/* submit / stop button */} {isLoading ? // stop button : // submit button (up arrow) }
-
- +
+
} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 3c3e00d3..6a2b1943 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -27,7 +27,7 @@ export const SidebarThreadSelector = () => { const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1) return ( -
+
{/* X button at top right */}
@@ -49,7 +49,7 @@ export const SidebarThreadSelector = () => {
{/* a list of all the past threads */} -
+
{sortedThreadIds.map((threadId) => { if (!allThreads) return <>Error: Threads not found. diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx deleted file mode 100644 index f85cc74e..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' -import { titleOfProviderName, displayInfoOfSettingName, ProviderName, providerNames, featureNames, SettingsOfProvider, SettingName, defaultVoidProviderState } from '../../../../../../../platform/void/common/voidConfigTypes.js' -import { VoidInputBox } from './inputs.js' -import { useConfigState, useService } from '../util/services.js' -import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import ErrorBoundary from './ErrorBoundary.js' - - -const Setting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => { - - const { title, type, placeholder } = displayInfoOfSettingName(providerName, settingName) - const voidConfigService = useService('configStateService') - - - let weChangedText = false - - return <> - - { - if (weChangedText) return - - voidConfigService.setSettingOfProvider(providerName, settingName, newVal) - // if we just disabeld this provider, we should unselect all models that use it - if (settingName === 'enabled' && newVal !== 'true') { - for (let featureName of featureNames) { - if (voidConfigService.state.modelSelectionOfFeature[featureName]?.providerName === providerName) - voidConfigService.setModelSelectionOfFeature(featureName, null) - } - } - }, [voidConfigService, providerName, settingName])} - - // we are responsible for setting the initial value. always sync the instance whenever there's a change to state. - onCreateInstance={useCallback((instance: InputBox) => { - const syncInstance = () => { - const settingsAtProvider = voidConfigService.state.settingsOfProvider[providerName]; - const stateVal = settingsAtProvider[settingName as keyof typeof settingsAtProvider] - weChangedText = true - instance.value = stateVal as string - weChangedText = false - } - syncInstance() - const disposable = voidConfigService.onDidChangeState(syncInstance) - return [disposable] - }, [voidConfigService, providerName, settingName])} - multiline={false} - /> - - -} - - -const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { - const voidConfigState = useConfigState() - const { models, ...others } = voidConfigState[providerName] - - return <> -

{titleOfProviderName(providerName)}

- {/* settings besides models (e.g. api key) */} - {Object.keys(others).map((sName, i) => { - const settingName = sName as keyof typeof others - return - })} - -} - - -export const VoidProviderSettings = () => { - - return <> - {providerNames.map(providerName => - - )} - - - -} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx new file mode 100644 index 00000000..a174f0ad --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/index.tsx @@ -0,0 +1,6 @@ +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { Sidebar } from './Sidebar.js' + +export const mountSidebar = mountFnGenerator(Sidebar) + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx similarity index 77% rename from src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx rename to src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 896762fe..504b88d2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -5,19 +5,23 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { useService } from '../util/services.js'; -import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; -import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles, defaultToggleStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; -import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; +import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'; +import { 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 { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollableElementCreationOptions } from '../../../../../../../base/browser/ui/scrollbar/scrollableElementOptions.js'; -export const WidgetComponent = ({ ctor, propsFn, dispose, onCreateInstance } +export const WidgetComponent = ({ ctor, propsFn, dispose, onCreateInstance, children, className } : { ctor: { new(...params: CtorParams): Instance }, propsFn: (container: HTMLDivElement) => CtorParams, onCreateInstance: (instance: Instance) => IDisposable[], dispose: (instance: Instance) => void, + children?: React.ReactNode, + className?: string } ) => { const containerRef = useRef(null); @@ -31,20 +35,20 @@ export const WidgetComponent = ({ ctor, prop } }, [ctor, propsFn, dispose, onCreateInstance, containerRef]) - return
+ return
{children}
} -export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: { +export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: { onChangeText: (value: string) => void; + styles?: Partial, onCreateInstance?: (instance: InputBox) => void | IDisposable[]; inputBoxRef?: { current: InputBox | null }; placeholder: string; multiline: boolean; }) => { - const contextViewProvider = useService('contextViewService'); return ({ onChangeSelection, onCreateInstance, selectB let containerRef = useRef(null); return { containerRef.current = container @@ -125,7 +132,7 @@ export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectB instance.render(containerRef.current) disposables.push( - instance.onDidSelect(e => { onChangeSelection(options[e.index].value ); }) + instance.onDidSelect(e => { onChangeSelection(options[e.index].value); }) ) if (onCreateInstance) { @@ -142,6 +149,32 @@ export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectB }; +// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => { +// const instanceRef = useRef(null); +// const [childrenPortal, setChildrenPortal] = useState(null) + +// return <> +// { +// return [container, options] as const; +// }, [options])} +// onCreateInstance={useCallback((instance: DomScrollableElement) => { +// instanceRef.current = instance; +// setChildrenPortal(createPortal(children, instance.getDomNode())) +// return [] +// }, [setChildrenPortal, children])} +// dispose={useCallback((instance: DomScrollableElement) => { +// console.log('calling dispose!!!!') +// // instance.dispose(); +// // instance.getDomNode().remove() +// }, [])} +// >{children} + +// {childrenPortal} + +// +// } // export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, options }: { // initVal: T; 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 d6b24f8c..d67932dd 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 @@ -5,11 +5,11 @@ import React, { useEffect, useState } from 'react'; import * as ReactDOM from 'react-dom/client' -import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js'; import { _registerServices } from './services.js'; +import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'; -export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => { +export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => { if (typeof document === 'undefined') { console.error('index.tsx error: document was undefined') return @@ -17,8 +17,9 @@ export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLEleme const disposables = _registerServices(services) + const root = ReactDOM.createRoot(rootElement) - root.render(); + 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 975f5be6..250363ce 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 @@ -4,43 +4,53 @@ *--------------------------------------------------------------------------------------------*/ import { useState, useEffect } from 'react' -import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js' -import { ThreadsState } from '../../../registerThreads.js' -import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidConfigTypes.js' +import { ThreadsState } from '../../../threadHistoryService.js' +import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import { RefreshModelState } from '../../../../../../../platform/void/common/refreshModelService.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' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes let services: ReactServicesType -// even if React hasn't mounted yet, these variables are always updated to the latest state: +// 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 sidebarState: VoidSidebarState -let threadsState: ThreadsState -let settingsOfProvider: SettingsOfProvider -let refreshModelState: RefreshModelState - -// React listens by adding a setState function to these: const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() + +let threadsState: ThreadsState const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set() -const settingsOfProviderListeners: Set<(s: SettingsOfProvider) => void> = new Set() + +let settingsState: VoidSettingsState +const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set() + +let refreshModelState: RefreshModelState const refreshModelStateListeners: Set<(s: RefreshModelState) => void> = new Set() +let colorThemeState: ColorScheme +const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() + // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! - let wasCalled = false - export const _registerServices = (services_: ReactServicesType) => { const disposables: IDisposable[] = [] - if (wasCalled) console.error(`⚠️ Void _registerServices was called again! It should only be called once.`) + // don't register services twice + if (wasCalled) { + return + // console.error(`⚠️ Void _registerServices was called again! It should only be called once.`) + } wasCalled = true services = services_ - const { sidebarStateService, configStateService, threadsStateService, refreshModelService } = services + const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services sidebarState = sidebarStateService.state disposables.push( @@ -58,11 +68,11 @@ export const _registerServices = (services_: ReactServicesType) => { }) ) - settingsOfProvider = configStateService.state.settingsOfProvider + settingsState = settingsStateService.state disposables.push( - configStateService.onDidChangeState(() => { - settingsOfProvider = configStateService.state.settingsOfProvider - settingsOfProviderListeners.forEach(l => l(settingsOfProvider)) + settingsStateService.onDidChangeState(() => { + settingsState = settingsStateService.state + settingsStateListeners.forEach(l => l(settingsState)) }) ) @@ -74,6 +84,14 @@ export const _registerServices = (services_: ReactServicesType) => { }) ) + colorThemeState = themeService.getColorTheme().type + disposables.push( + themeService.onDidColorThemeChange(theme => { + colorThemeState = theme.type + colorThemeStateListeners.forEach(l => l(colorThemeState)) + }) + ) + return disposables } @@ -98,12 +116,12 @@ export const useSidebarState = () => { return s } -export const useConfigState = () => { - const [s, ss] = useState(settingsOfProvider) +export const useSettingsState = () => { + const [s, ss] = useState(settingsState) useEffect(() => { - ss(settingsOfProvider) - settingsOfProviderListeners.add(ss) - return () => { settingsOfProviderListeners.delete(ss) } + ss(settingsState) + settingsStateListeners.add(ss) + return () => { settingsStateListeners.delete(ss) } }, [ss]) return s } @@ -128,3 +146,21 @@ export const useRefreshModelState = () => { }, [ss]) return s } + + + + + +export const useIsDark = () => { + const [s, ss] = useState(colorThemeState) + useEffect(() => { + ss(colorThemeState) + colorThemeStateListeners.add(ss) + return () => { colorThemeStateListeners.delete(ss) } + }, [ss]) + + // s is the theme, return isDark instead of s + const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK + return isDark + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx new file mode 100644 index 00000000..59abfcde --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + +import { useCallback, useEffect, useRef, useState } from 'react' +import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import { useSettingsState, useRefreshModelState, useService } from '../util/services.js' +import { VoidSelectBox } from '../util/inputs.js' +import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js' + + +const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => { + const voidSettingsService = useService('settingsStateService') + const settingsState = useSettingsState() + + let weChangedText = false + + return { + if (weChangedText) return + voidSettingsService.setModelSelectionOfFeature(featureName, newVal) + }, [voidSettingsService, featureName])} + // we are responsible for setting the initial state here. always sync instance when state changes. + onCreateInstance={useCallback((instance: SelectBox) => { + const syncInstance = () => { + const modelsListRef = voidSettingsService.state._modelOptions // as a ref + const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName] + const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider)) + weChangedText = true + instance.select(selectionIdx === -1 ? 0 : selectionIdx) + weChangedText = false + } + syncInstance() + const disposable = voidSettingsService.onDidChangeState(syncInstance) + return [disposable] + }, [voidSettingsService, featureName])} + /> +} + +const DummySelectBox = () => { + return { }} + /> +} + +export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => { + const settingsState = useSettingsState() + return <> + {settingsState._modelOptions.length === 0 ? : } + +} 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 new file mode 100644 index 00000000..d9c34da9 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -0,0 +1,274 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' +import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' +import { VoidInputBox, VoidSelectBox } from '../util/inputs.js' +import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js' +import { X } from 'lucide-react' + + + +// models + +const RefreshableModels = () => { + const settingsState = useSettingsState() + + const refreshModelState = useRefreshModelState() + const refreshModelService = useService('refreshModelService') + + if (!settingsState.settingsOfProvider.ollama.enabled) + return null + + return
+ + {refreshModelState === 'loading' ? 'loading...' : 'good!'} +
+} + + + +const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { + const settingsStateService = useService('settingsStateService') + const settingsState = useSettingsState() + + const providerNameRef = useRef(null) + const modelNameRef = useRef(null) + + const [errorString, setErrorString] = useState('') + + const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames]) + + return <> +
+ {/* model */} +
+ { modelNameRef.current = modelName }, [])} + multiline={false} + /> +
+ + {/* provider */} +
+ { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state + onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])} + options={providerOptions} + /> +
+ + {/* button */} +
+ +
+
+ + {!errorString ? null :
+ {errorString} +
} + + +} + +const AddModelButton = () => { + const [open, setOpen] = useState(false) + + return <> + {open ? + { setOpen(false) }} /> + : + } + +} + + +export const ModelDump = () => { + + const settingsStateService = useService('settingsStateService') + const settingsState = useSettingsState() + + // a dump of all the enabled providers' models + const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] + for (let providerName of providerNames) { + const providerSettings = settingsState.settingsOfProvider[providerName] + // if (!providerSettings.enabled) continue + modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: providerSettings.enabled }))) + } + + return
+ {modelDump.map(m => { + const { isHidden, isDefault, modelName, providerName, providerEnabled } = m + + return
+ {/* left part is width:full */} +
+ {`${modelName} (${providerName})`} +
+ {/* right part is anything that fits */} +
+ {isDefault ? '' : '(custom model)'} + +
+ {isDefault ? null : } +
+
+
+ })} +
+} + + + +// providers + +const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => { + + const { title, placeholder } = displayInfoOfSettingName(providerName, settingName) + const voidSettingsService = useService('settingsStateService') + + + let weChangedTextRef = false + + return <> + + { + if (weChangedTextRef) return + voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) + }, [voidSettingsService, providerName, settingName])} + + // we are responsible for setting the initial value. always sync the instance whenever there's a change to state. + onCreateInstance={useCallback((instance: InputBox) => { + const syncInstance = () => { + const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName]; + const stateVal = settingsAtProvider[settingName as SettingName] + // console.log('SYNCING TO', providerName, settingName, stateVal) + weChangedTextRef = true + instance.value = stateVal as string + weChangedTextRef = false + } + syncInstance() + const disposable = voidSettingsService.onDidChangeState(syncInstance) + return [disposable] + }, [voidSettingsService, providerName, settingName])} + multiline={false} + /> + + +} + +const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { + const voidSettingsState = useSettingsState() + const voidSettingsService = useService('settingsStateService') + + const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName] + + return <> + +
+

{titleOfProviderName(providerName)}

+ +
+ {/* settings besides models (e.g. api key) */} + {Object.keys(others).map((sName, i) => { + const settingName = sName as keyof typeof others + return + })} + +} + + +export const VoidProviderSettings = () => { + return <> + {providerNames.map(providerName => + + )} + +} + + + +// full settings + +export const Settings = () => { + const isDark = useIsDark() + + const [tab, setTab] = useState<'models' | 'features'>('models') + + return
+
+ +
+ +

Void Settings

+ + {/* separator */} +
+ +
+ + {/* tabs */} +
+ + +
+ + {/* separator */} +
+ + + {/* content */} +
+ +
+

Models

+ + + + + + + + +
+ +
+

{ setTab('features') }}>Features

+
+ +
+
+ +
+
+ +
+} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/index.tsx new file mode 100644 index 00000000..ff596b24 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/index.tsx @@ -0,0 +1,6 @@ +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { Settings } from './Settings.js' + +export const mountVoidSettings = mountFnGenerator(Settings) + + diff --git a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js index 46a22c09..8a41657b 100644 --- a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js @@ -5,6 +5,7 @@ /** @type {import('tailwindcss').Config} */ module.exports = { + darkMode: 'selector', // '{prefix-}dark' className is used to identify `dark:` content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file theme: { extend: { @@ -38,31 +39,31 @@ module.exports = { "input-bg": "var(--vscode-input-background)", "input-border": "var(--vscode-input-border)", "input-fg": "var(--vscode-input-foreground)", - "input-placeholder-fg": "input-var(--vscode-placeholderForeground)", - "input-active-bg": "inputOption-var(--vscode-activeBackground)", - "input-option-active-border": "inputOption-var(--vscode-activeBorder)", - "input-option-active-fg": "inputOption-var(--vscode-activeForeground)", - "input-option-hover-bg": "inputOption-var(--vscode-hoverBackground)", - "input-validation-error-bg": "inputValidation-var(--vscode-errorBackground)", - "input-validation-error-fg": "inputValidation-var(--vscode-errorForeground)", - "input-validation-error-border": "inputValidation-var(--vscode-errorBorder)", - "input-validation-info-bg": "inputValidation-var(--vscode-infoBackground)", - "input-validation-info-fg": "inputValidation-var(--vscode-infoForeground)", - "input-validation-info-border": "inputValidation-var(--vscode-infoBorder)", - "input-validation-warning-bg": "inputValidation-var(--vscode-warningBackground)", - "input-validation-warning-fg": "inputValidation-var(--vscode-warningForeground)", - "input-validation-warning-border": "inputValidation-var(--vscode-warningBorder)", + "input-placeholder-fg": "var(--vscode-placeholderForeground)", + "input-active-bg": "var(--vscode-activeBackground)", + "input-option-active-border": "var(--vscode-activeBorder)", + "input-option-active-fg": "var(--vscode-activeForeground)", + "input-option-hover-bg": "var(--vscode-hoverBackground)", + "input-validation-error-bg": "var(--vscode-errorBackground)", + "input-validation-error-fg": "var(--vscode-errorForeground)", + "input-validation-error-border": "var(--vscode-errorBorder)", + "input-validation-info-bg": "var(--vscode-infoBackground)", + "input-validation-info-fg": "var(--vscode-infoForeground)", + "input-validation-info-border": "var(--vscode-infoBorder)", + "input-validation-warning-bg": "var(--vscode-warningBackground)", + "input-validation-warning-fg": "var(--vscode-warningForeground)", + "input-validation-warning-border": "var(--vscode-warningBorder)", // command center colors (the top bar) - "commandcenter-fg": "commandCenter.foreground", - "commandcenter-active-fg": "commandCenter.activeForeground", - "commandcenter-bg": "commandCenter.background", - "commandcenter-active-bg": "commandCenter.activeBackground", - "commandcenter-border": "commandCenter.border", - "commandcenter-inactive-fg": "commandCenter.inactiveForeground", - "commandcenter-inactive-border": "commandCenter.inactiveBorder", - "commandcenter-active-border": "commandCenter.activeBorder", - "commandcenter-debugging-bg": "commandCenter.debuggingBackground", + "commandcenter-fg": "var(--vscode-commandCenter-foreground)", + "commandcenter-active-fg": "var(--vscode-commandCenter-activeForeground)", + "commandcenter-bg": "var(--vscode-commandCenter-background)", + "commandcenter-active-bg": "var(--vscode-commandCenter-activeBackground)", + "commandcenter-border": "var(--vscode-commandCenter-border)", + "commandcenter-inactive-fg": "var(--vscode-commandCenter-inactiveForeground)", + "commandcenter-inactive-border": "var(--vscode-commandCenter-inactiveBorder)", + "commandcenter-active-border": "var(--vscode-commandCenter-activeBorder)", + "commandcenter-debugging-bg": "var(--vscode-commandCenter-debuggingBackground)", // badge colors "badge-fg": "var(--vscode-badge-foreground)", @@ -84,7 +85,6 @@ module.exports = { "checkbox-border": "var(--vscode-checkbox-border)", "checkbox-select-bg": "var(--vscode-checkbox-selectBackground)", - // sidebar colors "sidebar-bg": "var(--vscode-sideBar-background)", "sidebar-fg": "var(--vscode-sideBar-foreground)", @@ -101,7 +101,6 @@ module.exports = { "sidebar-stickyscroll-border": "var(--vscode-sideBarStickyScroll-border)", "sidebar-stickyscroll-shadow": "var(--vscode-sideBarStickyScroll-shadow)", - // other colors (these are partially complete) // editor colors @@ -113,7 +112,6 @@ module.exports = { "editorwidget-bg": "var(--vscode-editorWidget-background)", "editorwidget-border": "var(--vscode-editorWidget-border)", - }, }, }, 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 c190a71e..5e0df9c0 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -7,8 +7,9 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: [ - './src2/sidebar-tsx/Sidebar.tsx', - './src2/util/diffLines.tsx', + './src2/sidebar-tsx/index.tsx', + './src2/void-settings-tsx/index.tsx', + './src2/diff/index.tsx', ], outDir: './out', format: ['esm'], diff --git a/src/vs/workbench/contrib/void/browser/registerSidebar.ts b/src/vs/workbench/contrib/void/browser/registerSidebar.ts deleted file mode 100644 index 2c7104cf..00000000 --- a/src/vs/workbench/contrib/void/browser/registerSidebar.ts +++ /dev/null @@ -1,247 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Glass Devtools, Inc. All rights reserved. - * Void Editor additions licensed under the AGPL 3.0 License. - *--------------------------------------------------------------------------------------------*/ - -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { - Extensions as ViewContainerExtensions, IViewContainersRegistry, - ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions, - IViewDescriptorService, -} from '../../../common/views.js'; - -import * as nls from '../../../../nls.js'; - -// import { Codicon } from '../../../../base/common/codicons.js'; -// import { localize } from '../../../../nls.js'; -// import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; - -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; - - -import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js'; - -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IThreadHistoryService } from './registerThreads.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; - -import mountFn from './react/out/sidebar-tsx/Sidebar.js'; - -import { IVoidConfigStateService } from '../../../../platform/void/common/voidConfigService.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IInlineDiffsService } from './registerInlineDiffs.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IRefreshModelService } from '../../../../platform/void/common/refreshModelService.js'; - - -// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control) - -export type VoidSidebarState = { - isHistoryOpen: boolean; - currentTab: 'chat' | 'settings'; -} - -export type ReactServicesType = { - sidebarStateService: IVoidSidebarStateService; - configStateService: IVoidConfigStateService; - threadsStateService: IThreadHistoryService; - fileService: IFileService; - modelService: IModelService; - inlineDiffService: IInlineDiffsService; - llmMessageService: ILLMMessageService; - clipboardService: IClipboardService; - refreshModelService: IRefreshModelService; - - themeService: IThemeService, - hoverService: IHoverService, - - contextViewService: IContextViewService; - contextMenuService: IContextMenuService; -} - -// ---------- Define viewpane ---------- - -class VoidSidebarViewPane extends ViewPane { - - constructor( - options: IViewPaneOptions, - @IInstantiationService instantiationService: IInstantiationService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IKeybindingService keybindingService: IKeybindingService, - @IOpenerService openerService: IOpenerService, - @ITelemetryService telemetryService: ITelemetryService, - @IHoverService hoverService: IHoverService, - ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService) - - } - - - - protected override renderBody(parent: HTMLElement): void { - super.renderBody(parent); - parent.style.overflow = 'auto' - parent.style.userSelect = 'text' - - // gets set immediately - this.instantiationService.invokeFunction(accessor => { - const services: ReactServicesType = { - configStateService: accessor.get(IVoidConfigStateService), - sidebarStateService: accessor.get(IVoidSidebarStateService), - threadsStateService: accessor.get(IThreadHistoryService), - fileService: accessor.get(IFileService), - modelService: accessor.get(IModelService), - inlineDiffService: accessor.get(IInlineDiffsService), - llmMessageService: accessor.get(ILLMMessageService), - clipboardService: accessor.get(IClipboardService), - themeService: accessor.get(IThemeService), - hoverService: accessor.get(IHoverService), - refreshModelService: accessor.get(IRefreshModelService), - contextViewService: accessor.get(IContextViewService), - contextMenuService: accessor.get(IContextMenuService), - } - - // mount react - const disposables: IDisposable[] | undefined = mountFn(parent, services); - disposables?.forEach(d => this._register(d)) - }); - } - -} - - - -// ---------- Register viewpane inside the void container ---------- - -// const voidThemeIcon = Codicon.symbolObject; -// const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.')); - -// called VIEWLET_ID in other places for some reason -export const VOID_VIEW_CONTAINER_ID = 'workbench.view.void' -export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID // simplicity - -// Register view container -const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); -const viewContainer = viewContainerRegistry.registerViewContainer({ - id: VOID_VIEW_CONTAINER_ID, - title: nls.localize2('void', 'Void Chat'), // this is used to say "Void" (Ctrl + L) - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]), - hideIfEmpty: false, - // icon: voidViewIcon, - order: 1, -}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, }); - - - -// Register search default location to the container (sidebar) -const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ - id: VOID_VIEW_ID, - hideByDefault: false, // start open - // containerIcon: voidViewIcon, - name: nls.localize2('void chat', "Chat"), // this says ... : CHAT - ctorDescriptor: new SyncDescriptor(VoidSidebarViewPane), - canToggleVisibility: false, - canMoveView: true, - openCommandActionDescriptor: { - id: viewContainer.id, - keybindings: { - primary: KeyMod.CtrlCmd | KeyCode.KeyL, - }, - order: 1 - }, -}], viewContainer); - - - -// ---------- Register service that manages sidebar's state ---------- - -export interface IVoidSidebarStateService { - readonly _serviceBrand: undefined; - - readonly state: VoidSidebarState; // readonly to the user - setState(newState: Partial): void; - onDidChangeState: Event; - - onDidFocusChat: Event; - onDidBlurChat: Event; - fireFocusChat(): void; - fireBlurChat(): void; - - openSidebarView(): void; -} - -export const IVoidSidebarStateService = createDecorator('voidSidebarStateService'); -class VoidSidebarStateService extends Disposable implements IVoidSidebarStateService { - _serviceBrand: undefined; - - static readonly ID = 'voidSidebarStateService'; - - 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: VoidSidebarState - - constructor( - @IViewsService private readonly _viewsService: IViewsService, - ) { - super() - - // initial state - this.state = { isHistoryOpen: false, currentTab: 'chat', } - } - - - setState(newState: Partial) { - // make sure view is open if the tab changes - if ('currentTab' in newState) { - this.openSidebarView() - } - - this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - fireFocusChat() { - this._onFocusChat.fire() - } - - fireBlurChat() { - this._onBlurChat.fire() - } - - openSidebarView() { - this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID); - this._viewsService.openView(VOID_VIEW_ID); - } - -} - -registerSingleton(IVoidSidebarStateService, VoidSidebarStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/registerActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts similarity index 76% rename from src/vs/workbench/contrib/void/browser/registerActions.ts rename to src/vs/workbench/contrib/void/browser/sidebarActions.ts index d46f6bb4..e44fc864 100644 --- a/src/vs/workbench/contrib/void/browser/registerActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,16 +11,18 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { CodeStagingSelection, IThreadHistoryService } from './registerThreads.js'; +import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js'; // import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { IVoidSidebarStateService, VOID_VIEW_ID } from './registerSidebar.js'; +import { VOID_VIEW_ID } from './sidebarPane.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; -// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { ISidebarStateService } from './sidebarStateService.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { OPEN_VOID_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; // ---------- Register commands and keybindings ---------- @@ -61,7 +63,7 @@ registerAction2(class extends Action2 { if (!model) return - const stateService = accessor.get(IVoidSidebarStateService) + const stateService = accessor.get(ISidebarStateService) const metricsService = accessor.get(IMetricsService) metricsService.capture('Chat Navigation', { type: 'Ctrl+L' }) @@ -73,23 +75,33 @@ registerAction2(class extends Action2 { accessor.get(IEditorService).activeTextEditorControl?.getSelection() ) - // add selection - const threadHistoryService = accessor.get(IThreadHistoryService) - const currentStaging = threadHistoryService.state._currentStagingSelections - const currentStagingEltIdx = currentStaging?.findIndex(s => - s.fileURI.fsPath === model.uri.fsPath - && s.range?.startLineNumber === selectionRange?.startLineNumber - && s.range?.endLineNumber === selectionRange?.endLineNumber - ) if (selectionRange) { - const selection: CodeStagingSelection = { - selectionStr: getContentInRange(model, selectionRange), + + const selectionStr = getContentInRange(model, selectionRange) + + const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? { + type: 'File', fileURI: model.uri, + selectionStr: null, + range: null, + } : { + type: 'Selection', + fileURI: model.uri, + selectionStr: selectionStr, range: selectionRange, } - // overwrite selections that match with this one (compares by `fileURI` and line numbers in `range`) + // add selection to staging + const threadHistoryService = accessor.get(IThreadHistoryService) + const currentStaging = threadHistoryService.state._currentStagingSelections + const currentStagingEltIdx = currentStaging?.findIndex(s => + s.fileURI.fsPath === model.uri.fsPath + && s.range?.startLineNumber === selection.range?.startLineNumber + && s.range?.endLineNumber === selection.range?.endLineNumber + ) + + // if matches with existing selection, overwrite if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) { threadHistoryService.setStaging([ ...currentStaging!.slice(0, currentStagingEltIdx), @@ -97,6 +109,7 @@ registerAction2(class extends Action2 { ...currentStaging!.slice(currentStagingEltIdx + 1, Infinity) ]) } + // if no match, add else { threadHistoryService.setStaging([...(currentStaging ?? []), selection]) } @@ -117,7 +130,7 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise { - const stateService = accessor.get(IVoidSidebarStateService) + const stateService = accessor.get(ISidebarStateService) const metricsService = accessor.get(IMetricsService) metricsService.capture('Chat Navigation', { type: 'New Chat' }) @@ -140,7 +153,7 @@ registerAction2(class extends Action2 { }); } async run(accessor: ServicesAccessor): Promise { - const stateService = accessor.get(IVoidSidebarStateService) + const stateService = accessor.get(ISidebarStateService) const metricsService = accessor.get(IMetricsService) metricsService.capture('Chat Navigation', { type: 'History' }) @@ -150,23 +163,19 @@ registerAction2(class extends Action2 { } }) -// Settings (API config) menu button + +// Settings gear registerAction2(class extends Action2 { constructor() { super({ - id: 'void.viewSettings', + id: 'void.settingsAction', title: 'Void Settings', icon: { id: 'settings-gear' }, menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }] }); } async run(accessor: ServicesAccessor): Promise { - const stateService = accessor.get(IVoidSidebarStateService) - const metricsService = accessor.get(IMetricsService) - - metricsService.capture('Chat Navigation', { type: 'Settings' }) - - stateService.setState({ isHistoryOpen: false, currentTab: stateService.state.currentTab === 'settings' ? 'chat' : 'settings' }) - stateService.fireBlurChat() + const commandService = accessor.get(ICommandService) + commandService.executeCommand(OPEN_VOID_SETTINGS_ACTION_ID) } }) diff --git a/src/vs/workbench/contrib/void/browser/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts new file mode 100644 index 00000000..576850fa --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { + Extensions as ViewContainerExtensions, IViewContainersRegistry, + ViewContainerLocation, IViewsRegistry, Extensions as ViewExtensions, + IViewDescriptorService, +} from '../../../common/views.js'; + +import * as nls from '../../../../nls.js'; + +// import { Codicon } from '../../../../base/common/codicons.js'; +// import { localize } from '../../../../nls.js'; +// import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; +import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js'; + +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +// import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; + + +import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js'; + +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; + +import { mountSidebar } from './react/out/sidebar-tsx/index.js'; + +import { getReactServices } from './helpers/reactServicesHelper.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; +// import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; +// import { Codicon } from '../../../../base/common/codicons.js'; +// import { Codicon } from '../../../../base/common/codicons.js'; + + +// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control) + +// ---------- Define viewpane ---------- + +class SidebarViewPane extends ViewPane { + + constructor( + options: IViewPaneOptions, + @IInstantiationService instantiationService: IInstantiationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @IOpenerService openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, + @IHoverService hoverService: IHoverService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService) + + } + + + + protected override renderBody(parent: HTMLElement): void { + super.renderBody(parent); + // parent.style.overflow = 'auto' + parent.style.userSelect = 'text' + + // gets set immediately + this.instantiationService.invokeFunction(accessor => { + const services = getReactServices(accessor) + + // mount react + const disposables: IDisposable[] | undefined = mountSidebar(parent, services); + disposables?.forEach(d => this._register(d)) + }); + } + + override layoutBody(height: number, width: number): void { + super.layoutBody(height, width) + this.element.style.height = `${height}px` + this.element.style.width = `${width}px` + + + } + +} + + + +// ---------- Register viewpane inside the void container ---------- + +// const voidThemeIcon = Codicon.symbolObject; +// const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.')); + +// called VIEWLET_ID in other places for some reason +export const VOID_VIEW_CONTAINER_ID = 'workbench.view.void' +export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID + +// Register view container +const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); +const container = viewContainerRegistry.registerViewContainer({ + id: VOID_VIEW_CONTAINER_ID, + title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L) + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, { + mergeViewWithContainerWhenSingleView: true, + orientation: Orientation.HORIZONTAL, + }]), + hideIfEmpty: false, + order: 1, + + rejectAddedViews: true, + icon: Codicon.symbolMethod, + + +}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true, isDefault: true }); + + + +// Register search default location to the container (sidebar) +const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); +viewsRegistry.registerViews([{ + id: VOID_VIEW_ID, + hideByDefault: false, // start open + // containerIcon: voidViewIcon, + name: nls.localize2('voidChat', ''), // this says ... : CHAT + ctorDescriptor: new SyncDescriptor(SidebarViewPane), + canToggleVisibility: false, + canMoveView: false, // can't move this out of its container + weight: 80, + order: 1, + // singleViewPaneContainerTitle: 'hi', + + // openCommandActionDescriptor: { + // id: VOID_VIEW_CONTAINER_ID, + // keybindings: { + // primary: KeyMod.CtrlCmd | KeyCode.KeyL, + // }, + // order: 1 + // }, +}], container); + diff --git a/src/vs/workbench/contrib/void/browser/sidebarStateService.ts b/src/vs/workbench/contrib/void/browser/sidebarStateService.ts new file mode 100644 index 00000000..683a3ed4 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/sidebarStateService.ts @@ -0,0 +1,84 @@ +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 { IViewsService } from '../../../services/views/common/viewsService.js'; +import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js'; + + +// service that manages sidebar's state +export type VoidSidebarState = { + isHistoryOpen: boolean; + currentTab: 'chat'; +} + +export interface ISidebarStateService { + readonly _serviceBrand: undefined; + + readonly state: VoidSidebarState; // readonly to the user + setState(newState: Partial): void; + onDidChangeState: Event; + + onDidFocusChat: Event; + onDidBlurChat: Event; + fireFocusChat(): void; + fireBlurChat(): void; + + openSidebarView(): void; +} + +export const ISidebarStateService = createDecorator('voidSidebarStateService'); +class VoidSidebarStateService extends Disposable implements ISidebarStateService { + _serviceBrand: undefined; + + static readonly ID = 'voidSidebarStateService'; + + 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: VoidSidebarState + + constructor( + @IViewsService private readonly _viewsService: IViewsService, + ) { + super() + + // initial state + this.state = { isHistoryOpen: false, currentTab: 'chat', } + } + + + setState(newState: Partial) { + // make sure view is open if the tab changes + if ('currentTab' in newState) { + this.openSidebarView() + } + + this.state = { ...this.state, ...newState } + this._onDidChangeState.fire() + } + + fireFocusChat() { + this._onFocusChat.fire() + } + + fireBlurChat() { + this._onBlurChat.fire() + } + + openSidebarView() { + this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID); + this._viewsService.openView(VOID_VIEW_ID); + } + +} + +registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/registerThreads.ts b/src/vs/workbench/contrib/void/browser/threadHistoryService.ts similarity index 96% rename from src/vs/workbench/contrib/void/browser/registerThreads.ts rename to src/vs/workbench/contrib/void/browser/threadHistoryService.ts index 797d1d23..9d704c65 100644 --- a/src/vs/workbench/contrib/void/browser/registerThreads.ts +++ b/src/vs/workbench/contrib/void/browser/threadHistoryService.ts @@ -10,7 +10,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { IAutocompleteService } from './registerAutocomplete.js'; +import { IAutocompleteService } from './autocompleteService.js'; import { IRange } from '../../../../editor/common/core/range.js'; export type CodeSelection = { @@ -22,9 +22,15 @@ export type CodeSelection = { // if selectionStr is null, it means to use the entire file at send time export type CodeStagingSelection = { - fileURI: URI; - selectionStr: string | null; - range: IRange; + type: 'Selection', + fileURI: URI, + selectionStr: string, + range: IRange +} | { + type: 'File', + fileURI: URI, + selectionStr: null, + range: null } @@ -74,7 +80,7 @@ const newThreadObject = () => { } } -const THREAD_STORAGE_KEY = 'void.threadsHistory' +const THREAD_STORAGE_KEY = 'void.threadHistory' export interface IThreadHistoryService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 2dde5f70..a67594a7 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -3,20 +3,23 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -// register keybinds -import './registerActions.js' // register inline diffs -import './registerInlineDiffs.js' +import './inlineDiffsService.js' -// register Sidebar chat -import './registerSidebar.js' +// register Sidebar pane, state, actions (keybinds, menus) +import './sidebarActions.js' +import './sidebarPane.js' +import './sidebarStateService.js' // register Thread History -import './registerThreads.js' +import './threadHistoryService.js' // register Autocomplete -import './registerAutocomplete.js' +import './autocompleteService.js' + +// settings pane +import './voidSettingsPane.js' // register css import './media/void.css' diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts new file mode 100644 index 00000000..3b7515b8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Glass Devtools, Inc. All rights reserved. + * Void Editor additions licensed under the AGPL 3.0 License. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import * as nls from '../../../../nls.js'; +import { EditorExtensions } from '../../../common/editor.js'; +import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { Dimension } from '../../../../base/browser/dom.js'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; + + +import { mountVoidSettings } from './react/out/void-settings-tsx/index.js' +import { getReactServices } from './helpers/reactServicesHelper.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; + + +// refer to preferences.contribution.ts keybindings editor + +class VoidSettingsInput extends EditorInput { + + static readonly ID: string = 'workbench.input.void.settings'; + + readonly resource = URI.from({ + scheme: 'void-editor-settings', + path: 'void-settings' // Give it a unique path + }); + + constructor() { + super(); + } + + override get typeId(): string { + return VoidSettingsInput.ID; + } + + override getName(): string { + return nls.localize('voidSettingsInputsName', 'Void Settings'); + } + + override getIcon() { + return Codicon.checklist // symbol for the actual editor pane + } + +} + + +class VoidSettingsPane extends EditorPane { + static readonly ID = 'workbench.test.myCustomPane'; + + private _scrollbar: DomScrollableElement | undefined; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IStorageService storageService: IStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(VoidSettingsPane.ID, group, telemetryService, themeService, storageService); + } + + protected createEditor(parent: HTMLElement): void { + parent.style.height = '100%'; + parent.style.width = '100%'; + + const scrollableContent = document.createElement('div'); + scrollableContent.style.height = '100%'; + scrollableContent.style.width = '100%'; + + this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {})); + parent.appendChild(this._scrollbar.getDomNode()); + this._scrollbar.scanDomNode(); + + // Mount React into the scrollable content + this.instantiationService.invokeFunction(accessor => { + const services = getReactServices(accessor); + const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, services); + + setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here + this._scrollbar?.scanDomNode(); + }, 1000) + disposables?.forEach(d => this._register(d)); + }); + } + + layout(dimension: Dimension): void { + if (!this._scrollbar) return; + + this._scrollbar.getDomNode().style.height = `${dimension.height}px`; + this._scrollbar.getDomNode().style.width = `${dimension.width}px`; + this._scrollbar.scanDomNode(); + + } + + + override get minimumWidth() { return 700 } + +} + +// register Settings pane +Registry.as(EditorExtensions.EditorPane).registerEditorPane( + EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")), + [new SyncDescriptor(VoidSettingsInput)] +); + + +export const OPEN_VOID_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings' +// register the gear on the top right +registerAction2(class extends Action2 { + constructor() { + super({ + id: OPEN_VOID_SETTINGS_ACTION_ID, + title: nls.localize2('voidSettings', "Void: Settings"), + f1: true, + icon: Codicon.settingsGear, + menu: [ + { + id: MenuId.LayoutControlMenuSubmenu, + group: 'z_end', + }, + { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.equals('config.workbench.layoutControl.type', 'both'), + group: 'z_end' + } + ] + }); + } + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const instantiationService = accessor.get(IInstantiationService); + const input = instantiationService.createInstance(VoidSettingsInput); + await editorService.openEditor(input); + } +}) + + +// add to settings gear on bottom left +MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '0_command', + command: { + id: OPEN_VOID_SETTINGS_ACTION_ID, + title: nls.localize('voidSettings', "Void Settings") + }, + order: 1 +}); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index e1ea851c..14f3fec1 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -17,7 +17,7 @@ import './browser/workbench.contribution.js'; //#region --- Void // Void added this: import './contrib/void/browser/void.contribution.js'; -import '../platform/void/common/void.contribution.js'; +import '../platform/void/browser/void.contribution.js'; //#endregion @@ -334,7 +334,7 @@ import './contrib/surveys/browser/nps.contribution.js'; import './contrib/surveys/browser/languageSurveys.contribution.js'; // Welcome -import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; +// import './contrib/welcomeGettingStarted/browser/gettingStarted.contribution.js'; // Void commented this out (removes Welcome page on start) import './contrib/welcomeWalkthrough/browser/walkThrough.contribution.js'; import './contrib/welcomeViews/common/viewsWelcome.contribution.js'; import './contrib/welcomeViews/common/newFile.contribution.js';