Merge pull request #185 from voideditor/model-selection

Daily UI improvements
This commit is contained in:
Andrew Pareles 2024-12-20 02:07:49 -08:00 committed by GitHub
commit 9b1f21d7c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 827 additions and 230 deletions

View file

@ -31,6 +31,10 @@
"nodejsRepository": "https://nodejs.org",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
"extensionsGallery": {
"serviceUrl": "https://open-vsx.org/vscode/gallery",
"itemUrl": "https://open-vsx.org/vscode/item"
},
"builtInExtensions": [
{
"name": "ms-vscode.js-debug-companion",

View file

@ -9,25 +9,22 @@ import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export type RefreshableProviderName = typeof refreshableProviderNames[number]
type RefreshableState = {
type RefreshableState = ({
state: 'init',
timeoutId: null,
} | {
state: 'refreshing',
timeoutId: NodeJS.Timeout | null,
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
} | {
state: 'success',
timeoutId: null,
}
})
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
@ -38,7 +35,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide
ollama: ['enabled', 'endpoint'],
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5000
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
@ -64,6 +62,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidAutoEnable = new Emitter<RefreshableProviderName>();
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
@ -73,8 +73,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
const disposables: Set<IDisposable> = new Set()
const startRefreshing = () => {
const initializePollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
@ -83,12 +82,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
const refresh = () => {
// const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success
}
refresh()
const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled)
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
@ -97,7 +92,22 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
refresh()
const prevEnabled = prevVals[0] as boolean
const enabled = newVals[0] as boolean
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
// if user just clicked enable, refresh
this.refreshModels(providerName, !enabled)
}
else {
// else if user just clicked disable, don't refresh
// //give cooldown before re-enabling (or at least re-fetching)
// const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
// this._setTimeoutId(providerName, timeoutId)
}
prevVals = newVals
}
})
@ -107,9 +117,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
voidSettingsService.waitForInitState.then(() => {
startRefreshing()
initializePollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() })
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
)
})
@ -122,7 +132,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// start listening for models (and don't stop until success)
async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) {
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
this._clearProviderTimeout(providerName)
// start loading models
@ -140,15 +150,17 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
else throw new Error('refreshMode fn: unknown provider', providerName)
}))
if (options?.enableProviderOnSuccess)
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true)
this._onDidAutoEnable.fire(providerName)
}
this._setRefreshState(providerName, 'success')
},
onError: ({ error }) => {
// poll
console.log('retrying list models:', providerName, error)
const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL)
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
})

View file

@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../storage/comm
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettingsI'
const STORAGE_KEY = 'void.voidSettingsStorage'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,

View file

