Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64760aae13 | |||
| d0d1124543 | |||
| 90a3fd0a21 | |||
| 11ed2545c7 | |||
| 3d1df92f6a | |||
| 4ca16caa4e | |||
| 06b5cca59a | |||
| e3c292eee7 | |||
| 99c3786d06 | |||
| 6e40a92315 | |||
| a9b2ff631d | |||
| ef8bba00b9 | |||
| ff7830e704 | |||
| 01a345a391 | |||
| 33909b3143 | |||
| 9a9d0da91e | |||
| 1df044fb02 | |||
| dfbd729fa4 | |||
| ef9d0daef4 | |||
| ffe5a41b01 | |||
| 372beb7a2d | |||
| 406f29bb5a | |||
| ee8b98ff74 | |||
| ffb0549c7c | |||
| 8f9c2f0630 | |||
| 84bfc26dce | |||
| 80a87ff3c6 | |||
| 806ec939e5 | |||
| 98a22d2eb6 | |||
| ffa31bc289 | |||
| a1ab8a5796 | |||
| 70c7d0261c | |||
| e7af8d9448 | |||
| e1feaba74a | |||
| 8456f979ba | |||
| 6630d596f6 | |||
| a435a59a2e | |||
| 9cf4980004 | |||
| 6298be394d | |||
| e647ce0054 | |||
| 5fb30fed87 | |||
| 7cc57bf086 | |||
| 3fb6a4890d | |||
| 97fd332c4e | |||
| 81282e16b3 | |||
| 382275fa9f | |||
| 91f8321278 | |||
| 9b1f21d7c1 | |||
| 739ac2562b | |||
| 298423928e | |||
| 702d11ed3e | |||
| 4513b732d6 | |||
| 78e72f0c80 | |||
| f457f006fb | |||
| d6074e7ab5 | |||
| 3669401819 | |||
| ee7d0ed836 | |||
| ba87b4f0de | |||
| e4bc21ffb1 | |||
| 81b78b7765 | |||
| c3c53b5e81 | |||
| f49a6f8e41 | |||
| 5a26aa8746 | |||
| 8661fe1417 | |||
| 2a519157da | |||
| 610b52587b |
@@ -21,3 +21,4 @@ vscode.db
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp/
|
||||
|
||||
+4
-5
@@ -46,7 +46,7 @@ First, run `npm install -g node-gyp`. Then:
|
||||
To build Void, open `void/` inside VSCode. Then:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run buildreact` to build Void's browser dependencies like React.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React.
|
||||
3. Build.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
@@ -54,7 +54,7 @@ To build Void, open `void/` inside VSCode. Then:
|
||||
4. Run.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building, unless they're React changes.
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
|
||||
|
||||
|
||||
#### Building Void from Terminal
|
||||
@@ -74,16 +74,15 @@ 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.
|
||||
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to 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.
|
||||
|
||||
|
||||
|
||||
|
||||
## Bundling
|
||||
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
|
||||
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
|
||||
Generated
+4162
-3844
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
|
||||
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
|
||||
"test": "echo Please run any of the test scripts from the scripts folder.",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
|
||||
@@ -102,6 +103,7 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"ajv": "^8.17.1",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
@@ -218,6 +220,7 @@
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
|
||||
+1
-50
@@ -35,54 +35,5 @@
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
"version": "1.1.3",
|
||||
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
|
||||
"metadata": {
|
||||
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.js-debug",
|
||||
"version": "1.93.0",
|
||||
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug",
|
||||
"metadata": {
|
||||
"id": "25629058-ddac-4e17-abba-74678e126c5d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.vscode-js-profile-table",
|
||||
"version": "1.0.9",
|
||||
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
|
||||
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
|
||||
"metadata": {
|
||||
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
}
|
||||
]
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
// THIS FILE IS OLD!!!
|
||||
export interface IInlineDiffService {
|
||||
readonly _serviceBrand: undefined;
|
||||
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorCore,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceOllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams, MainOllamaListParams } from './llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
@@ -21,7 +21,8 @@ export interface ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceOllamaListParams) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
}
|
||||
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
@@ -36,9 +37,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventOllamaListOnSuccessParams) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventOllamaListOnErrorParams) => void) } = {}
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
|
||||
|
||||
// openAICompatibleList
|
||||
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@@ -65,12 +69,19 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventOllamaListOnSuccessParams>)(e => {
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventOllamaListOnErrorParams>)(e => {
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onError_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
@@ -113,7 +124,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
}
|
||||
|
||||
|
||||
ollamaList = (params: ServiceOllamaListParams) => {
|
||||
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
@@ -127,7 +138,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainOllamaListParams)
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_openAICompatible[requestId_] = onSuccess
|
||||
this.onError_openAICompatible[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,14 +17,14 @@ export type LLMMessage = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type LLMFeatureSelection = {
|
||||
featureName: 'Ctrl+K',
|
||||
range: IRange
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
featureName: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
featureName: 'Ctrl+L',
|
||||
featureName: 'Ctrl+L';
|
||||
} | {
|
||||
featureName: 'Autocomplete',
|
||||
range: IRange
|
||||
featureName: 'Autocomplete';
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// params to the true sendLLMMessage function
|
||||
@@ -54,7 +54,7 @@ export type ServiceSendLLMMessageParams = {
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
} & LLMFeatureSelection
|
||||
} & ServiceSendLLMFeatureParams
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
|
||||
@@ -97,7 +97,7 @@ export type _InternalSendLLMMessageFnType = (params: {
|
||||
|
||||
|
||||
// These are from 'ollama' SDK
|
||||
interface ModelDetails {
|
||||
interface OllamaModelDetails {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
@@ -106,35 +106,44 @@ interface ModelDetails {
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
export type ModelResponse = {
|
||||
export type OllamaModelResponse = {
|
||||
name: string;
|
||||
modified_at: Date;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: ModelDetails;
|
||||
details: OllamaModelDetails;
|
||||
expires_at: Date;
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type OllamaListParams = {
|
||||
export type ModelListParams<modelResponse> = {
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
export type ServiceOllamaListParams = {
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
// params to the service
|
||||
export type ServiceModelListParams<modelResponse> = {
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainOllamaListParams = 'onSuccess' | 'onError'
|
||||
export type MainOllamaListParams = Omit<OllamaListParams, BlockedMainOllamaListParams> & { requestId: string }
|
||||
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
|
||||
|
||||
export type EventOllamaListOnSuccessParams = Parameters<OllamaListParams['onSuccess']>[0] & { requestId: string }
|
||||
export type EventOllamaListOnErrorParams = Parameters<OllamaListParams['onError']>[0] & { requestId: string }
|
||||
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
||||
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||
|
||||
|
||||
|
||||
export type _InternalOllamaListFnType = (params: OllamaListParams) => void
|
||||
|
||||
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
|
||||
|
||||
@@ -8,10 +8,35 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
|
||||
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';
|
||||
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
|
||||
|
||||
export type RefreshModelState = 'done' | 'loading'
|
||||
|
||||
|
||||
type RefreshableState = ({
|
||||
state: 'init',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'refreshing',
|
||||
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
|
||||
} | {
|
||||
state: 'success',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
@@ -23,9 +48,9 @@ function eq<T>(a: T[], b: T[]): boolean {
|
||||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
refreshOllamaModels(): void;
|
||||
onDidChangeState: Event<void>;
|
||||
state: RefreshModelState;
|
||||
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
|
||||
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
|
||||
@@ -34,8 +59,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
|
||||
readonly _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
|
||||
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
|
||||
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@@ -43,62 +69,122 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
) {
|
||||
super()
|
||||
|
||||
// on mount, refresh ollama models
|
||||
this.refreshOllamaModels()
|
||||
|
||||
// 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.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
this.refreshOllamaModels()
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializePollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: 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])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
initializePollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelState = 'done'
|
||||
|
||||
private _timeoutId: NodeJS.Timeout | null = null
|
||||
private _cancelTimeout = () => {
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId)
|
||||
this._timeoutId = null
|
||||
}
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
openAICompatible: { state: 'init', timeoutId: null },
|
||||
}
|
||||
async refreshOllamaModels() {
|
||||
// cancel any existing poll
|
||||
this._cancelTimeout()
|
||||
|
||||
// if ollama is disabled, obivously done
|
||||
if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) {
|
||||
this._setState('done')
|
||||
return
|
||||
}
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
// start loading models
|
||||
this._setState('loading')
|
||||
this._setRefreshState(providerName, 'refreshing')
|
||||
|
||||
this.llmMessageService.ollamaList({
|
||||
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
||||
fn({
|
||||
onSuccess: ({ models }) => {
|
||||
this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name))
|
||||
this._setState('done')
|
||||
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName)
|
||||
}))
|
||||
|
||||
if (enableProviderOnSuccess) {
|
||||
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
this._setRefreshState(providerName, 'success')
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
// poll
|
||||
console.log('retrying ollamaList:', error)
|
||||
this._timeoutId = setTimeout(() => this.refreshOllamaModels(), 5000)
|
||||
console.log('retrying list models:', providerName, error)
|
||||
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private _setState(state: RefreshModelState) {
|
||||
this.state = state
|
||||
this._onDidChangeState.fire()
|
||||
_clearAllTimeouts() {
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
_clearProviderTimeout(providerName: RefreshableProviderName) {
|
||||
// cancel any existing poll
|
||||
if (this.state[providerName].timeoutId) {
|
||||
clearTimeout(this.state[providerName].timeoutId)
|
||||
this._setTimeoutId(providerName, null)
|
||||
}
|
||||
}
|
||||
|
||||
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
|
||||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ 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';
|
||||
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,
|
||||
@@ -21,13 +21,13 @@ type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeature = <K extends FeatureName>(
|
||||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
|
||||
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
|
||||
|
||||
export type ModelOption = { text: string, value: ModelSelection }
|
||||
|
||||
@@ -36,18 +36,24 @@ export type ModelOption = { text: string, value: ModelSelection }
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly featureFlagSettings: FeatureFlagSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState;
|
||||
onDidChangeState: Event<void>;
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setFeatureFlag: SetFeatureFlagFn;
|
||||
|
||||
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
@@ -60,7 +66,7 @@ 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
|
||||
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 } })
|
||||
@@ -74,6 +80,7 @@ const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
@@ -84,10 +91,11 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
|
||||
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
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@@ -100,10 +108,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
this._onDidChangeState.fire()
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -136,13 +148,16 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
const newFeatureFlags = this.state.featureFlagSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === 'enabled'
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
featureFlagSettings: newFeatureFlags,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
@@ -166,11 +181,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => {
|
||||
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
|
||||
const newState = {
|
||||
...this.state,
|
||||
featureFlagSettings: {
|
||||
...this.state.featureFlagSettings,
|
||||
[flagName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('featureFlagSettings')
|
||||
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
@@ -185,7 +215,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
}
|
||||
|
||||
|
||||
@@ -203,7 +233,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newModels: ModelInfo[] = [
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
|
||||
|
||||
|
||||
export type ModelInfo = {
|
||||
export type VoidModelInfo = {
|
||||
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[] => {
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => {
|
||||
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 }))
|
||||
}
|
||||
@@ -96,7 +96,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const customProviderSettingsDefaults = {
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
@@ -110,8 +110,8 @@ export const customProviderSettingsDefaults = {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
apiKey: '',
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
@@ -121,22 +121,27 @@ export const customProviderSettingsDefaults = {
|
||||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof customProviderSettingsDefaults
|
||||
export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[]
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof customProviderSettingsDefaults[ProviderName]>
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[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,
|
||||
models: ModelInfo[], // if null, user can type in any string as a model
|
||||
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
@@ -148,21 +153,49 @@ 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'
|
||||
|
||||
|
||||
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',
|
||||
|
||||
}
|
||||
}
|
||||
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}"`)
|
||||
}
|
||||
@@ -170,6 +203,7 @@ export const titleOfProviderName = (providerName: ProviderName) => {
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
@@ -182,19 +216,31 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'(never)',
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
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' ? customProviderSettingsDefaults.ollama.endpoint
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
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') {
|
||||
else if (settingName === '_enabled') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
@@ -247,46 +293,46 @@ export const voidInitModelOptions = {
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.anthropic,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
enabled: false,
|
||||
},
|
||||
openAI: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAI,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
enabled: false,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.gemini,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
enabled: false,
|
||||
_enabled: undefined,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.groq,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
enabled: false,
|
||||
_enabled: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.ollama,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
enabled: false,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openRouter,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
enabled: false,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...customProviderSettingsDefaults.openAICompatible,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
enabled: false,
|
||||
_enabled: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -306,3 +352,43 @@ export type ModelSelectionOfFeature = {
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
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;
|
||||
}
|
||||
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
|
||||
autoRefreshModels: true,
|
||||
}
|
||||
|
||||
export type FeatureFlagName = keyof FeatureFlagSettings
|
||||
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
|
||||
|
||||
type FeatureFlagDisplayInfo = {
|
||||
description: string,
|
||||
}
|
||||
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
|
||||
if (featureFlag === 'autoRefreshModels') {
|
||||
return {
|
||||
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
|
||||
}
|
||||
}
|
||||
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: ModelResponse[] }) => {
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
@@ -16,9 +17,11 @@ export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSucce
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
|
||||
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) => {
|
||||
@@ -39,6 +42,8 @@ export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSucce
|
||||
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 = ''
|
||||
|
||||
|
||||
@@ -4,10 +4,46 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
@@ -43,6 +79,7 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText,
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
|
||||
openai.models.list()
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainOllamaListParams, OllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams } from '../common/llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
import { openaiCompatibleList } from './llmMessage/openai.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
@@ -25,8 +26,12 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventOllamaListOnSuccessParams>();
|
||||
private readonly _onError_ollama = new Emitter<EventOllamaListOnErrorParams>();
|
||||
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
|
||||
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
|
||||
|
||||
// openaiCompatibleList
|
||||
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
|
||||
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
@@ -50,6 +55,12 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else if (event === 'onSuccess_openAICompatible') {
|
||||
return this._onSuccess_openAICompatible.event;
|
||||
}
|
||||
else if (event === 'onError_openAICompatible') {
|
||||
return this._onError_openAICompatible.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
@@ -67,6 +78,9 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
@@ -100,10 +114,10 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainOllamaListParams) {
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: OllamaListParams = {
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
@@ -111,5 +125,16 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
ollamaList(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,82 +4,113 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
|
||||
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { append, clearNode, $, h } from '../../../../base/browser/dom.js';
|
||||
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
|
||||
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
|
||||
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
|
||||
import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
|
||||
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
|
||||
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
|
||||
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
|
||||
import { splitRecentLabel } from '../../../../base/common/labels.js';
|
||||
import { IHostService } from '../../../services/host/browser/host.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
|
||||
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
|
||||
|
||||
interface WatermarkEntry {
|
||||
readonly text: string;
|
||||
readonly id: string;
|
||||
readonly mac?: boolean;
|
||||
readonly when?: ContextKeyExpression;
|
||||
}
|
||||
// interface WatermarkEntry {
|
||||
// readonly text: string;
|
||||
// readonly id: string;
|
||||
// readonly mac?: boolean;
|
||||
// readonly when?: ContextKeyExpression;
|
||||
// }
|
||||
|
||||
const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
|
||||
// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
|
||||
// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
|
||||
// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
|
||||
// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
|
||||
// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
|
||||
// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
|
||||
// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
|
||||
// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
|
||||
// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
|
||||
// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
|
||||
|
||||
const noFolderEntries = [
|
||||
showCommands,
|
||||
openFileNonMacOnly,
|
||||
openFolderNonMacOnly,
|
||||
openFileOrFolderMacOnly,
|
||||
openRecent,
|
||||
newUntitledFileMacOnly
|
||||
];
|
||||
// // shown when Void is emtpty
|
||||
// const noFolderEntries = [
|
||||
// // showCommands,
|
||||
// openFileNonMacOnly,
|
||||
// openFolderNonMacOnly,
|
||||
// openFileOrFolderMacOnly,
|
||||
// openRecent,
|
||||
// // newUntitledFileMacOnly
|
||||
// ];
|
||||
|
||||
const folderEntries = [
|
||||
showCommands,
|
||||
quickAccess,
|
||||
findInFiles,
|
||||
startDebugging,
|
||||
toggleTerminal,
|
||||
toggleFullscreen,
|
||||
showSettings
|
||||
];
|
||||
// const folderEntries = [
|
||||
// showCommands,
|
||||
// // quickAccess,
|
||||
// // findInFiles,
|
||||
// // startDebugging,
|
||||
// // toggleTerminal,
|
||||
// // toggleFullscreen,
|
||||
// // showSettings
|
||||
// ];
|
||||
|
||||
export class EditorGroupWatermark extends Disposable {
|
||||
private readonly shortcuts: HTMLElement;
|
||||
private readonly transientDisposables = this._register(new DisposableStore());
|
||||
private enabled: boolean = false;
|
||||
// private enabled: boolean = false;
|
||||
private workbenchState: WorkbenchState;
|
||||
private keybindingLabels = new Set<KeybindingLabel>();
|
||||
private currentDisposables = new Set<IDisposable>();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
// @IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const elements = h('.editor-group-watermark', [
|
||||
h('.letterpress'),
|
||||
h('.letterpress@icon'),
|
||||
h('.shortcuts@shortcuts'),
|
||||
]);
|
||||
|
||||
append(container, elements.root);
|
||||
this.shortcuts = elements.shortcuts;
|
||||
this.shortcuts = elements.shortcuts; // shortcuts div is modified on render()
|
||||
|
||||
// void icon style
|
||||
const updateTheme = () => {
|
||||
const theme = this.themeService.getColorTheme().type
|
||||
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
|
||||
elements.icon.style.maxWidth = '220px'
|
||||
elements.icon.style.opacity = '50%'
|
||||
elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
|
||||
}
|
||||
updateTheme()
|
||||
this._register(
|
||||
this.themeService.onDidColorThemeChange(updateTheme)
|
||||
)
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
@@ -103,56 +134,162 @@ export class EditorGroupWatermark extends Disposable {
|
||||
this.render();
|
||||
}));
|
||||
|
||||
const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
const allKeys = new Set<string>();
|
||||
allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
if (e.affectsSome(allKeys)) {
|
||||
this.render();
|
||||
}
|
||||
}));
|
||||
// const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
|
||||
// const allKeys = new Set<string>();
|
||||
// allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
|
||||
// this._register(this.contextKeyService.onDidChangeContext(e => {
|
||||
// if (e.affectsSome(allKeys)) {
|
||||
// this.render();
|
||||
// }
|
||||
// }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private render(): void {
|
||||
const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
|
||||
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
// if (enabled === this.enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.enabled = enabled;
|
||||
|
||||
|
||||
// if (!enabled) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
// const selected = (hasFolder ? folderEntries : noFolderEntries)
|
||||
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
|
||||
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
|
||||
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
|
||||
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
|
||||
this.enabled = enabled;
|
||||
this.clear();
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = append(this.shortcuts, $('.watermark-box'));
|
||||
const folder = this.workbenchState !== WorkbenchState.EMPTY;
|
||||
const selected = (folder ? folderEntries : noFolderEntries)
|
||||
.filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
|
||||
.filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
|
||||
.filter(entry => !!CommandsRegistry.getCommand(entry.id))
|
||||
.filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
|
||||
const boxBelow = append(this.shortcuts, $(''))
|
||||
|
||||
|
||||
const update = async () => {
|
||||
|
||||
const update = () => {
|
||||
clearNode(box);
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.keybindingLabels.clear();
|
||||
clearNode(boxBelow);
|
||||
|
||||
for (const entry of selected) {
|
||||
const keys = this.keybindingService.lookupKeybinding(entry.id);
|
||||
if (!keys) {
|
||||
continue;
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
this.currentDisposables.clear();
|
||||
|
||||
|
||||
// Void - if the workbench is empty, show open
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
|
||||
// Open Folder
|
||||
const button = h('button')
|
||||
button.root.textContent = 'Open Folder'
|
||||
button.root.onclick = () => {
|
||||
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
|
||||
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
|
||||
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
|
||||
// } else {
|
||||
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
|
||||
// }
|
||||
}
|
||||
box.appendChild(button.root);
|
||||
|
||||
// Recents
|
||||
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
|
||||
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
|
||||
|
||||
|
||||
|
||||
box.append(
|
||||
...recentlyOpened.map(w => {
|
||||
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
if (isRecentFolder(w)) {
|
||||
windowOpenable = { folderUri: w.folderUri };
|
||||
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
|
||||
}
|
||||
else {
|
||||
return null
|
||||
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
|
||||
// windowOpenable = { workspaceUri: w.workspace.configPath };
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { name, parentPath } = splitRecentLabel(fullPath);
|
||||
|
||||
const li = $('li');
|
||||
const link = $('button.button-link');
|
||||
|
||||
link.innerText = name;
|
||||
link.title = fullPath;
|
||||
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
|
||||
link.addEventListener('click', e => {
|
||||
this.hostService.openWindow([windowOpenable], {
|
||||
forceNewWindow: e.ctrlKey || e.metaKey,
|
||||
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
li.appendChild(link);
|
||||
|
||||
const span = $('span');
|
||||
span.classList.add('path');
|
||||
span.classList.add('detail');
|
||||
span.innerText = parentPath;
|
||||
span.title = fullPath;
|
||||
li.appendChild(span);
|
||||
|
||||
|
||||
return li
|
||||
}).filter(v => !!v)
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
// show them Void keybindings
|
||||
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
|
||||
const dl = append(box, $('dl'));
|
||||
const dt = append(dl, $('dt'));
|
||||
dt.textContent = entry.text;
|
||||
dt.textContent = 'Chat'
|
||||
const dd = append(dl, $('dd'));
|
||||
const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
label.set(keys);
|
||||
this.keybindingLabels.add(label);
|
||||
if (keys)
|
||||
label.set(keys);
|
||||
this.currentDisposables.add(label);
|
||||
|
||||
|
||||
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
|
||||
const dl2 = append(box, $('dl'));
|
||||
const dt2 = append(dl2, $('dt'));
|
||||
dt2.textContent = 'Quick Edit'
|
||||
const dd2 = append(dl2, $('dd'));
|
||||
const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys2)
|
||||
label2.set(keys2);
|
||||
this.currentDisposables.add(label2);
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(boxBelow, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
|
||||
if (keys3)
|
||||
label3.set(keys3);
|
||||
button3.onclick = () => {
|
||||
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
this.currentDisposables.add(label3);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
update();
|
||||
@@ -167,6 +304,6 @@ export class EditorGroupWatermark extends Disposable {
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
this.clear();
|
||||
this.keybindingLabels.forEach(label => label.dispose());
|
||||
this.currentDisposables.forEach(label => label.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5; /* dimmed to indicate inactive state */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty {
|
||||
opacity: 0.5;
|
||||
/* dimmed to indicate inactive state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.active,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over {
|
||||
opacity: 1; /* indicate active/dragged-over group through undimmed state */
|
||||
opacity: 1;
|
||||
/* indicate active/dragged-over group through undimmed state */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus {
|
||||
@@ -24,12 +26,13 @@
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus {
|
||||
outline: none; /* never show outline for empty group if it is the last */
|
||||
outline: none;
|
||||
/* never show outline for empty group if it is the last */
|
||||
}
|
||||
|
||||
/* Watermark & shortcuts */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 290px;
|
||||
@@ -49,26 +52,27 @@
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
/* light */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-image: url('./letterpress-light.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
background-size: contain;
|
||||
background-position-x: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-dark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcLight.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
|
||||
background-image: url('./letterpress-hcDark.svg');
|
||||
background-image: url('./void_cube_noshadow.png');
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
|
||||
@@ -109,12 +113,13 @@
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) {
|
||||
display: flex; /* when tabs are not shown, use flex layout */
|
||||
display: flex;
|
||||
/* when tabs are not shown, use flex layout */
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
@@ -144,7 +149,8 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar,
|
||||
.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar {
|
||||
display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
display: block;
|
||||
/* show toolbar when more than one editor group or always when auxiliary or locked */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container {
|
||||
@@ -157,7 +163,7 @@
|
||||
|
||||
/* Editor */
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 850 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 795 KiB |
@@ -0,0 +1,178 @@
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
|
||||
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
|
||||
|
||||
|
||||
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
|
||||
|
||||
export interface IConsistentItemService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getEditorsOnURI(uri: URI): ICodeEditor[];
|
||||
addConsistentItemToURI(inputs: AddItemInputs): string;
|
||||
removeConsistentItemFromURI(consistentItemId: string): void;
|
||||
}
|
||||
|
||||
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
|
||||
|
||||
export class ConsistentItemService extends Disposable {
|
||||
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
// the items that are attached to each URI, completely independent from current state of editors
|
||||
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
|
||||
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
|
||||
|
||||
|
||||
// current state of items on each editor, and the fns to call to remove them
|
||||
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
|
||||
private readonly consistentItemIdOfItemId: Record<string, string> = {}
|
||||
private readonly disposeFnOfItemId: Record<string, () => void> = {}
|
||||
|
||||
|
||||
constructor(
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
const removeItemsFromEditor = (editor: ICodeEditor) => {
|
||||
const editorId = editor.getId()
|
||||
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
|
||||
// put items on the editor, based on the consistent items for that URI
|
||||
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
|
||||
if (!uri) return
|
||||
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
}
|
||||
|
||||
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(
|
||||
editor.onDidChangeModel(e => {
|
||||
removeItemsFromEditor(editor)
|
||||
putItemsOnEditor(editor, e.newModelUrl)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const addDisposeListener = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidDispose(() => {
|
||||
// anything on the editor has been disposed already
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
}))
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
addDisposeListener(editor)
|
||||
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
|
||||
}
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
|
||||
// when an editor is deleted, remove its items
|
||||
this._register(this._editorService.onCodeEditorRemove(editor => {
|
||||
removeItemsFromEditor(editor)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
|
||||
const { fn } = this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
// add item
|
||||
const dispose = fn(editor)
|
||||
|
||||
const itemId = generateUuid()
|
||||
const editorId = editor.getId()
|
||||
|
||||
if (!(editorId in this.itemIdsOfEditorId))
|
||||
this.itemIdsOfEditorId[editorId] = new Set()
|
||||
this.itemIdsOfEditorId[editorId]!.add(itemId)
|
||||
|
||||
|
||||
this.consistentItemIdOfItemId[itemId] = consistentItemId
|
||||
|
||||
this.disposeFnOfItemId[itemId] = () => {
|
||||
// console.log('calling remove for', itemId)
|
||||
dispose?.()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
|
||||
|
||||
const editorId = editor.getId()
|
||||
this.itemIdsOfEditorId[editorId]?.delete(itemId)
|
||||
|
||||
this.disposeFnOfItemId[itemId]?.()
|
||||
delete this.disposeFnOfItemId[itemId]
|
||||
|
||||
delete this.consistentItemIdOfItemId[itemId]
|
||||
}
|
||||
|
||||
getEditorsOnURI(uri: URI) {
|
||||
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
|
||||
return editors
|
||||
}
|
||||
|
||||
consistentItemIdPool = 0
|
||||
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
|
||||
const consistentItemId = (this.consistentItemIdPool++) + ''
|
||||
|
||||
if (!(uri.fsPath in this.consistentItemIdsOfURI))
|
||||
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
|
||||
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
|
||||
|
||||
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
|
||||
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
for (const editor of editors)
|
||||
this._putItemOnEditor(editor, consistentItemId)
|
||||
|
||||
return consistentItemId
|
||||
}
|
||||
|
||||
|
||||
removeConsistentItemFromURI(consistentItemId: string) {
|
||||
|
||||
if (!(consistentItemId in this.infoOfConsistentItemId))
|
||||
return
|
||||
|
||||
const { uri } = this.infoOfConsistentItemId[consistentItemId]
|
||||
const editors = this.getEditorsOnURI(uri)
|
||||
|
||||
for (const editor of editors) {
|
||||
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
|
||||
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
|
||||
this._removeItemFromEditor(editor, itemId)
|
||||
}
|
||||
}
|
||||
|
||||
// clear
|
||||
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
|
||||
delete this.infoOfConsistentItemId[consistentItemId]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,10 @@
|
||||
background-color: var(--vscode-void-sweepBG);
|
||||
}
|
||||
|
||||
.void-highlightBG {
|
||||
background-color: var(--vscode-void-highlightBG);
|
||||
}
|
||||
|
||||
.void-greenBG {
|
||||
background-color: var(--vscode-void-greenBG);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 chat_systemMessage = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
Instructions:
|
||||
1. Output the changes to make to the entire file.
|
||||
1. Do not re-write the entire file.
|
||||
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
|
||||
4. You must give enough context to apply the change in the correct location.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`math.ts\`:
|
||||
\`\`\`
|
||||
const addNumbers = (a, b) => a + b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
// existing code...
|
||||
\`\`\`
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\`
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
\`\`\`
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES\
|
||||
`
|
||||
|
||||
|
||||
|
||||
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 chat_prompt = (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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_systemMessage = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<div
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;\`\`\`
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
|
||||
return `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\`
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
${userMessage}
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const ctrlKStream_systemMessage = `\
|
||||
`
|
||||
|
||||
|
||||
export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
|
||||
|
||||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_CHARS = 1024
|
||||
/*
|
||||
|
||||
a
|
||||
a
|
||||
a <-- final i (prefix = a\na\n)
|
||||
a
|
||||
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
|
||||
c
|
||||
d| <-- endLine-1 <-- initial j (moves down)
|
||||
e
|
||||
e <-- final j (suffix = e\ne\n)
|
||||
e
|
||||
e
|
||||
*/
|
||||
|
||||
let prefix = ''
|
||||
let i = startLine - 1 // 0-indexed exclusive
|
||||
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
|
||||
while (i !== 0) {
|
||||
const newLine = fullFileLines[i - 1]
|
||||
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
prefix = `${newLine}\n${prefix}`
|
||||
i -= 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
let suffix = ''
|
||||
let j = endLine - 1
|
||||
while (j !== fullFileLines.length - 1) {
|
||||
const newLine = fullFileLines[j + 1]
|
||||
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
suffix = `${suffix}\n${newLine}`
|
||||
j += 1
|
||||
}
|
||||
else break
|
||||
}
|
||||
|
||||
return { prefix, suffix }
|
||||
|
||||
}
|
||||
|
||||
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
|
||||
const onlySpeaksFIM = false
|
||||
|
||||
if (onlySpeaksFIM) {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
<${preTag}>
|
||||
/* Original Selection:
|
||||
${selection}*/
|
||||
/* Instructions: ${userMessage}*/
|
||||
${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
// prompt the model on how to do FIM
|
||||
else {
|
||||
const preTag = 'PRE'
|
||||
const sufTag = 'SUF'
|
||||
const midTag = 'MID'
|
||||
return `\
|
||||
Here is the user's original selection:
|
||||
\`\`\`
|
||||
<${midTag}>${selection}</${midTag}>
|
||||
\`\`\`
|
||||
|
||||
The user wants to apply the following instructions to the selection:
|
||||
${userMessage}
|
||||
|
||||
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:
|
||||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// export const searchDiffChunkInstructions = `
|
||||
// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
||||
// # Example 1:
|
||||
|
||||
// FILES
|
||||
// selected file \`Sidebar.tsx\`:
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// <li key={index}>
|
||||
// <button
|
||||
// className={styles.sidebarButton}
|
||||
// onClick={() => onItemSelect?.(item.label)}
|
||||
// >
|
||||
// {item.label}
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// Extra Action
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Sidebar;
|
||||
// \`\`\`
|
||||
|
||||
// DIFF
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -<div className={styles.sidebar}>
|
||||
// -<ul>
|
||||
// - {items.map((item, index) => (
|
||||
// - <li key={index}>
|
||||
// - <button
|
||||
// - className={styles.sidebarButton}
|
||||
// - onClick={() => onItemSelect?.(item.label)}
|
||||
// - >
|
||||
// - {item.label}
|
||||
// - </button>
|
||||
// - </li>
|
||||
// - ))}
|
||||
// -</ul>
|
||||
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// - Extra Action
|
||||
// -</button>
|
||||
// -</div>
|
||||
// +<div className={styles.sidebar}>
|
||||
// +<ul>
|
||||
// + {items.map((item, index) => (
|
||||
// + <li key={index}>
|
||||
// + <div
|
||||
// + className={styles.sidebarButton}
|
||||
// + onClick={() => onItemSelect?.(item.label)}
|
||||
// + >
|
||||
// + {item.label}
|
||||
// + </div>
|
||||
// + </li>
|
||||
// + ))}
|
||||
// +</ul>
|
||||
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// + Extra Action
|
||||
// +</div>
|
||||
// +</div>
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// \`\`\`
|
||||
|
||||
// RESULT
|
||||
// The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
|
||||
|
||||
// OUTPUT
|
||||
// \`true\`
|
||||
// `
|
||||
|
||||
|
||||
|
||||
// export const generateDiffInstructions = `
|
||||
// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
// All changes made to files must be outputted in unified diff format.
|
||||
// Unified diff format instructions:
|
||||
// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
|
||||
// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
|
||||
// 3. Make diffs more than a few lines.
|
||||
// 4. Make high-level diffs rather than many one-line diffs.
|
||||
|
||||
// Here's an example of unified diff format:
|
||||
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -def factorial(n):
|
||||
// - if n == 0:
|
||||
// - return 1
|
||||
// - else:
|
||||
// - return n * factorial(n-1)
|
||||
// +def factorial(number):
|
||||
// + if number == 0:
|
||||
// + return 1
|
||||
// + else:
|
||||
// + return number * factorial(number-1)
|
||||
// \`\`\`
|
||||
|
||||
// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
|
||||
|
||||
// \`\`\`
|
||||
// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
|
||||
// -def factorial(n):
|
||||
// +def factorial(number):
|
||||
// - if n == 0:
|
||||
// + if number == 0:
|
||||
// return 1
|
||||
// else:
|
||||
// - return n * factorial(n-1)
|
||||
// + return number * factorial(number-1)
|
||||
// \`\`\`
|
||||
|
||||
// # Example 1:
|
||||
|
||||
// FILES
|
||||
// selected file \`test.ts\`:
|
||||
// \`\`\`
|
||||
// x = 1
|
||||
|
||||
// {{selection}}
|
||||
|
||||
// z = 3
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\`const y = 2\`\`\`
|
||||
|
||||
// INSTRUCTIONS
|
||||
// \`\`\`y = 3\`\`\`
|
||||
|
||||
// EXPECTED RESULT
|
||||
|
||||
// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -x = 1
|
||||
// -
|
||||
// -y = 2
|
||||
// +x = 1
|
||||
// +
|
||||
// +y = 3
|
||||
// \`\`\`
|
||||
|
||||
// # Example 2:
|
||||
|
||||
// FILES
|
||||
// selected file \`Sidebar.tsx\`:
|
||||
// \`\`\`
|
||||
// import React from 'react';
|
||||
// import styles from './Sidebar.module.css';
|
||||
|
||||
// interface SidebarProps {
|
||||
// items: { label: string; href: string }[];
|
||||
// onItemSelect?: (label: string) => void;
|
||||
// onExtraButtonClick?: () => void;
|
||||
// }
|
||||
|
||||
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
// return (
|
||||
// <div className={styles.sidebar}>
|
||||
// <ul>
|
||||
// {items.map((item, index) => (
|
||||
// <li key={index}>
|
||||
// {{selection}}
|
||||
// className={styles.sidebarButton}
|
||||
// onClick={() => onItemSelect?.(item.label)}
|
||||
// >
|
||||
// {item.label}
|
||||
// </button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// Extra Action
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default Sidebar;
|
||||
// \`\`\`
|
||||
|
||||
// SELECTION
|
||||
// \`\`\` <button\`\`\`
|
||||
|
||||
// INSTRUCTIONS
|
||||
// \`\`\`make all the buttons like this into divs\`\`\`
|
||||
|
||||
// EXPECTED OUTPUT
|
||||
|
||||
// We should change all the buttons like the one selected into a div component. Here is the change:
|
||||
// \`\`\`
|
||||
// @@ ... @@
|
||||
// -<div className={styles.sidebar}>
|
||||
// -<ul>
|
||||
// - {items.map((item, index) => (
|
||||
// - <li key={index}>
|
||||
// - <button
|
||||
// - className={styles.sidebarButton}
|
||||
// - onClick={() => onItemSelect?.(item.label)}
|
||||
// - >
|
||||
// - {item.label}
|
||||
// - </button>
|
||||
// - </li>
|
||||
// - ))}
|
||||
// -</ul>
|
||||
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// - Extra Action
|
||||
// -</button>
|
||||
// -</div>
|
||||
// +<div className={styles.sidebar}>
|
||||
// +<ul>
|
||||
// + {items.map((item, index) => (
|
||||
// + <li key={index}>
|
||||
// + <div
|
||||
// + className={styles.sidebarButton}
|
||||
// + onClick={() => onItemSelect?.(item.label)}
|
||||
// + >
|
||||
// + {item.label}
|
||||
// + </div>
|
||||
// + </li>
|
||||
// + ))}
|
||||
// +</ul>
|
||||
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
// + Extra Action
|
||||
// +</div>
|
||||
// +</div>
|
||||
// \`\`\`
|
||||
// `;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,523 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// // used for ctrl+l
|
||||
// const partialGenerationInstructions = ``
|
||||
|
||||
|
||||
// // used for ctrl+k, autocomplete
|
||||
// const fimInstructions = ``
|
||||
|
||||
|
||||
// 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}
|
||||
|
||||
// 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 generateCtrlLInstructions = `\
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
Instructions:
|
||||
1. Output the changes to make to the entire file.
|
||||
1. Do not re-write the entire file.
|
||||
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
|
||||
4. You must give enough context to apply the change in the correct location.
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`math.ts\`:
|
||||
\`\`\`
|
||||
const addNumbers = (a, b) => a + b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
add a function that multiplies numbers below this
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b;
|
||||
const multiplyNumbers = (a, b) => a * b;
|
||||
// existing code...
|
||||
\`\`\`
|
||||
|
||||
## EXAMPLE
|
||||
|
||||
FILES
|
||||
selected file \`fib.ts\`:
|
||||
\`\`\`
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`
|
||||
memoize results
|
||||
\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
\`\`\`
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
\`\`\`
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES
|
||||
`
|
||||
|
||||
export const generateDiffInstructions = `
|
||||
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
|
||||
|
||||
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
|
||||
|
||||
All changes made to files must be outputted in unified diff format.
|
||||
Unified diff format instructions:
|
||||
1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
|
||||
2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
|
||||
3. Make diffs more than a few lines.
|
||||
4. Make high-level diffs rather than many one-line diffs.
|
||||
|
||||
Here's an example of unified diff format:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-def factorial(n):
|
||||
- if n == 0:
|
||||
- return 1
|
||||
- else:
|
||||
- return n * factorial(n-1)
|
||||
+def factorial(number):
|
||||
+ if number == 0:
|
||||
+ return 1
|
||||
+ else:
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
|
||||
|
||||
\`\`\`
|
||||
@@ ... @@ # This is less preferred because edits are close together and should be grouped:
|
||||
-def factorial(n):
|
||||
+def factorial(number):
|
||||
- if n == 0:
|
||||
+ if number == 0:
|
||||
return 1
|
||||
else:
|
||||
- return n * factorial(n-1)
|
||||
+ return number * factorial(number-1)
|
||||
\`\`\`
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`test.ts\`:
|
||||
\`\`\`
|
||||
x = 1
|
||||
|
||||
{{selection}}
|
||||
|
||||
z = 3
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`const y = 2\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`y = 3\`\`\`
|
||||
|
||||
EXPECTED RESULT
|
||||
|
||||
We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-x = 1
|
||||
-
|
||||
-y = 2
|
||||
+x = 1
|
||||
+
|
||||
+y = 3
|
||||
\`\`\`
|
||||
|
||||
# Example 2:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{{selection}}
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\` <button\`\`\`
|
||||
|
||||
INSTRUCTIONS
|
||||
\`\`\`make all the buttons like this into divs\`\`\`
|
||||
|
||||
EXPECTED OUTPUT
|
||||
|
||||
We should change all the buttons like the one selected into a div component. Here is the change:
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
|
||||
export const searchDiffChunkInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
|
||||
|
||||
Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
|
||||
|
||||
# Example 1:
|
||||
|
||||
FILES
|
||||
selected file \`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
SELECTION
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
\`\`\`
|
||||
|
||||
RESULT
|
||||
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
|
||||
|
||||
OUTPUT
|
||||
\`true\`
|
||||
`
|
||||
|
||||
|
||||
export const writeFileWithDiffInstructions = `
|
||||
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
|
||||
|
||||
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
|
||||
|
||||
Directions:
|
||||
1. Continue exactly where the new file \`new_file\` left off.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
|
||||
|
||||
# Example 1:
|
||||
|
||||
ORIGINAL_FILE
|
||||
\`Sidebar.tsx\`:
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
\`\`\`
|
||||
|
||||
DIFF
|
||||
\`\`\`
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
- {items.map((item, index) => (
|
||||
- <li key={index}>
|
||||
- <button
|
||||
- className={styles.sidebarButton}
|
||||
- onClick={() => onItemSelect?.(item.label)}
|
||||
- >
|
||||
- {item.label}
|
||||
- </button>
|
||||
- </li>
|
||||
- ))}
|
||||
-</ul>
|
||||
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
- Extra Action
|
||||
-</button>
|
||||
-</div>
|
||||
+<div className={styles.sidebar}>
|
||||
+<ul>
|
||||
+ {items.map((item, index) => (
|
||||
+ <li key={index}>
|
||||
+ <div
|
||||
+ className={styles.sidebarButton}
|
||||
+ onClick={() => onItemSelect?.(item.label)}
|
||||
+ >
|
||||
+ {item.label}
|
||||
+ </div>
|
||||
+ </li>
|
||||
+ ))}
|
||||
+</ul>
|
||||
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
+ Extra Action
|
||||
+</div>
|
||||
+</div>
|
||||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
interface SidebarProps {
|
||||
items: { label: string; href: string }[];
|
||||
onItemSelect?: (label: string) => void;
|
||||
onExtraButtonClick?: () => void;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
|
||||
return (
|
||||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<div
|
||||
className={styles.sidebarButton}
|
||||
onClick={() => onItemSelect?.(item.label)}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.extraButton} onClick={onExtraButtonClick}>
|
||||
Extra Action
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;\`\`\`
|
||||
`
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
onGetInputBox: (i: InputBox) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onUserUpdateText: (text: string) => void;
|
||||
initText: string | null;
|
||||
}
|
||||
|
||||
export type QuickEdit = {
|
||||
startLine: number, // 0-indexed
|
||||
beforeCode: string,
|
||||
afterCode?: string,
|
||||
instructions?: string,
|
||||
responseText?: string, // model can produce a text response too
|
||||
}
|
||||
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor(
|
||||
) {
|
||||
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> {
|
||||
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
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 { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
// deselect - clear selection
|
||||
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
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(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
this.state = { quickEditsOfDocument: {} }
|
||||
}
|
||||
|
||||
|
||||
setState(newState: Partial<VoidQuickEditState>) {
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
fireFocusChat() {
|
||||
this._onFocusChat.fire()
|
||||
}
|
||||
|
||||
fireBlurChat() {
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
|
||||
@@ -3,16 +3,63 @@
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { spawn, execSync } from 'child_process';
|
||||
|
||||
// clear temp dirs
|
||||
execSync('npx rimraf out/ && npx rimraf src2/')
|
||||
const args = process.argv.slice(2);
|
||||
const isWatch = args.includes('--watch') || args.includes('-w');
|
||||
|
||||
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-" ')
|
||||
if (isWatch) {
|
||||
// Watch mode
|
||||
// Create a watcher for scope-tailwind using nodemon
|
||||
const scopeTailwindWatcher = spawn('npx', [
|
||||
'nodemon',
|
||||
'--watch', 'src',
|
||||
'--ext', 'ts,tsx,css',
|
||||
'--exec',
|
||||
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
|
||||
]);
|
||||
|
||||
// tsup to build src2/ into out/
|
||||
execSync('npx tsup')
|
||||
// Create a watcher for tsup in watch mode
|
||||
const tsupWatcher = spawn('npx', [
|
||||
'tsup',
|
||||
'--watch'
|
||||
]);
|
||||
|
||||
// Handle scope-tailwind watcher output
|
||||
scopeTailwindWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[scope-tailwind] ${data}`);
|
||||
});
|
||||
|
||||
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
|
||||
scopeTailwindWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[scope-tailwind] ${data}`);
|
||||
});
|
||||
|
||||
// Handle tsup watcher output
|
||||
tsupWatcher.stdout.on('data', (data) => {
|
||||
console.log(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
tsupWatcher.stderr.on('data', (data) => {
|
||||
console.error(`[tsup] ${data}`);
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
scopeTailwindWatcher.kill();
|
||||
tsupWatcher.kill();
|
||||
process.exit();
|
||||
});
|
||||
|
||||
console.log('🔄 Watchers started! Press Ctrl+C to stop both watchers.');
|
||||
} else {
|
||||
// Build mode
|
||||
console.log('📦 Building...');
|
||||
|
||||
// Run scope-tailwind once
|
||||
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"', { stdio: 'inherit' });
|
||||
|
||||
// Run tsup once
|
||||
execSync('npx tsup', { stdio: 'inherit' });
|
||||
|
||||
console.log('✅ Build complete!');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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' : ''}`}>
|
||||
<ErrorBoundary>
|
||||
<CtrlKChat {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } 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';
|
||||
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService')
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current
|
||||
if (!inputContainer) return;
|
||||
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const height = entries[0].borderBoxSize[0].blockSize
|
||||
onChangeHeight(height)
|
||||
})
|
||||
resizeObserver.observe(inputContainer);
|
||||
|
||||
return () => { resizeObserver?.disconnect(); };
|
||||
}, [onChangeHeight]);
|
||||
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructions(newStr)
|
||||
onUserUpdateText(newStr)
|
||||
}, [setInstructions])
|
||||
const isDisabled = !instructions.trim()
|
||||
|
||||
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
|
||||
const onSubmit = useCallback((e: FormEvent) => {
|
||||
if (currentlyStreamingIdRef.current !== undefined) return
|
||||
inputBoxRef.current?.disable()
|
||||
|
||||
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
|
||||
featureName: 'Ctrl+K',
|
||||
diffareaid: diffareaid,
|
||||
userMessage: instructions,
|
||||
})
|
||||
setIsStreaming(true)
|
||||
}, [inlineDiffsService, diffareaid, instructions])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currentlyStreamingIdRef.current !== undefined)
|
||||
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
|
||||
setIsStreaming(false)
|
||||
}, [inlineDiffsService])
|
||||
|
||||
|
||||
// sync init value
|
||||
const alreadySetRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!inputBoxRef.current) return
|
||||
if (alreadySetRef.current) return
|
||||
alreadySetRef.current = true
|
||||
inputBoxRef.current.value = instructions
|
||||
}, [initText, instructions])
|
||||
|
||||
return <div className='py-2 w-full max-w-xl' ref={sizerRef}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
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)
|
||||
return
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
if (isDisabled) {
|
||||
// __TODO__ show disabled
|
||||
return
|
||||
}
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.currentTarget === e.target) {
|
||||
inputBoxRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
<div // this div is used to position the input box properly
|
||||
className={`w-full p-2 z-[999]`}
|
||||
>
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
{/* left (input) */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full
|
||||
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+K to select`}
|
||||
onChangeText={onChangeText}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
inputBoxRef.current = instance;
|
||||
onGetInputBox(instance);
|
||||
instance.focus()
|
||||
}, [onGetInputBox])}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* right (button) */}
|
||||
<div className='flex flex-row items-end'>
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Glass Devtools, Inc. All rights reserved.
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { CtrlK } from './CtrlK.js'
|
||||
|
||||
|
||||
export const mountCtrlK = mountFnGenerator(CtrlK)
|
||||
|
||||
|
||||
@@ -4,30 +4,85 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ReactNode } from "react"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||
import { VoidCodeEditor } from '../util/inputs.js';
|
||||
|
||||
|
||||
|
||||
export function getLanguageFromFileName(fileName: string): string {
|
||||
if (!fileName) return 'plaintext';
|
||||
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
if (!ext) return 'plaintext';
|
||||
|
||||
const extensionMap: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile'
|
||||
};
|
||||
|
||||
return extensionMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
const customStyle = {
|
||||
...atomOneDarkReasonable,
|
||||
'code[class*="language-"]': {
|
||||
...atomOneDarkReasonable['code[class*="language-"]'],
|
||||
background: "none",
|
||||
},
|
||||
}
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
|
||||
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
<VoidCodeEditor
|
||||
initValue={text}
|
||||
language={language}
|
||||
/>
|
||||
{/* <div
|
||||
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
@@ -38,7 +93,7 @@ export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, bu
|
||||
{text}
|
||||
</SyntaxHighlighter>
|
||||
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useService } from '../util/services.js'
|
||||
import { useAccessor } from '../util/services.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
@@ -18,13 +18,13 @@ enum CopyButtonState {
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = useService('inlineDiffService')
|
||||
|
||||
const clipboardService = useService('clipboardService')
|
||||
|
||||
|
||||
const inlineDiffService = accessor.get('IInlineDiffsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
@@ -47,9 +47,11 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
|
||||
onClick={async () => {
|
||||
|
||||
inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
|
||||
onClick={() => {
|
||||
inlineDiffService.startApplying({
|
||||
featureName: 'Ctrl+L',
|
||||
userMessage: text,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
@@ -169,7 +171,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>
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Sidebar = ({ className }: { className: string }) => {
|
||||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'}`}>
|
||||
<div className={`mb-2 w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -3,17 +3,14 @@
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useSettingsState, useService, useSidebarState, useThreadsState } from '../util/services.js';
|
||||
import { generateCtrlLInstructions, generateDiffInstructions } from '../../../prompt/systemPrompts.js';
|
||||
import { userInstructionsStr } from '../../../prompt/stringifySelections.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../threadHistoryService.js';
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { BlockCode, getLanguageFromFileName } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
@@ -21,11 +18,15 @@ import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { getCmdKey } from '../../../helpers/getCmdKey.js'
|
||||
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { VoidInputBox } from '../util/inputs.js';
|
||||
import { VoidCodeEditor, VoidInputBox } from '../util/inputs.js';
|
||||
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
|
||||
|
||||
const IconX = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@@ -33,8 +34,9 @@ const IconX = ({ size, className = '' }: { size: number, className?: string }) =
|
||||
height={size}
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='black'
|
||||
stroke='currentColor'
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
@@ -86,6 +88,57 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string
|
||||
};
|
||||
|
||||
|
||||
export const IconWarning = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.56 1h.88l6.54 12.26-.44.74H1.44L1 13.26 7.56 1zM8 2.28L2.28 13H13.7L8 2.28zM8.625 12v-1h-1.25v1h1.25zm-1.25-2V6h1.25v4h-1.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
|
||||
const DEFAULT_BUTTON_SIZE = 20;
|
||||
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
|
||||
|
||||
return <button
|
||||
type='submit'
|
||||
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
${className}
|
||||
`}
|
||||
type='button'
|
||||
{...props}
|
||||
>
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
@@ -160,11 +213,11 @@ export const SelectedFiles = (
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div
|
||||
className='flex flex-wrap gap-4 p-2 text-left'
|
||||
className='flex flex-wrap gap-2 text-left'
|
||||
>
|
||||
{selections.map((selection, i) => {
|
||||
|
||||
const showSelectionText = selection.selectionStr && selectionIsOpened[i]
|
||||
const showSelectionText = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
|
||||
return (
|
||||
<div key={i} // container for `selectionSummary` and `selectionText`
|
||||
@@ -173,11 +226,14 @@ export const SelectedFiles = (
|
||||
{/* 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
|
||||
className={`flex items-center gap-1 relative
|
||||
rounded-md p-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
bg-vscode-badge-bg border border-vscode-button-border rounded-md
|
||||
w-fit h-fit min-w-[81px] p-1
|
||||
`}
|
||||
bg-vscode-editor-bg hover:brightness-95
|
||||
border border-vscode-commandcenter-border rounded-xs
|
||||
text-xs text-vscode-editor-fg text-nowrap
|
||||
`}
|
||||
onClick={() => {
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
@@ -186,18 +242,37 @@ export const SelectedFiles = (
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className='truncate'>
|
||||
<span className=''>
|
||||
{/* 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
|
||||
{type === 'staging' &&
|
||||
<span
|
||||
className='
|
||||
cursor-pointer
|
||||
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
|
||||
rounded-md
|
||||
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] text-vscode-toolbar-foreground" />
|
||||
</span>
|
||||
}
|
||||
|
||||
{/* type of selection */}
|
||||
{/* <span className='truncate'>{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();
|
||||
@@ -208,12 +283,13 @@ export const SelectedFiles = (
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
</span>
|
||||
}
|
||||
} */}
|
||||
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{showSelectionText &&
|
||||
<div className='w-full'>
|
||||
<BlockCode text={selection.selectionStr!} />
|
||||
<div className='w-full p-1 rounded-sm border-vscode-editor-border bg-vscode-sidebar-bg'>
|
||||
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -247,7 +323,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 overflow-auto`}>
|
||||
<div className={`text-left 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>
|
||||
@@ -259,11 +335,12 @@ export const SidebarChat = () => {
|
||||
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
|
||||
const modelService = useService('modelService')
|
||||
const accessor = useAccessor()
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
@@ -275,7 +352,9 @@ export const SidebarChat = () => {
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
|
||||
const llmMessageService = accessor.get('ILLMMessageService')
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
@@ -286,7 +365,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 +403,11 @@ export const SidebarChat = () => {
|
||||
|
||||
|
||||
// add system message to chat history
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: generateCtrlLInstructions }
|
||||
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
|
||||
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: chat_prompt(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
|
||||
@@ -427,7 +505,7 @@ export const SidebarChat = () => {
|
||||
|
||||
{/* input box */}
|
||||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<form
|
||||
ref={(ref) => { if (ref) { setFormHeight(ref.clientHeight); } }}
|
||||
@@ -474,16 +552,22 @@ 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`
|
||||
// ` outline-none border-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-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`
|
||||
}
|
||||
>
|
||||
|
||||
@@ -501,34 +585,24 @@ export const SidebarChat = () => {
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='w-[250px]'>
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
|
||||
+7
-3
@@ -4,7 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useService, useThreadsState } from '../util/services.js';
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
@@ -18,8 +20,10 @@ const truncate = (s: string) => {
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useThreadsState()
|
||||
const threadsStateService = useService('threadsStateService')
|
||||
const sidebarStateService = useService('sidebarStateService')
|
||||
|
||||
const accessor = useAccessor()
|
||||
const threadsStateService = accessor.get('IThreadHistoryService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const { allThreads } = threadsState
|
||||
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
.select-child-restyle select {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
@@ -4,20 +4,26 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { 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 { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
import { ScrollableElementCreationOptions } from '../../../../../../../base/browser/ui/scrollbar/scrollableElementOptions.js';
|
||||
import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js';
|
||||
|
||||
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
|
||||
import { useAccessor } from './services.js';
|
||||
|
||||
|
||||
// type guard
|
||||
const isConstructor = (f: any)
|
||||
: f is { new(...params: any[]): any } => {
|
||||
return !!f.prototype && f.prototype.constructor === f;
|
||||
}
|
||||
|
||||
export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, propsFn, dispose, onCreateInstance, children, className }
|
||||
: {
|
||||
ctor: { new(...params: CtorParams): Instance },
|
||||
propsFn: (container: HTMLDivElement) => CtorParams,
|
||||
ctor: { new(...params: CtorParams): Instance } | ((container: HTMLDivElement) => Instance),
|
||||
propsFn: (container: HTMLDivElement) => CtorParams, // unused if fn
|
||||
onCreateInstance: (instance: Instance) => IDisposable[],
|
||||
dispose: (instance: Instance) => void,
|
||||
children?: React.ReactNode,
|
||||
@@ -27,7 +33,7 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const instance = new ctor(...propsFn(containerRef.current!));
|
||||
const instance = isConstructor(ctor) ? new ctor(...propsFn(containerRef.current!)) : ctor(containerRef.current!)
|
||||
const disposables = onCreateInstance(instance);
|
||||
return () => {
|
||||
disposables.forEach(d => d.dispose());
|
||||
@@ -49,8 +55,9 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
multiline: boolean;
|
||||
}) => {
|
||||
|
||||
const contextViewProvider = useService('contextViewService');
|
||||
const accessor = useAccessor()
|
||||
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
propsFn={useCallback((container) => [
|
||||
@@ -95,18 +102,109 @@ 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[]);
|
||||
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
|
||||
options: readonly { text: string, value: T }[];
|
||||
}) => {
|
||||
const contextViewProvider = useService('contextViewService');
|
||||
const accessor = useAccessor()
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
|
||||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className='text-ellipsis whitespace-nowrap pr-6'
|
||||
className='@@select-child-restyle'
|
||||
ctor={SelectBox}
|
||||
propsFn={useCallback((container) => {
|
||||
containerRef.current = container
|
||||
@@ -148,6 +246,143 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
||||
/>;
|
||||
};
|
||||
|
||||
// makes it so that code in the sidebar isnt too tabbed out
|
||||
const normalizeIndentation = (code: string): string => {
|
||||
const lines = code.split('\n')
|
||||
|
||||
let minLeadingSpaces = Infinity
|
||||
|
||||
// find the minimum number of leading spaces
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') continue;
|
||||
let leadingSpaces = 0;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
leadingSpaces += 1;
|
||||
} else { break; }
|
||||
}
|
||||
minLeadingSpaces = Math.min(minLeadingSpaces, leadingSpaces)
|
||||
}
|
||||
|
||||
// remove the leading spaces
|
||||
return lines.map(line => {
|
||||
if (line.trim() === '') return line;
|
||||
|
||||
let spacesToRemove = minLeadingSpaces;
|
||||
let i = 0;
|
||||
while (spacesToRemove > 0 && i < line.length) {
|
||||
const char = line[i];
|
||||
if (char === '\t' || char === ' ') {
|
||||
spacesToRemove -= 1;
|
||||
i++;
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
return line.slice(i);
|
||||
|
||||
}).join('\n')
|
||||
|
||||
}
|
||||
|
||||
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
|
||||
|
||||
const MAX_HEIGHT = Infinity;
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const instantiationService = accessor.get('IInstantiationService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
return <div ref={divRef}>
|
||||
<WidgetComponent
|
||||
className='relative z-0 text-sm'
|
||||
ctor={useCallback((container) =>
|
||||
instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{
|
||||
automaticLayout: true,
|
||||
wordWrap: 'off',
|
||||
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
lineNumbers: 'off',
|
||||
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
readOnlyMessage: { value: '' },
|
||||
|
||||
minimap: {
|
||||
enabled: false,
|
||||
// maxColumn: 0,
|
||||
},
|
||||
|
||||
selectionHighlight: false, // highlights whole words
|
||||
renderLineHighlight: 'none',
|
||||
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerBorder: false,
|
||||
glyphMargin: false,
|
||||
|
||||
stickyScroll: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
isSimpleWidget: true,
|
||||
})
|
||||
, [instantiationService])
|
||||
}
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelService.createModel(initValue, null)
|
||||
editor.setModel(model);
|
||||
model.setLanguage(language ?? 'plaintext')
|
||||
|
||||
const container = editor.getDomNode()
|
||||
const parentNode = container?.parentElement
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
|
||||
// Listen for content changes and update height
|
||||
const disposable = editor.onDidContentSizeChange(() => {
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
});
|
||||
|
||||
return [disposable]
|
||||
}, [modelService, initValue, language])}
|
||||
|
||||
dispose={useCallback((editor: CodeEditorWidget) => {
|
||||
editor.dispose();
|
||||
}, [])}
|
||||
|
||||
propsFn={useCallback(() => { return [] }, [])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
|
||||
// const instanceRef = useRef<DomScrollableElement | null>(null);
|
||||
@@ -182,8 +417,6 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
||||
// options: readonly { text: string, value: T }[];
|
||||
// onChangeSelection: (value: T) => void;
|
||||
// }) => {
|
||||
// const contextViewProvider = useService('contextViewService');
|
||||
// const contextMenuProvider = useService('contextMenuService');
|
||||
|
||||
|
||||
// return <WidgetComponent
|
||||
@@ -229,9 +462,6 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
||||
// }) => {
|
||||
// const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// const themeService = useService('themeService');
|
||||
// const contextViewService = useService('contextViewService');
|
||||
// const hoverService = useService('hoverService');
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
@@ -6,20 +6,21 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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) => {
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
|
||||
export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => (rootElement: HTMLElement, accessor: ServicesAccessor, props?: any) => {
|
||||
if (typeof document === 'undefined') {
|
||||
console.error('index.tsx error: document was undefined')
|
||||
return
|
||||
}
|
||||
|
||||
const disposables = _registerServices(services)
|
||||
const disposables = _registerServices(accessor)
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Component />); // tailwind dark theme indicator
|
||||
root.render(<Component {...props} />); // tailwind dark theme indicator
|
||||
|
||||
return disposables
|
||||
}
|
||||
|
||||
@@ -3,23 +3,52 @@
|
||||
* Void Editor additions licensed under the AGPL 3.0 License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { RefreshModelState } from '../../../../../../../platform/void/common/refreshModelService.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 { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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 { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'
|
||||
import { INotificationService } from '../../../../../../../platform/notification/common/notification.js'
|
||||
import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'
|
||||
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
|
||||
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.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, 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()
|
||||
|
||||
@@ -29,8 +58,9 @@ const threadsStateListeners: Set<(s: ThreadsState) => 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 refreshModelState: RefreshModelStateOfProvider
|
||||
const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set()
|
||||
const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set()
|
||||
|
||||
let colorThemeState: ColorScheme
|
||||
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
|
||||
@@ -38,7 +68,7 @@ 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) => {
|
||||
export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
|
||||
const disposables: IDisposable[] = []
|
||||
|
||||
@@ -49,8 +79,26 @@ export const _registerServices = (services_: ReactServicesType) => {
|
||||
}
|
||||
wasCalled = true
|
||||
|
||||
services = services_
|
||||
const { sidebarStateService, settingsStateService, threadsStateService, refreshModelService, themeService } = services
|
||||
_registerAccessor(accessor)
|
||||
|
||||
const stateServices = {
|
||||
quickEditStateService: accessor.get(IQuickEditStateService),
|
||||
sidebarStateService: accessor.get(ISidebarStateService),
|
||||
threadsStateService: accessor.get(IThreadHistoryService),
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
}
|
||||
|
||||
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
|
||||
|
||||
quickEditState = quickEditStateService.state
|
||||
disposables.push(
|
||||
quickEditStateService.onDidChangeState(() => {
|
||||
quickEditState = quickEditStateService.state
|
||||
quickEditStateListeners.forEach(l => l(quickEditState))
|
||||
})
|
||||
)
|
||||
|
||||
sidebarState = sidebarStateService.state
|
||||
disposables.push(
|
||||
@@ -78,9 +126,10 @@ export const _registerServices = (services_: ReactServicesType) => {
|
||||
|
||||
refreshModelState = refreshModelService.state
|
||||
disposables.push(
|
||||
refreshModelService.onDidChangeState(() => {
|
||||
refreshModelService.onDidChangeState((providerName) => {
|
||||
refreshModelState = refreshModelService.state
|
||||
refreshModelStateListeners.forEach(l => l(refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -96,16 +145,68 @@ export const _registerServices = (services_: ReactServicesType) => {
|
||||
}
|
||||
|
||||
|
||||
// -- services --
|
||||
export const useService = <T extends keyof ReactServicesType,>(serviceName: T): ReactServicesType[T] => {
|
||||
if (services === null) {
|
||||
throw new Error('useAccessor must be used within an AccessorProvider')
|
||||
}
|
||||
return services[serviceName]
|
||||
const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = {
|
||||
IModelService: accessor.get(IModelService),
|
||||
IClipboardService: accessor.get(IClipboardService),
|
||||
IContextViewService: accessor.get(IContextViewService),
|
||||
IContextMenuService: accessor.get(IContextMenuService),
|
||||
IFileService: accessor.get(IFileService),
|
||||
IHoverService: accessor.get(IHoverService),
|
||||
IThemeService: accessor.get(IThemeService),
|
||||
ILLMMessageService: accessor.get(ILLMMessageService),
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
IThreadHistoryService: accessor.get(IThreadHistoryService),
|
||||
|
||||
IInstantiationService: accessor.get(IInstantiationService),
|
||||
ICodeEditorService: accessor.get(ICodeEditorService),
|
||||
ICommandService: accessor.get(ICommandService),
|
||||
IContextKeyService: accessor.get(IContextKeyService),
|
||||
INotificationService: accessor.get(INotificationService),
|
||||
IAccessibilityService: accessor.get(IAccessibilityService),
|
||||
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
}
|
||||
|
||||
type ReactAccessor = ReturnType<typeof getReactAccessor>
|
||||
|
||||
|
||||
let reactAccessor_: ReactAccessor | null = null
|
||||
const _registerAccessor = (accessor: ServicesAccessor) => {
|
||||
const reactAccessor = getReactAccessor(accessor)
|
||||
reactAccessor_ = reactAccessor
|
||||
}
|
||||
|
||||
// -- services --
|
||||
export const useAccessor = () => {
|
||||
if (!reactAccessor_) {
|
||||
throw new Error(`⚠️ Void useAccessor was called before _registerServices!`)
|
||||
}
|
||||
|
||||
return { get: <S extends keyof ReactAccessor,>(service: S): ReactAccessor[S] => reactAccessor_![service] }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -- 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(() => {
|
||||
@@ -148,7 +249,12 @@ export const useRefreshModelState = () => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const useRefreshModelListener = (listener: (providerName: RefreshableProviderName, s: RefreshModelStateOfProvider) => void) => {
|
||||
useEffect(() => {
|
||||
refreshModelProviderListeners.add(listener)
|
||||
return () => { refreshModelProviderListeners.delete(listener) }
|
||||
}, [listener])
|
||||
}
|
||||
|
||||
|
||||
export const useIsDark = () => {
|
||||
|
||||
+34
-7
@@ -5,13 +5,16 @@
|
||||
|
||||
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 { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { VoidSelectBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
|
||||
const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
let weChangedText = false
|
||||
@@ -40,10 +43,34 @@ const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
}
|
||||
|
||||
const DummySelectBox = () => {
|
||||
return <VoidSelectBox
|
||||
options={[{ text: 'Please add a model!', value: null }]}
|
||||
onChangeSelection={() => { }}
|
||||
/>
|
||||
|
||||
const accessor = useAccessor()
|
||||
const comandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => {
|
||||
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
};
|
||||
|
||||
return <div
|
||||
className={`
|
||||
flex items-center
|
||||
flex-nowrap text-ellipsis
|
||||
text-vscode-charts-yellow
|
||||
hover:brightness-90 transition-all duration-200
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={openSettings}
|
||||
>
|
||||
<IconWarning
|
||||
size={20}
|
||||
className='mr-1 brightness-90'
|
||||
/>
|
||||
<span>Model required</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
|
||||
@@ -1,34 +1,80 @@
|
||||
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 { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings } 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'
|
||||
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
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={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
<span className='opacity-50'>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
// models
|
||||
const RefreshModelButton = ({ providerName }: { providerName: RefreshableProviderName }) => {
|
||||
const refreshModelState = useRefreshModelState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
|
||||
const [justFinished, setJustSucceeded] = useState(false)
|
||||
|
||||
useRefreshModelListener(
|
||||
useCallback((providerName2, refreshModelState) => {
|
||||
if (providerName2 !== providerName) return
|
||||
const { state } = refreshModelState[providerName]
|
||||
if (state !== 'success') return
|
||||
// now we know we just entered 'success' state for this providerName
|
||||
setJustSucceeded(true)
|
||||
const tid = setTimeout(() => { setJustSucceeded(false) }, 2000)
|
||||
return () => clearTimeout(tid)
|
||||
}, [providerName])
|
||||
)
|
||||
|
||||
const { state } = refreshModelState[providerName]
|
||||
const isRefreshing = state === 'refreshing'
|
||||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => { refreshModelService.refreshModels(providerName) }}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
/>
|
||||
}
|
||||
|
||||
const RefreshableModels = () => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const refreshModelState = useRefreshModelState()
|
||||
const refreshModelService = useService('refreshModelService')
|
||||
|
||||
if (!settingsState.settingsOfProvider.ollama.enabled)
|
||||
return null
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
return <RefreshModelButton key={providerName} providerName={providerName} />
|
||||
})
|
||||
|
||||
return <>
|
||||
{buttons}
|
||||
</>
|
||||
|
||||
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 accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const providerNameRef = useRef<ProviderName | null>(null)
|
||||
@@ -36,10 +82,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
|
||||
@@ -59,8 +106,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
|
||||
@@ -84,54 +132,76 @@ 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>}
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
const AddModelButton = () => {
|
||||
const AddModelMenuFull = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return <>
|
||||
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 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>
|
||||
}
|
||||
|
||||
|
||||
export const ModelDump = () => {
|
||||
|
||||
const settingsStateService = useService('settingsStateService')
|
||||
const accessor = useAccessor()
|
||||
const settingsStateService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
// a dump of all the enabled providers' models
|
||||
const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
|
||||
const modelDump: (VoidModelInfo & { 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 })))
|
||||
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 || isHidden) ? '❌' : '✅'}</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>
|
||||
@@ -146,59 +216,100 @@ 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 accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
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])}
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidInputBox
|
||||
placeholder={`${providerTitle} ${settingTitle} (${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></>
|
||||
// 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
|
||||
|
||||
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
|
||||
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
|
||||
})
|
||||
|
||||
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
|
||||
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
|
||||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
}
|
||||
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [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 }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const voidSettingsService = useService('settingsStateService')
|
||||
// const voidSettingsState = useSettingsState()
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName]
|
||||
// const { enabled } = voidSettingsState.settingsOfProvider[providerName]
|
||||
const settingNames = customSettingNamesOfProvider(providerName)
|
||||
|
||||
return <>
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
|
||||
<div className='flex items-center gap-4'>
|
||||
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
|
||||
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
|
||||
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) */}
|
||||
{Object.keys(others).map((sName, i) => {
|
||||
const settingName = sName as keyof typeof others
|
||||
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 >
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +321,50 @@ export const VoidProviderSettings = () => {
|
||||
</>
|
||||
}
|
||||
|
||||
// export const VoidFeatureFlagSettings = () => {
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// const voidSettingsState = useSettingsState()
|
||||
|
||||
// return <>
|
||||
// {featureFlagNames.map((flagName) => {
|
||||
// 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'>
|
||||
// <VoidCheckBox
|
||||
// label=''
|
||||
// value={value}
|
||||
// onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
|
||||
// />
|
||||
// <h4 className='text-sm'>{description}</h4>
|
||||
// </div>
|
||||
// </div>
|
||||
// })}
|
||||
// </>
|
||||
// }
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
return featureFlagNames.map((flagName) => {
|
||||
|
||||
const enabled = voidSettingsState.featureFlagSettings[flagName]
|
||||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
|
||||
return <SubtleButton
|
||||
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
|
||||
text={description}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// full settings
|
||||
@@ -233,10 +388,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-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
|
||||
<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-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
|
||||
<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>
|
||||
@@ -249,19 +404,23 @@ export const Settings = () => {
|
||||
<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>
|
||||
<h2 className={`text-3xl mb-2`}>Providers</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-4`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -41,18 +41,18 @@ module.exports = {
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"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)",
|
||||
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
|
||||
"input-validation-error-bg": "var(--vscode-inputValidation-errorBackground)",
|
||||
"input-validation-error-fg": "var(--vscode-inputValidation-errorForeground)",
|
||||
"input-validation-error-border": "var(--vscode-inputValidation-errorBorder)",
|
||||
"input-validation-info-bg": "var(--vscode-inputValidation-infoBackground)",
|
||||
"input-validation-info-fg": "var(--vscode-inputValidation-infoForeground)",
|
||||
"input-validation-info-border": "var(--vscode-inputValidation-infoBorder)",
|
||||
"input-validation-warning-bg": "var(--vscode-inputValidation-warningBackground)",
|
||||
"input-validation-warning-fg": "var(--vscode-inputValidation-warningForeground)",
|
||||
"input-validation-warning-border": "var(--vscode-inputValidation-warningBorder)",
|
||||
|
||||
// command center colors (the top bar)
|
||||
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
|
||||
@@ -107,11 +107,18 @@ module.exports = {
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
|
||||
// editorwidget colors
|
||||
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
|
||||
|
||||
|
||||
// other
|
||||
"editorwidget-bg": "var(--vscode-editorWidget-background)",
|
||||
"toolbar-hover-bg": "var(--vscode-toolbar-hoverBackground)",
|
||||
"toolbar-foreground": "var(--vscode-editorActionList-foreground)",
|
||||
|
||||
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
|
||||
"editorwidget-border": "var(--vscode-editorWidget-border)",
|
||||
|
||||
"charts-orange": "var(--vscode-charts-orange)",
|
||||
"charts-yellow": "var(--vscode-charts-yellow)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
@@ -18,7 +19,7 @@ export default defineConfig({
|
||||
// dts: true,
|
||||
// sourcemap: true,
|
||||
|
||||
clean: true,
|
||||
clean: false,
|
||||
platform: 'browser', // 'node'
|
||||
target: 'esnext',
|
||||
injectStyle: true, // bundle css into the output file
|
||||
|
||||
@@ -12,9 +12,7 @@ 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 './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';
|
||||
@@ -22,7 +20,7 @@ import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { OPEN_VOID_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
@@ -53,9 +51,10 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
}
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
@@ -65,18 +64,23 @@ registerAction2(class extends Action2 {
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'Ctrl+L' })
|
||||
metricsService.capture('User Action', { type: 'Ctrl+L' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(
|
||||
accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
editor?.getSelection()
|
||||
)
|
||||
|
||||
|
||||
if (selectionRange) {
|
||||
// select whole lines
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
@@ -176,6 +180,6 @@ registerAction2(class extends Action2 {
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
commandService.executeCommand(OPEN_VOID_SETTINGS_ACTION_ID)
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPan
|
||||
|
||||
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.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';
|
||||
@@ -33,16 +33,17 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi
|
||||
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';
|
||||
|
||||
// import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
|
||||
// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control)
|
||||
|
||||
@@ -62,6 +63,8 @@ class SidebarViewPane extends ViewPane {
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
// @ICodeEditorService private readonly editorService: ICodeEditorService,
|
||||
// @IContextKeyService private readonly editorContextKeyService: IContextKeyService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
|
||||
|
||||
@@ -76,20 +79,16 @@ class SidebarViewPane extends ViewPane {
|
||||
|
||||
// gets set immediately
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const services = getReactServices(accessor)
|
||||
|
||||
// mount react
|
||||
const disposables: IDisposable[] | undefined = mountSidebar(parent, services);
|
||||
const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor);
|
||||
disposables?.forEach(d => this._register(d))
|
||||
});
|
||||
}
|
||||
|
||||
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`
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -148,3 +147,28 @@ viewsRegistry.registerViews([{
|
||||
// },
|
||||
}], container);
|
||||
|
||||
|
||||
// open sidebar
|
||||
export const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.openSidebar'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SIDEBAR_ACTION_ID,
|
||||
title: 'Open Void Sidebar',
|
||||
})
|
||||
}
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const viewsService = accessor.get(IViewsService)
|
||||
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
}
|
||||
});
|
||||
|
||||
export class SidebarStartContribution implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.startupVoidSidebar';
|
||||
constructor(
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
}
|
||||
registerWorkbenchContribution2(SidebarStartContribution.ID, SidebarStartContribution, WorkbenchPhase.AfterRestored);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.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';
|
||||
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||
|
||||
|
||||
// service that manages sidebar's state
|
||||
@@ -23,8 +23,6 @@ export interface ISidebarStateService {
|
||||
onDidBlurChat: Event<void>;
|
||||
fireFocusChat(): void;
|
||||
fireBlurChat(): void;
|
||||
|
||||
openSidebarView(): void;
|
||||
}
|
||||
|
||||
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
|
||||
@@ -47,7 +45,7 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
|
||||
state: VoidSidebarState
|
||||
|
||||
constructor(
|
||||
@IViewsService private readonly _viewsService: IViewsService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -59,7 +57,7 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
|
||||
setState(newState: Partial<VoidSidebarState>) {
|
||||
// make sure view is open if the tab changes
|
||||
if ('currentTab' in newState) {
|
||||
this.openSidebarView()
|
||||
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
this.state = { ...this.state, ...newState }
|
||||
@@ -74,11 +72,6 @@ class VoidSidebarStateService extends Disposable implements ISidebarStateService
|
||||
this._onBlurChat.fire()
|
||||
}
|
||||
|
||||
openSidebarView() {
|
||||
this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
this._viewsService.openView(VOID_VIEW_ID);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
// register inline diffs
|
||||
import './inlineDiffsService.js'
|
||||
|
||||
// register Sidebar pane, state, actions (keybinds, menus)
|
||||
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
|
||||
import './sidebarActions.js'
|
||||
import './sidebarPane.js'
|
||||
import './sidebarStateService.js'
|
||||
|
||||
// register quick edit (Ctrl+K)
|
||||
import './quickEditActions.js'
|
||||
|
||||
// register Thread History
|
||||
import './threadHistoryService.js'
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
||||
|
||||
|
||||
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';
|
||||
@@ -36,10 +35,11 @@ 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
|
||||
});
|
||||
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
|
||||
scheme: 'void', // Custom scheme for our editor
|
||||
path: 'settings'
|
||||
})
|
||||
readonly resource = VoidSettingsInput.RESOURCE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -89,8 +89,7 @@ class VoidSettingsPane extends EditorPane {
|
||||
|
||||
// Mount React into the scrollable content
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const services = getReactServices(accessor);
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, services);
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, accessor);
|
||||
|
||||
setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
|
||||
this._scrollbar?.scanDomNode();
|
||||
@@ -120,12 +119,12 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane
|
||||
);
|
||||
|
||||
|
||||
export const OPEN_VOID_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
|
||||
export const VOID_OPEN_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,
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Settings"),
|
||||
f1: true,
|
||||
icon: Codicon.settingsGear,
|
||||
@@ -142,9 +141,20 @@ registerAction2(class extends Action2 {
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
|
||||
// close all instances if found
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
return;
|
||||
}
|
||||
|
||||
// else open it
|
||||
const input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
}
|
||||
@@ -155,7 +165,7 @@ registerAction2(class extends Action2 {
|
||||
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
||||
group: '0_command',
|
||||
command: {
|
||||
id: OPEN_VOID_SETTINGS_ACTION_ID,
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void Settings")
|
||||
},
|
||||
order: 1
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user