This commit is contained in:
Andrew Pareles 2024-12-31 13:45:48 -08:00
commit 90a3fd0a21
10 changed files with 1383 additions and 101 deletions

View file

@ -32,8 +32,8 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['enabled', 'endpoint'],
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
ollama: ['_enabled', 'endpoint'],
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
@ -81,7 +81,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled)
// every time providerName.enabled changes, refresh models too, like a useEffect
@ -150,7 +150,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}))
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true)
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
this._setRefreshState(providerName, 'success')

View file

@ -66,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 } })
@ -151,7 +151,7 @@ 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 = {

View file

@ -137,7 +137,7 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
type CommonProviderSettings = {
enabled: boolean | undefined, // undefined initially
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
@ -240,7 +240,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
undefined,
}
}
else if (settingName === 'enabled') {
else if (settingName === '_enabled') {
return {
title: '(never)',
placeholder: '(never)',
@ -293,13 +293,13 @@ export const voidInitModelOptions = {
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
enabled: undefined,
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
enabled: undefined,
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
@ -308,31 +308,31 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
enabled: undefined,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
enabled: undefined,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
enabled: undefined,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
enabled: undefined,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
enabled: undefined,
_enabled: undefined,
},
}

View file

@ -0,0 +1,999 @@
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Glass Devtools, Inc. All rights reserved.
// * Void Editor additions licensed under the AGPL 3.0 License.
// *--------------------------------------------------------------------------------------------*/
// import { Disposable } from '../../../../base/common/lifecycle.js';
// import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
// import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js';
// // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
// import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// // import { throttle } from '../../../../base/common/decorators.js';
// import { inlineDiff_systemMessage } from './prompt/prompts.js';
// import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
// import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
// import { IRange } from '../../../../editor/common/core/range.js';
// import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
// import { Color, RGBA } from '../../../../base/common/color.js';
// import { IModelService } from '../../../../editor/common/services/model.js';
// import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
// import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
// import { LineTokens } from '../../../../editor/common/tokens/lineTokens.js';
// import { ILanguageService } from '../../../../editor/common/languages/language.js';
// // import { IModelService } from '../../../../editor/common/services/model.js';
// import * as dom from '../../../../base/browser/dom.js';
// import { Widget } from '../../../../base/browser/ui/widget.js';
// import { URI } from '../../../../base/common/uri.js';
// import { LLMFeatureSelection, ServiceSendLLMMessageParams } from '../../../../platform/void/common/llmMessageTypes.js';
// import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
// import { IZoneStyleService } from './helperServices/zoneStyleService.js';
// const configOfBG = (color: Color) => {
// return { dark: color, light: color, hcDark: color, hcLight: color, }
// }
// // gets converted to --vscode-void-greenBG, see void.css
// const greenBG = new Color(new RGBA(155, 185, 85, .3)); // default is RGBA(155, 185, 85, .2)
// registerColor('void.greenBG', configOfBG(greenBG), '', true);
// const redBG = new Color(new RGBA(255, 0, 0, .3)); // default is RGBA(255, 0, 0, .2)
// registerColor('void.redBG', configOfBG(redBG), '', true);
// const sweepBG = new Color(new RGBA(100, 100, 100, .2));
// registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
// const highlightBG = new Color(new RGBA(100, 100, 100, .1));
// registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
// const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
// registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
// export type Diff = {
// diffid: number;
// diffareaid: number; // the diff area this diff belongs to, "computed"
// } & ComputedDiff
// // _ means anything we don't include if we clone it
// // DiffArea.originalStartLine is the line in originalCode (not the file)
// type DiffArea = {
// diffareaid: number;
// originalCode: string;
// startLine: number;
// endLine: number;
// shouldHighlight: boolean; // should visually highlight this DiffArea
// _URI: URI; // typically we get the URI from model
// _diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
// } & ({
// _sweepState: {
// isStreaming: true;
// line: number;
// } | {
// isStreaming: false;
// line: null;
// };
// })
// const diffAreaSnapshotKeys = [
// 'diffareaid',
// 'originalCode',
// 'startLine',
// 'endLine',
// 'shouldHighlight',
// ] as const satisfies (keyof DiffArea)[]
// type DiffAreaSnapshot = Pick<DiffArea, typeof diffAreaSnapshotKeys[number]>
// type HistorySnapshot = {
// snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
// entireFileCode: string;
// } &
// ({
// type: 'Ctrl+K';
// ctrlKText: string;
// } | {
// type: 'Ctrl+L';
// })
// export interface IInlineDiffsService {
// readonly _serviceBrand: undefined;
// startStreaming(params: LLMFeatureSelection, str: string): void;
// }
// export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffAreasService');
// class InlineDiffsService extends Disposable implements IInlineDiffsService {
// _serviceBrand: undefined;
// // URI <--> model
// removeStylesFnsOfURI: Record<string, Set<Function>> = {} // functions that remove the styles of this uri
// diffAreasOfURI: Record<string, Set<string>> = {}
// diffAreaOfId: Record<string, DiffArea> = {};
// diffOfId: Record<string, Diff> = {}; // redundant with diffArea._diffs
// _diffareaidPool = 0 // each diffarea has an id
// _diffidPool = 0 // each diff has an id
// constructor(
// // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
// @IModelService private readonly _modelService: IModelService,
// @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
// @ILanguageService private readonly _langService: ILanguageService,
// @ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
// @IZoneStyleService private readonly _zoneStyleService: IZoneStyleService,
// ) {
// super();
// // this function initializes data structures and listens for changes
// const initializeModel = (model: ITextModel) => {
// if (!(model.uri.fsPath in this.diffAreasOfURI)) {
// this.diffAreasOfURI[model.uri.fsPath] = new Set();
// }
// if (!(model.uri.fsPath in this.removeStylesFnsOfURI)) {
// this.removeStylesFnsOfURI[model.uri.fsPath] = new Set();
// }
// // when the user types, realign diff areas and re-render them
// this._register(
// model.onDidChangeContent(e => {
// // it's as if we just called _write, now all we need to do is realign and refresh
// if (this._weAreWriting) return
// const uri = model.uri
// for (const change of e.changes) { this._realignAllDiffAreasLines(uri, change.text, change.range) }
// this._refreshDiffsInURI(uri)
// })
// )
// }
// // initialize all existing models + initialize when a new model mounts
// for (let model of this._modelService.getModels()) { initializeModel(model) }
// this._register(this._modelService.onModelAdded(model => initializeModel(model)));
// // this function adds listeners to refresh styles when editor changes tab
// let initializeEditor = (editor: ICodeEditor) => {
// const uri = editor.getModel()?.uri ?? null
// if (uri) this._refreshDiffsInURI(uri)
// // called when the user switches tabs (typically there's only 1 editor on the screen, it switches between models, make sure you understand this)
// this._register(editor.onDidChangeModel((e) => {
// if (e.oldModelUrl) this._refreshDiffsInURI(e.oldModelUrl)
// if (e.newModelUrl) this._refreshDiffsInURI(e.newModelUrl)
// }))
// }
// // add listeners for all existing editors + listen for editor being added
// for (let editor of this._editorService.listCodeEditors()) { initializeEditor(editor) }
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// }
// // highlight the region
// private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial<IModelDecorationOptions>) => {
// if (model === null) return
// const id = model.changeDecorations(accessor => accessor.addDecoration(
// { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER },
// {
// className: className,
// description: className,
// isWholeLine: true,
// ...options
// }))
// const disposeHighlight = () => {
// if (id) model.changeDecorations(accessor => accessor.removeDecoration(id))
// }
// return disposeHighlight
// }
// private _addDiffAreaStylesToURI = (uri: URI) => {
// const model = this._getModel(uri)
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
// const diffArea = this.diffAreaOfId[diffareaid]
// // add sweep styles to the diffArea
// if (diffArea._sweepState.isStreaming) {
// // sweepLine ... sweepLine
// const fn1 = this._addLineDecoration(model, diffArea._sweepState.line, diffArea._sweepState.line, 'void-sweepIdxBG')
// // sweepLine+1 ... endLine
// const fn2 = this._addLineDecoration(model, diffArea._sweepState.line + 1, diffArea.endLine, 'void-sweepBG')
// this.removeStylesFnsOfURI[uri.fsPath].add(() => { fn1?.(); fn2?.(); })
// }
// // highlight the diffArea
// if (diffArea.shouldHighlight) {
// const fn = this._addLineDecoration(model, diffArea.startLine, diffArea.endLine, 'void-highlightBG')
// this.removeStylesFnsOfURI[uri.fsPath].add(() => fn?.());
// }
// }
// }
// private _addDiffStylesToURI = (uri: URI, diff: Diff) => {
// const { type, diffid } = diff
// const disposeInThisEditorFns: (() => void)[] = []
// const model = this._modelService.getModel(uri)
// // green decoration and minimap decoration
// if (type !== 'deletion') {
// const fn = this._addLineDecoration(model, diff.startLine, diff.endLine, 'void-greenBG', {
// minimap: { color: { id: 'minimapGutter.addedBackground' }, position: 2 },
// overviewRuler: { color: { id: 'editorOverviewRuler.addedForeground' }, position: 7 }
// })
// disposeInThisEditorFns.push(() => { fn?.() })
// }
// // red in a view zone
// if (type !== 'insertion') {
// const consistentZoneId = this._zoneStyleService.addConsistentZoneToURI(
// uri,
// (editor) => {
// const domNode = document.createElement('div');
// domNode.className = 'void-redBG'
// const renderOptions = RenderOptions.fromEditor(editor);
// // applyFontInfo(domNode, renderOptions.fontInfo)
// // Compute view-lines based on redText
// const redText = diff.originalCode
// const lines = redText.split('\n');
// const lineTokens = lines.map(line => LineTokens.createFromTextAndMetadata([{ text: line, metadata: 0 }], this._langService.languageIdCodec));
// const source = new LineSource(lineTokens, lines.map(() => null), false, false)
// const result = renderLines(source, renderOptions, [], domNode);
// const viewZone: IViewZone = {
// // afterLineNumber: computedDiff.startLine - 1,
// afterLineNumber: type === 'edit' ? diff.endLine : diff.startLine - 1,
// heightInLines: result.heightInLines,
// minWidthInPx: result.minWidthInPx,
// domNode: domNode,
// marginDomNode: document.createElement('div'), // displayed to left
// suppressMouseDown: true,
// };
// return viewZone
// },
// (editor) => {
// // Accept | Reject widget
// const buttonsWidget = new AcceptRejectWidget({
// editor,
// onAccept: () => { this.acceptDiff({ diffid }) },
// onReject: () => { this.rejectDiff({ diffid }) },
// diffid: diffid.toString(),
// startLine: diff.startLine,
// })
// return () => buttonsWidget.dispose()
// }
// )
// disposeInThisEditorFns.push(() => { this._zoneStyleService.removeConsistentZoneFromURI(consistentZoneId) })
// }
// const disposeInEditor = () => { disposeInThisEditorFns.forEach(f => f()) }
// return disposeInEditor;
// }
// private _getModel(uri: URI) {
// const model = this._modelService.getModel(uri)
// if (!model || model.isDisposed()) {
// return null
// }
// return model
// }
// private _readURI(uri: URI): string | null {
// return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null
// }
// private _getNumLines(uri: URI): number | null {
// return this._getModel(uri)?.getLineCount() ?? null
// }
// _weAreWriting = false
// private _writeText(uri: URI, text: string, range: IRange) {
// const model = this._getModel(uri)
// if (!model) return
// this._weAreWriting = true
// model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack
// this._weAreWriting = false
// this._realignAllDiffAreasLines(uri, text, range)
// }
// private _addToHistory(uri: URI) {
// const getCurrentSnapshot = (): HistorySnapshot => {
// const diffAreaOfId = this.diffAreaOfId
// const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
// for (const diffareaid in diffAreaOfId) {
// const diffArea = diffAreaOfId[diffareaid]
// snapshottedDiffAreaOfId[diffareaid] = structuredClone( // a structured clone must be on a JSON object
// Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
// ) as DiffAreaSnapshot
// }
// return {
// snapshottedDiffAreaOfId,
// entireFileCode: this._readURI(uri) ?? '', // the whole file's code
// type: 'Ctrl+L',
// }
// }
// const restoreDiffAreas = (snapshot: HistorySnapshot) => {
// const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot
// // delete all current decorations (diffs, sweep styles) so we don't have any unwanted leftover decorations
// this._clearAllDiffsAndStyles(uri)
// // restore diffAreaOfId and diffAreasOfModelId
// this.diffAreaOfId = {}
// this.diffAreasOfURI[uri.fsPath].clear()
// for (const diffareaid in snapshottedDiffAreaOfId) {
// this.diffAreaOfId[diffareaid] = {
// ...snapshottedDiffAreaOfId[diffareaid],
// _diffOfId: {},
// _URI: uri,
// _sweepState: {
// isStreaming: false,
// line: null,
// },
// }
// this.diffAreasOfURI[uri.fsPath].add(diffareaid)
// }
// // restore file content
// const numLines = this._getNumLines(uri)
// if (numLines === null) return
// this._writeText(uri, entireModelCode, { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER })
// // restore all the decorations
// this._refreshDiffsInURI(uri)
// }
// const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
// let afterSnapshot: HistorySnapshot | null = null
// const elt: IUndoRedoElement = {
// type: UndoRedoElementType.Resource,
// resource: uri,
// label: 'Void Changes',
// code: 'undoredo.inlineDiffs',
// undo: () => { restoreDiffAreas(beforeSnapshot) },
// redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
// }
// this._undoRedoService.pushElement(elt)
// const onFinishEdit = () => { afterSnapshot = getCurrentSnapshot() }
// return { onFinishEdit }
// }
// // delete diffOfId and diffArea._diffOfId
// private _deleteDiff(diff: Diff) {
// const diffArea = this.diffAreaOfId[diff.diffareaid]
// delete diffArea._diffOfId[diff.diffid]
// delete this.diffOfId[diff.diffid]
// }
// private _deleteDiffs(diffArea: DiffArea) {
// for (const diffid in diffArea._diffOfId) {
// const diff = diffArea._diffOfId[diffid]
// this._deleteDiff(diff)
// }
// }
// private _clearAllDiffsAndStyles(uri: URI) {
// for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
// const diffArea = this.diffAreaOfId[diffareaid]
// this._deleteDiffs(diffArea)
// }
// for (const removeStyleFn of this.removeStylesFnsOfURI[uri.fsPath]) {
// removeStyleFn()
// }
// this.removeStylesFnsOfURI[uri.fsPath].clear()
// }
// // delete all diffs, update diffAreaOfId, update diffAreasOfModelId
// private _deleteDiffArea(diffArea: DiffArea) {
// this._deleteDiffs(diffArea)
// delete this.diffAreaOfId[diffArea.diffareaid]
// this.diffAreasOfURI[diffArea._URI.fsPath].delete(diffArea.diffareaid.toString())
// }
// // changes the start/line locations of all DiffAreas on the page (adjust their start/end based on the change) based on the change that was recently made
// private _realignAllDiffAreasLines(uri: URI, text: string, recentChange: { startLineNumber: number; endLineNumber: number }) {
// const model = this._getModel(uri)
// if (!model) return
// // compute net number of newlines lines that were added/removed
// const startLine = recentChange.startLineNumber
// const endLine = recentChange.endLineNumber
// const changeRangeHeight = endLine - startLine + 1
// const newTextHeight = (text.match(/\n/g) || []).length + 1 // number of newlines is number of \n's + 1, e.g. "ab\ncd"
// const deltaNewlines = newTextHeight - changeRangeHeight
// // compute overlap with each diffArea and shrink/elongate each diffArea accordingly
// for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) {
// const diffArea = this.diffAreaOfId[diffareaid]
// // if the diffArea is above the range, it is not affected
// if (diffArea.endLine < startLine) {
// console.log('A')
// continue
// }
// // console.log('Changing DiffArea:', diffArea.startLine, diffArea.endLine)
// // if the diffArea fully contains the change, elongate it by the delta amount of newlines
// if (startLine >= diffArea.startLine && endLine <= diffArea.endLine) {
// diffArea.endLine += deltaNewlines
// }
// // if the change fully contains the diffArea, make the diffArea have the same range as the change
// else if (diffArea.startLine > startLine && diffArea.endLine < endLine) {
// diffArea.startLine = startLine
// diffArea.endLine = startLine + newTextHeight
// console.log('B', diffArea.startLine, diffArea.endLine)
// }
// // if the change contains only the diffArea's top
// else if (diffArea.startLine > startLine) {
// // TODO fill in this case
// console.log('C', diffArea.startLine, diffArea.endLine)
// }
// // if the change contains only the diffArea's bottom
// else if (diffArea.endLine < endLine) {
// const numOverlappingLines = diffArea.endLine - startLine + 1
// diffArea.endLine += newTextHeight - numOverlappingLines // TODO double check this
// console.log('D', diffArea.startLine, diffArea.endLine)
// }
// // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
// else if (diffArea.startLine > endLine) {
// diffArea.startLine += deltaNewlines
// diffArea.endLine += deltaNewlines
// console.log('E', diffArea.startLine, diffArea.endLine)
// }
// // console.log('To:', diffArea.startLine, diffArea.endLine)
// }
// }
// private _refreshDiffsInURI(uri: URI) {
// const content = this._readURI(uri)
// if (content === null) return
// // 1. clear Diffs and styles
// this._clearAllDiffsAndStyles(uri)
// // 2. recompute all diffs on each editor with this URI
// const fullFileText = this._readURI(uri) ?? ''
// // go thru all diffareas in this URI, creating diffs and adding styles to it
// for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) {
// const diffArea = this.diffAreaOfId[diffareaid]
// const newDiffAreaCode = fullFileText.split('\n').slice((diffArea.startLine - 1), (diffArea.endLine - 1) + 1).join('\n')
// const computedDiffs = findDiffs(diffArea.originalCode, newDiffAreaCode)
// for (let computedDiff of computedDiffs) {
// const diffid = this._diffidPool++
// // create a Diff of it
// const newDiff: Diff = {
// ...computedDiff,
// diffid: diffid,
// diffareaid: diffArea.diffareaid,
// }
// const fn = this._addDiffStylesToURI(uri, newDiff)
// this.removeStylesFnsOfURI[uri.fsPath].add(fn)
// this.diffOfId[diffid] = newDiff
// diffArea._diffOfId[diffid] = newDiff
// }
// // update styles on this DiffArea
// this._addDiffAreaStylesToURI(uri)
// }
// }
// // @throttle(100)
// private _writeDiffAreaLLMText(diffArea: DiffArea, newCodeSoFar: string) {
// // ----------- 1. Write the new code to the document -----------
// // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out
// const uri = diffArea._URI
// const computedDiffs = findDiffs(diffArea.originalCode, newCodeSoFar)
// // if not streaming, just write the new code
// if (!diffArea._sweepState.isStreaming) {
// this._writeText(uri, newCodeSoFar,
// { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed
// )
// }
// // if streaming, use diffs to figure out where to write new code
// else {
// // these are two different coordinate systems - new and old line number
// let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
// let oldFileStartLine: number // get original[oldStartingPoint...]
// const lastDiff = computedDiffs.pop()
// if (!lastDiff) {
// // if the writing is identical so far, display no changes
// newFileEndLine = 1
// oldFileStartLine = 1
// }
// else {
// if (lastDiff.type === 'insertion') {
// newFileEndLine = lastDiff.endLine
// oldFileStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'deletion') {
// newFileEndLine = lastDiff.startLine
// oldFileStartLine = lastDiff.originalStartLine
// }
// else if (lastDiff.type === 'edit') {
// newFileEndLine = lastDiff.endLine
// oldFileStartLine = lastDiff.originalStartLine
// }
// else {
// throw new Error(`Void: diff.type not recognized on: ${lastDiff}`)
// }
// }
// diffArea._sweepState.line = newFileEndLine
// // lines are 1-indexed
// const newFileTop = newCodeSoFar.split('\n').slice(0, (newFileEndLine - 1)).join('\n')
// const oldFileBottom = diffArea.originalCode.split('\n').slice((oldFileStartLine - 1), Infinity).join('\n')
// const newCode = `${newFileTop}\n${oldFileBottom}`
// this._writeText(uri, newCode,
// { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER, } // 1-indexed
// )
// }
// return computedDiffs
// }
// private async _initializeStream(opts: LLMFeatureSelection, diffRepr: string, uri: URI,) {
// // diff area begin and end line
// const numLines = this._getNumLines(uri)
// if (numLines === null) return
// const beginLine = 1
// const endLine = numLines
// // check if there's overlap with any other diffAreas and return early if there is
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) {
// const da2 = this.diffAreaOfId[diffareaid]
// if (!da2) continue
// const noOverlap = da2.startLine > endLine || da2.endLine < beginLine
// if (!noOverlap) {
// // TODO add a message here that says this to the user too
// console.error('Not diffing because found overlap:', this.diffAreasOfURI[uri.fsPath], beginLine, endLine)
// return
// }
// }
// const currentFileStr = this._readURI(uri)
// if (currentFileStr === null) return
// const originalCode = currentFileStr.split('\n').slice((beginLine - 1), (endLine - 1) + 1).join('\n')
// // add to history
// const { onFinishEdit } = this._addToHistory(uri)
// // create a diffArea for the stream
// const diffareaid = this._diffareaidPool++
// // in ctrl+L the start and end lines are the full document
// const diffArea: DiffArea = {
// diffareaid: diffareaid,
// // originalStartLine: beginLine,
// // originalEndLine: endLine,
// originalCode: originalCode,
// startLine: beginLine,
// endLine: endLine, // starts out the same as the current file
// shouldHighlight: false,
// _URI: uri,
// _sweepState: {
// isStreaming: true,
// line: 1,
// },
// _diffOfId: {}, // added later
// }
// console.log('adding uri.fspath', uri.fsPath, diffArea.diffareaid.toString())
// this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString())
// this.diffAreaOfId[diffArea.diffareaid] = diffArea
// // actually call the LLM
// const promptContent = `\
// ORIGINAL_CODE
// \`\`\`
// ${originalCode}
// \`\`\`
// DIFF
// \`\`\`
// ${diffRepr}
// \`\`\`
// 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.
// `
// await new Promise<void>((resolve, reject) => {
// let streamRequestId: string | null = null
// const object: ServiceSendLLMMessageParams = {
// logging: { loggingName: 'streamChunk' },
// messages: [
// { role: 'system', content: inlineDiff_systemMessage, },
// // TODO include more context too
// { role: 'user', content: promptContent, }
// ],
// onText: ({ newText, fullText }) => {
// this._writeDiffAreaLLMText(diffArea, fullText)
// this._refreshDiffsInURI(uri)
// },
// onFinalMessage: ({ fullText }) => {
// this._writeText(uri, fullText,
// { startLineNumber: diffArea.startLine, startColumn: 1, endLineNumber: diffArea.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
// )
// diffArea._sweepState = { isStreaming: false, line: null }
// this._refreshDiffsInURI(uri)
// resolve();
// },
// onError: (e: any) => {
// console.error('Error rewriting file with diff', e);
// // TODO indicate there was an error
// if (streamRequestId)
// this._llmMessageService.abort(streamRequestId)
// diffArea._sweepState = { isStreaming: false, line: null }
// resolve();
// },
// ...opts
// }
// streamRequestId = this._llmMessageService.sendLLMMessage(object)
// })
// onFinishEdit()
// }
// async startStreaming(opts: LLMFeatureSelection, userMessage: string) {
// const editor = this._editorService.getActiveCodeEditor()
// if (!editor) return
// const uri = editor.getModel()?.uri
// if (!uri) return
// // TODO reject all diffs in the diff area
// // TODO deselect user's cursor
// this._initializeStream(opts, userMessage, uri)
// }
// interruptStreaming() {
// // TODO add abort
// }
// addDiffArea({ uri, startLine, endLine, originalCode }: { uri: URI, startLine: number, endLine: number, originalCode: string }) {
// const diffareaid = this._diffareaidPool++
// const diffArea: DiffArea = {
// diffareaid: diffareaid,
// originalCode,
// startLine,
// endLine,
// shouldHighlight: true,
// _URI: uri,
// _sweepState: {
// isStreaming: false,
// line: null,
// },
// _diffOfId: {},
// }
// this.diffAreasOfURI[uri.fsPath].add(diffArea.diffareaid.toString())
// this.diffAreaOfId[diffArea.diffareaid] = diffArea
// this._refreshDiffsInURI(uri)
// }
// // called on void.acceptDiff
// public async acceptDiff({ diffid }: { diffid: number }) {
// const diff = this.diffOfId[diffid]
// if (!diff) return
// const { diffareaid } = diff
// const diffArea = this.diffAreaOfId[diffareaid]
// if (!diffArea) return
// const uri = diffArea._URI
// // add to history
// const { onFinishEdit } = this._addToHistory(uri)
// const originalLines = diffArea.originalCode.split('\n')
// let newOriginalCode: string
// if (diff.type === 'deletion') {
// newOriginalCode = [
// ...originalLines.slice(0, (diff.originalStartLine - 1)), // everything before startLine
// // <-- deletion has nothing here
// ...originalLines.slice((diff.originalEndLine - 1) + 1, Infinity) // everything after endLine
// ].join('\n')
// }
// else if (diff.type === 'insertion') {
// newOriginalCode = [
// ...originalLines.slice(0, (diff.originalStartLine - 1)), // everything before startLine
// diff.code, // code
// ...originalLines.slice((diff.originalStartLine - 1), Infinity) // startLine (inclusive) and on (no +1)
// ].join('\n')
// }
// else if (diff.type === 'edit') {
// newOriginalCode = [
// ...originalLines.slice(0, (diff.originalStartLine - 1)), // everything before startLine
// diff.code, // code
// ...originalLines.slice((diff.originalEndLine - 1) + 1, Infinity) // everything after endLine
// ].join('\n')
// }
// else {
// throw new Error(`Void error: ${diff}.type not recognized`)
// }
// // console.log('DIFF', diff)
// // console.log('DIFFAREA', diffArea)
// // console.log('ORIGINAL', diffArea.originalCode)
// // console.log('new original Code', newOriginalCode)
// // update code now accepted as original
// diffArea.originalCode = newOriginalCode
// // delete the diff
// this._deleteDiff(diff)
// // diffArea should be removed if it has no more diffs in it
// if (Object.keys(diffArea._diffOfId).length === 0) {
// this._deleteDiffArea(diffArea)
// }
// this._refreshDiffsInURI(uri)
// onFinishEdit()
// }
// // called on void.rejectDiff
// public async rejectDiff({ diffid }: { diffid: number }) {
// const diff = this.diffOfId[diffid]
// if (!diff) return
// const { diffareaid } = diff
// const diffArea = this.diffAreaOfId[diffareaid]
// if (!diffArea) return
// const uri = diffArea._URI
// // add to history
// const { onFinishEdit } = this._addToHistory(uri)
// let writeText: string
// let toRange: IRange
// // if it was a deletion, need to re-insert
// // (this image applies to writeText and toRange, not newOriginalCode)
// // A
// // |B <-- deleted here, diff.startLine == diff.endLine
// // C
// if (diff.type === 'deletion') {
// writeText = diff.originalCode + '\n'
// toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.startLine, endColumn: 1 }
// }
// // if it was an insertion, need to delete all the lines
// // (this image applies to writeText and toRange, not newOriginalCode)
// // |A <-- startLine
// // B| <-- endLine (we want to delete this whole line)
// // C
// else if (diff.type === 'insertion') {
// writeText = ''
// toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed
// }
// // if it was an edit, just edit the range
// // (this image applies to writeText and toRange, not newOriginalCode)
// // |A <-- startLine
// // B| <-- endLine (just swap out these lines for the originalCode)
// // C
// else if (diff.type === 'edit') {
// writeText = diff.originalCode
// toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine, endColumn: Number.MAX_SAFE_INTEGER } // 1-indexed
// }
// else {
// throw new Error(`Void error: ${diff}.type not recognized`)
// }
// // update the file
// this._writeText(uri, writeText, toRange)
// // originalCode does not change!
// // delete the diff
// this._deleteDiff(diff)
// // diffArea should be removed if it has no more diffs in it
// if (Object.keys(diffArea._diffOfId).length === 0) {
// this._deleteDiffArea(diffArea)
// }
// this._refreshDiffsInURI(uri)
// onFinishEdit()
// }
// }
// registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager);
// class AcceptRejectWidget extends Widget implements IOverlayWidget {
// public getId() { return this.ID }
// public getDomNode() { return this._domNode; }
// public getPosition() { return null }
// private readonly _domNode: HTMLElement;
// private readonly editor
// private readonly ID
// private readonly startLine
// constructor({ editor, onAccept, onReject, diffid, startLine }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number }) {
// super()
// this.ID = editor.getModel()?.uri.fsPath + diffid;
// this.editor = editor;
// this.startLine = startLine;
// // Create container div with buttons
// const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
// dom.h('button@acceptButton', []),
// dom.h('button@rejectButton', [])
// ]);
// // Style the container
// buttons.style.display = 'flex';
// buttons.style.position = 'absolute';
// buttons.style.gap = '4px';
// buttons.style.padding = '4px';
// buttons.style.zIndex = '1000';
// // Style accept button
// acceptButton.onclick = onAccept;
// acceptButton.textContent = 'Accept';
// acceptButton.style.backgroundColor = '#28a745';
// acceptButton.style.color = 'white';
// acceptButton.style.border = 'none';
// acceptButton.style.padding = '4px 8px';
// acceptButton.style.borderRadius = '3px';
// acceptButton.style.cursor = 'pointer';
// // Style reject button
// rejectButton.onclick = onReject;
// rejectButton.textContent = 'Reject';
// rejectButton.style.backgroundColor = '#dc3545';
// rejectButton.style.color = 'white';
// rejectButton.style.border = 'none';
// rejectButton.style.padding = '4px 8px';
// rejectButton.style.borderRadius = '3px';
// rejectButton.style.cursor = 'pointer';
// this._domNode = buttons;
// const updateTop = () => {
// const topPx = editor.getTopForLineNumber(this.startLine) - editor.getScrollTop()
// this._domNode.style.top = `${topPx}px`
// }
// const updateLeft = () => {
// const leftPx = 0//editor.getScrollLeft() - editor.getScrollWidth()
// this._domNode.style.left = `${leftPx}px`
// }
// updateTop()
// updateLeft()
// this._register(editor.onDidScrollChange(e => { updateTop() }))
// this._register(editor.onDidChangeModelContent(e => { updateTop() }))
// this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() }))
// // mount this widget
// editor.addOverlayWidget(this);
// // console.log('created elt', this._domNode)
// }
// public override dispose(): void {
// this.editor.removeOverlayWidget(this)
// super.dispose()
// }
// }

View file

@ -0,0 +1,178 @@
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { ICodeEditor, IViewZone } 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 zone to a Model (aka URI), instead of just to a single editor
export interface IZoneStyleService {
readonly _serviceBrand: undefined;
addConsistentZoneToURI(uri: URI, iZoneFn: (editor: ICodeEditor) => IViewZone, iOther?: (editor: ICodeEditor) => (() => void)): string;
removeConsistentZoneFromURI(consistentZoneId: string): void;
}
export const IZoneStyleService = createDecorator<IZoneStyleService>('zoneStyleService');
export class ZoneStyleService extends Disposable {
readonly _serviceBrand: undefined
// the zones that are attached to each URI, completely independent from current state of editors
private readonly consistentZoneIdsOfURI: Record<string, Set<string> | undefined> = {}
private readonly infoOfConsistentZoneId: Record<string, {
uri: URI
iZoneFn: (editor: ICodeEditor) => IViewZone,
iOther?: (editor: ICodeEditor) => (() => void),
}> = {}
// listener disposables
private readonly disposablesOfEditorId: Record<string, Set<IDisposable> | undefined> = {}
// current state of zones on each editor, and the fns to call to remove them. A zone is the actual zone plus whatever iOther you put on it.
private readonly zoneIdsOfEditorId: Record<string, Set<string> | undefined> = {}
private readonly removeFnOfZoneId: Record<string, () => void> = {}
private readonly consistentZoneIdOfZoneId: Record<string, string> = {}
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
const removeZonesFromEditor = (editor: ICodeEditor) => {
const editorId = editor.getId()
for (const zoneId of this.zoneIdsOfEditorId[editorId] ?? [])
this._removeZoneIdFromEditor(editor, zoneId)
}
// put zones on the editor, based on the consistentZones for that URI
const putZonesOnEditor = (editor: ICodeEditor, uri: URI | null) => {
if (!uri) return
for (const consistentZoneId of this.consistentZoneIdsOfURI[uri.fsPath] ?? [])
this._putZoneOnEditor(editor, consistentZoneId)
}
const addTabSwitchListeners = (editor: ICodeEditor) => {
const editorId = editor.getId()
if (!(editorId in this.disposablesOfEditorId))
this.disposablesOfEditorId[editorId] = new Set()
this.disposablesOfEditorId[editorId]!.add(
editor.onDidChangeModel(e => {
removeZonesFromEditor(editor)
putZonesOnEditor(editor, e.newModelUrl)
})
)
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
putZonesOnEditor(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 zones and call any disposables it has
this._register(this._editorService.onCodeEditorRemove(editor => {
const editorId = editor.getId()
removeZonesFromEditor(editor)
for (const d of this.disposablesOfEditorId[editorId] ?? [])
d.dispose()
delete this.disposablesOfEditorId[editorId]
}))
}
_putZoneOnEditor(editor: ICodeEditor, consistentZoneId: string) {
const { iZoneFn, iOther } = this.infoOfConsistentZoneId[consistentZoneId]
editor.changeViewZones(accessor => {
// add zone + other
const zoneId = accessor.addZone(iZoneFn(editor))
const rmFn = iOther?.(editor)
const editorId = editor.getId()
if (!(editorId in this.zoneIdsOfEditorId))
this.zoneIdsOfEditorId[editorId] = new Set()
this.zoneIdsOfEditorId[editorId]!.add(zoneId)
// fn that describes how to remove zone + other
this.removeFnOfZoneId[zoneId] = () => {
editor.changeViewZones(accessor => accessor.removeZone(zoneId))
rmFn?.()
}
this.consistentZoneIdOfZoneId[zoneId] = consistentZoneId
})
}
_removeZoneIdFromEditor(editor: ICodeEditor, zoneId: string) {
const editorId = editor.getId()
this.zoneIdsOfEditorId[editorId]?.delete(zoneId)
this.removeFnOfZoneId[zoneId]?.()
delete this.removeFnOfZoneId[zoneId]
delete this.consistentZoneIdOfZoneId[zoneId]
}
addConsistentZoneToURI(uri: URI, iZoneFn: (editor: ICodeEditor) => IViewZone, iOther?: (editor: ICodeEditor) => (() => void)) {
const consistentZoneId = generateUuid()
this.infoOfConsistentZoneId[consistentZoneId] = { iZoneFn, iOther, uri }
if (!(uri.fsPath in this.consistentZoneIdsOfURI))
this.consistentZoneIdsOfURI[uri.fsPath] = new Set()
this.consistentZoneIdsOfURI[uri.fsPath]!.add(consistentZoneId)
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors)
this._putZoneOnEditor(editor, consistentZoneId)
return consistentZoneId
}
removeConsistentZoneFromURI(consistentZoneId: string) {
if (!(consistentZoneId in this.infoOfConsistentZoneId))
return
const { uri } = this.infoOfConsistentZoneId[consistentZoneId]
const editors = this._editorService.listCodeEditors().filter(e => e.getModel()?.uri.fsPath === uri.fsPath)
for (const editor of editors) {
for (const zoneId of this.zoneIdsOfEditorId[editor.getId()] ?? []) {
if (this.consistentZoneIdOfZoneId[zoneId] === consistentZoneId)
this._removeZoneIdFromEditor(editor, zoneId)
}
}
// clear
this.consistentZoneIdsOfURI[uri.fsPath]?.delete(consistentZoneId)
delete this.infoOfConsistentZoneId[consistentZoneId]
}
}
registerSingleton(IZoneStyleService, ZoneStyleService, InstantiationType.Eager);

View file

@ -26,7 +26,7 @@ import { ILLMMessageService } from '../../../../../../../platform/void/common/ll
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'
@ -34,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'
@ -94,23 +95,24 @@ export const IconWarning = ({ size, className = '' }: { size: number, className?
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
viewBox="0 0 16 16"
width={size}
height={size}
xmlns="http://www.w3.org/2000/svg"
>
{/* Warning triangle */}
<path d="M12 3L2 21h20L12 3zm0 3.3L19.3 19H4.7L12 6.3z" />
{/* Exclamation mark */}
<rect x="11" y="10" width="2" height="6" />
<circle cx="12" cy="18" r="1" />
<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
@ -119,20 +121,20 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re
`}
{...props}
>
<IconArrowUp size={20} className="stroke-[2]" />
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
</button>
}
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
className={`size-[20px] rounded-full bg-white cursor-pointer flex items-center justify-center
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
${className}
`}
type='button'
{...props}
>
<IconSquare size={10} className="stroke-[2]" />
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
</button>
}
@ -211,7 +213,7 @@ 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) => {
@ -224,13 +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 p-1
className={`flex items-center gap-1 relative
rounded-md p-1
w-fit h-fit
select-none
bg-vscode-editor-bg hover:brightness-90
border border-vscode-button-border rounded-md
text-vscode-commandcenter-inactive-fg
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]
@ -239,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'>{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();
@ -261,11 +283,12 @@ export const SelectedFiles = (
>
<IconX size={16} className="p-[2px] stroke-[3]" />
</span>
}
} */}
</div>
{/* selection text */}
{showSelectionText &&
<div className='w-full'>
<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>
}
@ -482,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); } }}
@ -538,7 +561,13 @@ export const SidebarChat = () => {
// .split(' ')
// .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-border-none @@[&_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`
}
>
@ -556,7 +585,10 @@ export const SidebarChat = () => {
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px] @@[&_select]:!void-border-none @@[&_select]:!void-outline-none'>
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+L' />
</div>

