Merge remote-tracking branch 'origin/react-build-watcher' into recovered-code

Added a watcher that syncs react with watch-client without crashing#
This commit is contained in:
Joaquin Coromina 2024-12-18 18:47:37 +08:00
commit 2a519157da
47 changed files with 1759 additions and 1133 deletions

View file

@ -10,6 +10,7 @@
"jsdoc",
"header",
"local"
// "react" // Void
],
"rules": {
"constructor-super": "warn",

View file

@ -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

View file

@ -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'

View file

@ -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();

View file

@ -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

View file

@ -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<void> = 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 }) => {

View file

@ -1,11 +0,0 @@
// llmMessage
import './llmMessageService.js'
// voidConfig
import './voidConfigService.js'
// refreshModel
import './refreshModelService.js'
// metrics
import './metricsService.js'

View file

@ -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'],
}

View file

@ -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 = <S extends SettingName>(
providerName: ProviderName,
settingName: S,
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeature = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
) => Promise<void>;
type VoidConfigState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
}
export interface IVoidConfigStateService {
readonly _serviceBrand: undefined;
readonly state: VoidConfigState;
onDidChangeState: Event<void>;
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<IVoidConfigStateService>('VoidConfigStateService');
class VoidConfigService extends Disposable implements IVoidConfigStateService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = 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<VoidConfigState> {
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);

View file

@ -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> = T extends T ? keyof T : never;
export type SettingName = UnionOfKeys<SettingsOfProvider[ProviderName]>
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

View file

@ -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 = <S extends SettingName>(
providerName: ProviderName,
settingName: S,
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeature = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
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<void>;
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<IVoidSettingsService>('VoidSettingsService');
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = 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<VoidSettingsState> {
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);

View file

@ -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> = 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<typeof customProviderSettingsDefaults[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[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<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
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<CustomSettingName, undefined> = {
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

View file

@ -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
}

View file

@ -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())

View file

@ -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())

View file

@ -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}`)

View file

@ -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
}

View file

@ -1 +0,0 @@
void-imports/

View file

@ -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';

View file

@ -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

View file

@ -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),
}
}

View file

@ -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

View file

@ -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[]) => {

View file

@ -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 <>
<h2>{featureName}</h2>
{
<VoidSelectBox
options={modelOptions}
onChangeSelection={useCallback((newVal: [string, string]) => {
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 <>
<button onClick={() => refreshModelService.refreshOllamaModels()}>
refresh
</button>
{refreshModelState === 'loading' ? 'loading...' : '✅'}
</>
}
export const ModelSelectionSettings = () => {
return <>
{featureNames.map(featureName => <ModelSelectionOfFeature
key={featureName}
featureName={featureName}
/>)}
<RefreshModels />
</>
}

View file

@ -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 <div className='@@void-scope'>
<div className={`flex flex-col w-full px-2 py-2`}>
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@ -33,27 +31,27 @@ const Sidebar = () => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`mb-2 ${isHistoryOpen ? '' : 'hidden'}`}>
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'}`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div>
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
<ErrorBoundary>
{/* <ErrorBoundary>
<ModelSelectionSettings />
</ErrorBoundary>
</ErrorBoundary> */}
</div>
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
</div> */}
</div>
</div>
@ -61,7 +59,3 @@ const Sidebar = () => {
}
const mountFn = mountFnGenerator(Sidebar)
export default mountFn

View file

