Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e7e54b88f | |||
| 1b43799371 | |||
| 38370c751e | |||
| 764b1a2ccf | |||
| 1dffbfb061 | |||
| df6de2d7fc | |||
| 4ebfa91a83 | |||
| 9f20476eea | |||
| fd5e523434 | |||
| 2c2714273e | |||
| c650091418 | |||
| d96a9d5f6b | |||
| a9b3cc146b | |||
| bdb897d032 | |||
| 2a876d8efe | |||
| bf4392a52f | |||
| dd24c3180d | |||
| f40acd76f1 | |||
| 96de4f028f | |||
| 2ecce822f3 | |||
| ea10765abd | |||
| 16f6181395 | |||
| 19ebf4a1a2 | |||
| 63b71dec24 | |||
| 1079893527 | |||
| 64ac6d4a12 | |||
| 9005de65a6 | |||
| 86dfc5521d | |||
| b005b1e95a | |||
| ce14986d2f | |||
| 06ce9e1017 | |||
| 09f5d3e14b | |||
| 73c3a8133a | |||
| 8918a14404 | |||
| 1959c53d75 | |||
| 33d80bed80 | |||
| 970f0bdb9d | |||
| 7711c74ef0 | |||
| e14aab632d | |||
| 62c2622ced | |||
| 0f26ee2288 | |||
| 6a44d668db | |||
| 858b6f6a91 | |||
| 02f64b7ff6 | |||
| ac1788ae9a | |||
| 5699cf19f4 | |||
| 9ab9196b38 | |||
| baa89cc17d | |||
| 667769c987 | |||
| d3aa0bc3cc | |||
| d3547134e7 | |||
| b92420012c | |||
| 0fd10f404e | |||
| 137e0068a3 | |||
| 366dbf0b52 | |||
| 89d08071fc | |||
| 8a8ed1ac56 | |||
| 491312218f | |||
| b74b031906 | |||
| 5608aca168 | |||
| fecfbd924a | |||
| a78a6169f8 | |||
| 628adedaec | |||
| 432a1766af | |||
| 74f8303803 | |||
| 2bc3d67e39 | |||
| af41d6a439 | |||
| 7244d433dd | |||
| 131493b5e1 | |||
| 8591d06244 | |||
| 152e605856 | |||
| bc6150aeac | |||
| 05d8b3a982 | |||
| d91ec9da2e | |||
| 6ad48ffa20 | |||
| 0975f1bf5f | |||
| 9cfcf396c1 | |||
| 0bcd88dad6 | |||
| 343ee5eb94 | |||
| d9e4679b65 | |||
| b01684393a | |||
| 34b8027fb9 | |||
| f7af9c336b | |||
| a479a32ad5 | |||
| 7cdb003c47 | |||
| 4f9f16b93f | |||
| 0dfa81f637 | |||
| d504daffa5 | |||
| fc97949887 | |||
| 0346e90ca7 |
@@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action {
|
||||
label: string,
|
||||
@ICommandService private readonly commandService: ICommandService
|
||||
) {
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.starFull));
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.pinned));
|
||||
}
|
||||
|
||||
override run(context?: IEditorCommandsContext): Promise<void> {
|
||||
@@ -440,24 +440,6 @@ export class UnpinEditorAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
export class PinEditorAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.pinEditor';
|
||||
static readonly LABEL = localize('pinEditor', "Pin Editor");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@ICommandService private readonly commandService: ICommandService
|
||||
) {
|
||||
super(id, label, ThemeIcon.asClassName(Codicon.star));
|
||||
}
|
||||
|
||||
override async run(context?: IEditorCommandsContext): Promise<void> {
|
||||
return this.commandService.executeCommand('workbench.action.pinEditor', undefined, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class CloseEditorTabAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.action.closeActiveEditor';
|
||||
|
||||
@@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable {
|
||||
|
||||
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
|
||||
const button3 = append(recentsBox, $('button'));
|
||||
button3.textContent = 'Void Settings'
|
||||
button3.textContent = `Void Settings`
|
||||
button3.style.display = 'block'
|
||||
button3.style.marginLeft = 'auto'
|
||||
button3.style.marginRight = 'auto'
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-fixed > .tab-actions {
|
||||
flex: 0;
|
||||
overflow: visible; /* ensure tab actions are always visible */
|
||||
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
|
||||
@@ -399,8 +399,18 @@
|
||||
overflow: visible; /* ...but still show the tab actions on hover, focus and when dirty or sticky */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-action-off:not(.dirty) > .tab-actions,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky-compact > .tab-actions {
|
||||
display: none; /* only hide tab actions when sticky-compact */
|
||||
display: none; /* hide the tab actions when we are configured to hide it (unless dirty, but always when sticky-compact) */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-actions .action-label, /* always show tab actions for active tab */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-actions .action-label:focus, /* always show tab actions on focus */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-actions .action-label, /* always show tab actions on hover */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-actions .action-label, /* always show tab actions on hover */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label, /* always show tab actions for sticky tabs */
|
||||
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-actions .action-label { /* always show tab actions for dirty tabs */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container {
|
||||
@@ -434,11 +444,11 @@
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-actions .action-label,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.pinned-action-off) > .tab-actions .action-label,
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-actions .action-label {
|
||||
opacity: 1;
|
||||
opacity: 0.5; /* show tab actions dimmed for inactive group */
|
||||
}
|
||||
|
||||
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label {
|
||||
opacity: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Tab Actions: Off */
|
||||
|
||||
@@ -35,7 +35,7 @@ import { MergeGroupMode, IMergeGroupOptions } from '../../../services/editor/com
|
||||
import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from '../../../../base/browser/dom.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from './editor.js';
|
||||
import { CloseEditorTabAction, PinEditorAction, UnpinEditorAction } from './editorActions.js';
|
||||
import { CloseEditorTabAction, UnpinEditorAction } from './editorActions.js';
|
||||
import { assertAllDefined, assertIsDefined } from '../../../../base/common/types.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { basenameOrAuthority } from '../../../../base/common/resources.js';
|
||||
@@ -113,7 +113,6 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
|
||||
private readonly closeEditorAction = this._register(this.instantiationService.createInstance(CloseEditorTabAction, CloseEditorTabAction.ID, CloseEditorTabAction.LABEL));
|
||||
private readonly unpinEditorAction = this._register(this.instantiationService.createInstance(UnpinEditorAction, UnpinEditorAction.ID, UnpinEditorAction.LABEL));
|
||||
private readonly pinEditorAction = this._register(this.instantiationService.createInstance(PinEditorAction, PinEditorAction.ID, PinEditorAction.LABEL)); // Add this line
|
||||
|
||||
private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER));
|
||||
private tabLabels: IEditorInputLabel[] = [];
|
||||
@@ -1519,28 +1518,28 @@ export class MultiEditorTabsControl extends EditorTabsControl {
|
||||
this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel);
|
||||
|
||||
// Action
|
||||
const hasCloseAction = options.tabActionCloseVisibility;
|
||||
const hasAction = true; // Always show actions
|
||||
const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility;
|
||||
const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility;
|
||||
const hasAction = hasUnpinAction || hasCloseAction;
|
||||
|
||||
// Determine which action to show
|
||||
let tabAction;
|
||||
if (isTabSticky) {
|
||||
tabAction = this.unpinEditorAction;
|
||||
if (hasAction) {
|
||||
tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction;
|
||||
} else {
|
||||
tabAction = this.pinEditorAction; // Use pin action instead of close action
|
||||
// Even if the action is not visible, add it as it contains the dirty indicator
|
||||
tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction;
|
||||
}
|
||||
|
||||
// Update action bar
|
||||
if (!tabActionBar.hasAction(tabAction)) {
|
||||
if (!tabActionBar.isEmpty()) {
|
||||
tabActionBar.clear();
|
||||
}
|
||||
|
||||
tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) });
|
||||
}
|
||||
|
||||
tabContainer.classList.toggle('sticky', isTabSticky);
|
||||
tabContainer.classList.toggle(`pinned-action-off`, false);
|
||||
tabContainer.classList.toggle(`close-action-off`, !hasCloseAction);
|
||||
tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction);
|
||||
tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction);
|
||||
|
||||
for (const option of ['left', 'right']) {
|
||||
tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
export interface IMarkerCheckService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IMarkerCheckService = createDecorator<IMarkerCheckService>('markerCheckService');
|
||||
|
||||
class MarkerCheckService extends Disposable implements IMarkerCheckService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IMarkerService private readonly _markerService: IMarkerService,
|
||||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
|
||||
setInterval(async () => {
|
||||
const allMarkers = this._markerService.read();
|
||||
const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error);
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
|
||||
console.log(`----------------------------------------------`);
|
||||
|
||||
console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file
|
||||
|
||||
try {
|
||||
// Get the text model for the file
|
||||
const modelReference = await this._textModelService.createModelReference(error.resource);
|
||||
const model = modelReference.object.textEditorModel;
|
||||
|
||||
// Create a range from the marker
|
||||
const range = new Range(
|
||||
error.startLineNumber,
|
||||
error.startColumn,
|
||||
error.endLineNumber,
|
||||
error.endColumn
|
||||
);
|
||||
|
||||
// Get code action providers for this model
|
||||
const codeActionProvider = this._languageFeaturesService.codeActionProvider;
|
||||
const providers = codeActionProvider.ordered(model);
|
||||
|
||||
if (providers.length > 0) {
|
||||
// Request code actions from each provider
|
||||
for (const provider of providers) {
|
||||
const context: CodeActionContext = {
|
||||
trigger: CodeActionTriggerType.Invoke, // keeping 'trigger' since it works
|
||||
only: 'quickfix' // adding this to filter for quick fixes
|
||||
};
|
||||
|
||||
const actions = await provider.provideCodeActions(
|
||||
model,
|
||||
range,
|
||||
context,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
if (actions?.actions?.length) {
|
||||
|
||||
const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error
|
||||
const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports
|
||||
quickFixesForImports
|
||||
|
||||
if (quickFixes.length > 0) {
|
||||
console.log('Available Quick Fixes:');
|
||||
quickFixes.forEach(action => {
|
||||
console.log(`- ${action.title}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispose the model reference
|
||||
modelReference.dispose();
|
||||
} catch (e) {
|
||||
console.error('Error getting quick fixes:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fixErrorsInFiles(uris: URI[], contextSoFar: []) {
|
||||
// const allMarkers = this._markerService.read();
|
||||
|
||||
|
||||
// check errors in files
|
||||
|
||||
|
||||
// give LLM errors in files
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// private _onMarkersChanged = (changedResources: readonly URI[]): void => {
|
||||
// for (const resource of changedResources) {
|
||||
// const markers = this._markerService.read({ resource });
|
||||
|
||||
// if (markers.length === 0) {
|
||||
// console.log(`${resource.toString()}: No diagnostics`);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// console.log(`Diagnostics for ${resource.toString()}:`);
|
||||
// markers.forEach(marker => this._logMarker(marker));
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IMarkerCheckService, MarkerCheckService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,187 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
// import { URI } from '../../../../base/common/uri.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
// import { IToolService, ToolService } from '../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
|
||||
|
||||
export type SearchAndReplaceBlock = {
|
||||
search: string;
|
||||
replace: string;
|
||||
}
|
||||
|
||||
// service that manages state
|
||||
export type ApplyState = {
|
||||
[applyBoxId: string]: {
|
||||
searchAndReplaceBlocks: SearchAndReplaceBlock;
|
||||
}
|
||||
}
|
||||
|
||||
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
|
||||
|
||||
export interface IFastApplyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// readonly state: ApplyState; // readonly to the user
|
||||
// setState(newState: Partial<ApplyState>): void;
|
||||
// onDidChangeState: Event<void>;
|
||||
}
|
||||
|
||||
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
|
||||
class VoidFastApplyService extends Disposable implements IFastApplyService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// static readonly ID = 'voidFastApplyService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
|
||||
// state
|
||||
// state: ApplyState
|
||||
|
||||
constructor(
|
||||
// @IToolService private readonly toolService: ToolService
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
// this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<ApplyState>) {
|
||||
|
||||
// this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
aiSearch(searchStr: string) {
|
||||
|
||||
}
|
||||
|
||||
aiReplace(searchStr: string, replaceStr: string) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 1. search(ai)
|
||||
// - tool use to find all possible changes
|
||||
// - if search only: is this file related to the search?
|
||||
// - if search + replace: should I modify this file?
|
||||
// 2. replace(ai)
|
||||
// - what changes to make?
|
||||
// 3. postprocess errors
|
||||
// -fastapply changes simultaneously
|
||||
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
|
||||
|
||||
|
||||
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
|
||||
|
||||
// // const relevantURIs: URI[] = []
|
||||
// // const gatherPrompt = `\
|
||||
// // asdasdas
|
||||
// // `
|
||||
// // const filterPrompt = `\
|
||||
// // Is this file relevant?
|
||||
// // `
|
||||
|
||||
|
||||
// // // optimizations (DO THESE LATER!!!!!!)
|
||||
// // // if tool includes a uri in uriSet, skip it obviously
|
||||
// // let uriSet = new Set<URI>()
|
||||
// // // gather
|
||||
// // let messages = []
|
||||
// // while (true) {
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
// // onError: (error) => {
|
||||
// // rej(error)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push({ role: 'tool', content: turnToString(result) })
|
||||
|
||||
// // sendLLMMessage({
|
||||
// // messages: { 'Output ': result },
|
||||
// // onFinalMessage: (r) => {
|
||||
// // // output is file1\nfile2\nfile3\n...
|
||||
// // }
|
||||
// // })
|
||||
|
||||
// // uriSet.add(...)
|
||||
// // }
|
||||
|
||||
// // // writes
|
||||
// // if (!replaceClause) return
|
||||
|
||||
// // for (const uri of uriSet) {
|
||||
// // // in future, batch these
|
||||
// // applyWorkflow({ uri, applyStr: replaceClause })
|
||||
// // }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // while (true) {
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push(result)
|
||||
|
||||
// // }
|
||||
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
|
||||
|
||||
// for (const uri of relevantURIs) {
|
||||
|
||||
// uri
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// // should I change this file?
|
||||
// // if so what changes to make?
|
||||
|
||||
|
||||
|
||||
// // fast apply the changes
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
|
||||
@@ -16,9 +16,9 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorResourceAccessor } from '../../../common/editor.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
@@ -415,9 +415,6 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS
|
||||
// }
|
||||
|
||||
|
||||
const allLinebreakSymbols = ['\r\n', '\n']
|
||||
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
|
||||
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
|
||||
|
||||
@@ -798,26 +795,27 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
||||
},
|
||||
useProviderFor: 'Autocomplete',
|
||||
logging: { loggingName: 'Autocomplete' },
|
||||
onText: async ({ fullText, newText }) => {
|
||||
onText: () => { }, // unused in FIMMessage
|
||||
// onText: async ({ fullText, newText }) => {
|
||||
|
||||
newAutocompletion.insertText = fullText
|
||||
// newAutocompletion.insertText = fullText
|
||||
|
||||
// count newlines in newText
|
||||
const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
|
||||
newAutocompletion._newlineCount += numNewlines
|
||||
// // count newlines in newText
|
||||
// const numNewlines = newText.match(/\n|\r\n/g)?.length || 0
|
||||
// newAutocompletion._newlineCount += numNewlines
|
||||
|
||||
// if too many newlines, resolve up to last newline
|
||||
if (newAutocompletion._newlineCount > 10) {
|
||||
const lastNewlinePos = fullText.lastIndexOf('\n')
|
||||
newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
|
||||
resolve(newAutocompletion.insertText)
|
||||
return
|
||||
}
|
||||
// // if too many newlines, resolve up to last newline
|
||||
// if (newAutocompletion._newlineCount > 10) {
|
||||
// const lastNewlinePos = fullText.lastIndexOf('\n')
|
||||
// newAutocompletion.insertText = fullText.substring(0, lastNewlinePos)
|
||||
// resolve(newAutocompletion.insertText)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// reject('LLM response did not match user\'s text.')
|
||||
// }
|
||||
},
|
||||
// // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
|
||||
// // reject('LLM response did not match user\'s text.')
|
||||
// // }
|
||||
// },
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
|
||||
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
|
||||
|
||||
@@ -12,10 +12,23 @@ import { URI } from '../../../../base/common/uri.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles } from './prompt/prompts.js';
|
||||
import { LLMChatMessage } from '../common/llmMessageTypes.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js';
|
||||
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js';
|
||||
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (condition(arr[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
|
||||
export type CodeSelection = {
|
||||
@@ -34,9 +47,24 @@ export type FileSelection = {
|
||||
|
||||
export type StagingSelectionItem = CodeSelection | FileSelection
|
||||
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
name: T; // internal use
|
||||
params: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // result
|
||||
result: ToolCallReturnType[T]; // text message of result
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
} | {
|
||||
role: 'user';
|
||||
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
@@ -45,22 +73,20 @@ export type ChatMessage =
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
isBeingEdited: boolean;
|
||||
}
|
||||
}
|
||||
| {
|
||||
} | {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
}
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
}
|
||||
| ToolMessage<ToolName>
|
||||
|
||||
type UserMessageType = ChatMessage & { role: 'user' }
|
||||
type UserMessageState = UserMessageType['state']
|
||||
|
||||
export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false }
|
||||
export const defaultMessageState: UserMessageState = {
|
||||
stagingSelections: [],
|
||||
isBeingEdited: false,
|
||||
}
|
||||
|
||||
// a 'thread' means a chat message history
|
||||
export type ChatThreads = {
|
||||
@@ -98,7 +124,7 @@ export type ThreadStreamState = {
|
||||
const newThreadObject = () => {
|
||||
const now = new Date().toISOString()
|
||||
return {
|
||||
id: new Date().getTime().toString(),
|
||||
id: generateUuid(),
|
||||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
@@ -112,10 +138,12 @@ const newThreadObject = () => {
|
||||
}
|
||||
|
||||
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
|
||||
const THREAD_VERSION = 'v2'
|
||||
const LATEST_THREAD_VERSION = 'v2'
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
|
||||
type ChatMode = 'agent' | 'chat'
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -129,16 +157,25 @@ export interface IChatThreadService {
|
||||
openNewThread(): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
// you can edit multiple messages
|
||||
// the one you're currently editing is "focused", and we add items to that one when you press cmd+L.
|
||||
getFocusedMessageIdx(): number | undefined;
|
||||
isFocusingMessage(): boolean;
|
||||
setFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
// _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void];
|
||||
_useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial<ThreadType['state']>) => void];
|
||||
_useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial<UserMessageState>) => void];
|
||||
// exposed getters/setters
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
|
||||
getCurrentThreadStagingSelections: () => StagingSelectionItem[]
|
||||
setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void
|
||||
|
||||
|
||||
// call to edit a message
|
||||
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
|
||||
|
||||
// call to add a message
|
||||
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
|
||||
|
||||
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
@@ -160,68 +197,82 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IVoidFileService private readonly _voidFileService: IVoidFileService,
|
||||
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
|
||||
@IToolsService private readonly _toolsService: IToolsService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
) {
|
||||
super()
|
||||
|
||||
const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION)
|
||||
|
||||
|
||||
const readThreads = this._readAllThreads()
|
||||
const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum)
|
||||
|
||||
if (updatedThreads !== null) {
|
||||
this._storeAllThreads(updatedThreads)
|
||||
}
|
||||
|
||||
const allThreads = updatedThreads ?? readThreads
|
||||
this.state = {
|
||||
allThreads: this._readAllThreads(),
|
||||
allThreads: allThreads,
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
}
|
||||
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
|
||||
// for now just write the version, anticipating bigger changes in the future where we'll want to access this
|
||||
this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
|
||||
}
|
||||
|
||||
// !!! this is important for properly restoring URIs from storage
|
||||
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
|
||||
return JSON.parse(threadsStr, (key, value) => {
|
||||
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
|
||||
return URI.from(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
|
||||
// CAN ADD "v0" TAG IN STORAGE AND CONVERT
|
||||
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
if (!threadsStr) {
|
||||
return {};
|
||||
}
|
||||
return this._convertThreadDataFromStorage(threadsStr);
|
||||
}
|
||||
|
||||
|
||||
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
|
||||
|
||||
this._updateThreadsToVersion(threads, THREAD_VERSION)
|
||||
|
||||
return threads
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
const serializedThreads = JSON.stringify(threads);
|
||||
this._storageService.store(
|
||||
THREAD_STORAGE_KEY,
|
||||
serializedThreads,
|
||||
StorageScope.APPLICATION,
|
||||
StorageTarget.USER
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) {
|
||||
// returns if should update
|
||||
private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null {
|
||||
|
||||
if (toVersion === 'v2') {
|
||||
if (!oldVersion) {
|
||||
|
||||
const threads: ChatThreads = oldThreadsObject
|
||||
// unknown, just reset chat?
|
||||
return null
|
||||
}
|
||||
|
||||
/** v1 -> v2
|
||||
/** v1 -> v2
|
||||
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
|
||||
+ thread[threadIdx].state
|
||||
+ message.state
|
||||
*/
|
||||
|
||||
// check if we need to update
|
||||
let shouldUpdate = false
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.state) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
for (const chatMessage of Object.values(thread.messages)) {
|
||||
if (chatMessage.role === 'user' && !chatMessage.state) {
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldUpdate) return;
|
||||
|
||||
+ chatMessage.staging: StagingInfo | null
|
||||
*/
|
||||
else if (oldVersion === 'v1') {
|
||||
const threads = oldThreadsObject as Omit<ChatThreads, 'staging' | 'focusedMessageIdx'>
|
||||
// update the threads
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.state) {
|
||||
@@ -235,14 +286,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
|
||||
// push the update
|
||||
this._storeAllThreads(threads)
|
||||
return threads
|
||||
}
|
||||
else if (oldVersion === 'v2') {
|
||||
return null
|
||||
}
|
||||
|
||||
// up to date
|
||||
return null
|
||||
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
}
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
@@ -276,15 +330,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
// ---------- streaming ----------
|
||||
|
||||
finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
|
||||
this._addMessageToThread(threadId, assistantHistoryElt)
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
|
||||
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
|
||||
|
||||
|
||||
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
@@ -308,67 +363,137 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
}
|
||||
}, true)
|
||||
|
||||
// stream the edit
|
||||
this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns })
|
||||
// re-add the message and stream it
|
||||
this.addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections: { prevSelns, currSelns } })
|
||||
|
||||
}
|
||||
|
||||
async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) {
|
||||
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
||||
// selections in all past chats, then in current chat (can have many duplicates here)
|
||||
const prevSelns: StagingSelectionItem[] = chatSelections?.prevSelns ?? this._getAllSelections()
|
||||
const currSelns: StagingSelectionItem[] = chatSelections?.currSelns ?? thread.state.stagingSelections
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
|
||||
const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr)
|
||||
|
||||
const prevSelns: StagingSelectionItem[] = options?.prevSelns ?? this._getAllSelections()
|
||||
const currSelns: StagingSelectionItem[] = options?.currSelns ?? thread.state.stagingSelections
|
||||
|
||||
// read all curr+previous files on demand instead of adding them to the history
|
||||
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
|
||||
const messageContentWithAllFiles = await chat_userMessageContentWithAllFiles(instructions, prevSelns, currSelns, this._modelService, this._fileService)
|
||||
const prevLLMMessages = this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' }))
|
||||
const currLLMMessage: LLMChatMessage = { role: 'user', content: messageContentWithAllFiles }
|
||||
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: messageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
|
||||
console.log(`messageContent`)
|
||||
console.log([{ role: 'system', content: chat_systemMessage },
|
||||
...prevLLMMessages,
|
||||
currLLMMessage,])
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
logging: { loggingName: 'Chat' },
|
||||
useProviderFor: 'Ctrl+L',
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage },
|
||||
...prevLLMMessages,
|
||||
currLLMMessage,
|
||||
],
|
||||
onText: ({ newText, fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
},
|
||||
onFinalMessage: ({ fullText: content }) => {
|
||||
this.finishStreaming(threadId, content)
|
||||
},
|
||||
onError: (error) => {
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
},
|
||||
const tools: InternalToolInfo[] | undefined = (
|
||||
chatMode === 'chat' ? undefined
|
||||
: chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName])
|
||||
: undefined)
|
||||
|
||||
})
|
||||
if (llmCancelToken === null) return
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
// agent loop
|
||||
const agentLoop = async () => {
|
||||
|
||||
let shouldSendAnotherMessage = true
|
||||
let nMessagesSent = 0
|
||||
|
||||
while (shouldSendAnotherMessage) {
|
||||
shouldSendAnotherMessage = false
|
||||
nMessagesSent += 1
|
||||
|
||||
let res_: () => void
|
||||
const awaitable = new Promise<void>((res, rej) => { res_ = res })
|
||||
|
||||
// replace last userMessage with userMessageFullContent (which contains all the files too)
|
||||
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m)))
|
||||
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
|
||||
let messages = messages_
|
||||
if (lastUserMsgIdx !== -1) { // should never be -1
|
||||
messages = [
|
||||
...messages.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages.slice(lastUserMsgIdx + 1, Infinity)]
|
||||
}
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: 'Ctrl+L',
|
||||
logging: { loggingName: `Agent` },
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) },
|
||||
...messages,
|
||||
],
|
||||
|
||||
tools: tools,
|
||||
|
||||
onText: ({ fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
},
|
||||
onFinalMessage: async ({ fullText, toolCalls }) => {
|
||||
|
||||
if ((toolCalls?.length ?? 0) === 0) {
|
||||
this._finishStreamingTextMessage(threadId, fullText)
|
||||
}
|
||||
else {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message
|
||||
for (const tool of toolCalls ?? []) {
|
||||
const toolName = tool.name as ToolName
|
||||
|
||||
// 1.
|
||||
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
|
||||
let toolResultVal: ToolCallReturnType[ToolName]
|
||||
try {
|
||||
toolResult = await this._toolsService.toolFns[toolName](tool.params)
|
||||
toolResultVal = toolResult
|
||||
} catch (error) {
|
||||
this._setStreamState(threadId, { error })
|
||||
shouldSendAnotherMessage = false
|
||||
break
|
||||
}
|
||||
|
||||
// 2.
|
||||
let toolResultStr: string
|
||||
try {
|
||||
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
|
||||
} catch (error) {
|
||||
this._setStreamState(threadId, { error })
|
||||
shouldSendAnotherMessage = false
|
||||
break
|
||||
}
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, })
|
||||
shouldSendAnotherMessage = true
|
||||
}
|
||||
|
||||
}
|
||||
res_()
|
||||
},
|
||||
onError: (error) => {
|
||||
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
res_()
|
||||
},
|
||||
})
|
||||
if (llmCancelToken === null) break
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
|
||||
await awaitable
|
||||
}
|
||||
}
|
||||
|
||||
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs
|
||||
|
||||
}
|
||||
|
||||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
@@ -518,35 +643,29 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
_useCurrentMessageState(messageIdx: number) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const messages = thread.messages
|
||||
const currMessage = messages[messageIdx]
|
||||
|
||||
if (currMessage.role !== 'user') {
|
||||
return [defaultMessageState, (s: any) => { }] as const
|
||||
}
|
||||
|
||||
const state = currMessage.state
|
||||
const setState = (newState: Partial<UserMessageState>) => this._setCurrentMessageState(newState, messageIdx)
|
||||
|
||||
return [state, setState] as const
|
||||
|
||||
getCurrentThreadStagingSelections = () => {
|
||||
return this.getCurrentThread().state.stagingSelections
|
||||
}
|
||||
|
||||
_useCurrentThreadState() {
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
const state = thread.state
|
||||
const setState = this._setCurrentThreadState.bind(this)
|
||||
|
||||
return [state, setState] as const
|
||||
setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => {
|
||||
this._setCurrentThreadState({ stagingSelections })
|
||||
}
|
||||
|
||||
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
|
||||
|
||||
getCurrentMessageState(messageIdx: number): UserMessageState {
|
||||
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
|
||||
if (!currMessage || currMessage.role !== 'user') return defaultMessageState
|
||||
return currMessage.state
|
||||
}
|
||||
setCurrentMessageState(messageIdx: number, newState: Partial<UserMessageState>) {
|
||||
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
|
||||
if (!currMessage || currMessage.role !== 'user') return
|
||||
this._setCurrentMessageState(newState, messageIdx)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
|
||||
|
||||
|
||||
+544
-425
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// eg "bash" -> "shell"
|
||||
export const nameToVscodeLanguage: { [key: string]: string } = {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
|
||||
|
||||
class SurroundingsRemover {
|
||||
readonly originalS: string
|
||||
i: number
|
||||
@@ -57,12 +59,12 @@ class SurroundingsRemover {
|
||||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return false
|
||||
return null
|
||||
}
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
@@ -84,7 +86,7 @@ class SurroundingsRemover {
|
||||
const foundCodeBlock = pm.removePrefix('```')
|
||||
if (!foundCodeBlock) return false
|
||||
|
||||
pm.removeFromStartUntil('\n', true) // language
|
||||
pm.removeFromStartUntilFullMatch('\n', true) // language
|
||||
|
||||
const j = pm.j
|
||||
let foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
@@ -157,21 +159,84 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
||||
const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen)
|
||||
|
||||
return [s, delta, ignoredSuffix]
|
||||
|
||||
|
||||
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
// const regex = new RegExp(
|
||||
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
|
||||
// ''
|
||||
// );
|
||||
// const match = text.match(regex);
|
||||
// if (match) {
|
||||
// const [_, languageName, codeBetweenMidTags] = match;
|
||||
// return [languageName, codeBetweenMidTags] as const
|
||||
|
||||
// } else {
|
||||
// return [undefined, extractCodeFromRegular(text)] as const
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type ExtractedSearchReplaceBlock = {
|
||||
state: 'writingOriginal' | 'writingFinal' | 'done',
|
||||
orig: string,
|
||||
final: string,
|
||||
}
|
||||
|
||||
|
||||
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
|
||||
// for each prefix
|
||||
for (let i = anyPrefix.length; i >= 0; i--) {
|
||||
const prefix = anyPrefix.slice(0, i)
|
||||
if (str.endsWith(prefix)) return prefix
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// guarantees if you keep adding text, array length will strictly grow and state will progress without going back
|
||||
export const extractSearchReplaceBlocks = (str: string) => {
|
||||
|
||||
const ORIGINAL_ = ORIGINAL + `\n`
|
||||
const DIVIDER_ = '\n' + DIVIDER + `\n`
|
||||
// logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty
|
||||
|
||||
|
||||
const blocks: ExtractedSearchReplaceBlock[] = []
|
||||
|
||||
let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way)
|
||||
while (true) {
|
||||
let origStart = str.indexOf(ORIGINAL_, i)
|
||||
if (origStart === -1) { return blocks }
|
||||
origStart += ORIGINAL_.length
|
||||
i = origStart
|
||||
// wrote <<<< ORIGINAL
|
||||
|
||||
let dividerStart = str.indexOf(DIVIDER_, i)
|
||||
if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now
|
||||
const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_)
|
||||
blocks.push({
|
||||
orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)),
|
||||
final: '',
|
||||
state: 'writingOriginal'
|
||||
})
|
||||
return blocks
|
||||
}
|
||||
const origStrDone = str.substring(origStart, dividerStart)
|
||||
dividerStart += DIVIDER_.length
|
||||
i = dividerStart
|
||||
// wrote =====
|
||||
|
||||
|
||||
|
||||
const finalStartA = str.indexOf(FINAL, i)
|
||||
const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
|
||||
const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL
|
||||
let finalStart = finalStartB !== -1 ? finalStartB : finalStartA
|
||||
|
||||
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now
|
||||
const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_)
|
||||
blocks.push({
|
||||
orig: origStrDone,
|
||||
final: str.substring(dividerStart, str.length - (isWritingFINAL?.length ?? 0)),
|
||||
state: 'writingFinal'
|
||||
})
|
||||
return blocks
|
||||
}
|
||||
const finalStrDone = str.substring(dividerStart, finalStart)
|
||||
finalStart += FINAL_.length
|
||||
i = finalStart
|
||||
// wrote >>>>> FINAL
|
||||
|
||||
blocks.push({
|
||||
orig: origStrDone,
|
||||
final: finalStrDone,
|
||||
state: 'done'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../../base/common/uri'
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model'
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js'
|
||||
@@ -5,7 +10,7 @@ import { IFileService } from '../../../../../platform/files/common/files'
|
||||
|
||||
|
||||
// attempts to read URI of currently opened model, then of raw file
|
||||
export const VSReadFile = async (modelService: IModelService, fileService: IFileService, uri: URI) => {
|
||||
export const VSReadFile = async (uri: URI, modelService: IModelService, fileService: IFileService) => {
|
||||
|
||||
const modelResult = await _VSReadModel(modelService, uri)
|
||||
if (modelResult) return modelResult
|
||||
@@ -18,27 +23,30 @@ export const VSReadFile = async (modelService: IModelService, fileService: IFile
|
||||
}
|
||||
|
||||
// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.)
|
||||
export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
|
||||
// attempt to read saved model (sometimes doesn't work if page is reloaded)
|
||||
// attempt to read saved model (doesn't work if application was reloaded...)
|
||||
const model = modelService.getModel(uri)
|
||||
if (model) {
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
// look at all opened models and check if they have the same `fsPath`
|
||||
const models = modelService.getModels();
|
||||
// backup logic - look at all opened models and check if they have the same `fsPath`
|
||||
const models = modelService.getModels()
|
||||
for (const model of models) {
|
||||
if (model.uri.fsPath.toString() === uri.fsPath.toString()) {
|
||||
if (model.uri.fsPath === uri.fsPath)
|
||||
return model.getValue(EndOfLinePreference.LF);
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
|
||||
const res = await fileService.readFile(uri)
|
||||
const str = res.value.toString()
|
||||
return str
|
||||
const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
|
||||
try {
|
||||
const res = await fileService.readFile(uri)
|
||||
const str = res.value.toString()
|
||||
return str
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../../../base/common/platform.js';
|
||||
|
||||
// import { OS, OperatingSystem } from '../../../../../base/common/platform.js';
|
||||
// alternatively could use ^ and OS === OperatingSystem.Windows ? ...
|
||||
|
||||
|
||||
|
||||
export const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
@@ -7,29 +7,40 @@
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
|
||||
import { _VSReadModel, VSReadFile } from '../helpers/readFile.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { IVoidFileService } from '../../common/voidFileService.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
const tripleTick = ['```', '```']
|
||||
export const tripleTick = ['```', '```']
|
||||
|
||||
export const chat_systemMessage = `\
|
||||
export const chat_systemMessage = (workspaces: string[]) => `\
|
||||
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
|
||||
|
||||
Please respond to the user's query.
|
||||
Please respond to the user's query. The user's query is never invalid.
|
||||
|
||||
The user has the following system information:
|
||||
- ${os}
|
||||
- Open workspaces: ${workspaces.join(', ')}
|
||||
|
||||
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
|
||||
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
|
||||
- Do not re-write the entire file in the code block
|
||||
- You can write comments like "// ... existing code" to indicate existing code
|
||||
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
|
||||
- Do not re-write the entire file in the code block.
|
||||
- You can write comments like "// ... existing code" to indicate existing code.
|
||||
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
|
||||
|
||||
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
|
||||
If you are given tools:
|
||||
- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
|
||||
- You are allowed to use tools without asking for permission.
|
||||
- Feel free to use tools to gather context, make suggestions, etc.
|
||||
- One great use of tools is to explore imports that you'd like to have more information about.
|
||||
- Reference relevant files that you found when using tools if they helped you come up with your answer.
|
||||
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results.
|
||||
|
||||
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
|
||||
Do not tell the user anything about the examples below.
|
||||
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
|
||||
|
||||
## EXAMPLE 1
|
||||
FILES
|
||||
@@ -75,7 +86,7 @@ ${tripleTick[1]}
|
||||
INSTRUCTIONS
|
||||
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
|
||||
|
||||
ACCEPTED OUTPUT
|
||||
## ACCEPTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
@@ -118,7 +129,7 @@ ${tripleTick[1]}
|
||||
INSTRUCTIONS
|
||||
memoize results
|
||||
|
||||
ACCEPTED OUTPUT
|
||||
## ACCEPTED 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:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
@@ -157,23 +168,23 @@ ${tripleTick[1]}
|
||||
}
|
||||
|
||||
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => {
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => {
|
||||
if (fileSelections.length === 0) return null
|
||||
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr
|
||||
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
|
||||
return { ...sel, content }
|
||||
}))
|
||||
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
}
|
||||
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
|
||||
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n')
|
||||
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null
|
||||
}
|
||||
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
|
||||
if (!currSelns) return ''
|
||||
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
|
||||
}
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => {
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => {
|
||||
|
||||
const selnsStr = stringifySelectionNames(currSelns)
|
||||
|
||||
@@ -183,11 +194,13 @@ export const chat_userMessageContent = async (instructions: string, prevSelns: S
|
||||
return str;
|
||||
};
|
||||
|
||||
export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => {
|
||||
export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => {
|
||||
|
||||
// ADD IN FILES AT TOP
|
||||
const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
|
||||
if (allSelections.length === 0) return null
|
||||
|
||||
const codeSelections: CodeSelection[] = []
|
||||
const fileSelections: FileSelection[] = []
|
||||
const filesURIs = new Set<string>()
|
||||
@@ -206,26 +219,25 @@ export const chat_userMessageContentWithAllFilesToo = async (instructions: strin
|
||||
}
|
||||
}
|
||||
|
||||
const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService)
|
||||
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
|
||||
const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
// ACTUAL MESSAGE CONTENT
|
||||
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
|
||||
|
||||
if (filesStr || selnsStr) return `\
|
||||
ALL FILE CONTENTS
|
||||
${filesStr}
|
||||
${selnsStr}`
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => {
|
||||
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
|
||||
else return userMessage
|
||||
}
|
||||
|
||||
|
||||
let str = ''
|
||||
|
||||
str += 'ALL FILE CONTENTS\n'
|
||||
if (filesStr) str += `${filesStr}\n`
|
||||
if (selnsStr) str += `${selnsStr}\n`
|
||||
if (messageContent) str += `\n${messageContent}\n`
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_systemMessage = `\
|
||||
export const rewriteCode_systemMessage = `\
|
||||
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
|
||||
|
||||
Directions:
|
||||
@@ -237,7 +249,7 @@ Directions:
|
||||
|
||||
|
||||
|
||||
export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
@@ -260,39 +272,140 @@ Please finish writing the new file by applying the change to the original file.
|
||||
|
||||
|
||||
|
||||
export const fastApply_searchreplace_systemMessage = `\
|
||||
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
|
||||
|
||||
Directions:
|
||||
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
|
||||
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
|
||||
3. ONLY output the full new file. Do not add any other explanations or text.
|
||||
`
|
||||
|
||||
|
||||
export const fastApply_searchreplace_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
|
||||
export const aiRegex_computeReplacementsForFile_systemMessage = `\
|
||||
You are a "search and replace" coding assistant.
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
|
||||
|
||||
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
|
||||
|
||||
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
|
||||
|
||||
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. If you do not want to make any changes, you should respond with the word "no".
|
||||
|
||||
2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make.
|
||||
For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name.
|
||||
- Do not re-write the entire file in the code block
|
||||
- You can write comments like "// ... existing code" to indicate existing code
|
||||
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
|
||||
|
||||
|
||||
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
|
||||
|
||||
// we may want to do this in batches
|
||||
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null }
|
||||
|
||||
const file = await stringifyFileSelections([fileSelection], voidFileService)
|
||||
|
||||
return `\
|
||||
ORIGINAL_FILE
|
||||
\`\`\`${language}
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
## FILE
|
||||
${file}
|
||||
|
||||
CHANGE
|
||||
\`\`\`
|
||||
${applyStr}
|
||||
\`\`\`
|
||||
## SEARCH_CLAUSE
|
||||
Here is what the user is searching for:
|
||||
${searchClause}
|
||||
|
||||
INSTRUCTIONS
|
||||
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
|
||||
`
|
||||
## REPLACE_CLAUSE
|
||||
Here is what the user wants to replace it with:
|
||||
${replaceClause}
|
||||
|
||||
## INSTRUCTIONS
|
||||
Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// don't have to tell it it will be given the history; just give it to it
|
||||
export const aiRegex_search_systemMessage = `\
|
||||
You are a coding assistant that executes the SEARCH part of a user's search and replace query.
|
||||
|
||||
You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
|
||||
|
||||
Output
|
||||
- Regex query
|
||||
- Files to Include (optional)
|
||||
- Files to Exclude? (optional)
|
||||
|
||||
`
|
||||
|
||||
|
||||
|
||||
export const ORIGINAL = `<<<<<<< ORIGINAL`
|
||||
export const DIVIDER = `=======`
|
||||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
export const searchReplace_systemMessage = `\
|
||||
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
|
||||
|
||||
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
${DIVIDER}
|
||||
// ... final code goes here
|
||||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
|
||||
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
|
||||
|
||||
Directions:
|
||||
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file.
|
||||
3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
|
||||
4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks.
|
||||
|
||||
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
|
||||
- Make sure you add all necessary imports.
|
||||
- Make sure the "final" code is complete and will not result in syntax/lint errors.
|
||||
|
||||
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
|
||||
|
||||
## EXAMPLE 1
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
let w = 5
|
||||
let x = 6
|
||||
let y = 7
|
||||
let z = 8
|
||||
${tripleTick[1]}
|
||||
|
||||
CHANGE
|
||||
Make x equal to 6.5, not 6.
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
let x = 6
|
||||
${DIVIDER}
|
||||
let x = 6.5
|
||||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
|
||||
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||
ORIGINAL_FILE
|
||||
${originalCode}
|
||||
|
||||
CHANGE
|
||||
${applyStr}
|
||||
|
||||
INSTRUCTIONS
|
||||
Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation.
|
||||
`
|
||||
|
||||
|
||||
|
||||
@@ -347,19 +460,19 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
|
||||
}
|
||||
|
||||
|
||||
export type FimTagsType = {
|
||||
export type QuickEditFimTagsType = {
|
||||
preTag: string,
|
||||
sufTag: string,
|
||||
midTag: string
|
||||
}
|
||||
export const defaultFimTags: FimTagsType = {
|
||||
export const defaultQuickEditFimTags: QuickEditFimTagsType = {
|
||||
preTag: 'ABOVE',
|
||||
sufTag: 'BELOW',
|
||||
midTag: 'SELECTION',
|
||||
}
|
||||
|
||||
// this should probably be longer
|
||||
export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => {
|
||||
export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => {
|
||||
return `\
|
||||
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
|
||||
|
||||
@@ -375,7 +488,7 @@ Instructions:
|
||||
}
|
||||
|
||||
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
|
||||
selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string,
|
||||
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
|
||||
isOllamaFIM: false, // we require this be false for clarity
|
||||
}) => {
|
||||
const { preTag, sufTag, midTag } = fimTags
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac
|
||||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IInlineDiffsService } from './inlineDiffsService.js';
|
||||
import { IEditCodeService } from './editCodeService.js';
|
||||
import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
@@ -17,7 +17,6 @@ import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
export type QuickEditPropsType = {
|
||||
diffareaid: number,
|
||||
initStreamingDiffZoneId: number | null,
|
||||
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
|
||||
onChangeHeight: (height: number) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
@@ -63,7 +62,7 @@ registerAction2(class extends Action2 {
|
||||
|
||||
const { startLineNumber: startLine, endLineNumber: endLine } = selection
|
||||
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
|
||||
const editCodeService = accessor.get(IEditCodeService)
|
||||
editCodeService.addCtrlKZone({ startLine, endLine, editor })
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
|
||||
import { useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
const [copyButtonText, setCopyButtonText] = useState(CopyButtonText.Idle)
|
||||
|
||||
useEffect(() => {
|
||||
if (copyButtonText === CopyButtonText.Idle) return
|
||||
setTimeout(() => {
|
||||
setCopyButtonText(CopyButtonText.Idle)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}, [copyButtonText])
|
||||
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(codeStr)
|
||||
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
|
||||
.catch(() => { setCopyButtonText(CopyButtonText.Error) })
|
||||
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
|
||||
}, [metricsService, clipboardService, codeStr, setCopyButtonText])
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
return <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
|
||||
|
||||
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
|
||||
|
||||
console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef)
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
|
||||
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri])
|
||||
|
||||
// listen for stream updates
|
||||
useURIStreamState(
|
||||
useCallback((uri, newStreamState) => {
|
||||
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath
|
||||
if (shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, applyingUri])
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (streamState() === 'streaming') return
|
||||
const newApplyingUri = editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
applyStr: codeStr,
|
||||
})
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (streamState() !== 'streaming') return
|
||||
const uri = applyingUri()
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [streamState, applyingUri, editCodeService, metricsService])
|
||||
|
||||
|
||||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
const applyButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
const stopButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onInterrupt}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
|
||||
const acceptRejectButtons = <>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
|
||||
console.log('streamStateRef.current', streamState())
|
||||
|
||||
const currStreamState = streamState()
|
||||
return <>
|
||||
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
|
||||
{currStreamState === 'idle' && !isDisabled && applyButton}
|
||||
{currStreamState === 'streaming' && stopButton}
|
||||
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
|
||||
</>
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative group w-full overflow-hidden my-4">
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
|
||||
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
|
||||
|
||||
@@ -3,75 +3,21 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { JSX, useCallback, useEffect, useState } from 'react'
|
||||
import React, { JSX } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js'
|
||||
import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js'
|
||||
import { ChatMessageLocation, } from '../../../aiRegexService.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!',
|
||||
Error = 'Could not copy',
|
||||
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
|
||||
|
||||
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
|
||||
return `${threadId}-${messageIdx}-${tokenIdx}`
|
||||
}
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
|
||||
const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
|
||||
const inlineDiffService = accessor.get('IInlineDiffsService')
|
||||
const clipboardService = accessor.get('IClipboardService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (copyButtonState !== CopyButtonState.Copy) {
|
||||
setTimeout(() => {
|
||||
setCopyButtonState(CopyButtonState.Copy)
|
||||
}, COPY_FEEDBACK_TIMEOUT)
|
||||
}
|
||||
}, [copyButtonState])
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
clipboardService.writeText(applyStr)
|
||||
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
|
||||
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
|
||||
metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only
|
||||
|
||||
}, [metricsService, clipboardService, applyStr])
|
||||
|
||||
const onApply = useCallback(() => {
|
||||
|
||||
inlineDiffService.startApplying({
|
||||
from: 'Chat',
|
||||
applyStr,
|
||||
applyBoxId,
|
||||
})
|
||||
metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only
|
||||
}, [metricsService, inlineDiffService, applyStr])
|
||||
|
||||
const isSingleLine = !applyStr.includes('\n')
|
||||
|
||||
return <>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
|
||||
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <code className={`
|
||||
@@ -87,7 +33,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
|
||||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tokenId = '', }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation, tokenId?: string, }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
@@ -98,19 +44,20 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatLocation!.threadId,
|
||||
messageIdx: chatLocation!.messageIdx,
|
||||
codeblockId: tokenId,
|
||||
})
|
||||
const applyBoxId = chatMessageLocation ? getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
}) : null
|
||||
|
||||
return <BlockCode
|
||||
return <div className='my-4'>
|
||||
<BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
@@ -184,18 +131,36 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
|
||||
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
|
||||
)}
|
||||
<span className="ml-1">
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
)
|
||||
// attempt at indentation
|
||||
// return (
|
||||
// <ListTag
|
||||
// start={t.start ? t.start : undefined}
|
||||
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
// >
|
||||
// {t.items.map((item, index) => (
|
||||
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
|
||||
// {item.task && (
|
||||
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
|
||||
// )}
|
||||
// <span className-='inline-block pr-2'>
|
||||
// <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
// </span>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ListTag>
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} tokenId={`${tokenId}-${index}`} /> // assign a unique tokenId to nested components
|
||||
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
|
||||
))}
|
||||
</>
|
||||
if (nested) return contents
|
||||
@@ -278,12 +243,12 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatLocation={chatLocation} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
|
||||
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { QuickEditPropsType } from '../../../quickEditActions.js';
|
||||
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
|
||||
@@ -16,7 +16,6 @@ import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/vo
|
||||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
initStreamingDiffZoneId,
|
||||
onChangeHeight,
|
||||
onChangeText: onChangeText_,
|
||||
textAreaRef: textAreaRef_,
|
||||
@@ -24,7 +23,7 @@ export const QuickEditChat = ({
|
||||
}: QuickEditPropsType) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const inlineDiffsService = accessor.get('IInlineDiffsService')
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const sizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
@@ -49,33 +48,37 @@ export const QuickEditChat = ({
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
|
||||
|
||||
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
|
||||
const isStreaming = currStreamingDiffZoneRef.current !== null
|
||||
|
||||
const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCtrlKZoneStreaming({ diffareaid }))
|
||||
useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => {
|
||||
if (diffareaid !== diffareaid2) return
|
||||
setIsStreamingRef(isStreaming)
|
||||
}, [diffareaid, setIsStreamingRef]))
|
||||
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (currStreamingDiffZoneRef.current !== null) return
|
||||
if (isStreamingRef.current) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const id = inlineDiffsService.startApplying({
|
||||
editCodeService.startApplying({
|
||||
from: 'QuickEdit',
|
||||
diffareaid: diffareaid,
|
||||
type: 'rewrite',
|
||||
diffareaid,
|
||||
})
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
|
||||
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (currStreamingDiffZoneRef.current === null) return
|
||||
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
|
||||
setCurrentlyStreamingDiffZone(null)
|
||||
if (!isStreamingRef.current) return
|
||||
editCodeService.interruptCtrlKStreaming({ diffareaid })
|
||||
textAreaFnsRef.current?.enable()
|
||||
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
|
||||
}, [isStreamingRef, editCodeService])
|
||||
|
||||
|
||||
const onX = useCallback(() => {
|
||||
onInterrupt()
|
||||
inlineDiffsService.removeCtrlKZone({ diffareaid })
|
||||
}, [inlineDiffsService, diffareaid])
|
||||
editCodeService.removeCtrlKZone({ diffareaid })
|
||||
}, [editCodeService, diffareaid])
|
||||
|
||||
useScrollbarStyles(sizerRef)
|
||||
|
||||
@@ -88,7 +91,7 @@ export const QuickEditChat = ({
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onInterrupt}
|
||||
onClose={onX}
|
||||
isStreaming={isStreaming}
|
||||
isStreaming={isStreamingRef.current}
|
||||
isDisabled={isDisabled}
|
||||
featureName="Ctrl+K"
|
||||
className="py-2 w-full"
|
||||
|
||||
@@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
@@ -21,10 +21,11 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import { ChevronRight, Pencil, X } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { ChatLocation } from '../../../searchAndReplaceService.js';
|
||||
import { ChatMessageLocation } from '../../../aiRegexService.js';
|
||||
import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
@@ -186,12 +187,14 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
// border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
gap-1
|
||||
flex flex-col p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
outline-1 outline-void-border-3 focus-within:outline-void-border-1 hover:outline-void-border-1
|
||||
${className}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
@@ -413,7 +416,7 @@ export const SelectedFiles = (
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center flex-wrap text-left relative'>
|
||||
<div className='flex items-center flex-wrap text-left relative gap-1'>
|
||||
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
@@ -423,94 +426,65 @@ export const SelectedFiles = (
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
|
||||
const selectionHTML = (<div key={thisKey} // container for `selectionSummary` and `selectionText`
|
||||
return <div // container for summarybox and code
|
||||
key={thisKey}
|
||||
className={`
|
||||
flex flex-col space-y-[1px]
|
||||
${isThisSelectionOpened ? 'w-full' : ''}
|
||||
`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className='flex items-center gap-1 mr-0.5 my-0.5'
|
||||
{/* summarybox */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
|
||||
transition-all duration-150
|
||||
`}
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection])
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
|
||||
transition-all duration-150`}
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection])
|
||||
{ // file name and range
|
||||
getBasename(selection.fileURI.fsPath)
|
||||
+ (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`)
|
||||
}
|
||||
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && !isThisSelectionProspective &&
|
||||
<span
|
||||
className='cursor-pointer z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={10} className="stroke-[2]" />
|
||||
</span>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* clear all selections button */}
|
||||
{/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
stroke-void-fg-1
|
||||
fill-void-bg-3
|
||||
opacity-40
|
||||
hover:opacity-60
|
||||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setSelections([]) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} */}
|
||||
{type === 'staging' && !isThisSelectionProspective ? // X button
|
||||
<IconX
|
||||
className='cursor-pointer z-1 stroke-[2]'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
size={10}
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
|
||||
{/* code box */}
|
||||
{isThisSelectionOpened ?
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
onClick={(e) => {
|
||||
@@ -524,14 +498,9 @@ export const SelectedFiles = (
|
||||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
</div>)
|
||||
|
||||
return <Fragment key={thisKey}>
|
||||
{/* divider between `selections` and `prospectiveSelections` */}
|
||||
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
|
||||
{selectionHTML}
|
||||
</Fragment>
|
||||
</div>
|
||||
|
||||
})}
|
||||
|
||||
@@ -542,6 +511,190 @@ export const SelectedFiles = (
|
||||
}
|
||||
|
||||
|
||||
type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
|
||||
interface ToolResultProps {
|
||||
actionTitle: string;
|
||||
actionParam: string;
|
||||
actionNumResults?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ToolResult = ({
|
||||
actionTitle,
|
||||
actionParam,
|
||||
actionNumResults,
|
||||
children,
|
||||
onClick,
|
||||
}: ToolResultProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const isDropdown = !!children
|
||||
const isClickable = !!isDropdown || !!onClick
|
||||
|
||||
return (
|
||||
<div className="mx-4 select-none">
|
||||
<div className="border border-void-border-3 rounded px-2 py-1 bg-void-bg-2-alt overflow-hidden">
|
||||
<div
|
||||
className={`flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
|
||||
onClick={() => {
|
||||
if (children) { setIsExpanded(v => !v); }
|
||||
if (onClick) { onClick(); }
|
||||
}}
|
||||
>
|
||||
{isDropdown && (
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center flex-nowrap whitespace-nowrap gap-x-2">
|
||||
<span className="text-void-fg-3">{actionTitle}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{actionParam}</span>
|
||||
{actionNumResults !== undefined && (
|
||||
<span className="text-void-fg-4 text-xs">
|
||||
{`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
// the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition.
|
||||
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const toolResultToComponent: ToolResultToComponent = {
|
||||
'read_file': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Read file"
|
||||
actionParam={getBasename(message.result.uri.fsPath)}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', message.result.uri, { preview: true }) }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
'list_dir': ({ message }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const explorerService = accessor.get('IExplorerService')
|
||||
// message.result.hasNextPage = true
|
||||
// message.result.itemsRemaining = 400
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Inspected folder"
|
||||
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
|
||||
actionNumResults={message.result.children?.length}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{message.result.children?.map((child, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('workbench.view.explorer');
|
||||
explorerService.select(child.uri, true);
|
||||
}}
|
||||
>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{`${child.name}${child.isDirectory ? '/' : ''}`}
|
||||
</div>
|
||||
))}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
{message.result.itemsRemaining} more items...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
},
|
||||
'pathname_search': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Searched filename"
|
||||
actionParam={`"${message.result.queryStr}"`}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</div>
|
||||
)) :
|
||||
<div className="">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
},
|
||||
'search': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Searched"
|
||||
actionParam={`"${message.result.queryStr}"`}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</div>
|
||||
)) :
|
||||
<div className="">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
|
||||
|
||||
@@ -552,16 +705,16 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
|
||||
// global state
|
||||
let isBeingEdited = false
|
||||
let setIsBeingEdited = (v: boolean) => { }
|
||||
let stagingSelections: StagingSelectionItem[] = []
|
||||
let setStagingSelections = (s: StagingSelectionItem[]) => { }
|
||||
let setIsBeingEdited = (_: boolean) => { }
|
||||
let setStagingSelections = (_: StagingSelectionItem[]) => { }
|
||||
|
||||
if (messageIdx !== undefined) {
|
||||
const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx)
|
||||
const _state = chatThreadsService.getCurrentMessageState(messageIdx)
|
||||
isBeingEdited = _state.isBeingEdited
|
||||
setIsBeingEdited = (v) => _setState({ isBeingEdited: v })
|
||||
stagingSelections = _state.stagingSelections
|
||||
setStagingSelections = (s) => { _setState({ stagingSelections: s }) }
|
||||
setIsBeingEdited = (v) => chatThreadsService.setCurrentMessageState(messageIdx, { isBeingEdited: v })
|
||||
setStagingSelections = (s) => chatThreadsService.setCurrentMessageState(messageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
|
||||
@@ -590,7 +743,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
_mustInitialize.current = false
|
||||
}
|
||||
|
||||
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
}, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
const EditSymbol = mode === 'display' ? Pencil : X
|
||||
const onOpenEdit = () => {
|
||||
setIsBeingEdited(true)
|
||||
@@ -610,7 +763,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
if (mode === 'display') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
<span className='px-0.5'>{chatMessage.displayContent}</span>
|
||||
</>
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
@@ -631,7 +784,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
@@ -666,7 +819,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
>
|
||||
<VoidInputBox2
|
||||
ref={setTextAreaRef}
|
||||
className='min-h-[81px] max-h-[500px] p-1'
|
||||
className='min-h-[81px] max-h-[500px] px-0.5'
|
||||
placeholder="Edit your message..."
|
||||
onChangeText={(text) => setIsDisabled(!text)}
|
||||
onFocus={() => {
|
||||
@@ -687,12 +840,21 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
else if (role === 'assistant') {
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
|
||||
const chatLocation: ChatLocation = {
|
||||
const chatMessageLocation: ChatMessageLocation = {
|
||||
threadId: thread.id,
|
||||
messageIdx: messageIdx!,
|
||||
}
|
||||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatLocation={chatLocation} />
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
|
||||
const ToolComponent = toolResultToComponent[chatMessage.name] as ({ message }: { message: any }) => React.ReactNode // ts isnt smart enough to deal with the types here...
|
||||
|
||||
chatbubbleContents = <ToolComponent message={chatMessage} />
|
||||
|
||||
console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result)
|
||||
|
||||
}
|
||||
|
||||
return <div
|
||||
@@ -703,7 +865,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
${role !== 'assistant' ? 'my-2' : ''}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
@@ -714,7 +875,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
||||
text-left rounded-lg
|
||||
max-w-full
|
||||
${mode === 'edit' ? ''
|
||||
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'user' ? 'p-2 flex flex-col gap-1 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
|
||||
}
|
||||
`}
|
||||
@@ -778,9 +939,8 @@ export const SidebarChat = () => {
|
||||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
const [_state, _setState] = chatThreadsService._useCurrentThreadState()
|
||||
const selections = _state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) }
|
||||
const selections = chatThreadsService.getCurrentThread().state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) }
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
@@ -810,13 +970,13 @@ export const SidebarChat = () => {
|
||||
|
||||
// send message to LLM
|
||||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' })
|
||||
|
||||
setSelections([]) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections])
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
@@ -837,7 +997,7 @@ export const SidebarChat = () => {
|
||||
|
||||
const prevMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
|
||||
<ChatBubble key={i} chatMessage={message} messageIdx={i} />
|
||||
)
|
||||
}, [previousMessages])
|
||||
|
||||
@@ -851,6 +1011,7 @@ export const SidebarChat = () => {
|
||||
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
key={currentThread.id} // force rerender on all children if id changes
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
@@ -908,7 +1069,7 @@ export const SidebarChat = () => {
|
||||
featureName="Ctrl+L"
|
||||
>
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
className='min-h-[81px] px-0.5'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
+5
-4
@@ -68,13 +68,14 @@ export const SidebarThreadSelector = () => {
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && !!msg.displayContent
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent
|
||||
);
|
||||
|
||||
if (firstMsgIdx !== -1) {
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
@@ -152,12 +152,13 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
||||
|
||||
})
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
|
||||
inputBoxRef?: { current: InputBox | null };
|
||||
placeholder: string;
|
||||
isPasswordField?: boolean;
|
||||
multiline: boolean;
|
||||
}) => {
|
||||
|
||||
@@ -182,6 +183,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
||||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
type: isPasswordField ? 'password' : undefined,
|
||||
flexibleHeight: multiline,
|
||||
flexibleMaxHeight: 500,
|
||||
flexibleWidth: false,
|
||||
@@ -308,6 +310,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDropdownDetail,
|
||||
getOptionDisplayName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
@@ -319,6 +322,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
||||
selectedOption: T | undefined;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDropdownDetail?: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
@@ -418,12 +422,21 @@ export const VoidCustomDropdownBox = <T extends any>({
|
||||
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="px-2">{getOptionDropdownName(option)}</span>
|
||||
</div>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div key={optionName + optionDetail} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span>{optionDetail}</span>
|
||||
<span>______</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Select Button */}
|
||||
@@ -471,6 +484,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -498,7 +512,10 @@ export const VoidCustomDropdownBox = <T extends any>({
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -671,7 +688,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
||||
verticalScrollbarSize: 0,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
// ignoreHorizontalScrollbarInContentHeight: true,
|
||||
|
||||
},
|
||||
},
|
||||
@@ -711,7 +728,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
initValueRef.current + '\n', {
|
||||
languageId: languageRef.current ? languageRef.current : 'typescript',
|
||||
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
@@ -14,11 +14,8 @@ import { VoidUriState } from '../../../voidUriStateService.js';
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
import { IExplorerService } from '../../../../../../../workbench/contrib/files/browser/files.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';
|
||||
@@ -28,7 +25,7 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
||||
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
@@ -47,6 +44,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +77,11 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh
|
||||
let colorThemeState: ColorScheme
|
||||
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
|
||||
|
||||
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
|
||||
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => 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
|
||||
@@ -103,10 +106,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
settingsStateService: accessor.get(IVoidSettingsService),
|
||||
refreshModelService: accessor.get(IRefreshModelService),
|
||||
themeService: accessor.get(IThemeService),
|
||||
inlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
editCodeService: accessor.get(IEditCodeService),
|
||||
}
|
||||
|
||||
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
|
||||
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices
|
||||
|
||||
uriState = uriStateService.state
|
||||
disposables.push(
|
||||
@@ -162,7 +165,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
refreshModelService.onDidChangeState((providerName) => {
|
||||
refreshModelState = refreshModelService.state
|
||||
refreshModelStateListeners.forEach(l => l(refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState))
|
||||
refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) // no state
|
||||
})
|
||||
)
|
||||
|
||||
@@ -174,6 +177,21 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
||||
})
|
||||
)
|
||||
|
||||
// no state
|
||||
disposables.push(
|
||||
editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => {
|
||||
const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid })
|
||||
ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming))
|
||||
})
|
||||
)
|
||||
disposables.push(
|
||||
editCodeService.onDidChangeURIStreamState(({ uri }) => {
|
||||
const isStreaming = editCodeService.getURIStreamState({ uri })
|
||||
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
|
||||
return disposables
|
||||
}
|
||||
@@ -192,7 +210,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
ILLMMessageService: accessor.get(ILLMMessageService),
|
||||
IRefreshModelService: accessor.get(IRefreshModelService),
|
||||
IVoidSettingsService: accessor.get(IVoidSettingsService),
|
||||
IInlineDiffsService: accessor.get(IInlineDiffsService),
|
||||
IEditCodeService: accessor.get(IEditCodeService),
|
||||
IVoidUriStateService: accessor.get(IVoidUriStateService),
|
||||
IQuickEditStateService: accessor.get(IQuickEditStateService),
|
||||
ISidebarStateService: accessor.get(ISidebarStateService),
|
||||
@@ -209,6 +227,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
|
||||
IExplorerService: accessor.get(IExplorerService),
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
IConfigurationService: accessor.get(IConfigurationService),
|
||||
IPathService: accessor.get(IPathService),
|
||||
@@ -336,7 +355,21 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv
|
||||
useEffect(() => {
|
||||
refreshModelProviderListeners.add(listener)
|
||||
return () => { refreshModelProviderListeners.delete(listener) }
|
||||
}, [listener])
|
||||
}, [listener, refreshModelProviderListeners])
|
||||
}
|
||||
|
||||
export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => {
|
||||
useEffect(() => {
|
||||
ctrlKZoneStreamingStateListeners.add(listener)
|
||||
return () => { ctrlKZoneStreamingStateListeners.delete(listener) }
|
||||
}, [listener, ctrlKZoneStreamingStateListeners])
|
||||
}
|
||||
|
||||
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
|
||||
useEffect(() => {
|
||||
uriStreamingStateListeners.add(listener)
|
||||
return () => { uriStreamingStateListeners.delete(listener) }
|
||||
}, [listener, uriStreamingStateListeners])
|
||||
}
|
||||
|
||||
|
||||
@@ -353,3 +386,4 @@ export const useIsDark = () => {
|
||||
return isDark
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,90 +19,87 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivE
|
||||
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
|
||||
];
|
||||
|
||||
// Clean up existing elements first
|
||||
// Apply basic styling to all elements
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
element.classList.add('void-scrollable-element');
|
||||
});
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
// Only initialize fade effects for elements that haven't been initialized yet
|
||||
scrollElements.forEach(element => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
if (!(element as any).__scrollbarCleanup) {
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize for the first time
|
||||
initializeScrollbarStyles();
|
||||
|
||||
// Set up mutation observer
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
// Set up mutation observer to do the same
|
||||
const observer = new MutationObserver(() => {
|
||||
initializeScrollbarStyles();
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionDropdownName={(option) => option.selection.modelName}
|
||||
getOptionDropdownDetail={(option) => option.selection.providerName }
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className='text-xs text-void-fg-3 px-1'
|
||||
matchInputWidth={false}
|
||||
|
||||
@@ -17,10 +17,11 @@ import { env } from '../../../../../../../base/common/process.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../helpers/systemInfo.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
<button className='flex items-center' disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
@@ -81,9 +82,7 @@ const RefreshableModels = () => {
|
||||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
return <RefreshModelButton key={providerName} providerName={providerName} />
|
||||
})
|
||||
|
||||
return <>
|
||||
@@ -256,7 +255,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
|
||||
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
|
||||
|
||||
const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
@@ -268,6 +267,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
@@ -290,6 +290,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||
return [disposable]
|
||||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
isPasswordField={isPasswordField}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
@@ -338,7 +339,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
||||
{needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} below (Models).`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
@@ -367,15 +368,16 @@ export const AutoRefreshToggle = () => {
|
||||
// right now this is just `enabled_autoRefreshModels`
|
||||
const enabled = voidSettingsState.globalSettings[settingName]
|
||||
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting(settingName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
|
||||
}}
|
||||
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
return <SubtleButton
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting(settingName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
|
||||
}}
|
||||
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
/>
|
||||
|
||||
}
|
||||
|
||||
export const AIInstructionsBox = () => {
|
||||
@@ -385,7 +387,7 @@ export const AIInstructionsBox = () => {
|
||||
return <VoidInputBox2
|
||||
className='min-h-[81px] p-3 rounded-sm'
|
||||
initValue={voidSettingsState.globalSettings.aiInstructions}
|
||||
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
|
||||
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Write new code using Rust if possible. `}
|
||||
multiline
|
||||
onChangeText={(newText) => {
|
||||
voidSettingsService.setGlobalSetting('aiInstructions', newText)
|
||||
@@ -395,7 +397,17 @@ export const AIInstructionsBox = () => {
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
return <>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<div className='py-2'/>
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
@@ -420,13 +432,20 @@ export const FeaturesTab = () => {
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Models</h2>
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
|
||||
<ErrorBoundary>
|
||||
<AutoRefreshToggle />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
{featureNames.map(featureName =>
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -489,7 +508,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe
|
||||
throw new Error(`os '${os}' not recognized`)
|
||||
}
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
let transferTheseFiles: TransferFilesInfo = []
|
||||
let transferError: string | null = null
|
||||
|
||||
@@ -588,17 +607,6 @@ const GeneralTab = () => {
|
||||
<AIInstructionsBox />
|
||||
</div>
|
||||
|
||||
<div className='mt-12'>
|
||||
<h2 className={`text-3xl mb-2`}>Model Selection</h2>
|
||||
{featureNames.map(featureName =>
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -618,7 +626,7 @@ export const Settings = () => {
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
<h1 className='text-2xl w-full'>Void Settings</h1>
|
||||
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
|
||||
|
||||
{/* separator */}
|
||||
<div className='w-full h-[1px] my-4' />
|
||||
|
||||
@@ -28,17 +28,25 @@ module.exports = {
|
||||
|
||||
colors: {
|
||||
"void-bg-1": "var(--vscode-input-background)",
|
||||
"void-bg-1-alt": "var(--vscode-badge-background)",
|
||||
"void-bg-2": "var(--vscode-sideBar-background)",
|
||||
"void-bg-2-alt": "color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%)",
|
||||
"void-bg-3": "var(--vscode-editor-background)",
|
||||
|
||||
|
||||
"void-fg-1": "var(--vscode-editor-foreground)",
|
||||
"void-fg-2": "var(--vscode-input-foreground)",
|
||||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
// "void-fg-4": "var(--vscode-tab-inactiveForeground)",
|
||||
"void-fg-4": "var(--vscode-list-deemphasizedForeground)",
|
||||
|
||||
|
||||
"void-warning": "var(--vscode-charts-yellow)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"void-border-4": "var(--vscode-editorGroup-border)",
|
||||
|
||||
|
||||
vscode: {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
|
||||
|
||||
|
||||
export type ChatLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
|
||||
export type ApplyBoxLocation = ChatLocation & { codeblockId: string }
|
||||
|
||||
export const getApplyBoxId = ({ threadId, messageIdx, codeblockId }: ApplyBoxLocation) => {
|
||||
return `${threadId}-${messageIdx}-${codeblockId}}`
|
||||
}
|
||||
|
||||
export type SearchAndReplaceBlock = {
|
||||
search: string;
|
||||
replace: string;
|
||||
}
|
||||
|
||||
// service that manages state
|
||||
export type ApplyState = {
|
||||
[applyBoxId: string]: {
|
||||
searchAndReplaceBlocks: SearchAndReplaceBlock;
|
||||
}
|
||||
}
|
||||
|
||||
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
|
||||
|
||||
export interface IFastApplyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// readonly state: ApplyState; // readonly to the user
|
||||
// setState(newState: Partial<ApplyState>): void;
|
||||
// onDidChangeState: Event<void>;
|
||||
}
|
||||
|
||||
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
|
||||
class VoidFastApplyService extends Disposable implements IFastApplyService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidFastApplyService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
|
||||
// state
|
||||
// state: ApplyState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
// this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<ApplyState>) {
|
||||
|
||||
// this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
export interface ISearchReplaceService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
|
||||
class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
constructor(
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
send(params: Omit<ServiceSendLLMMessageParams, 'onText'> & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
|
||||
this.llmMessageService.sendLLMMessage({
|
||||
...params as ServiceSendLLMMessageParams,
|
||||
onText: (p) => {
|
||||
const { retry } = params.onText(p)
|
||||
if (retry) {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ISearchReplaceService, SearchReplaceService, InstantiationType.Eager);
|
||||
@@ -141,13 +141,11 @@ registerAction2(class extends Action2 {
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
const [state, setState] = chatThreadService._useCurrentThreadState()
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
selections = chatThreadService.getCurrentThreadStagingSelections()
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s)
|
||||
} else {
|
||||
const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx)
|
||||
selections = state.stagingSelections
|
||||
setSelections = (s) => setState({ stagingSelections: s })
|
||||
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
@@ -241,7 +239,7 @@ registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.settingsAction',
|
||||
title: 'Void Settings',
|
||||
title: `Void's Settings`,
|
||||
icon: { id: 'settings-gear' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
// register inline diffs
|
||||
import './inlineDiffsService.js'
|
||||
import './editCodeService.js'
|
||||
|
||||
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
|
||||
import './sidebarActions.js'
|
||||
|
||||
@@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput {
|
||||
}
|
||||
|
||||
override getName(): string {
|
||||
return nls.localize('voidSettingsInputsName', 'Void Settings');
|
||||
return nls.localize('voidSettingsInputsName', 'Void\'s Settings');
|
||||
}
|
||||
|
||||
override getIcon() {
|
||||
@@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane {
|
||||
|
||||
// register Settings pane
|
||||
Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane(
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")),
|
||||
EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")),
|
||||
[new SyncDescriptor(VoidSettingsInput)]
|
||||
);
|
||||
|
||||
@@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
||||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void Settings")
|
||||
title: nls.localize('voidSettings', "Void\'s Settings")
|
||||
},
|
||||
order: 1
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js';
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
@@ -24,27 +24,39 @@ export interface ILLMMessageService {
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
|
||||
}
|
||||
|
||||
|
||||
// open this file side by side with llmMessageChannel
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
// sendLLMMessage
|
||||
private readonly llmMessageHooks = {
|
||||
onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) },
|
||||
onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) },
|
||||
onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) },
|
||||
}
|
||||
|
||||
|
||||
// ollamaList
|
||||
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) } = {}
|
||||
// list hooks
|
||||
private readonly listHooks = {
|
||||
ollama: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
|
||||
},
|
||||
vLLM: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
|
||||
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@@ -59,32 +71,14 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._onRequestIdDone(e.requestId) }))
|
||||
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
|
||||
// ollama .list()
|
||||
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<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible .list()
|
||||
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)
|
||||
}))
|
||||
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event<EventModelListOnSuccessParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onError_list_vLLM') satisfies Event<EventModelListOnErrorParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) }))
|
||||
|
||||
}
|
||||
|
||||
@@ -99,15 +93,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
let message: string
|
||||
|
||||
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
|
||||
message = `Please add a provider in Void Settings.`
|
||||
message = `Please add a provider in Void's Settings.`
|
||||
else if (isDisabled === 'addModel')
|
||||
message = `Please add a model.`
|
||||
else if (isDisabled === 'needToEnableModel')
|
||||
message = `Please enable a model.`
|
||||
else if (isDisabled === 'notFilledIn')
|
||||
message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
|
||||
message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
|
||||
else
|
||||
message = 'Please add a provider in Void Settings.'
|
||||
message = `Please add a provider in Void's Settings.`
|
||||
|
||||
onError({ message, fullError: null })
|
||||
return null
|
||||
@@ -117,9 +111,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
// add state for request id
|
||||
const requestId = generateUuid();
|
||||
this.onTextHooks_llm[requestId] = onText
|
||||
this.onFinalMessageHooks_llm[requestId] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId] = onError
|
||||
this.llmMessageHooks.onText[requestId] = onText
|
||||
this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage
|
||||
this.llmMessageHooks.onError[requestId] = onError
|
||||
|
||||
const { aiInstructions } = this.voidSettingsService.state.globalSettings
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
@@ -151,43 +145,46 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
this.listHooks.ollama.success[requestId_] = onSuccess
|
||||
this.listHooks.ollama.error[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'ollama',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
|
||||
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.listHooks.vLLM.success[requestId_] = onSuccess
|
||||
this.listHooks.vLLM.error[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
this.channel.call('vLLMList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'vLLM',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
} satisfies MainModelListParams<VLLMModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
delete this.llmMessageHooks.onText[requestId]
|
||||
delete this.llmMessageHooks.onFinalMessage[requestId]
|
||||
delete this.llmMessageHooks.onError[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
delete this.listHooks.ollama.success[requestId]
|
||||
delete this.listHooks.ollama.error[requestId]
|
||||
|
||||
delete this.listHooks.vLLM.success[requestId]
|
||||
delete this.listHooks.vLLM.error[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChatMessage } from '../browser/chatThreadService.js'
|
||||
import { InternalToolInfo, ToolName } from './toolsService.js'
|
||||
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
@@ -20,22 +22,50 @@ export const errorDetails = (fullError: Error | null): string | null => {
|
||||
return null
|
||||
}
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
|
||||
export type LLMChatMessage = {
|
||||
role: 'system' | 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
} | {
|
||||
role: 'tool';
|
||||
content: string; // result
|
||||
name: string;
|
||||
params: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
export type ToolCallType = {
|
||||
name: ToolName;
|
||||
params: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMChatMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
|
||||
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
|
||||
if (c.role === 'system' || c.role === 'user') {
|
||||
return { role: c.role, content: c.content || '(empty message)' }
|
||||
}
|
||||
else if (c.role === 'assistant')
|
||||
return { role: c.role, content: c.content || '(empty message)' }
|
||||
else if (c.role === 'tool')
|
||||
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' }
|
||||
else {
|
||||
throw 1
|
||||
}
|
||||
}
|
||||
|
||||
export type _InternalLLMChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
type _InternalSendFIMMessage = {
|
||||
export type LLMFIMMessage = {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
stopTokens: string[];
|
||||
@@ -44,9 +74,11 @@ type _InternalSendFIMMessage = {
|
||||
type SendLLMType = {
|
||||
messagesType: 'chatMessages';
|
||||
messages: LLMChatMessage[];
|
||||
tools?: InternalToolInfo[];
|
||||
} | {
|
||||
messagesType: 'FIMMessage';
|
||||
messages: _InternalSendFIMMessage;
|
||||
messages: LLMFIMMessage;
|
||||
tools?: undefined;
|
||||
}
|
||||
|
||||
// service types
|
||||
@@ -86,34 +118,6 @@ export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0]
|
||||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
|
||||
export type _InternalSendLLMChatMessageFnType = (
|
||||
params: {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
|
||||
messages: _InternalLLMChatMessage[];
|
||||
}
|
||||
) => void
|
||||
|
||||
export type _InternalSendLLMFIMMessageFnType = (
|
||||
params: {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
|
||||
messages: _InternalSendFIMMessage;
|
||||
}
|
||||
) => void
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
|
||||
@@ -145,18 +149,22 @@ export type OllamaModelResponse = {
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export type VLLMModelResponse = OpenaiCompatibleModelResponse
|
||||
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type ModelListParams<modelResponse> = {
|
||||
export type ModelListParams<ModelResponse> = {
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onSuccess: (param: { models: ModelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
@@ -175,4 +183,3 @@ export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListPar
|
||||
|
||||
|
||||
|
||||
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
@@ -45,6 +45,7 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
@@ -140,10 +141,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
vLLM: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
// start listening for models (and don't stop)
|
||||
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
|
||||
|
||||
this._clearProviderTimeout(providerName)
|
||||
@@ -158,7 +160,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
|
||||
: () => { }
|
||||
|
||||
listFn({
|
||||
@@ -169,7 +171,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js'
|
||||
import { IFileService } from '../../../../platform/files/common/files.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
|
||||
import { _VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js'
|
||||
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
|
||||
import { IVoidFileService } from './voidFileService.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
@@ -15,6 +15,7 @@ import { ISearchService } from '../../../../workbench/services/search/common/sea
|
||||
|
||||
// we do this using Anthropic's style and convert to OpenAI style later
|
||||
export type InternalToolInfo = {
|
||||
name: string,
|
||||
description: string,
|
||||
params: {
|
||||
[paramName: string]: { type: string, description: string | undefined } // name -> type
|
||||
@@ -22,15 +23,15 @@ export type InternalToolInfo = {
|
||||
required: string[], // required paramNames
|
||||
}
|
||||
|
||||
// helper
|
||||
const pagination = {
|
||||
const paginationHelper = {
|
||||
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
|
||||
} as const
|
||||
|
||||
export const contextTools = {
|
||||
export const voidTools = {
|
||||
read_file: {
|
||||
description: 'Returns file contents of a given URI.',
|
||||
name: 'read_file',
|
||||
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
},
|
||||
@@ -38,145 +39,326 @@ export const contextTools = {
|
||||
},
|
||||
|
||||
list_dir: {
|
||||
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
|
||||
name: 'list_dir',
|
||||
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...pagination.param
|
||||
...paginationHelper.param
|
||||
},
|
||||
required: ['uri'],
|
||||
},
|
||||
|
||||
pathname_search: {
|
||||
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`,
|
||||
name: 'pathname_search',
|
||||
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
|
||||
search: {
|
||||
description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`,
|
||||
name: 'search',
|
||||
description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
|
||||
// go_to_definition:
|
||||
|
||||
// go_to_usages:
|
||||
|
||||
// create_file: {
|
||||
// name: 'create_file',
|
||||
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
|
||||
// params: {
|
||||
// uri: { type: 'string', description: undefined },
|
||||
// }
|
||||
// }
|
||||
|
||||
// semantic_search: {
|
||||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
// },
|
||||
} satisfies { [name: string]: InternalToolInfo }
|
||||
|
||||
} as const satisfies { [name: string]: InternalToolInfo }
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
export type ContextToolName = keyof typeof contextTools
|
||||
type ContextToolParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
|
||||
type ContextToolParams<T extends ContextToolName> = { [paramName in ContextToolParamNames<T>]: unknown }
|
||||
|
||||
type AllContextToolCallFns = {
|
||||
[ToolName in ContextToolName]: ((p: (ContextToolParams<ToolName>)) => Promise<string>)
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
export type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
|
||||
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
|
||||
|
||||
export type ToolCallReturnType = {
|
||||
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
|
||||
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
|
||||
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
|
||||
'create_file': {}
|
||||
}
|
||||
|
||||
type DirectoryItem = {
|
||||
uri: URI;
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
export type ToolFns = { [T in ToolName]: (p: string) => Promise<ToolCallReturnType[T]> }
|
||||
export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string }
|
||||
|
||||
|
||||
// pagination info
|
||||
const MAX_FILE_CHARS_PAGE = 50_000
|
||||
const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO check to make sure in workspace
|
||||
// TODO check to make sure is not gitignored
|
||||
|
||||
|
||||
async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise<string> {
|
||||
let output = ''
|
||||
function traverseChildren(children: IFileStat[], depth: number) {
|
||||
const indentation = ' '.repeat(depth);
|
||||
for (const child of children) {
|
||||
output += `${indentation}- ${child.name}\n`;
|
||||
traverseChildren(child.children ?? [], depth + 1);
|
||||
}
|
||||
}
|
||||
const computeDirectoryResult = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1
|
||||
): Promise<ToolCallReturnType['list_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
// kickstart recursion
|
||||
output += `${stat.name}\n`;
|
||||
traverseChildren(stat.children ?? [], 1);
|
||||
const originalChildrenLength = stat.children?.length ?? 0;
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
const children: DirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
rootURI,
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${result.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) {
|
||||
output += `${result.rootURI}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
try {
|
||||
const o = JSON.parse(s)
|
||||
return o
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Tool parameter was not a valid JSON: "${s}".`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const validateQueryStr = (queryStr: unknown) => {
|
||||
if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.')
|
||||
return queryStr
|
||||
}
|
||||
|
||||
|
||||
// TODO!!!! check to make sure in workspace
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('(uri was not a string)')
|
||||
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
export interface IToolService {
|
||||
const validatePageNum = (pageNumberUnknown: unknown) => {
|
||||
const proposedPageNum = Number.parseInt(pageNumberUnknown + '')
|
||||
const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1
|
||||
const pageNumber = num < 1 ? 1 : num
|
||||
return pageNumber
|
||||
}
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
callContextTool: <T extends ContextToolName>(toolName: T, params: ContextToolParams<T>) => Promise<string>
|
||||
toolFns: ToolFns;
|
||||
toolResultToString: ToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolService = createDecorator<IToolService>('ToolService');
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
|
||||
export class ToolService implements IToolService {
|
||||
export class ToolsService implements IToolsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
contextToolCallFns: AllContextToolCallFns
|
||||
public toolFns: ToolFns
|
||||
public toolResultToString: ToolResultToString
|
||||
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IVoidFileService voidFileService: IVoidFileService,
|
||||
) {
|
||||
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.contextToolCallFns = {
|
||||
read_file: async ({ uri: uriStr }) => {
|
||||
this.toolFns = {
|
||||
read_file: async (s: string) => {
|
||||
console.log('read_file')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const fileContents = await _VSReadFileRaw(fileService, uri)
|
||||
return fileContents ?? '(could not read file)'
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const readFileContents = await voidFileService.readFile(uri)
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
|
||||
|
||||
console.log('read_file result:', fileContents)
|
||||
|
||||
|
||||
return { uri, fileContents, hasNextPage }
|
||||
},
|
||||
list_dir: async ({ uri: uriStr }) => {
|
||||
list_dir: async (s: string) => {
|
||||
console.log('list_dir')
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const treeStr = await generateDirectoryTreeMd(fileService, uri)
|
||||
return treeStr
|
||||
},
|
||||
pathname_search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, });
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
},
|
||||
search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri));
|
||||
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
|
||||
console.log('list_dir result:', dirResult)
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
return dirResult
|
||||
},
|
||||
pathname_search: async (s: string) => {
|
||||
console.log('pathname_search')
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateQueryStr(queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
console.log('pathname_search result:', uris)
|
||||
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
search: async (s: string) => {
|
||||
console.log('search')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateQueryStr(queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
|
||||
console.log('search result:', uris)
|
||||
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
this.toolResultToString = {
|
||||
read_file: (result) => {
|
||||
return nextPageStr(result.hasNextPage)
|
||||
},
|
||||
list_dir: (result) => {
|
||||
const dirTreeStr = directoryResultToString(result)
|
||||
return dirTreeStr + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
pathname_search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
callContextTool: IToolService['callContextTool'] = (toolName, params) => {
|
||||
return this.contextToolCallFns[toolName](params)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolService, ToolService, InstantiationType.Eager);
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
// linebreak symbols
|
||||
export const allLinebreakSymbols = ['\r\n', '\n']
|
||||
export const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
export interface IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string>;
|
||||
readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null;
|
||||
}
|
||||
|
||||
export const IVoidFileService = createDecorator<IVoidFileService>('VoidFileService');
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidFileService implements IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string> => {
|
||||
|
||||
// attempt to read the model
|
||||
const modelResult = this.readModel(uri, range);
|
||||
if (modelResult) return modelResult;
|
||||
|
||||
// if no model, read the raw file
|
||||
const fileResult = await this._readFileRaw(uri, range);
|
||||
if (fileResult) return fileResult;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
|
||||
|
||||
try { // this throws an error if no file exists (eg it was deleted)
|
||||
|
||||
const res = await this.fileService.readFile(uri);
|
||||
|
||||
if (range) {
|
||||
return res.value.toString()
|
||||
.split(_ln)
|
||||
.slice(range.startLineNumber - 1, range.endLineNumber)
|
||||
.join(_ln)
|
||||
}
|
||||
|
||||
return res.value.toString();
|
||||
|
||||
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => {
|
||||
|
||||
// read saved model (sometimes null if the user reloads application)
|
||||
let model = this.modelService.getModel(uri);
|
||||
|
||||
// check all opened models for the same `fsPath`
|
||||
if (!model) {
|
||||
const models = this.modelService.getModels();
|
||||
for (const m of models) {
|
||||
if (m.uri.fsPath === uri.fsPath) {
|
||||
model = m
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if still not found, return
|
||||
if (!model) { return null }
|
||||
|
||||
// if range, read it
|
||||
if (range) {
|
||||
return model.getValueInRange({
|
||||
startLineNumber: range.startLineNumber,
|
||||
endLineNumber: range.endLineNumber,
|
||||
startColumn: 1,
|
||||
endColumn: Number.MAX_VALUE
|
||||
}, EndOfLinePreference.LF);
|
||||
} else {
|
||||
return model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager);
|
||||
@@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
@@ -89,7 +89,7 @@ const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>)
|
||||
// update model options
|
||||
let newModelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerTitle = displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName
|
||||
const providerTitle = providerName // displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName
|
||||
if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) {
|
||||
if (isHidden) continue
|
||||
@@ -131,7 +131,7 @@ const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>)
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null },
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
_modelOptions: [], // computed later
|
||||
}
|
||||
@@ -175,6 +175,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
|
||||
...{ mistral: defaultSettingsOfProvider.mistral },
|
||||
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
|
||||
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
|
||||
...{ vLLM: defaultSettingsOfProvider.vLLM },
|
||||
|
||||
|
||||
...readS.settingsOfProvider,
|
||||
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
@@ -189,7 +196,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'FastApply': null },
|
||||
...{ 'Apply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
}
|
||||
|
||||
@@ -289,27 +296,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
|
||||
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
|
||||
setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
const oldModelNames = models.map(m => m.modelName)
|
||||
|
||||
const newDefaultModelInfo = modelInfoOfDefaultModelNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
|
||||
const newModelInfo = [
|
||||
...newDefaultModelInfo, // swap out all the default models for the new default models
|
||||
...models.filter(m => !m.isDefault), // keep any non-defaul (custom) models
|
||||
|
||||
const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models })
|
||||
const newModels = [
|
||||
...newDefaultModels, // swap out all the default models for the new default models
|
||||
...models.filter(m => !m.isDefault), // keep any non-default (custom) models
|
||||
]
|
||||
|
||||
|
||||
this.setSettingOfProvider(providerName, 'models', newModelInfo)
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
const new_names = newModelInfo.map(m => m.modelName)
|
||||
const new_names = newModels.map(m => m.modelName)
|
||||
if (!(oldModelNames.length === new_names.length
|
||||
&& oldModelNames.every((_, i) => oldModelNames[i] === new_names[i]))
|
||||
) {
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels: newModelInfo, ...logging })
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels: newModels, ...logging })
|
||||
}
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
@@ -335,7 +341,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
{ ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
|
||||
@@ -4,144 +4,13 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
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 (switched off)
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
}
|
||||
|
||||
// creates `modelInfo` from `modelNames`
|
||||
export const modelInfoOfDefaultModelNames = (defaultModelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
|
||||
|
||||
const { isAutodetected, existingModels } = options ?? {}
|
||||
|
||||
if (!existingModels) { // default settings
|
||||
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: defaultModelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
|
||||
} else { // settings if there are existing models (keep existing `isHidden` property)
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultModelNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultModelNames([
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'o3-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// 'gpt-4o-mini-2024-07-18',
|
||||
// 'gpt-4-turbo',
|
||||
// 'gpt-4-turbo-2024-04-09',
|
||||
// 'gpt-4-turbo-preview',
|
||||
// 'gpt-4-0125-preview',
|
||||
// 'gpt-4-1106-preview',
|
||||
// 'gpt-4',
|
||||
// 'gpt-4-0613',
|
||||
// 'gpt-3.5-turbo-0125',
|
||||
// 'gpt-3.5-turbo',
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultDeepseekModels = modelInfoOfDefaultModelNames([
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
])
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultModelNames([
|
||||
"llama3-70b-8192",
|
||||
"llama-3.3-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"gemma2-9b-it",
|
||||
"mixtral-8x7b-32768"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultModelNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-thinking-exp-1219',
|
||||
'learnlm-1.5-pro-experimental'
|
||||
])
|
||||
|
||||
export const defaultMistralModels = modelInfoOfDefaultModelNames([
|
||||
"codestral-latest",
|
||||
"open-codestral-mamba",
|
||||
"open-mistral-nemo",
|
||||
"mistral-large-latest",
|
||||
"pixtral-large-latest",
|
||||
"ministral-3b-latest",
|
||||
"ministral-8b-latest",
|
||||
"mistral-small-latest",
|
||||
])
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
@@ -155,6 +24,9 @@ export const defaultProviderSettings = {
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
vLLM: {
|
||||
endpoint: 'http://localhost:8000',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
@@ -170,13 +42,16 @@ export const defaultProviderSettings = {
|
||||
},
|
||||
mistral: {
|
||||
apiKey: ''
|
||||
}
|
||||
},
|
||||
xAI: {
|
||||
apiKey: ''
|
||||
},
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names
|
||||
export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
@@ -189,6 +64,14 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
|
||||
|
||||
|
||||
export type VoidModelInfo = { // <-- STATEFUL
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||
|
||||
|
||||
|
||||
type CommonProviderSettings = {
|
||||
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
@@ -205,10 +88,6 @@ export type SettingsOfProvider = {
|
||||
|
||||
export type SettingName = keyof SettingsAtProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type DisplayInfoForProviderName = {
|
||||
title: string,
|
||||
desc?: string,
|
||||
@@ -238,7 +117,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
else if (providerName === 'ollama') {
|
||||
return {
|
||||
title: 'Ollama',
|
||||
|
||||
}
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
return {
|
||||
title: 'vLLM',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
@@ -261,14 +144,21 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
||||
title: 'Mistral API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return {
|
||||
title: 'xAI API',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
title: string;
|
||||
placeholder: string;
|
||||
subTextMd?: string;
|
||||
isPasswordField?: boolean;
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
@@ -285,7 +175,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'mistral' ? 'key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'',
|
||||
providerName === 'xAI' ? 'xai-key...' :
|
||||
'',
|
||||
|
||||
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).' :
|
||||
@@ -294,22 +185,27 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||
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 === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
|
||||
providerName === 'openAICompatible' ? undefined :
|
||||
'',
|
||||
isPasswordField: true,
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
undefined,
|
||||
providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === '_didFillInProviderSettings') {
|
||||
@@ -338,92 +234,83 @@ const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
deepseek: {
|
||||
models: defaultDeepseekModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
mistral: {
|
||||
models: defaultMistralModels,
|
||||
const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => {
|
||||
return {
|
||||
models: defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.anthropic),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
deepseek: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.deepseek,
|
||||
...voidInitModelOptions.deepseek,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.deepseek),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
mistral: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.mistral,
|
||||
...voidInitModelOptions.mistral,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
xAI: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.xAI,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
groq: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openRouter: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
openAICompatible: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
ollama: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
vLLM: { // aggregator
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.vLLM,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
@@ -436,18 +323,20 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'FastApply'] as const
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
|
||||
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
|
||||
export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
||||
// editor:
|
||||
if (featureName === 'Autocomplete')
|
||||
return 'Autocomplete'
|
||||
else if (featureName === 'Ctrl+K')
|
||||
return 'Quick-Edit'
|
||||
return 'Quick Edit'
|
||||
// sidebar:
|
||||
else if (featureName === 'Ctrl+L')
|
||||
return 'Chat'
|
||||
else if (featureName === 'FastApply')
|
||||
else if (featureName === 'Apply')
|
||||
return 'Apply'
|
||||
else
|
||||
throw new Error(`Feature Name ${featureName} not allowed`)
|
||||
@@ -528,77 +417,3 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const recognizedModels = [
|
||||
|
||||
// chat
|
||||
'OpenAI 4o',
|
||||
'Anthropic Claude',
|
||||
'Llama 3.x',
|
||||
'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model
|
||||
// 'xAI Grok',
|
||||
// 'Google Gemini, Gemma',
|
||||
// 'Microsoft Phi4',
|
||||
|
||||
|
||||
// coding (autocomplete)
|
||||
'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5
|
||||
'Mistral Codestral',
|
||||
|
||||
// thinking
|
||||
'OpenAI o1, o3',
|
||||
'Deepseek R1',
|
||||
|
||||
// general
|
||||
'<General>'
|
||||
// 'Mixtral 8x7b'
|
||||
// 'Qwen2.5',
|
||||
|
||||
] as const
|
||||
|
||||
|
||||
|
||||
|
||||
type RecognizedModel = (typeof recognizedModels)[number]
|
||||
|
||||
|
||||
// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = {
|
||||
// 'OpenAI 4o': {
|
||||
// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\
|
||||
// `
|
||||
// }
|
||||
// }
|
||||
|
||||
export function getRecognizedModel(modelName: string): RecognizedModel {
|
||||
const lower = modelName.toLowerCase();
|
||||
|
||||
if (lower.includes('gpt-4o')) {
|
||||
return 'OpenAI 4o';
|
||||
}
|
||||
if (lower.includes('claude')) {
|
||||
return 'Anthropic Claude';
|
||||
}
|
||||
if (lower.includes('llama')) {
|
||||
return 'Llama 3.x';
|
||||
}
|
||||
if (lower.includes('qwen2.5-coder')) {
|
||||
return 'Alibaba Qwen2.5 Coder Instruct';
|
||||
}
|
||||
if (lower.includes('mistral')) {
|
||||
return 'Mistral Codestral';
|
||||
}
|
||||
// Check for "o1" or "o3"
|
||||
if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) {
|
||||
return 'OpenAI o1, o3';
|
||||
}
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) {
|
||||
return 'Deepseek R1';
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Fallback:
|
||||
return '<General>';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,776 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { Model as OpenAIModel } from 'openai/resources/models.js';
|
||||
import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js';
|
||||
import { InternalToolInfo, isAToolName } from '../../common/toolsService.js';
|
||||
import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareMessages } from './preprocessLLMMessages.js';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { Ollama } from 'ollama';
|
||||
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-5-sonnet-latest',
|
||||
'claude-3-5-haiku-latest',
|
||||
'claude-3-opus-latest',
|
||||
],
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'o3-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
],
|
||||
deepseek: [ // https://platform.openai.com/docs/models/gp
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
],
|
||||
ollama: [],
|
||||
vLLM: [],
|
||||
openRouter: [],
|
||||
openAICompatible: [],
|
||||
gemini: [
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-2.0-flash-thinking-exp-1219',
|
||||
'learnlm-1.5-pro-experimental'
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
"llama3-70b-8192",
|
||||
"llama-3.3-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
"gemma2-9b-it",
|
||||
"mixtral-8x7b-32768"
|
||||
],
|
||||
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
"codestral-latest",
|
||||
"open-codestral-mamba",
|
||||
"open-mistral-nemo",
|
||||
"mistral-large-latest",
|
||||
"pixtral-large-latest",
|
||||
"ministral-3b-latest",
|
||||
"ministral-8b-latest",
|
||||
"mistral-small-latest",
|
||||
],
|
||||
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
|
||||
'grok-3-latest',
|
||||
'grok-2-latest',
|
||||
],
|
||||
} satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
|
||||
type ModelOptions = {
|
||||
contextWindow: number;
|
||||
cost: {
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
}
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style';
|
||||
supportsFIM: false | 'TODO_FIM_FORMAT';
|
||||
|
||||
supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens
|
||||
manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model
|
||||
}
|
||||
|
||||
type ProviderReasoningOptions = {
|
||||
// include this in payload to get reasoning
|
||||
input?: { includeInPayload?: { [key: string]: any }, };
|
||||
// nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField]
|
||||
// needsManualParse: whether we must manually parse out the <think> tags
|
||||
output?:
|
||||
| { nameOfFieldInDelta?: string, needsManualParse?: undefined, }
|
||||
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
|
||||
}
|
||||
|
||||
type ProviderSettings = {
|
||||
providerReasoningOptions?: ProviderReasoningOptions;
|
||||
modelOptions: { [key: string]: ModelOptions };
|
||||
modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid
|
||||
}
|
||||
|
||||
|
||||
type ModelSettingsOfProvider = {
|
||||
[providerName in ProviderName]: ProviderSettings
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.`
|
||||
|
||||
|
||||
|
||||
// ---------------- OPENAI ----------------
|
||||
const openAIModelOptions = {
|
||||
"o1": {
|
||||
contextWindow: 128_000,
|
||||
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: 'developer-role',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
"o3-mini": {
|
||||
contextWindow: 200_000,
|
||||
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: 'developer-role',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
"gpt-4o": {
|
||||
contextWindow: 128_000,
|
||||
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
const openAISettings: ProviderSettings = {
|
||||
modelOptions: openAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
if (modelName.includes('o1')) return openAIModelOptions['o1']
|
||||
if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini']
|
||||
if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o']
|
||||
throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI'))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- ANTHROPIC ----------------
|
||||
const anthropicModelOptions = {
|
||||
"claude-3-5-sonnet-20241022": {
|
||||
contextWindow: 200_000,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
|
||||
},
|
||||
"claude-3-5-haiku-20241022": {
|
||||
contextWindow: 200_000,
|
||||
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
"claude-3-opus-20240229": {
|
||||
contextWindow: 200_000,
|
||||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
"claude-3-sonnet-20240229": {
|
||||
contextWindow: 200_000, cost: { input: 3.00, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
}
|
||||
} as const
|
||||
|
||||
const anthropicSettings: ProviderSettings = {
|
||||
modelOptions: anthropicModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------- XAI ----------------
|
||||
const XAIModelOptions = {
|
||||
"grok-2-latest": {
|
||||
contextWindow: 131_072,
|
||||
cost: { input: 2.00, output: 10.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const
|
||||
|
||||
const XAISettings: ProviderSettings = {
|
||||
modelOptions: XAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const modelSettingsOfProvider: ModelSettingsOfProvider = {
|
||||
openAI: openAISettings,
|
||||
anthropic: anthropicSettings,
|
||||
xAI: XAISettings,
|
||||
gemini: {
|
||||
modelOptions: {
|
||||
|
||||
}
|
||||
},
|
||||
googleVertex: {
|
||||
|
||||
},
|
||||
microsoftAzure: {
|
||||
|
||||
},
|
||||
openRouter: {
|
||||
providerReasoningOptions: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
|
||||
input: { includeInPayload: { include_reasoning: true } },
|
||||
output: { nameOfFieldInDelta: 'reasoning' },
|
||||
}
|
||||
},
|
||||
vLLM: {
|
||||
providerReasoningOptions: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
}
|
||||
},
|
||||
deepseek: {
|
||||
providerReasoningOptions: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
},
|
||||
ollama: {
|
||||
providerReasoningOptions: {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
output: { needsManualParse: true },
|
||||
},
|
||||
},
|
||||
|
||||
openAICompatible: {
|
||||
},
|
||||
mistral: {
|
||||
},
|
||||
groq: {
|
||||
},
|
||||
|
||||
|
||||
|
||||
} as const satisfies ModelSettingsOfProvider
|
||||
|
||||
|
||||
const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => {
|
||||
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
|
||||
if (modelName in modelOptions) return modelOptions[modelName]
|
||||
return modelOptionsFallback(modelName)
|
||||
}
|
||||
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
aiInstructions: string;
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: name,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
type ToolCallOfIndex = { [index: string]: { name: string, params: string, id: string } }
|
||||
|
||||
const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => {
|
||||
return Object.keys(toolCallOfIndex).map(index => {
|
||||
const tool = toolCallOfIndex[index]
|
||||
return isAToolName(tool.name) ? { name: tool.name, id: tool.id, params: tool.params } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
dangerouslyAllowBrowser: true,
|
||||
...includeInPayload,
|
||||
}
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: thisConfig.apiKey,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
...commonPayloadOpts,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
|
||||
else throw new Error(`Void providerName was invalid: ${providerName}.`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const manualParseOnText = (
|
||||
providerName: ProviderName,
|
||||
modelName: string,
|
||||
onText_: OnText
|
||||
): OnText => {
|
||||
return onText_
|
||||
}
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const {
|
||||
supportsReasoning: modelSupportsReasoning,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
} = modelOptionsOfProvider(providerName, modelName)
|
||||
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
|
||||
const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined
|
||||
|
||||
const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {}
|
||||
|
||||
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
|
||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj }
|
||||
|
||||
const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {}
|
||||
if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText)
|
||||
|
||||
let fullReasoning = ''
|
||||
let fullText = ''
|
||||
const toolCallOfIndex: ToolCallOfIndex = {}
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
// tool call
|
||||
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
|
||||
const index = tool.index
|
||||
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', params: '', id: '' }
|
||||
toolCallOfIndex[index].name += tool.function?.name ?? ''
|
||||
toolCallOfIndex[index].params += tool.function?.arguments ?? '';
|
||||
toolCallOfIndex[index].id = tool.id ?? ''
|
||||
}
|
||||
// message
|
||||
const newText = chunk.choices[0]?.delta?.content ?? ''
|
||||
fullText += newText
|
||||
|
||||
// reasoning
|
||||
let newReasoning = ''
|
||||
if (nameOfReasoningFieldInDelta) {
|
||||
// @ts-ignore
|
||||
newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + ''
|
||||
fullReasoning += newReasoning
|
||||
}
|
||||
|
||||
onText({ newText, fullText, newReasoning, fullReasoning })
|
||||
}
|
||||
onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); }
|
||||
else { onError({ message: error + '', fullError: error }); }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal<OpenAIModel>) => {
|
||||
const onSuccess = ({ models }: { models: OpenAIModel[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: OpenAIModel[] = []
|
||||
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 ------------
|
||||
const sendOpenAIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[]) => {
|
||||
return content.map(c => {
|
||||
if (c.type !== 'tool_use') return null
|
||||
if (!isAToolName(c.name)) return null
|
||||
return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const {
|
||||
// supportsReasoning: modelSupportsReasoning,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
contextWindow,
|
||||
} = modelOptionsOfProvider(providerName, modelName)
|
||||
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: separateSystemMessageStr,
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: contextWindow,
|
||||
tools: tools,
|
||||
tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time
|
||||
})
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText, newReasoning: '', fullReasoning: '' })
|
||||
})
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (response) => {
|
||||
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n')
|
||||
const toolCalls = toolCallsFromAnthropicContent(response.content)
|
||||
onFinalMessage({ fullText: content, toolCalls })
|
||||
})
|
||||
// on error
|
||||
stream.on('error', (error) => {
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }) }
|
||||
else { onError({ message: error + '', fullError: error }) }
|
||||
})
|
||||
_setAborter(() => stream.controller.abort())
|
||||
}
|
||||
|
||||
// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming...
|
||||
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
// stream.on('streamEvent', e => {
|
||||
// if (e.type === 'content_block_start') {
|
||||
// if (e.content_block.type !== 'tool_use') return
|
||||
// const index = e.index
|
||||
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
// toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
// toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
// }
|
||||
// else if (e.type === 'content_block_delta') {
|
||||
// if (e.delta.type !== 'input_json_delta') return
|
||||
// toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
// ------------ XAI ------------
|
||||
const sendXAIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ GEMINI ------------
|
||||
const sendGeminiAPIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ OLLAMA ------------
|
||||
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!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: endpoint })
|
||||
return ollama
|
||||
}
|
||||
|
||||
const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal<OllamaModelResponse>) => {
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
|
||||
let fullText = ''
|
||||
ollama.generate({
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
options: {
|
||||
stop: messages.stopTokens,
|
||||
num_predict: 300, // max tokens
|
||||
// repeat_penalty: 1,
|
||||
},
|
||||
raw: true,
|
||||
stream: true, // stream is not necessary but lets us expose the
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.response
|
||||
fullText += newText
|
||||
}
|
||||
onFinalMessage({ fullText })
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat!
|
||||
const sendOllamaChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE ------------
|
||||
// TODO!!! FIM
|
||||
|
||||
// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration
|
||||
const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ OPENROUTER ------------
|
||||
const sendOpenRouterChat = (params: SendChatParams_Internal) => {
|
||||
_sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ VLLM ------------
|
||||
const vLLMList = async (params: ListParams_Internal<OpenAIModel>) => {
|
||||
return _openaiCompatibleList(params)
|
||||
}
|
||||
const sendVLLMFIM = (params: SendFIMParams_Internal) => {
|
||||
// TODO!!!
|
||||
}
|
||||
|
||||
// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration
|
||||
const sendVLLMChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ DEEPSEEK API ------------
|
||||
const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ MISTRAL ------------
|
||||
const sendMistralAPIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
// ------------ GROQ ------------
|
||||
const sendGroqAPIChat = (params: SendChatParams_Internal) => {
|
||||
return _sendOpenAICompatibleChat(params)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
FIM:
|
||||
|
||||
qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327
|
||||
<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
|
||||
|
||||
codestral https://ollama.com/library/codestral/blobs/51707752a87c
|
||||
[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }}
|
||||
|
||||
deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0
|
||||
<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|>
|
||||
|
||||
starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe
|
||||
<file_sep>
|
||||
<fim_prefix>
|
||||
{{ .Prompt }}<fim_suffix>{{ .Suffix }}<fim_middle>
|
||||
<|end_of_text|>
|
||||
|
||||
codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749
|
||||
<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
type CallFnOfProvider = {
|
||||
[providerName in ProviderName]: {
|
||||
sendChat: (params: SendChatParams_Internal) => void;
|
||||
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
||||
list: ((params: ListParams_Internal<any>) => void) | null;
|
||||
}
|
||||
}
|
||||
export const sendLLMMessageToProviderImplementation = {
|
||||
openAI: {
|
||||
sendChat: sendOpenAIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
anthropic: {
|
||||
sendChat: sendAnthropicChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
xAI: {
|
||||
sendChat: sendXAIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
gemini: {
|
||||
sendChat: sendGeminiAPIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
ollama: {
|
||||
sendChat: sendOllamaChat,
|
||||
sendFIM: sendOllamaFIM,
|
||||
list: ollamaList,
|
||||
},
|
||||
openAICompatible: {
|
||||
sendChat: sendOpenAICompatibleChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
openRouter: {
|
||||
sendChat: sendOpenRouterChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
vLLM: {
|
||||
sendChat: sendVLLMChat,
|
||||
sendFIM: sendVLLMFIM,
|
||||
list: vLLMList,
|
||||
},
|
||||
deepseek: {
|
||||
sendChat: sendDeepSeekAPIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
groq: {
|
||||
sendChat: sendGroqAPIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
mistral: {
|
||||
sendChat: sendMistralAPIChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
|
||||
} satisfies CallFnOfProvider
|
||||
@@ -1,76 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
||||
export const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => {
|
||||
const { description, params, required } = toolInfo
|
||||
return {
|
||||
name: toolName,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
// system: systemMessage,
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: content })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const thisConfig = settingsOfProvider.gemini
|
||||
|
||||
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
|
||||
const model = genAI.getGenerativeModel({ model: modelName });
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({
|
||||
// systemInstruction: systemMessage,
|
||||
contents: geminiMessages,
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
|
||||
const groq = new Groq({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
await groq.chat.completions
|
||||
.create({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Mistral } from '@mistralai/mistralai';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Mistral
|
||||
export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.mistral;
|
||||
|
||||
const mistral = new Mistral({
|
||||
apiKey: thisConfig.apiKey,
|
||||
})
|
||||
|
||||
await mistral.chat
|
||||
.stream({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
})
|
||||
.then(async response => {
|
||||
// Mistral has a really nonstandard API - no interrupt and weird stream types
|
||||
_setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') });
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const c = chunk.data.choices[0].delta.content || ''
|
||||
const newText = (
|
||||
typeof c === 'string' ? c
|
||||
: c?.map(c => c.type === 'text' ? c.text : c.type).join('\n')
|
||||
)
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
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) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ 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 = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.generate({
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
options: {
|
||||
stop: messages.stopTokens,
|
||||
num_predict: 300, // max tokens
|
||||
// repeat_penalty: 1,
|
||||
},
|
||||
raw: true,
|
||||
stream: true,
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.response;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ 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 = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
|
||||
@@ -1,159 +0,0 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
|
||||
|
||||
export const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => {
|
||||
const { description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type NewParams = Pick<Parameters<_InternalSendLLMChatMessageFnType>[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'>
|
||||
const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
return new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider.deepseek
|
||||
return new OpenAI({
|
||||
baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
})
|
||||
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
return new OpenAI({
|
||||
baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// might not currently be used in the code
|
||||
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 openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider })
|
||||
|
||||
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 + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
|
||||
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
|
||||
|
||||
onFinalMessage({ fullText: 'TODO' })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])),
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
|
||||
let newText = ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.name ?? ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.arguments ?? ''
|
||||
newText += chunk.choices[0]?.delta?.content ?? ''
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -0,0 +1,324 @@
|
||||
|
||||
|
||||
import { LLMChatMessage } from '../../common/llmMessageTypes.js';
|
||||
import { deepClone } from '../../../../../base/common/objects.js';
|
||||
|
||||
|
||||
export const parseObject = (args: unknown) => {
|
||||
if (typeof args === 'object')
|
||||
return args
|
||||
if (typeof args === 'string')
|
||||
try { return JSON.parse(args) }
|
||||
catch (e) { return { args } }
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_cloneAndTrim = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => {
|
||||
const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), }))
|
||||
return { messages }
|
||||
}
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_systemMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
})
|
||||
: { separateSystemMessageStr?: string, messages: any[] } => {
|
||||
|
||||
// find system messages and concatenate them
|
||||
let systemMessageStr = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n') || undefined;
|
||||
|
||||
if (aiInstructions)
|
||||
systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}`
|
||||
|
||||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// remove all system messages
|
||||
const newMessages: (LLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
|
||||
|
||||
|
||||
// if (!supportsTools) {
|
||||
// if (!systemMessageStr) systemMessageStr = ''
|
||||
// systemMessageStr += '' // TODO!!! add tool use system message here
|
||||
// }
|
||||
|
||||
|
||||
if (systemMessageStr) {
|
||||
// if supports system message
|
||||
if (supportsSystemMessage) {
|
||||
if (supportsSystemMessage === 'separated')
|
||||
separateSystemMessageStr = systemMessageStr
|
||||
else if (supportsSystemMessage === 'system-role')
|
||||
newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message
|
||||
else if (supportsSystemMessage === 'developer-role')
|
||||
newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message
|
||||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
if (supportsSystemMessage) {
|
||||
if (newMessages.length === 0)
|
||||
newMessages.push({ role: 'user', content: systemMessageStr })
|
||||
// add system mesasges to first message (should be a user message)
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessageStr
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ newMessages[0].content
|
||||
)
|
||||
} as const
|
||||
newMessages.splice(0, 1) // delete first message
|
||||
newMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { messages: newMessages, separateSystemMessageStr }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// convert messages as if about to send to openai
|
||||
/*
|
||||
reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps
|
||||
openai MESSAGE (role=assistant):
|
||||
"tool_calls":[{
|
||||
"type": "function",
|
||||
"id": "call_12345xyz",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}"
|
||||
}]
|
||||
|
||||
openai RESPONSE (role=user):
|
||||
{ "role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": str(result) }
|
||||
|
||||
also see
|
||||
openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_openai = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'tool' }> | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
tool_calls?: {
|
||||
type: 'function';
|
||||
id: string;
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
}[]
|
||||
} | {
|
||||
role: 'tool',
|
||||
id: string; // old val
|
||||
tool_call_id: string; // new val
|
||||
content: string;
|
||||
}
|
||||
)[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const currMsg = messages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') {
|
||||
newMessages.push(currMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// edit previous assistant message to have called the tool
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
prevMsg.tool_calls = [{
|
||||
type: 'function',
|
||||
id: currMsg.id,
|
||||
function: {
|
||||
name: currMsg.name,
|
||||
arguments: JSON.stringify(currMsg.params)
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// add the tool
|
||||
newMessages.push({
|
||||
role: 'tool',
|
||||
id: currMsg.id,
|
||||
content: currMsg.content,
|
||||
tool_call_id: currMsg.id,
|
||||
})
|
||||
}
|
||||
return { messages: newMessages }
|
||||
|
||||
}
|
||||
|
||||
|
||||
// convert messages as if about to send to anthropic
|
||||
/*
|
||||
https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples
|
||||
anthropic MESSAGE (role=assistant):
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "<thinking>I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
}, {
|
||||
"type": "tool_use",
|
||||
"id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"name": "get_weather",
|
||||
"input": { "location": "San Francisco, CA", "unit": "celsius" }
|
||||
}]
|
||||
anthropic RESPONSE (role=user):
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
|
||||
"content": "15 degrees"
|
||||
}]
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'user' }> | {
|
||||
role: 'assistant',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_use';
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
id: string;
|
||||
})[]
|
||||
} | {
|
||||
role: 'user',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content: string;
|
||||
})[]
|
||||
}
|
||||
)[] = messages;
|
||||
|
||||
|
||||
for (let i = 0; i < newMessages.length; i += 1) {
|
||||
const currMsg = newMessages[i]
|
||||
|
||||
if (currMsg.role !== 'tool') continue
|
||||
|
||||
const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined
|
||||
|
||||
if (prevMsg?.role === 'assistant') {
|
||||
if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }]
|
||||
prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) })
|
||||
}
|
||||
|
||||
// turn each tool into a user message with tool results at the end
|
||||
newMessages[i] = {
|
||||
role: 'user',
|
||||
content: [
|
||||
...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const,
|
||||
...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [],
|
||||
]
|
||||
}
|
||||
}
|
||||
return { messages: newMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
|
||||
if (!supportsTools) {
|
||||
return { messages: messages }
|
||||
}
|
||||
else if (supportsTools === 'anthropic-style') {
|
||||
return prepareMessages_tools_anthropic({ messages })
|
||||
}
|
||||
else if (supportsTools === 'openai-style') {
|
||||
return prepareMessages_tools_openai({ messages })
|
||||
}
|
||||
else {
|
||||
throw 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gemini has this, but they're openai-compat so we don't need to implement this
|
||||
gemini request:
|
||||
{ "role": "assistant",
|
||||
"content": null,
|
||||
"function_call": {
|
||||
"name": "get_weather",
|
||||
"arguments": {
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gemini response:
|
||||
{ "role": "assistant",
|
||||
"function_response": {
|
||||
"name": "get_weather",
|
||||
"response": {
|
||||
"temperature": "15°C",
|
||||
"condition": "Cloudy"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const prepareMessages = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style',
|
||||
}) => {
|
||||
const { messages: messages1 } = prepareMessages_cloneAndTrim({ messages })
|
||||
const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools })
|
||||
return {
|
||||
messages: messages3 as any,
|
||||
separateSystemMessageStr
|
||||
} as const
|
||||
}
|
||||
|
||||
@@ -3,50 +3,10 @@
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMChatMessage, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js';
|
||||
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicChat } from './anthropic.js';
|
||||
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
|
||||
import { sendOpenAIChat, sendOpenAIFIM } from './openai.js';
|
||||
import { sendGeminiChat } from './gemini.js';
|
||||
import { sendGroqChat } from './groq.js';
|
||||
import { sendMistralChat } from './mistral.js';
|
||||
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
|
||||
const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => {
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n') || undefined;
|
||||
|
||||
// remove all system messages
|
||||
const noSystemMessages = messages
|
||||
.filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[]
|
||||
|
||||
// add system mesasges to first message (should be a user message)
|
||||
if (systemMessage && (noSystemMessages.length !== 0)) {
|
||||
const newFirstMessage = {
|
||||
role: noSystemMessages[0].role,
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessage
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ noSystemMessages[0].content
|
||||
)
|
||||
}
|
||||
noSystemMessages.splice(0, 1) // delete first message
|
||||
noSystemMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
|
||||
return noSystemMessages
|
||||
}
|
||||
import { sendLLMMessageToProviderImplementation } from './MODELS.js';
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
@@ -61,27 +21,23 @@ export const sendLLMMessage = ({
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
tools,
|
||||
}: SendLLMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
let messagesArr: _InternalLLMChatMessage[] = []
|
||||
if (messagesType === 'chatMessages') {
|
||||
messagesArr = cleanChatMessages([
|
||||
{ role: 'system', content: aiInstructions },
|
||||
...messages_
|
||||
])
|
||||
}
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureLLMEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
modelName,
|
||||
customEndpointURL: settingsOfProvider[providerName]?.endpoint,
|
||||
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
|
||||
...messagesType === 'chatMessages' ? {
|
||||
numMessages: messagesArr?.length,
|
||||
messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
numMessages: messages_?.length,
|
||||
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
origNumMessages: messages_?.length,
|
||||
origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
|
||||
@@ -100,16 +56,17 @@ export const sendLLMMessage = ({
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
const onText: OnText = (params) => {
|
||||
const { fullText } = params
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
onText_(params)
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => {
|
||||
if (_didAbort) return
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
onFinalMessage_({ fullText, toolCalls })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
@@ -118,7 +75,7 @@ export const sendLLMMessage = ({
|
||||
|
||||
// handle failed to fetch errors, which give 0 information by design
|
||||
if (error === 'TypeError: fetch failed')
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.`
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
|
||||
|
||||
captureLLMEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
@@ -132,37 +89,32 @@ export const sendLLMMessage = ({
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messagesArr[messagesArr.length - 1]?.content.length })
|
||||
if (messagesType === 'chatMessages')
|
||||
captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_[messages_.length - 1]?.content.length })
|
||||
else if (messagesType === 'FIMMessage')
|
||||
captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics
|
||||
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'mistral':
|
||||
sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
const implementation = sendLLMMessageToProviderImplementation[providerName]
|
||||
if (!implementation) {
|
||||
onError({ message: `Error: Provider "${providerName}" not recognized.`, fullError: null })
|
||||
return
|
||||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
if (sendFIM) {
|
||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
|
||||
@@ -8,30 +8,42 @@
|
||||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } 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';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
private readonly llmMessageEmitters = {
|
||||
onText: new Emitter<EventLLMMessageOnTextParams>(),
|
||||
onFinalMessage: new Emitter<EventLLMMessageOnFinalMessageParams>(),
|
||||
onError: new Emitter<EventLLMMessageOnErrorParams>(),
|
||||
}
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
// aborters for above
|
||||
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
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>>();
|
||||
// list
|
||||
private readonly listEmitters = {
|
||||
ollama: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
||||
},
|
||||
vLLM: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
success: Emitter<EventModelListOnSuccessParams<any>>,
|
||||
error: Emitter<EventModelListOnErrorParams<any>>,
|
||||
}
|
||||
}
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
@@ -40,30 +52,17 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
// text
|
||||
if (event === 'onText_sendLLMMessage') return this.llmMessageEmitters.onText.event;
|
||||
else if (event === 'onFinalMessage_sendLLMMessage') return this.llmMessageEmitters.onFinalMessage.event;
|
||||
else if (event === 'onError_sendLLMMessage') return this.llmMessageEmitters.onError.event;
|
||||
// list
|
||||
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
|
||||
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
|
||||
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
|
||||
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
|
||||
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
|
||||
@@ -78,8 +77,8 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
else if (command === 'vLLMList') {
|
||||
this._callVLLMList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
@@ -94,47 +93,50 @@ export class LLMMessageChannel implements IServerChannel {
|
||||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
if (!(requestId in this.abortRefOfRequestId))
|
||||
this.abortRefOfRequestId[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: SendLLMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
|
||||
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
|
||||
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
|
||||
abortRef: this.abortRefOfRequestId[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
_callOllamaList = (params: MainModelListParams<OllamaModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.ollama
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.vLLM
|
||||
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this.abortRefOfRequestId)) return
|
||||
this.abortRefOfRequestId[requestId].current?.()
|
||||
delete this.abortRefOfRequestId[requestId]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
|
||||
modelName -> {
|
||||
system_message_type: 'system' | 'developer' (openai) | null // if null, we will just do a string of system message
|
||||
supports_tools: boolean // we will just do a string of tool use if it doesn't support
|
||||
supports_autocomplete_FIM (suffix) // we will just do a description of FIM if it doens't support <|fim_hole|>
|
||||
|
||||
supports_streaming: boolean // (o1 does NOT) we will just dump the final result if doesn't support it
|
||||
max_tokens: number // required, DEFAULT is Infinity
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user