View file

@ -299,7 +299,7 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
return <div ref={divRef}>
<WidgetComponent
className='relative z-0'
className='relative z-0 text-sm'
ctor={useCallback((container) =>
instantiationService.createInstance(
CodeEditorWidget,
@ -312,6 +312,8 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
alwaysConsumeMouseWheel: false,
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
},
scrollBeyondLastLine: false,

View file

@ -9,7 +9,7 @@ import { useSettingsState, useRefreshModelState, useAccessor } from '../util/ser
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 accessor = useAccessor()
@ -44,12 +44,28 @@ const ModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const DummySelectBox = () => {
return <div>
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={24}
className='text-orange-900'
size={20}
className='mr-1 brightness-90'
/>
<span className='text-orange-900'>Add a model</span>
<span>Model required</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}

View file

@ -1,12 +1,23 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName } 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 { 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
@ -34,14 +45,12 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
const isRefreshing = state === 'refreshing'
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <div className='flex items-center py-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-200/10'>
<button className='flex items-center' disabled={isRefreshing || justFinished} onClick={() => { refreshModelService.refreshModels(providerName) }}>
{isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
</button>
<span className='opacity-50'>{
justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`
}</span>
</div>
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 = () => {
@ -49,7 +58,7 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName].enabled) return null
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
return <RefreshModelButton key={providerName} providerName={providerName} />
})
@ -162,7 +171,7 @@ export const ModelDump = () => {
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
@ -220,7 +229,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
placeholder={`Enter your ${providerTitle} ${settingTitle} (${placeholder}).`}
placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@ -231,10 +240,27 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
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)
@ -251,21 +277,22 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
// const voidSettingsState = useSettingsState()
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
const { enabled } = voidSettingsState.settingsOfProvider[providerName]
// const { enabled } = voidSettingsState.settingsOfProvider[providerName]
const settingNames = customSettingNamesOfProvider(providerName)
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <div className='my-4'>
<div className='flex items-center w-full gap-4'>
<h3 className='text-xl truncate'>{providerTitle}</h3>
{/* enable provider switch */}
<VoidSwitch
{/* <VoidSwitch
value={!!enabled}
onChange={
useCallback(() => {
@ -273,7 +300,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef)
}, [voidSettingsService, providerName])}
size='sm+'
/>
/> */}
</div>
<div className='px-0'>
@ -282,7 +309,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</div>
</div>
</div >
}
@ -294,29 +321,49 @@ 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 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>
})}
</>
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}
/>
})
}
@ -359,20 +406,21 @@ export const Settings = () => {
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<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 />
<RefreshableModels />
</ErrorBoundary>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
<VoidFeatureFlagSettings />
{/* <VoidFeatureFlagSettings /> */}
</div>
</div>

View file

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