@ -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
>
<path
fill="black"
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
></path>
</svg>
@ -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<HTMLDivElement>(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 (
<div
// options={{ vertical: ScrollbarVisibility.Auto, horizontal: ScrollbarVisibility.Auto }}
ref={divRef}
onScroll={onScroll}
className={className}
style={style}
>
{children}
</div>
);
};
// read files from VSCode
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
@ -112,68 +159,66 @@ export const SelectedFiles = (
return (
!!selections && selections.length !== 0 && (
<div className='flex flex-wrap gap-4'>
{selections.map((selection, i) => (
<Fragment key={i}>
{/* selected file summary */}
<div
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
className={`grid grid-rows-2 gap-1 relative
<div
className='flex flex-wrap gap-4 p-2 text-left'
>
{selections.map((selection, i) => {
const showSelectionText = selection.selectionStr && selectionIsOpened[i]
return (
<div key={i} // container for `selectionSummary` and `selectionText`
className={`${showSelectionText ? 'w-full' : ''}`}
>
{/* selection summary */}
<div
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
className={`grid grid-rows-2 gap-1 relative
select-none
bg-vscode-badge-bg border border-vscode-button-border rounded-md
w-fit h-fit min-w-[80px] p-1
w-fit h-fit min-w-[81px] p-1
`}
onClick={() => {
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
}}
>
<span className='truncate'>
{/* file name */}
{getBasename(selection.fileURI.fsPath)}
{/* selection range */}
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
</span>
{/* type of selection */}
<span className='truncate text-opacity-75'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span>
{/* X button */}
{type === 'staging' && // hoveredIdx === i
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
onClick={() => {
if (type !== 'staging') return;
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
}}
>
<IconX size={16} className="p-[2px] stroke-[3]" />
onClick={() => {
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
}}
>
<span className='truncate'>
{/* file name */}
{getBasename(selection.fileURI.fsPath)}
{/* selection range */}
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
</span>
{/* type of selection */}
<span className='truncate text-opacity-75'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span>
{/* X button */}
{type === 'staging' && // hoveredIdx === i
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
onClick={(e) => {
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)])
}}
>
<IconX size={16} className="p-[2px] stroke-[3]" />
</span>
}
</div>
{/* selection text */}
{showSelectionText &&
<div className='w-full'>
<BlockCode text={selection.selectionStr!} />
</div>
}
</div>
{/* selection full text */}
{selection.selectionStr && selectionIsOpened[i] &&
<BlockCode
text={selection.selectionStr}
// buttonsOnHover={(<button
// // onClick={() => { // 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</button>
// )}
/>
}
</Fragment>
))
}
)
})}
</div>
)
)
@ -202,7 +247,7 @@ const ChatBubble = ({ chatMessage }: {
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full overflow-auto`}>
{chatbubbleContents}
</div>
</div>
@ -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<HTMLFormElement | null>(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<HTMLFormElement>) => {
@ -351,39 +394,50 @@ export const SidebarChat = () => {
}
const currentThread = threadsStateService.getCurrentThread(threadsState)
const selections = threadsState._currentStagingSelections
const previousMessages = currentThread?.messages ?? []
return <>
<div className="overflow-x-hidden space-y-4">
// const [_test_messages, _set_test_messages] = useState<string[]>([])
return <div
ref={(ref) => { if (ref) { setSidebarHeight(ref.clientHeight); } }}
className={`w-full h-full`}
>
<ScrollToBottomContainer
className={`overflow-x-hidden overflow-y-auto`}
style={{ maxHeight: sidebarHeight - formHeight - 30 }}
>
{/* previous messages */}
{previousMessages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
</div>
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
<div>{`sidebarHeight: ${sidebarHeight}`}</div>
<div>{`formHeight: ${formHeight}`}</div>
<button type='button' onClick={() => { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div</button> */}
</ScrollToBottomContainer>
{/* input box */}
<div // this div is used to position the input box properly
className={`right-0 left-0 m-2
${previousMessages.length === 0 ? '' : 'absolute bottom-0'}
`}
className={`right-0 left-0 m-2 z-[999] ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<form
ref={formRef}
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-border hover:border-vscode-commandcenter-active-border
`}
ref={(ref) => { 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 */}
<div className=''>
<>
{/* selections */}
{(selections && selections.length !== 0) &&
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
@ -410,10 +469,24 @@ export const SidebarChat = () => {
showDismiss={true}
/>
}
</div>
</>
{/* middle row */}
<div className=''>
<div
className={
// // overwrite vscode styles (generated with this code):
// `bg-transparent outline-none text-vscode-input-fg min-h-[81px] max-h-[500px]`
// .split(' ')
// .map(style => `@@[&_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 */}
<VoidInputBox
placeholder={`${getCmdKey()}+L to select`}
@ -424,41 +497,45 @@ export const SidebarChat = () => {
</div>
{/* bottom row */}
<div className='flex flex-row justify-between items-end'>
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div>
<ModelSelectionOfFeature featureName='Ctrl+L' />
<div className='w-[250px]'>
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}
{isLoading ?
// stop button
<button
className="p-[5px] bg-white rounded-full cursor-pointer"
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center`}
onClick={onAbort}
type='button'
>
<IconSquare size={24} className="stroke-[2]" />
<IconSquare size={16} className="stroke-[2]" />
</button>
:
// submit button (up arrow)
<button
className={`${isDisabled ? 'bg-vscode-disabled-fg cursor-not-allowed' : 'bg-white cursor-pointer'}
rounded-full
shrink-0 grow-0
`}
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
${isDisabled ?
'bg-vscode-disabled-fg' // cursor-not-allowed
: 'bg-white' // cursor-pointer
}
`}
disabled={isDisabled}
type='submit'
>
<IconArrowUp size={24} className="stroke-[2]" />
<IconArrowUp size={20} className="stroke-[2]" />
</button>
}
</div>
</form>
</div>
</>
</div >
</div >
}

View file

@ -27,7 +27,7 @@ export const SidebarThreadSelector = () => {
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
return (
<div className="flex flex-col gap-y-1 overflow-y-auto h-[30vh]">
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
{/* X button at top right */}
<div className="text-right">
@ -49,7 +49,7 @@ export const SidebarThreadSelector = () => {
</div>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
<div className='flex flex-col gap-y-1 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>

View file

@ -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 <><ErrorBoundary>
<label>{title}</label>
<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
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}
/>
</ErrorBoundary></>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidConfigState = useConfigState()
const { models, ...others } = voidConfigState[providerName]
return <>
<h1 className='text-xl'>{titleOfProviderName(providerName)}</h1>
{/* settings besides models (e.g. api key) */}
{Object.keys(others).map((sName, i) => {
const settingName = sName as keyof typeof others
return <Setting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
}
export const VoidProviderSettings = () => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
)}
</>
}

View file

@ -0,0 +1,6 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Sidebar } from './Sidebar.js'
export const mountSidebar = mountFnGenerator(Sidebar)

View file

@ -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 = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance }
export const WidgetComponent = <CtorParams extends any[], Instance>({ 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<HTMLDivElement | null>(null);
@ -31,20 +35,20 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
}
}, [ctor, propsFn, dispose, onCreateInstance, containerRef])
return <div ref={containerRef} className='w-full' />
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
}
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
onChangeText: (value: string) => void;
styles?: Partial<IInputBoxStyles>,
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
inputBoxRef?: { current: InputBox | null };
placeholder: string;
multiline: boolean;
}) => {
const contextViewProvider = useService('contextViewService');
return <WidgetComponent
@ -55,7 +59,9 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
{
inputBoxStyles: {
...defaultInputBoxStyles,
inputBackground: 'transparent',
// inputBackground: 'transparent',
// inputBorder: 'none',
...styles,
},
placeholder,
tooltip: '',
@ -100,6 +106,7 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='text-ellipsis whitespace-nowrap pr-6'
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@ -125,7 +132,7 @@ export const VoidSelectBox = <T,>({ 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 = <T,>({ onChangeSelection, onCreateInstance, selectB
};
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
// return <>
// <WidgetComponent
// ctor={DomScrollableElement}
// propsFn={useCallback((container) => {
// 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}</WidgetComponent>
// {childrenPortal}
// </>
// }
// export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, options }: {
// initVal: T;

View file

@ -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(<Component />);
root.render(<Component />); // tailwind dark theme indicator
return disposables
}

View file

@ -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
}

View file

@ -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 <VoidSelectBox
options={settingsState._modelOptions}
onChangeSelection={useCallback((newVal: ModelSelection) => {
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 <VoidSelectBox
options={[{ text: 'Please add a model!', value: null }]}
onChangeSelection={() => { }}
/>
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
return <>
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <ModelSelectBox featureName={featureName} />}
</>
}

View file

@ -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 <div>
<button onClick={() => refreshModelService.refreshOllamaModels()}>refresh Ollama built-in models</button>
{refreshModelState === 'loading' ? 'loading...' : 'good!'}
</div>
}
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const settingsStateService = useService('settingsStateService')
const settingsState = useSettingsState()
const providerNameRef = useRef<ProviderName | null>(null)
const modelNameRef = useRef<string | null>(null)
const [errorString, setErrorString] = useState('')
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames])
return <>
<div className='flex justify-center items-center gap-4'>
{/* model */}
<div className='max-w-40 w-full'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
multiline={false}
/>
</div>
{/* provider */}
<div className='max-w-40 w-full'>
<VoidSelectBox
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
options={providerOptions}
/>
</div>
{/* button */}
<div className='max-w-40 w-full'>
<button
onClick={() => {
const providerName = providerNameRef.current
const modelName = modelNameRef.current
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
settingsStateService.addModel(providerName, modelName)
onSubmit()
}}>Add model</button>
</div>
</div>
{!errorString ? null : <div className='text-center text-red-500'>
{errorString}
</div>}
</>
}
const AddModelButton = () => {
const [open, setOpen] = useState(false)
return <>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button onClick={() => setOpen(true)}>Add Model</button>
}
</>
}
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 <div className=''>
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
return <div key={`${modelName}${providerName}`} className='flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden cursor-default'>
{/* left part is width:full */}
<div className='w-full flex items-center gap-4'>
<span>{`${modelName} (${providerName})`}</span>
</div>
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{(!providerEnabled || isHidden) ? '❌' : '✅'}</button>
<div className='w-5 flex items-center justify-center'>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
</div>
})}
</div>
}
// providers
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title, placeholder } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
let weChangedTextRef = false
return <><ErrorBoundary>
<label>{title}</label>
<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
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}
/>
</ErrorBoundary></>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidSettingsState = useSettingsState()
const voidSettingsService = useService('settingsStateService')
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
return <>
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
</div>
{/* settings besides models (e.g. api key) */}
{Object.keys(others).map((sName, i) => {
const settingName = sName as keyof typeof others
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
}
export const VoidProviderSettings = () => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
)}
</>
}
// full settings
export const Settings = () => {
const isDark = useIsDark()
const [tab, setTab] = useState<'models' | 'features'>('models')
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div className='w-full h-full px-10 py-10 select-none'>
<div className='max-w-5xl mx-auto'>
<h1 className='text-2xl w-full'>Void Settings</h1>
{/* separator */}
<div className='w-full h-[1px] my-4' />
<div className='flex items-stretch'>
{/* tabs */}
<div className='flex flex-col w-full max-w-32'>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
onClick={() => { setTab('features') }}
>Features</button>
</div>
{/* separator */}
<div className='w-[1px] mx-4' />
{/* content */}
<div className='w-full overflow-y-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelButton />
<RefreshableModels />
</ErrorBoundary>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
</div>
</div>
</div>
</div>
</div>
</div>
}