@ -96,7 +96,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
export const customProviderSettings = {
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
@ -110,8 +110,8 @@ export const customProviderSettings = {
apiKey: '',
},
openAICompatible: {
apiKey: '',
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
@ -122,15 +122,19 @@ export const customProviderSettings = {
} as const
export type ProviderName = keyof typeof customProviderSettings
export const providerNames = Object.keys(customProviderSettings) as ProviderName[]
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
type CustomSettingName = UnionOfKeys<typeof customProviderSettings[ProviderName]>
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
type CommonProviderSettings = {
enabled: boolean | undefined, // undefined initially
@ -150,28 +154,48 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(customProviderSettings[providerName]) as CustomSettingName[]
type DisplayInfoForProviderName = {
title: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
export const titleOfProviderName = (providerName: ProviderName) => {
if (providerName === 'anthropic')
return 'Anthropic'
else if (providerName === 'openAI')
return 'OpenAI'
else if (providerName === 'ollama')
return 'Ollama'
else if (providerName === 'openRouter')
return 'OpenRouter'
else if (providerName === 'openAICompatible')
return 'OpenAI-Compatible'
else if (providerName === 'gemini')
return 'Gemini'
else if (providerName === 'groq')
return 'Groq'
}
}
else if (providerName === 'openAICompatible') {
return {
title: 'OpenAI-Compatible',
}
}
else if (providerName === 'gemini') {
return {
title: 'Gemini',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
@ -179,9 +203,7 @@ export const titleOfProviderName = (providerName: ProviderName) => {
type DisplayInfo = {
title: string,
placeholder: string,
helpfulUrl?: string,
urlPurpose?: string,
subTextMd?: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
@ -195,32 +217,27 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAICompatible' ? 'sk-key...' :
'(never)',
helpfulUrl: providerName === 'anthropic' ? 'https://console.anthropic.com/settings/keys' :
providerName === 'openAI' ? 'https://platform.openai.com/api-keys' :
providerName === 'openRouter' ? 'https://openrouter.ai/settings/keys' :
providerName === 'gemini' ? 'https://aistudio.google.com/apikey' :
providerName === 'groq' ? 'https://console.groq.com/keys' :
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'openAICompatible' ? undefined :
undefined,
urlPurpose: 'to get your API key.',
}
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
helpfulUrl: providerName === 'ollama' ? 'https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network'
: providerName === 'openAICompatible' ? undefined
: undefined,
urlPurpose: 'for more information.',
subTextMd: providerName === 'ollama' ? 'Read about Ollama [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
undefined,
}
}
else if (settingName === 'enabled') {
@ -278,42 +295,42 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
enabled: undefined,
...defaultCustomSettings,
...customProviderSettings.anthropic,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
enabled: undefined,
...defaultCustomSettings,
...customProviderSettings.openAI,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
},
gemini: {
...defaultCustomSettings,
...customProviderSettings.gemini,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
enabled: undefined,
},
groq: {
...defaultCustomSettings,
...customProviderSettings.groq,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...customProviderSettings.ollama,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...customProviderSettings.openRouter,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...customProviderSettings.openAICompatible,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
enabled: undefined,
},
@ -341,11 +358,19 @@ export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type FeatureFlagSettings = {
autoRefreshModels: boolean; // automatically scan for local models and enable when found
autoRefreshModels: boolean;
}
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
autoRefreshModels: true,
@ -360,7 +385,7 @@ type FeatureFlagDisplayInfo = {
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: 'Automatically scan for and enable local models.',
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)

View file

@ -5,6 +5,7 @@
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
@ -18,6 +19,9 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
try {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.list()
.then((response) => {
@ -38,6 +42,8 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
let fullText = ''

View file

@ -9,10 +9,12 @@ import { ILLMMessageService } from '../../../../../platform/void/common/llmMessa
import { IRefreshModelService } from '../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../platform/void/common/voidSettingsService.js';
import { IInlineDiffsService } from '../inlineDiffsService.js';
import { IQuickEditStateService } from '../quickEditStateService.js';
import { ISidebarStateService } from '../sidebarStateService.js';
import { IThreadHistoryService } from '../threadHistoryService.js';
export type ReactServicesType = {
quickEditStateService: IQuickEditStateService;
sidebarStateService: ISidebarStateService;
settingsStateService: IVoidSettingsService;
threadsStateService: IThreadHistoryService;
@ -33,6 +35,7 @@ export type ReactServicesType = {
export const getReactServices = (accessor: ServicesAccessor): ReactServicesType => {
return {
quickEditStateService: accessor.get(IQuickEditStateService),
settingsStateService: accessor.get(IVoidSettingsService),
sidebarStateService: accessor.get(ISidebarStateService),
threadsStateService: accessor.get(IThreadHistoryService),

View file

@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js';
import { writeFileWithDiffInstructions } from './prompt/prompts.js';
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';

View file

@ -3,37 +3,144 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// // used for ctrl+l
// const partialGenerationInstructions = ``
import { CodeSelection } from '../threadHistoryService.js';
const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
// // used for ctrl+k, autocomplete
// const fimInstructions = ``
export const generateCtrlLPrompt = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};
// CTRL+K prompt:
// const promptContent = `Here is the user's original selection:
// \`\`\`
// <MID>${selection}</MID>
// \`\`\`
// The user wants to apply the following instructions to the selection:
// ${instructions}
export const ctrlLSystem = `\
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
// Please rewrite the selection following the user's instructions.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
// Instructions to follow:
// 1. Follow the user's instructions
// 2. You may ONLY CHANGE the selection, and nothing else in the file
// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection
// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Instructions:
1. Output the changes to make to the entire file.
1. Do not re-write the entire file.
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
4. You must give enough context to apply the change in the correct location.
// Complete the following:
// \`\`\`
// <PRE>${prefix}</PRE>
// <SUF>${suffix}</SUF>
// <MID>`;
## EXAMPLE
FILES
selected file \`math.ts\`:
\`\`\`
const addNumbers = (a, b) => a + b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\`
const subtractNumbers = (a, b) => a - b
\`\`\`
INSTRUCTIONS
\`\`\`
add a function that multiplies numbers below this
\`\`\`
EXPECTED OUTPUT
We can add the following code to the file:
\`\`\`
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
// existing code...
\`\`\`
## EXAMPLE
FILES
selected file \`fib.ts\`:
\`\`\`
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
SELECTION
\`\`\`
return fib(n - 1) + fib(n - 2)
\`\`\`
INSTRUCTIONS
\`\`\`
memoize results
\`\`\`
EXPECTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
\`\`\`
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES\
`
export const generateCtrlKPrompt = ({ selection, prefix, suffix, instructions, }: { selection: string, prefix: string, suffix: string, instructions: string, }) => `\
Here is the user's original selection:
\`\`\`
<MID>${selection}</MID>
\`\`\`
The user wants to apply the following instructions to the selection:
${instructions}
Please rewrite the selection following the user's instructions.
Instructions to follow:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Complete the following:
\`\`\`
<PRE>${prefix}</PRE>
<SUF>${suffix}</SUF>
<MID>`;
export const generateDiffInstructions = `

View file

@ -1,32 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { CodeSelection } from '../threadHistoryService.js';
export const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};

View file

@ -1,9 +1,111 @@
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { ICodeEditor, IViewZone } from '../../../../editor/browser/editorBrowser.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { createDecorator, IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { Emitter, Event } from '../../../../base/common/event.js';
// import { IInlineDiffService } from '../../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { mountCtrlK } from './react/out/ctrl-k-tsx/index.js';
import { getReactServices } from './helpers/reactServicesHelper.js';
import { URI } from '../../../../base/common/uri.js';
type InitialZone = { uri: URI, startLine: number, selectedText: string, }
export type QuickEditPropsType = {
quickEditId: number,
}
export type QuickEdit = {
startLine: number, // 0-indexed
beforeCode: string,
afterCode?: string,
instructions?: string,
responseText?: string, // model can produce a text response too
}
export interface IQuickEditService {
readonly _serviceBrand: undefined;
readonly onDidChangeState: Event<void>;
addZone(zone: InitialZone): void;
}
export const IQuickEditService = createDecorator<IQuickEditService>('voidQuickEditService');
class VoidQuickEditService extends Disposable implements IQuickEditService {
_serviceBrand: undefined;
quickEditId: number = 0
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
// state
// state: {}
constructor(
// @IInlineDiffService private readonly _inlineDiffService: IInlineDiffService,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
}
addZone(zone: InitialZone) {
const addZoneToEditor = (editor: ICodeEditor) => {
const model = editor.getModel()
if (!model) return
editor.changeViewZones(accessor => {
const domNode = document.createElement('div');
domNode.style.zIndex = '1'
// domNode.className = 'void-redBG'
const viewZone: IViewZone = {
// afterLineNumber: computedDiff.startLine - 1,
afterLineNumber: 1,
heightInPx: 100,
// heightInLines: 1,
// minWidthInPx: 200,
domNode: domNode,
// marginDomNode: document.createElement('div'), // displayed to left
suppressMouseDown: false,
};
// const zoneId =
accessor.addZone(viewZone)
this._instantiationService.invokeFunction(accessor => {
const services = getReactServices(accessor)
const props: QuickEditPropsType = {
quickEditId: this.quickEditId++,
}
mountCtrlK(domNode, services, props)
})
// disposeInThisEditorFns.push(() => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) })
})
}
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === zone.uri.fsPath)
for (const editor of editors) {
addZoneToEditor(editor)
}
}
}
registerSingleton(IQuickEditService, VoidQuickEditService, InstantiationType.Eager);
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
@ -12,17 +114,25 @@ registerAction2(class extends Action2 {
super({ id: VOID_CTRL_K_ACTION_ID, title: 'Void: Quick Edit', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, weight: KeybindingWeight.BuiltinExtension } });
}
async run(accessor: ServicesAccessor): Promise<void> {
console.log('hello111!')
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
if (!model)
return
console.log('hello!')
const quickEditService = accessor.get(IQuickEditService)
const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('User Action', { type: 'Ctrl+K' })
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = editor.getSelection()
if (!selection) return;
const uri = model.uri
const startLine = selection.startLineNumber
const selectedText = model.getValueInRange(selection)
quickEditService.addZone({ uri, startLine, selectedText, })
console.log('bye!')
}
});

View file

@ -0,0 +1,82 @@
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { QuickEdit } from './quickEditActions.js';
// service that manages state
export type VoidQuickEditState = {
quickEditsOfDocument: { [uri: string]: QuickEdit }
}
export interface IQuickEditStateService {
readonly _serviceBrand: undefined;
readonly state: VoidQuickEditState; // readonly to the user
setState(newState: Partial<VoidQuickEditState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
_serviceBrand: undefined;
static readonly ID = 'voidQuickEditStateService';
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: VoidQuickEditState
constructor(
// @IViewsService private readonly _viewsService: IViewsService,
) {
super()
// initial state
this.state = { quickEditsOfDocument: {} }
}
setState(newState: Partial<VoidQuickEditState>) {
// make sure view is open if the tab changes
// if ('currentTab' in newState) {
// this.addQuickEdit()
// }
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
// addQuickEdit() {
// this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
// this._viewsService.openView(VOID_VIEW_ID);
// }
}
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);

View file

@ -0,0 +1,18 @@
import { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const CtrlK = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<ErrorBoundary>
<CtrlKChat {...props} />
</ErrorBoundary>
</div>
}

View file

@ -0,0 +1,83 @@
import React, { FormEvent, useCallback, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useService } from '../util/services.js';
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js';
import { VoidInputBox } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
export const CtrlKChat = (props: QuickEditPropsType) => {
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
// -- imported state --
// const threadsStateService = useService('service')
// const sidebarState = useSidebarState()
const quickEditState = useQuickEditState()
// -- local state --
// state of chat
const [messageStream, setMessageStream] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const latestRequestIdRef = useRef<string | null>(null)
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
const isDisabled = !instructions.trim()
const onSubmit = useCallback((e: FormEvent) => {
// TODO
}, [])
return <form
className={
// copied from SidebarChat.tsx
`flex flex-col gap-2 p-1 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)
}
}}
onSubmit={(e) => {
console.log('submit!')
onSubmit(e)
}}
onClick={(e) => {
if (e.currentTarget === e.target) {
inputBoxRef.current?.focus()
}
}}
>
<div
className={
// copied from SidebarChat.tsx
`@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-max-h-[100px] @@[&_div.monaco-inputbox]:!void- @@[&_div.monaco-inputbox]:!void-outline-none`
}
>
{/* text input */}
<VoidInputBox
placeholder={`${getCmdKey()}+K to select`}
onChangeText={onChangeText}
inputBoxRef={inputBoxRef}
multiline={true}
/>
</div>
</form>
}

View file

@ -0,0 +1,8 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { CtrlK } from './CtrlK.js'
export const mountCtrlK = mountFnGenerator(CtrlK)

View file

@ -169,7 +169,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "link") {
return (
<a href={t.href} title={t.title ?? undefined}>
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)

View file

@ -3,12 +3,10 @@
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js';
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
import { userInstructionsStr } from '../../../prompt/stringifySelections.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js';
import { BlockCode } from '../markdown/BlockCode.js';
@ -23,6 +21,7 @@ import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from '../util/inputs.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { ctrlLSystem, generateCtrlLPrompt } from '../../../prompt/prompts.js';
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
@ -85,6 +84,33 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string
);
};
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
return <button
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
${className}
`}
type='submit'
{...props}
>
<IconArrowUp size={20} className="stroke-[2]" />
</button>
}
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center
${className}
`}
type='button'
{...props}
>
<IconSquare size={16} className="stroke-[2]" />
</button>
}
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
@ -277,6 +303,8 @@ export const SidebarChat = () => {
const threadsState = useThreadsState()
const threadsStateService = useService('threadsStateService')
const llmMessageService = useService('llmMessageService')
// ----- SIDEBAR CHAT state (local) -----
// state of chat
@ -286,7 +314,6 @@ export const SidebarChat = () => {
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
const llmMessageService = useService('llmMessageService')
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
@ -325,11 +352,11 @@ export const SidebarChat = () => {
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
const systemPromptElt: ChatMessage = { role: 'system', content: ctrlLSystem }
threadsStateService.addMessageToCurrentThread(systemPromptElt)
// add user's message to chat history
const userHistoryElt: ChatMessage = { role: 'user', content: userInstructionsStr(instructions, selections), displayContent: instructions, selections: selections }
const userHistoryElt: ChatMessage = { role: 'user', content: generateCtrlLPrompt(instructions, selections), displayContent: instructions, selections: selections }
threadsStateService.addMessageToCurrentThread(userHistoryElt)
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
@ -474,16 +501,16 @@ export const SidebarChat = () => {
{/* middle row */}
<div
className={
// // overwrite vscode styles (generated with this code):
// // hack to 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
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
// .join(' ') +
// ` outline-none`
// .split(' ')
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`) // apply styles to ancestor input and textarea elements
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
// .join(' ');
`@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px]@@[&_div.monaco-inputbox]:!void- @@[&_div.monaco-inputbox]:!void-outline-none`
`@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_textarea]:!void-min-h-[81px] @@[&_textarea]:!void-max-h-[500px] @@[&_div.monaco-inputbox]:!void-outline-none`
}
>
@ -508,27 +535,14 @@ export const SidebarChat = () => {
{/* submit / stop button */}
{isLoading ?
// stop button
<button
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center`}
<ButtonStop
onClick={onAbort}
type='button'
>
<IconSquare size={16} className="stroke-[2]" />
</button>
/>
:
// submit button (up arrow)
<button
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
${isDisabled ?
'bg-vscode-disabled-fg' // cursor-not-allowed
: 'bg-white' // cursor-pointer
}
`}
<ButtonSubmit
disabled={isDisabled}
type='submit'
>
<IconArrowUp size={20} className="stroke-[2]" />
</button>
/>
}
</div>

View file

@ -8,14 +8,18 @@
@tailwind utilities;
@layer components {
.select-ellipsis select {
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 24px;
}
.select-child-restyle select {
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 24px;
}
* {
outline: none !important;
}
/* html {
font-size: var(--vscode-font-size);

View file

@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef } from 'react';
import { useService } from '../util/services.js';
import { useIsDark, useService } from '../util/services.js';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
@ -48,7 +49,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
}) => {
const contextViewProvider = useService('contextViewService');
return <WidgetComponent
ctor={InputBox}
propsFn={useCallback((container) => [
@ -93,6 +93,96 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
export const VoidSwitch = ({
value,
onChange,
size = 'md',
label,
disabled = false,
}: {
value: boolean;
onChange: (value: boolean) => void;
label?: string;
disabled?: boolean;
size?: 'xs' | 'sm' | 'sm+' | 'md';
}) => {
return (
<label className="inline-flex items-center cursor-pointer">
<div
onClick={() => !disabled && onChange(!value)}
className={`
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
${disabled ? 'opacity-25' : ''}
${size === 'xs' ? 'h-4 w-7' : ''}
${size === 'sm' ? 'h-5 w-9' : ''}
${size === 'sm+' ? 'h-5 w-10' : ''}
${size === 'md' ? 'h-6 w-11' : ''}
`}
>
<span
className={`
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
${size === 'sm' ? 'h-3 w-3' : ''}
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
${size === 'md' ? 'h-4 w-4' : ''}
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
${size === 'md' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
`}
/>
</div>
{label && (
<span className={`
ml-3 font-medium text-gray-900 dark:text-gray-100
${size === 'xs' ? 'text-xs' : 'text-sm'}
`}>
{label}
</span>
)}
</label>
);
};
export const VoidCheckBox = ({ label, value, onClick, className }: { label: string, value: boolean, onClick: (checked: boolean) => void, className?: string }) => {
const divRef = useRef<HTMLDivElement | null>(null)
const instanceRef = useRef<Checkbox | null>(null)
useEffect(() => {
if (!instanceRef.current) return
instanceRef.current.checked = value
}, [value])
return <WidgetComponent
className={className ?? ''}
ctor={Checkbox}
propsFn={useCallback((container: HTMLDivElement) => {
divRef.current = container
return [label, value, defaultCheckboxStyles] as const
}, [label, value])}
onCreateInstance={useCallback((instance: Checkbox) => {
instanceRef.current = instance;
divRef.current?.append(instance.domNode)
const d = instance.onChange(() => onClick(instance.checked))
return [d]
}, [onClick])}
dispose={useCallback((instance: Checkbox) => {
instance.dispose()
instance.domNode.remove()
}, [])}
/>
}
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
@ -104,7 +194,7 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='@@select-ellipsis'
className='@@select-child-restyle'
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container

View file

@ -8,8 +8,7 @@ import * as ReactDOM from 'react-dom/client'
import { _registerServices } from './services.js';
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js';
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType) => {
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, services: ReactServicesType, props?: any) => {
if (typeof document === 'undefined') {
console.error('index.tsx error: document was undefined')
return
@ -19,7 +18,7 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) =>
const root = ReactDOM.createRoot(rootElement)
root.render(<Component />); // tailwind dark theme indicator
root.render(<Component {...props} />); // tailwind dark theme indicator
return disposables
}

View file

@ -5,13 +5,14 @@
import { useState, useEffect } from 'react'
import { ThreadsState } from '../../../threadHistoryService.js'
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { ReactServicesType } from '../../../helpers/reactServicesHelper.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { RefreshableProviderName, RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@ -20,6 +21,9 @@ let services: ReactServicesType
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
@ -51,7 +55,15 @@ export const _registerServices = (services_: ReactServicesType) => {
wasCalled = true
services = services_
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = services
quickEditState = quickEditStateService.state
disposables.push(
quickEditStateService.onDidChangeState(() => {
quickEditState = quickEditStateService.state
quickEditStateListeners.forEach(l => l(quickEditState))
})
)
sidebarState = sidebarStateService.state
disposables.push(
@ -108,6 +120,16 @@ export const useService = <T extends keyof ReactServicesType,>(serviceName: T):
// -- state of services --
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
ss(quickEditState)
quickEditStateListeners.add(ss)
return () => { quickEditStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {

View file

@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidInputBox, VoidSelectBox } from '../util/inputs.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
import { RefreshableProviderName, refreshableProviderNames } from '../../../../../../../platform/void/common/refreshModelService.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
@ -31,12 +31,14 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
const { state } = refreshModelState[providerName]
const isRefreshing = state === 'refreshing'
const providerTitle = titleOfProviderName(providerName)
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <div className='flex items-center py-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-200/10'>
<button className='flex items-center' disabled={isRefreshing || justFinished} onClick={() => { refreshModelService.refreshModels(providerName) }}>
{isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
</button>
<span className='opacity-50'>Refresh Default Models for {providerTitle}.</span>
<span className='opacity-50'>{
justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`
}</span>
</div>
}
@ -66,10 +68,11 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const [errorString, setErrorString] = useState('')
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: titleOfProviderName(providerName), value: providerName })), [providerNames])
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
return <>
<div className='flex justify-center items-center gap-4'>
<div className='flex items-center gap-4'>
{/* model */}
<div className='max-w-40 w-full'>
<VoidInputBox
@ -89,8 +92,9 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
</div>
{/* button */}
<div className='max-w-40 w-full'>
<div className='max-w-40'>
<button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => {
const providerName = providerNameRef.current
const modelName = modelNameRef.current
@ -114,11 +118,14 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
}}>Add model</button>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
</div>
{!errorString ? null : <div className='text-center text-red-500'>
{errorString}
</div>}
</>
}
@ -126,10 +133,13 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const AddModelMenuFull = () => {
const [open, setOpen] = useState(false)
return <div className='my-2 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
return <div className='hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button className='' onClick={() => setOpen(true)}>Add Model</button>
: <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => setOpen(true)}
>Add Model</button>
}
</div>
}
@ -148,24 +158,34 @@ export const ModelDump = () => {
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings.enabled })))
}
// sort by hidden
modelDump.sort((a, b) => {
return Number(b.providerEnabled) - Number(a.providerEnabled)
})
return <div className=''>
{modelDump.map(m => {
const { isHidden, isDefault, modelName, providerName, providerEnabled } = m
const disabled = !providerEnabled
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'>
<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 ? '🌑' // provider disabled
: isHidden ? '❌' // model is disabled
: '✅'}
</button>
<div className='w-5 flex items-center justify-center'>
<VoidSwitch
value={disabled ? false : !isHidden}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
/>
<div className={`w-5 flex items-center justify-center`}>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
@ -180,16 +200,18 @@ export const ModelDump = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
const { title, placeholder, } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
const { title: providerTitle, } = displayInfoOfProviderName(providerName)
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const voidSettingsService = useService('settingsStateService')
let weChangedTextRef = false
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
placeholder={`Enter your ${title} here (${placeholder}).`}
placeholder={`Enter your ${providerTitle} ${settingTitle} (${placeholder}).`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@ -211,9 +233,12 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}, [voidSettingsService, providerName, settingName])}
multiline={false}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
<ChatMarkdownRender string={subTextMd} />
</div>}
</div>
</ErrorBoundary>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
@ -223,16 +248,31 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
const { enabled } = voidSettingsState.settingsOfProvider[providerName]
const settingNames = customSettingNamesOfProvider(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>
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <div className='my-4'>
<div className='flex items-center w-full gap-4'>
<h3 className='text-xl truncate'>{providerTitle}</h3>
{/* enable provider switch */}
<VoidSwitch
value={!!enabled}
onChange={
useCallback(() => {
const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
}, [voidSettingsService, providerName])}
size='sm+'
/>
</div>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
<div className='px-0'>
{/* settings besides models (e.g. api key) */}
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</div>
</div>
}
@ -254,10 +294,12 @@ export const VoidFeatureFlagSettings = () => {
const value = voidSettingsState.featureFlagSettings[flagName]
const { description } = displayInfoOfFeatureFlag(flagName)
return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
<div className='flex items-center gap-4'>
<button onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}>
{value ? '✅' : '❌'}
</button>
<div className='flex items-center'>
<VoidCheckBox
label=''
value={value}
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
/>
<h4 className='text-sm'>{description}</h4>
</div>
</div>
@ -287,10 +329,10 @@ export const Settings = () => {
{/* 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-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('features') }}
>Features</button>
</div>
@ -303,18 +345,17 @@ export const Settings = () => {
<div className='w-full overflow-y-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Models</h2>
<h2 className={`text-3xl mb-2`}>Providers</h2>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-4`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelMenuFull />
<RefreshableModels />
</ErrorBoundary>
<h2 className={`text-3xl mt-4 mb-2`}>Providers</h2>
<div className='px-3'>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>

View file

@ -9,6 +9,7 @@ export default defineConfig({
entry: [
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/ctrl-k-tsx/index.tsx',
'./src2/diff/index.tsx',
],
outDir: './out',

View file

@ -84,7 +84,7 @@ class SidebarViewPane extends ViewPane {
});
}
override layoutBody(height: number, width: number): void {
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width)
this.element.style.height = `${height}px`
this.element.style.width = `${width}px`