View file

@ -0,0 +1,6 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { Settings } from './Settings.js'
export const mountVoidSettings = mountFnGenerator(Settings)

View file

@ -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)",
},
},
},

View file

@ -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'],

View file

@ -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<IViewContainersRegistry>(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<IViewsRegistry>(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<VoidSidebarState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
openSidebarView(): void;
}
export const IVoidSidebarStateService = createDecorator<IVoidSidebarStateService>('voidSidebarStateService');
class VoidSidebarStateService extends Disposable implements IVoidSidebarStateService {
_serviceBrand: undefined;
static readonly ID = 'voidSidebarStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidSidebarState
constructor(
@IViewsService private readonly _viewsService: IViewsService,
) {
super()
// initial state
this.state = { isHistoryOpen: false, currentTab: 'chat', }
}
setState(newState: Partial<VoidSidebarState>) {
// 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);

View file

@ -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<void> {
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<void> {
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<void> {
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)
}
})

View file

@ -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<IViewContainersRegistry>(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<IViewsRegistry>(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);

View file

@ -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<VoidSidebarState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
openSidebarView(): void;
}
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
_serviceBrand: undefined;
static readonly ID = 'voidSidebarStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidSidebarState
constructor(
@IViewsService private readonly _viewsService: IViewsService,
) {
super()
// initial state
this.state = { isHistoryOpen: false, currentTab: 'chat', }
}
setState(newState: Partial<VoidSidebarState>) {
// 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);

View file

@ -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;

View file

@ -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'

View file

@ -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<IEditorPaneRegistry>(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<void> {
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
});

View file

@ -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';