Compare commits

...

73 Commits

Author SHA1 Message Date
Mathew Pareles 22f02d3ed1 Add star changes 2025-02-16 20:13:04 -08:00
Mathew Pareles 74f8303803 discard star changes 2025-02-16 20:11:13 -08:00
Mathew Pareles 41fe5c50e2 fix scrollbars 2025-02-16 19:46:41 -08:00
Mathew Pareles 36b1b56690 bug 2025-02-16 19:46:41 -08:00
Mathew Pareles 249eee341d better context and file reading 2025-02-16 19:46:40 -08:00
Mathew Pareles 198a948f6c star button commit 2025-02-14 21:30:20 -08:00
Mathew Pareles 7dcb08b09a comment out unwanted code 2025-02-12 20:50:04 -08:00
Mathew Pareles efeb85b48b Merge remote-tracking branch 'origin/model-selection' into edit-chats 2025-02-11 18:44:20 -08:00
Mathew Pareles d698e6f3c1 prepare fast apply 2025-02-11 18:42:40 -08:00
Andrew Pareles 2161d740df minor change 2025-02-11 16:44:40 -08:00
Andrew Pareles 96b5b72d30 triple tick 2025-02-10 20:22:31 -08:00
Andrew Pareles e2e8279fce add back imports 2025-02-10 20:18:45 -08:00
Andrew Pareles 5ed1f465a2 MOVE VOID FILES TO WORKBENCH 2025-02-10 18:59:26 -08:00
Andrew Pareles 995357710e misc 2025-02-10 18:33:51 -08:00
Andrew Pareles a98106072c Merge remote-tracking branch 'origin/edit-chats' into model-selection 2025-02-10 15:35:37 -08:00
Andrew Pareles c1be466467 tool tests 2025-02-10 15:34:07 -08:00
Andrew Pareles 9cbe83b57c comment 2025-02-09 00:46:39 -08:00
Andrew Pareles e46b295449 names 2025-02-09 00:45:06 -08:00
Andrew Pareles 3fd3634257 void message 2025-02-09 00:36:03 -08:00
Andrew Pareles 231990088a uri 2025-02-09 00:28:44 -08:00
Andrew Pareles 20da82762e rm comment 2025-02-09 00:27:36 -08:00
Andrew Pareles 0d9be68aff rm comment 2025-02-09 00:27:21 -08:00
Andrew Pareles 165b5a4fd2 + 2025-02-09 00:26:58 -08:00
Andrew Pareles 812014d6d3 all tools seem to work! 2025-02-09 00:26:42 -08:00
Andrew Pareles d3e73b92e0 + 2025-02-08 22:56:55 -08:00
Andrew Pareles 00da05f24f context tools 2025-02-08 22:53:13 -08:00
Mathew Pareles e4694256eb Merge remote-tracking branch 'origin/model-selection' into edit-chats 2025-02-08 21:47:27 -08:00
Andrew Pareles 18b278577d tools 2025-02-08 21:17:38 -08:00
Mathew Pareles a252bb7a30 prepare merge 2025-02-08 17:26:28 -08:00
Mathew Pareles c240360710 edit update locally (need to test) 2025-02-08 17:14:16 -08:00
Mathew Pareles 893308c38e edits work 2025-02-07 02:06:04 -08:00
Mathew Pareles 48defe849d edit backend 2025-02-06 22:34:39 -08:00
Andrew Pareles 3c03f58488 clarify API 2025-02-06 22:16:42 -08:00
Mathew Pareles 2470e0de7f refactor 2025-02-06 19:01:15 -08:00
Mathew Pareles 387fd64db0 edit ability 2025-02-06 03:18:48 -08:00
Andrew Pareles 4ffd269db5 + 2025-02-06 03:11:26 -08:00
Andrew Pareles 5ce25e20c3 css for <html> md tags 2025-02-06 03:10:07 -08:00
Andrew Pareles 63efa219f5 div 2025-02-06 03:04:48 -08:00
Andrew Pareles 826d148305 html 2025-02-06 03:03:14 -08:00
Andrew Pareles 323d80ef55 add divs for html 2025-02-06 03:02:46 -08:00
Andrew Pareles 902588638d Merge remote-tracking branch 'origin/main' into model-selection 2025-02-06 02:04:48 -08:00
Andrew Pareles a1ab094652 + 2025-02-06 02:01:24 -08:00
Andrew Pareles 902b460b9c + 2025-02-06 02:00:33 -08:00
Andrew Pareles 073205ace3 warningbox and misc 2025-02-06 01:59:26 -08:00
Andrew Pareles 98c1d81875 focus() fix 2025-02-06 01:55:36 -08:00
Andrew Pareles 2f49650e15 model and provider error handling 2025-02-06 01:32:48 -08:00
Mathew Pareles 1be9300dc4 typeo 2025-02-05 18:19:04 -08:00
Andrew Pareles d9b77b67b2 prefix 2025-02-05 01:50:29 -08:00
Andrew Pareles 2f674f5323 update openai models 2025-02-05 01:10:40 -08:00
Andrew Pareles c8fa39e552 openai fim progress 2025-02-05 00:57:03 -08:00
Andrew Pareles 3b73e3ab26 metrics for sendFIM 2025-02-05 00:40:09 -08:00
Andrew Pareles c606ac4361 chat name 2025-02-05 00:38:49 -08:00
Andrew Pareles da4c9c8c40 fim better abstraction 2025-02-05 00:37:28 -08:00
Mathew Pareles 31ed11393f fix non-ollama autocomplete 2025-02-04 21:24:49 -08:00
Andrew Pareles 1c5157d97e remove hi333 2025-02-04 20:56:58 -08:00
Andrew Pareles cf241867d8 add recognizedModels code 2025-02-04 20:34:02 -08:00
Andrew Pareles 5a9f9f70d9 separate fastapply model 2025-02-04 20:34:02 -08:00
Andrew Pareles 19ff19fb80 Merge branch 'copy-icons' of https://github.com/voideditor/void into model-selection 2025-02-04 20:34:02 -08:00
Mathew Pareles 87076ed586 remove comments 2025-02-04 20:07:57 -08:00
Mathew Pareles 473250d8bc autocomplete works without context 2025-02-04 20:02:14 -08:00
Mathew Pareles cd983f0dd9 refactor input box with no noticable errors 2025-02-04 01:20:33 -08:00
Mathew Pareles 5c4753555e chatbubble add edit button (need to refactor InputBox to finish) 2025-02-03 21:06:29 -08:00
Mathew Pareles 8f79222b8b fix spacing 2025-02-03 18:45:25 -08:00
Mathew Pareles 145fd9614b changes 2025-02-03 18:27:57 -08:00
Mathew Pareles 41057c20fc autocomplete type context 2025-02-03 18:17:05 -08:00
Mathew Pareles 528b8a6b9b better model onboard 2025-02-03 18:17:05 -08:00
Andrew Pareles e3941d5565 + 2025-02-01 18:41:56 -08:00
Andrew Pareles 4d11a4fd29 Merge pull request #252 from bawsdeep/main
script to generate appimages
2025-01-31 20:52:44 -08:00
Andrew Pareles aa580351d9 Merge pull request #259 from DoctorGlitchy/main
Update voidSettingsTypes.ts
2025-01-31 01:22:16 -08:00
Adrian Chan 570e13127d Update voidSettingsTypes.ts
Removed "distil-whisper-large-v3-en" because Whisper is not a model you chat with.
Added "llama3-70b-8192" + "mixtral-8x7b-32768"
2025-01-31 13:11:33 +11:00
Andrew Pareles fb7b50b36b Merge pull request #258 from voideditor/model-selection
Latest UI updates
2025-01-30 16:39:13 -08:00
Baws Deep d1c5cf518e Add files via upload 2025-01-29 05:41:12 +01:00
Andrew Pareles 4e392bee8e start adding icons for copying files and folders in the explorer 2025-01-06 22:25:41 -08:00
54 changed files with 2826 additions and 1123 deletions
+1
View File
@@ -22,4 +22,5 @@ product.overrides.json
*.snap.actual
.vscode-test
.tmp/
.tmp2/
.tool-versions
-2
View File
@@ -14,8 +14,6 @@ There are a few ways to contribute:
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
We are currently putting together our own articles on VSCode and Void's sourcecode organization. The best way to get this information right now is by attending a weekly meeting.
<!-- ADD BLOG HERE
We wrote a [guide to working in VSCode].
-->
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -e # Exit on error
set -x # Print commands as they are executed
# Configuration
APP_NAME="void"
APP_VERSION="1.0.0"
ARCH="x86_64"
export ARCH
# Check if void binary exists in current directory
if [ ! -f "./void" ]; then
echo "Error: void binary not found in current directory"
exit 1
fi
# Check if icon exists
if [ ! -f "./void.png" ]; then
echo "Error: void.png icon not found in current directory"
exit 1
fi
# Create temporary directory
TEMP_DIR="$(mktemp -d)"
echo "Created temporary directory: $TEMP_DIR"
APP_DIR="$TEMP_DIR/$APP_NAME.AppDir"
# Create basic AppDir structure
mkdir -pv "$APP_DIR/usr/bin"
mkdir -pv "$APP_DIR/usr/lib"
mkdir -pv "$APP_DIR/usr/share/applications"
mkdir -pv "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
# Exclude create-appimage.sh and appimagetool-x86_64.AppImage from being copied
echo "Copying files excluding create-appimage.sh and appimagetool-x86_64.AppImage..."
for file in ./*; do
if [[ "$file" != "./create-appimage.sh" && "$file" != "./appimagetool-x86_64.AppImage" ]]; then
cp -rv "$file" "$APP_DIR/usr/bin/"
fi
done
# Copy the icon to required locations
cp -v ./void.png "$APP_DIR/void.png"
cp -v ./void.png "$APP_DIR/usr/share/icons/hicolor/256x256/apps/void.png"
# Copy dependencies with error checking
echo "Copying dependencies..."
for lib in $(ldd ./void | grep "=> /" | awk '{print $3}'); do
if [ -f "$lib" ]; then
cp -v "$lib" "$APP_DIR/usr/lib/" || echo "Failed to copy $lib"
else
echo "Warning: Library $lib not found"
fi
done
# Create desktop file with error checking
echo "Creating desktop file..."
if ! cat > "$APP_DIR/$APP_NAME.desktop" <<EOF
[Desktop Entry]
Name=$APP_NAME
Exec=void
Icon=void
Type=Application
Categories=Utility;
Comment=Void Linux Application
EOF
then
echo "Error creating desktop file"
exit 1
fi
# Make desktop file executable
chmod +x "$APP_DIR/$APP_NAME.desktop"
# Copy the desktop file to the applications directory
cp -v "$APP_DIR/$APP_NAME.desktop" "$APP_DIR/usr/share/applications/"
# Create AppRun with error checking
echo "Creating AppRun..."
if ! cat > "$APP_DIR/AppRun" <<EOF
#!/bin/bash
cd "\$(dirname "\$0")/usr/bin"
export LD_LIBRARY_PATH="\$APPDIR/usr/lib:\$LD_LIBRARY_PATH"
exec ./void "\$@"
EOF
then
echo "Error creating AppRun"
exit 1
fi
# Make AppRun executable
chmod +x "$APP_DIR/AppRun"
# Download appimagetool if not present in the current directory
if [ ! -f "./appimagetool-x86_64.AppImage" ]; then
echo "Downloading appimagetool-x86_64.AppImage..."
wget "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool-x86_64.AppImage
else
echo "appimagetool-x86_64.AppImage is already present."
fi
# Create the AppImage
echo "Creating AppImage..."
ARCH=x86_64 ./appimagetool-x86_64.AppImage "$APP_DIR" "${APP_NAME}-${APP_VERSION}-${ARCH}.AppImage"
# Cleanup
echo "Cleaning up..."
rm -rf "$TEMP_DIR"
echo "AppImage creation complete!"
+7
View File
@@ -29,6 +29,7 @@
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"cookie": "^0.4.0",
"debounced": "1.0.2",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@@ -396,6 +397,12 @@
"node": ">= 0.6"
}
},
"node_modules/debounced": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz",
"integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+5 -5
View File
@@ -121,11 +121,11 @@ import { normalizeNFC } from '../../base/common/normalization.js';
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../platform/void/common/metricsService.js';
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../workbench/contrib/void/common/metricsService.js';
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@@ -1,21 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// ---------- common ----------
// llmMessage
import '../common/llmMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
// updates
import '../common/voidUpdateService.js'
@@ -1,68 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
// let fullText = ''
// const thisConfig = settingsOfProvider.greptile
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${thisConfig.apikey}`,
// 'X-Github-Token': `${thisConfig.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [thisConfig.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// // when receive text
// if (type === 'message') {
// fullText += message
// onText({ newText: message, fullText })
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText({ newText: filepath, fullText })
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage({ fullText })
// }
// }
// }
// })
// .catch(error => {
// onError({ error })
// });
// }
@@ -14,6 +14,8 @@ import { WorkspaceEdit } from '../../../editor/common/languages.js';
// import { IHistoryService } from '../../services/history/common/history.js';
// VOID: THIS FILE IS OUTDATED!!!!!! No longer used anywhere.
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
@@ -432,7 +432,7 @@ export class UnpinEditorAction extends Action {
label: string,
@ICommandService private readonly commandService: ICommandService
) {
super(id, label, ThemeIcon.asClassName(Codicon.pinned));
super(id, label, ThemeIcon.asClassName(Codicon.starFull));
}
override run(context?: IEditorCommandsContext): Promise<void> {
@@ -440,6 +440,24 @@ 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';
@@ -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: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink/fixed to make more room */
overflow: visible; /* ensure tab actions are always visible */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
@@ -399,18 +399,8 @@
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; /* 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;
display: none; /* only hide tab actions when sticky-compact */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .actions-container {
@@ -444,11 +434,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: 0.5; /* show tab actions dimmed for inactive group */
opacity: 1;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab > .tab-actions .action-label {
opacity: 0;
opacity: 1;
}
/* 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, UnpinEditorAction } from './editorActions.js';
import { CloseEditorTabAction, PinEditorAction, 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,6 +113,7 @@ 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));
private readonly tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER));
private tabLabels: IEditorInputLabel[] = [];
@@ -1510,6 +1511,8 @@ export class MultiEditorTabsControl extends EditorTabsControl {
this.layout(this.dimensions, options);
}
// In MultiEditorTabsControl.ts, modify the redrawTab method:
private redrawTab(editor: EditorInput, tabIndex: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void {
const isTabSticky = this.tabsModel.isSticky(tabIndex);
const options = this.groupsView.partOptions;
@@ -1518,47 +1521,46 @@ export class MultiEditorTabsControl extends EditorTabsControl {
this.redrawTabLabel(editor, tabIndex, tabContainer, tabLabelWidget, tabLabel);
// Action
const hasUnpinAction = isTabSticky && options.tabActionUnpinVisibility;
const hasCloseAction = !hasUnpinAction && options.tabActionCloseVisibility;
const hasAction = hasUnpinAction || hasCloseAction;
const hasCloseAction = options.tabActionCloseVisibility;
const hasAction = true; // Always show actions
let tabAction;
if (hasAction) {
tabAction = hasUnpinAction ? this.unpinEditorAction : this.closeEditorAction;
// Clear existing actions
if (!tabActionBar.isEmpty()) {
tabActionBar.clear();
}
// Add pin/unpin action based on sticky state
if (isTabSticky) {
tabActionBar.push(this.unpinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.unpinEditorAction) });
} else {
// Even if the action is not visible, add it as it contains the dirty indicator
tabAction = isTabSticky ? this.unpinEditorAction : this.closeEditorAction;
tabActionBar.push(this.pinEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.pinEditorAction) });
}
if (!tabActionBar.hasAction(tabAction)) {
if (!tabActionBar.isEmpty()) {
tabActionBar.clear();
}
tabActionBar.push(tabAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(tabAction) });
// Add close action
if (hasCloseAction) {
tabActionBar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
}
tabContainer.classList.toggle(`pinned-action-off`, isTabSticky && !hasUnpinAction);
tabContainer.classList.toggle(`close-action-off`, !hasUnpinAction && !hasCloseAction);
// Update tab classes and styles
tabContainer.classList.toggle('sticky', isTabSticky);
tabContainer.classList.toggle('close-action-off', !hasCloseAction);
tabContainer.classList.toggle('tab-actions-right', hasAction && options.tabActionLocation === 'right');
tabContainer.classList.toggle('tab-actions-left', hasAction && options.tabActionLocation === 'left');
for (const option of ['left', 'right']) {
tabContainer.classList.toggle(`tab-actions-${option}`, hasAction && options.tabActionLocation === option);
}
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' /* treat sticky shrink tabs as tabSizing: 'shrink' */ : options.tabSizing;
for (const option of ['fit', 'shrink', 'fixed']) {
tabContainer.classList.toggle(`sizing-${option}`, tabSizing === option);
}
// Update tab sizing classes
const tabSizing = isTabSticky && options.pinnedTabSizing === 'shrink' ? 'shrink' : options.tabSizing;
tabContainer.classList.toggle('sizing-fit', tabSizing === 'fit');
tabContainer.classList.toggle('sizing-shrink', tabSizing === 'shrink');
tabContainer.classList.toggle('sizing-fixed', tabSizing === 'fixed');
tabContainer.classList.toggle('has-icon', options.showIcons && options.hasIcons);
// Update sticky classes
tabContainer.classList.toggle('sticky', isTabSticky);
for (const option of ['normal', 'compact', 'shrink']) {
tabContainer.classList.toggle(`sticky-${option}`, isTabSticky && options.pinnedTabSizing === option);
}
tabContainer.classList.toggle('sticky-compact', isTabSticky && options.pinnedTabSizing === 'compact');
tabContainer.classList.toggle('sticky-shrink', isTabSticky && options.pinnedTabSizing === 'shrink');
// If not wrapping tabs, sticky compact/shrink tabs need a position to remain at their location
// when scrolling to stay in view (requirement for position: sticky)
// Update tab position if needed
if (!options.wrapTabs && isTabSticky && options.pinnedTabSizing !== 'normal') {
let stickyTabWidth = 0;
switch (options.pinnedTabSizing) {
@@ -1569,16 +1571,13 @@ export class MultiEditorTabsControl extends EditorTabsControl {
stickyTabWidth = MultiEditorTabsControl.TAB_WIDTH.shrink;
break;
}
tabContainer.style.left = `${tabIndex * stickyTabWidth}px`;
} else {
tabContainer.style.left = 'auto';
}
// Borders / outline
// Draw borders and selection state
this.redrawTabBorders(tabIndex, tabContainer);
// Selection / active / dirty state
this.redrawTabSelectedActiveAndDirty(this.groupsView.activeGroup === this.groupView, editor, tabContainer, tabActionBar);
}
@@ -289,6 +289,7 @@ export interface IFileTemplateData {
readonly templateDisposables: DisposableStore;
readonly elementDisposables: DisposableStore;
readonly label: IResourceLabel;
// readonly voidLabels: IResourceLabel;
readonly container: HTMLElement;
readonly contribs: IExplorerFileContribution[];
currentContext?: ExplorerItem;
@@ -347,15 +348,25 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
renderTemplate(container: HTMLElement): IFileTemplateData {
const templateDisposables = new DisposableStore();
// Void added this
// // Create void buttons container
// const voidButtonsContainer = DOM.append(container, DOM.$('div'));
// voidButtonsContainer.style.position = 'absolute'
// voidButtonsContainer.style.top = '0'
// voidButtonsContainer.style.right = '0'
// // const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
// // voidButtons.textContent = 'voidbuttons'
// // voidButtons.addEventListener('click', () => {
// // console.log('ON CLICK', templateData.currentContext?.children)
// // })
// const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, });
// voidLabels.element.textContent = 'hi333'
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
templateDisposables.add(label.onDidRender(() => {
try {
if (templateData.currentContext) {
this.updateWidth(templateData.currentContext);
}
} catch (e) {
// noop since the element might no longer be in the tree, no update of width necessary
}
try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); }
catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ }
}));
const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables);
@@ -365,10 +376,15 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
contr.setResource(templateData.currentContext?.resource);
}));
// const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
return templateData;
}
// Void cares about this function, this is where elements in the tree are rendered
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
const stat = node.element;
templateData.currentContext = stat;
@@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
templateData.label.element.style.display = 'flex';
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
}
// Input Box
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
else {
templateData.label.element.style.display = 'none';
templateData.contribs.forEach(c => c.setResource(undefined));
@@ -477,6 +492,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
domId
});
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
// hideIcon: true,
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
// forceLabel: true,
// });
}
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
@@ -95,7 +95,12 @@ suite('Files - ExplorerView', () => {
label: <any>{
container: label,
onDidRender: emitter.event
}
},
// voidLabels: <any>{
// container: label,
// onDidRender: emitter.event
// },
}, 1, false);
ds.add(navigationController);
@@ -8,10 +8,9 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext, LocationLink } from '../../../../editor/common/languages.js';
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Range } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { EditorResourceAccessor } from '../../../common/editor.js';
@@ -19,6 +18,8 @@ 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 { 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
@@ -155,6 +156,7 @@ type Autocompletion = {
llmPromise: Promise<string> | undefined,
insertText: string,
requestId: string | null,
_newlineCount: number,
}
const DEBOUNCE_TIME = 500
@@ -163,13 +165,16 @@ const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const joinSpaces = (result: string) => {
const processStartAndEndSpaces = (result: string) => {
// trim all whitespace except for a single leading/trailing space
// return result.trim()
[result,] = extractCodeFromRegular({ text: result, recentlyAddedTextLen: result.length })
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
@@ -196,22 +201,26 @@ const removeLeftTabsAndTrimEnds = (s: string): string => {
const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, '');
function isSubsequence({ of, subsequence }: { of: string, subsequence: string }): boolean {
if (subsequence.length === 0) return true;
if (of.length === 0) return false;
function getIsSubsequence({ of, subsequence }: { of: string, subsequence: string }): [boolean, string] {
if (subsequence.length === 0) return [true, ''];
if (of.length === 0) return [false, ''];
let subsequenceIndex = 0;
let lastMatchChar = '';
for (let i = 0; i < of.length; i++) {
if (of[i] === subsequence[subsequenceIndex]) {
lastMatchChar = of[i];
subsequenceIndex++;
}
if (subsequenceIndex === subsequence.length) {
return true;
return [true, lastMatchChar];
}
}
return false;
return [false, lastMatchChar];
}
@@ -251,7 +260,6 @@ function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): s
}
// further trim the autocompletion
const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => {
@@ -357,15 +365,24 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS
// if we redid the suffix, replace the suffix
if (autocompletion.type === 'single-line-redo-suffix') {
if (isSubsequence({ // check that the old text contains the same brackets + symbols as the new text
subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), // old suffix
of: removeAllWhitespace(autocompletion.insertText), // new suffix (note that this should not be `trimmedInsertText`)
})) {
const oldSuffix = prefixAndSuffix.suffixToTheRightOfCursor
const newSuffix = autocompletion.insertText
const [isSubsequence, lastMatchingChar] = getIsSubsequence({ // check that the old text contains the same brackets + symbols as the new text
subsequence: removeAllWhitespace(oldSuffix), // old suffix
of: removeAllWhitespace(newSuffix), // new suffix
})
if (isSubsequence) {
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER)
}
else {
// TODO redo the autocompletion
trimmedInsertText = '' // for now set the mismatched text to ''
const lastMatchupIdx = trimmedInsertText.lastIndexOf(lastMatchingChar)
trimmedInsertText = trimmedInsertText.slice(0, lastMatchupIdx + 1)
const numCharsToReplace = oldSuffix.lastIndexOf(lastMatchingChar) + 1
rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, position.column + numCharsToReplace)
// console.log('show____', trimmedInsertText, rangeToReplace)
}
}
@@ -504,12 +521,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string,
}
// const x = []
// const
// c[[]]
// asd[[]] =
// const [{{}}]
//
type CompletionOptions = {
predictionType: AutocompletionPredictionType,
shouldGenerate: boolean,
@@ -519,7 +531,13 @@ type CompletionOptions = {
}
const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => {
const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix
let { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines, prefixLines } = prefixAndSuffix
// trim prefix and suffix to not be very large
suffixLines = suffix.split(_ln).slice(0, 25)
prefixLines = prefix.split(_ln).slice(-25)
prefix = prefixLines.join(_ln)
suffix = suffixLines.join(_ln)
let completionOptions: CompletionOptions
@@ -552,7 +570,7 @@ const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantCont
stopTokens: allLinebreakSymbols
}
}
// if suffix is 3 or less characters, attempt to complete the line ignorning it
// if suffix is 3 or fewer characters, attempt to complete the line ignorning it
else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) {
const suffixLinesIgnoringThisLine = suffixLines.slice(1)
const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln)
@@ -615,8 +633,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
token: CancellationToken,
): Promise<InlineCompletion[]> {
console.log('START_0')
const testMode = false
const docUriStr = model.uri.toString();
@@ -733,12 +749,11 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// gather relevant context from the code around the user's selection and definitions
const relevantContext = await this._gatherRelevantContextForPosition(
model,
position,
3, //recursion depth
1 // number of lines to view in each recursion
);
// const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3);
// const relevantSnippetsList = this._contextGatheringService.getCachedSnippets();
// const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n')
// console.log('@@---------------------\n' + relevantSnippets)
const relevantContext = ''
const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion)
@@ -766,44 +781,51 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
llmPromise: undefined,
insertText: '',
requestId: null,
_newlineCount: 0,
}
console.log('BB')
console.log(predictionType)
console.log('starting autocomplete...', predictionType)
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
type: 'ollamaFIM',
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
useProviderFor: 'Autocomplete',
logging: { loggingName: 'Autocomplete' },
onText: async ({ fullText }) => {
onText: async ({ fullText, newText }) => {
newAutocompletion.insertText = fullText
// if generation doesn't match the prefix for the first few tokens generated, reject it
// 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 (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
// reject('LLM response did not match user\'s text.')
// reject('LLM response did not match user\'s text.')
// }
},
onFinalMessage: ({ fullText }) => {
console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
// console.log('____res: ', JSON.stringify(newAutocompletion.insertText))
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
newAutocompletion.insertText = joinSpaces(text)
newAutocompletion.insertText = processStartAndEndSpaces(text)
// handle special case for predicting starting on the next line, add a newline character
if (newAutocompletion.type === 'multi-line-start-on-next-line') {
@@ -818,7 +840,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
useProviderFor: 'Autocomplete',
})
newAutocompletion.requestId = requestId
@@ -853,89 +874,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
// helper method to gather ~N lines above and below the user's current line,
// and recursively gather lines around any symbol definitions encountered.
private async _gatherRelevantContextForPosition(
model: ITextModel,
position: Position,
recursionDepth: number,
linesAround: number
): Promise<string> {
// We'll do a BFS-like approach: for each position or definition, gather lines around it,
// then attempt to find the definition of any symbols in that range, up to 'recursionDepth' times.
// A set of "key" strings to avoid repeating the same location or line chunk
const visitedRanges = new Set<string>();
const collectedSnippets: string[] = [];
// A queue of tasks, each being a tuple of: (model, position, depth)
const tasks: Array<{ model: ITextModel, position: Position, depth: number }> = [];
tasks.push({ model, position, depth: recursionDepth });
const getSnippetAroundLine = (model: ITextModel, lineNumber: number, linesAround: number): string => {
const startLine = Math.max(1, lineNumber - linesAround);
const endLine = Math.min(model.getLineCount(), lineNumber + linesAround);
const lines: string[] = [];
for (let i = startLine; i <= endLine; i++) {
lines.push(model.getLineContent(i));
}
return lines.join('\n');
};
while (tasks.length > 0) {
const { model: currentModel, position: currentPos, depth } = tasks.shift()!;
if (depth < 0) {
continue;
}
// Gather snippet around the current line
const snippet = getSnippetAroundLine(currentModel, currentPos.lineNumber, linesAround);
const snippetKey = `${currentModel.uri.toString()}:${currentPos.lineNumber}`;
if (!visitedRanges.has(snippetKey)) {
visitedRanges.add(snippetKey);
collectedSnippets.push(`-- Snippet around line ${currentPos.lineNumber} --\n${snippet}\n`);
}
// Attempt to gather definitions for the symbol at this position
// We just pick all definition providers and see if any has a definition
const providers = this._langFeatureService.definitionProvider.ordered(currentModel);
for (const provider of providers) {
try {
const definitions = await provider.provideDefinition(currentModel, currentPos, CancellationToken.None);
if (!definitions) continue;
// definitions can be a single LocationLink or an array
const defArray: LocationLink[] = Array.isArray(definitions) ? definitions : [definitions];
for (const def of defArray) {
if (!def.uri) continue;
if (typeof def.range === 'undefined') continue;
const definitionModel = this._modelService.getModel(def.uri);
if (!definitionModel) continue;
// We'll queue up a new task for that definition range
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
const defKey = `${def.uri.toString()}:${defPos.lineNumber}`;
if (!visitedRanges.has(defKey)) {
tasks.push({ model: definitionModel, position: defPos, depth: depth - 1 });
}
}
} catch (err) {
// If a provider fails, ignore
}
}
}
// Return the joined context
return collectedSnippets.join('\n');
}
constructor(
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -966,8 +910,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// go through cached items and remove matching ones
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
// const matchup = getAutocompletionMatchup({ prefix, autocompletion })
// we can do this more efficiently, I just didn't want to deal with all of the edge cases
const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText)
if (matchup) {
console.log('ACCEPT', autocompletion.id)
this._lastCompletionAccept = Date.now()
@@ -11,9 +11,11 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
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 '../../../../platform/void/common/llmMessageService.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { chat_userMessage, chat_systemMessage } from './prompt/prompts.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';
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
@@ -32,14 +34,17 @@ export type FileSelection = {
export type StagingSelectionItem = CodeSelection | FileSelection
// 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: 'user';
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
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
selections: StagingSelectionItem[] | null; // the user's selection
state: {
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
}
| {
role: 'assistant';
@@ -52,6 +57,11 @@ export type ChatMessage =
displayContent?: undefined;
}
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false }
// a 'thread' means a chat message history
export type ChatThreads = {
[id: string]: {
@@ -59,18 +69,26 @@ export type ChatThreads = {
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
state: {
stagingSelections: StagingSelectionItem[];
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
isCheckedOfSelectionId: { [selectionId: string]: boolean };
}
};
}
type ThreadType = ChatThreads[string]
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
currentStagingSelections: StagingSelectionItem[] | null;
}
export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null };
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
streamingToken?: string;
}
@@ -84,11 +102,17 @@ const newThreadObject = () => {
createdAt: now,
lastModified: now,
messages: [],
state: {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
},
} satisfies ChatThreads[string]
}
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
const THREAD_VERSION = 'v1'
const THREAD_VERSION = 'v2'
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
@@ -105,8 +129,15 @@ export interface IChatThreadService {
openNewThread(): void;
switchToThread(threadId: string): void;
setStaging(stagingSelection: StagingSelectionItem[] | null): void;
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];
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
@@ -130,6 +161,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IModelService private readonly _modelService: IModelService,
@IFileService private readonly _fileService: IFileService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
) {
super()
@@ -137,7 +169,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this.state = {
allThreads: this._readAllThreads(),
currentThreadId: null as unknown as string, // gets set in startNewThread()
currentStagingSelections: null,
}
// always be in a thread
@@ -145,14 +176,68 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 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)
}
private _readAllThreads(): ChatThreads {
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
// CAN ADD "v0" TAG IN STORAGE AND CONVERT
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
return threads ? JSON.parse(threads) : {}
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 _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) {
if (toVersion === 'v2') {
const threads: ChatThreads = oldThreadsObject
/** 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;
// update the threads
for (const thread of Object.values(threads)) {
if (!thread.state) {
thread.state = defaultThreadState
}
for (const chatMessage of Object.values(thread.messages)) {
if (chatMessage.role === 'user' && !chatMessage.state) {
chatMessage.state = defaultMessageState
}
}
}
// push the update
this._storeAllThreads(threads)
}
}
private _storeAllThreads(threads: ChatThreads) {
@@ -169,6 +254,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._onDidChangeCurrentThread.fire()
}
private _getAllSelections() {
const thread = this.getCurrentThread()
return thread.messages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _getSelectionsUpToMessageIdx(messageIdx: number) {
const thread = this.getCurrentThread()
const prevMessages = thread.messages.slice(0, messageIdx)
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
@@ -187,26 +283,71 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
}
async addUserMessageAndStreamResponse(userMessage: string) {
const threadId = this.getCurrentThread().id
const currSelns = this.state.currentStagingSelections ?? []
async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) {
const thread = this.getCurrentThread()
if (thread.messages?.[messageIdx]?.role !== 'user') {
throw new Error("Error: editing a message with role !=='user'")
}
// get prev and curr selections before clearing the message
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
const currSelns = thread.messages[messageIdx].selections || []
// clear messages up to the index
const slicedMessages = thread.messages.slice(0, messageIdx)
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
messages: slicedMessages
}
}
}, true)
// stream the edit
this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns })
}
async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) {
const thread = this.getCurrentThread()
const threadId = thread.id
// add user's message to chat history
const instructions = userMessage
const content = await chat_userMessage(instructions, currSelns, this._modelService)
const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns }
this._addMessageToThread(threadId, userHistoryElt)
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 }
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({
type: 'sendLLMMessage',
messagesType: 'chatMessages',
logging: { loggingName: 'Chat' },
useProviderFor: 'Ctrl+L',
messages: [
{ role: 'system', content: chat_systemMessage },
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
...prevLLMMessages,
currLLMMessage,
],
onText: ({ newText, fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
@@ -217,7 +358,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
onError: (error) => {
this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
},
useProviderFor: 'Ctrl+L',
})
if (llmCancelToken === null) return
@@ -241,12 +381,29 @@ class ChatThreadService extends Disposable implements IChatThreadService {
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId];
return state.allThreads[state.currentThreadId]
}
getFocusedMessageIdx() {
const thread = this.getCurrentThread()
// get the focusedMessageIdx
const focusedMessageIdx = thread.state.focusedMessageIdx
if (focusedMessageIdx === undefined) return;
// check that the message is actually being edited
const focusedMessage = thread.messages[focusedMessageIdx]
if (focusedMessage.role !== 'user') return;
if (!focusedMessage.state) return;
return focusedMessageIdx
}
isFocusingMessage() {
return this.getFocusedMessageIdx() !== undefined
}
switchToThread(threadId: string) {
// console.log('threadId', threadId)
// console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ currentThreadId: threadId }, true)
}
@@ -291,11 +448,104 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
setFocusedMessageIdx(messageIdx: number | undefined) {
setStaging(stagingSelection: StagingSelectionItem[] | null): void {
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
state: {
...thread.state,
focusedMessageIdx: messageIdx,
}
}
}
}, true)
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
messages: thread.messages.map((m, i) =>
i === messageIdx && m.role === 'user' ? {
...m,
state: {
...m.state,
...state
},
} : m
)
}
}
}, true)
}
// set thread.state
private _setCurrentThreadState(state: Partial<ThreadType['state']>): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
state: {
...thread.state,
...state
}
}
}
}, true)
}
_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
}
_useCurrentThreadState() {
const thread = this.getCurrentThread()
const state = thread.state
const setState = this._setCurrentThreadState.bind(this)
return [state, setState] as const
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
@@ -0,0 +1,354 @@
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Position } from '../../../../editor/common/core/position.js';
import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Range, IRange } from '../../../../editor/common/core/range.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { URI } from '../../../../base/common/uri.js';
// make sure snippet logic works
// change logic for `visited` to intervals
// atomically set new snippets at end
// throttle cache setting
interface IVisitedInterval {
uri: string;
startLine: number;
endLine: number;
}
export interface IContextGatheringService {
readonly _serviceBrand: undefined;
updateCache(model: ITextModel, pos: Position): Promise<void>;
getCachedSnippets(): string[];
}
export const IContextGatheringService = createDecorator<IContextGatheringService>('contextGatheringService');
class ContextGatheringService extends Disposable implements IContextGatheringService {
_serviceBrand: undefined;
private readonly _NUM_LINES = 3;
private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context
// Cache holds the most recent list of snippets.
private _cache: string[] = [];
private _snippetIntervals: IVisitedInterval[] = [];
constructor(
@ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService,
@IModelService private readonly _modelService: IModelService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService
) {
super();
this._modelService.getModels().forEach(model => this._subscribeToModel(model));
this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model)));
}
private _subscribeToModel(model: ITextModel): void {
console.log("Subscribing to model:", model.uri.toString());
this._register(model.onDidChangeContent(() => {
const editor = this._codeEditorService.getFocusedCodeEditor();
if (editor && editor.getModel() === model) {
const pos = editor.getPosition();
console.log("updateCache called at position:", pos);
if (pos) {
this.updateCache(model, pos);
}
}
}));
}
public async updateCache(model: ITextModel, pos: Position): Promise<void> {
const snippets = new Set<string>();
this._snippetIntervals = []; // Reset intervals for new cache update
await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals);
// Convert to array and filter overlapping snippets
this._cache = Array.from(snippets);
console.log("Cache updated:", this._cache);
}
public getCachedSnippets(): string[] {
return this._cache;
}
// Basic snippet extraction.
private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string {
const startLine = Math.max(range.startLineNumber - numLines, 1);
const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount());
// Enforce maximum snippet size
const totalLines = endLine - startLine + 1;
const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES
? endLine - this._MAX_SNIPPET_LINES + 1
: startLine;
const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._cleanSnippet(model.getValueInRange(snippetRange));
}
private _cleanSnippet(snippet: string): string {
return snippet
.split('\n')
// Remove empty lines and lines with only comments
.filter(line => {
const trimmed = line.trim();
return trimmed && !/^\/\/+$/.test(trimmed);
})
// Rejoin with newlines
.join('\n')
// Remove excess whitespace
.trim();
}
private _normalizeSnippet(snippet: string): string {
return snippet
// Remove multiple newlines
.replace(/\n{2,}/g, '\n')
// Remove trailing whitespace
.trim();
}
private _addSnippetIfNotOverlapping(
model: ITextModel,
range: IRange,
snippets: Set<string>,
visited: IVisitedInterval[]
): void {
const startLine = range.startLineNumber;
const endLine = range.endLineNumber;
const uri = model.uri.toString();
if (!this._isRangeVisited(uri, startLine, endLine, visited)) {
visited.push({ uri, startLine, endLine });
const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES));
if (snippet.length > 0) {
snippets.add(snippet);
}
}
}
private async _gatherNearbySnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
this._addSnippetIfNotOverlapping(model, range, snippets, visited);
const symbols = await this._getSymbolsNearPosition(model, pos, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
}
private async _gatherParentSnippets(
model: ITextModel,
pos: Position,
numLines: number,
depth: number,
snippets: Set<string>,
visited: IVisitedInterval[]
): Promise<void> {
if (depth <= 0) return;
const container = await this._findContainerFunction(model, pos);
if (!container) return;
const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range;
this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited);
const symbols = await this._getSymbolsNearRange(model, containerRange, numLines);
for (const sym of symbols) {
const defs = await this._getDefinitionSymbols(model, sym);
for (const def of defs) {
const defModel = this._modelService.getModel(def.uri);
if (defModel) {
const defPos = new Position(def.range.startLineNumber, def.range.startColumn);
this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited);
await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited);
}
}
}
const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn);
await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited);
}
private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean {
return visited.some(interval =>
interval.uri === uri &&
!(endLine < interval.startLine || startLine > interval.endLine)
);
}
private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise<DocumentSymbol[]> {
const startLine = Math.max(pos.lineNumber - numLines, 1);
const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount());
const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, range);
}
private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise<DocumentSymbol[]> {
const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2);
const startLine = Math.max(centerLine - numLines, 1);
const endLine = Math.min(centerLine + numLines, model.getLineCount());
const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine));
return this._getSymbolsInRange(model, searchRange);
}
private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise<DocumentSymbol[]> {
const symbols: DocumentSymbol[] = [];
const providers = this._langFeaturesService.documentSymbolProvider.ordered(model);
for (const provider of providers) {
try {
const result = await provider.provideDocumentSymbols(model, CancellationToken.None);
if (result) {
const flat = this._flattenSymbols(result);
const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range));
symbols.push(...intersecting);
}
} catch (e) {
console.warn("Symbol provider error:", e);
}
}
// Also check reference providers.
const refProviders = this._langFeaturesService.referenceProvider.ordered(model);
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
const content = model.getLineContent(line);
const words = content.match(/[a-zA-Z_]\w*/g) || [];
for (const word of words) {
const startColumn = content.indexOf(word) + 1;
const pos = new Position(line, startColumn);
if (!this._positionInRange(pos, range)) continue;
for (const provider of refProviders) {
try {
const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None);
if (refs) {
const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range));
for (const ref of filtered) {
symbols.push({
name: word,
detail: '',
kind: SymbolKind.Variable,
range: ref.range,
selectionRange: ref.range,
children: [],
tags: []
});
}
}
} catch (e) {
console.warn("Reference provider error:", e);
}
}
}
}
return symbols;
}
private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] {
const flat: DocumentSymbol[] = [];
for (const sym of symbols) {
flat.push(sym);
if (sym.children && sym.children.length > 0) {
flat.push(...this._flattenSymbols(sym.children));
}
}
return flat;
}
private _rangesIntersect(a: IRange, b: IRange): boolean {
return !(
a.endLineNumber < b.startLineNumber ||
a.startLineNumber > b.endLineNumber ||
(a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) ||
(a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn)
);
}
private _positionInRange(pos: Position, range: IRange): boolean {
return pos.lineNumber >= range.startLineNumber &&
pos.lineNumber <= range.endLineNumber &&
(pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) &&
(pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn);
}
// Get definition symbols for a given symbol.
private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> {
const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
const providers = this._langFeaturesService.definitionProvider.ordered(model);
const defs: (DocumentSymbol & { uri: URI })[] = [];
for (const provider of providers) {
try {
const res = await provider.provideDefinition(model, pos, CancellationToken.None);
if (res) {
const links = Array.isArray(res) ? res : [res];
defs.push(...links.map(link => ({
name: symbol.name,
detail: symbol.detail,
kind: symbol.kind,
range: link.range,
selectionRange: link.range,
children: [],
tags: symbol.tags || [],
uri: link.uri // Now keeping it as URI instead of converting to string
})));
}
} catch (e) {
console.warn("Definition provider error:", e);
}
}
return defs;
}
private async _findContainerFunction(model: ITextModel, pos: Position): Promise<DocumentSymbol | null> {
const searchRange = new Range(
Math.max(pos.lineNumber - 1, 1), 1,
Math.min(pos.lineNumber + 1, model.getLineCount()),
model.getLineMaxColumn(pos.lineNumber)
);
const symbols = await this._getSymbolsInRange(model, searchRange);
const funcs = symbols.filter(s =>
(s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) &&
this._positionInRange(pos, s.range)
);
if (!funcs.length) return null;
return funcs.reduce((innermost, current) => {
if (!innermost) return current;
const moreInner =
(current.range.startLineNumber > innermost.range.startLineNumber ||
(current.range.startLineNumber === innermost.range.startLineNumber &&
current.range.startColumn > innermost.range.startColumn)) &&
(current.range.endLineNumber < innermost.range.endLineNumber ||
(current.range.endLineNumber === innermost.range.endLineNumber &&
current.range.endColumn < innermost.range.endColumn));
return moreInner ? current : innermost;
}, null as DocumentSymbol | null);
}
}
registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager);
@@ -1,10 +1,44 @@
import { URI } from '../../../../../base/common/uri'
import { EndOfLinePreference } from '../../../../../editor/common/model'
import { IModelService } from '../../../../../editor/common/services/model.js'
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) => {
const modelResult = await _VSReadModel(modelService, uri)
if (modelResult) return modelResult
const fileResult = await _VSReadFileRaw(fileService, uri)
if (fileResult) return fileResult
return ''
// read files from VSCode
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
if (!model) return null
return model.getValue(EndOfLinePreference.LF)
}
// 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> => {
// attempt to read saved model (sometimes doesn't work if page is 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();
for (const model of models) {
if (model.uri.fsPath.toString() === uri.fsPath.toString()) {
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
}
@@ -25,15 +25,12 @@ import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_userMessage, fastApply_systemMessage, defaultFimTags } from './prompt/prompts.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultFimTags } from './prompt/prompts.js';
import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
import { errorDetails, LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js';
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { filenameToVscodeLanguage } from './helpers/detectLanguage.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { isMacintosh } from '../../../../base/common/platform.js';
@@ -41,6 +38,9 @@ import { EditorOption } from '../../../../editor/common/config/editorOptions.js'
import { Emitter } from '../../../../base/common/event.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@@ -102,13 +102,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
// similar to ServiceLLM
export type StartApplyingOpts = {
featureName: 'Ctrl+K';
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
} | {
featureName: 'Ctrl+L';
from: 'Chat';
applyStr: string;
applyBoxId: string;
} | {
featureName: 'Autocomplete';
from: 'Autocomplete';
range: IRange;
userMessage: string;
}
@@ -1206,16 +1207,226 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
return null
}
// private _generateSearchAndReplaceBlocks({ filename, applyStr }: { filename: URI, applyStr: string }): DiffZone | undefined {
// // call LLM to generate search and replace blocks (outputs something like [{search: 'this is my code', replace: 'this is m'}, ... ])
// // 1a output search block
// let uri: URI
// const uri_ = this._getActiveEditorURI()
// if (!uri_) return
// uri = uri_
// // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
// this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
// // in ctrl+L the start and end lines are the full document
// const numLines = this._getNumLines(uri)
// if (numLines === null) return
// let startLine: number
// let endLine: number
// startLine = 1
// endLine = numLines
// const currentFileStr = this._readURI(uri)
// if (currentFileStr === null) return
// const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n')
// // 1b find the start and end line that the search block lives on (if can't find it, retry 1a)
// let streamRequestIdRef: { current: string | null } = { current: null }
// // add to history
// const { onFinishEdit } = this._addToHistory(uri)
// // __TODO__ let users customize modelFimTags
// const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama'
// const modelFimTags = defaultFimTags
// const adding: Omit<DiffZone, 'diffareaid'> = {
// type: 'DiffZone',
// originalCode,
// startLine,
// endLine,
// _URI: uri,
// _streamState: {
// isStreaming: true,
// streamRequestIdRef,
// line: startLine,
// },
// _diffOfId: {}, // added later
// _removeStylesFns: new Set(),
// }
// const diffZone = this._addDiffArea(adding)
// this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
// this._onDidAddOrDeleteDiffZones.fire({ uri })
// if (from === 'QuickEdit') {
// const { diffareaid } = opts
// const ctrlKZone = this.diffAreaOfId[diffareaid]
// if (ctrlKZone.type !== 'CtrlKZone') return
// ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid
// }
// // now handle messages
// let messages: LLMChatMessage[]
// if (from === 'Chat') {
// const userContent = fastApply_searchreplace_userMessage({ originalCode, applyStr: opts.applyStr, uri })
// messages = [
// { role: 'system', content: fastApply_rewritewholething_systemMessage, },
// { role: 'user', content: userContent, }
// ]
// }
// else if (from === 'QuickEdit') {
// const { diffareaid } = opts
// const ctrlKZone = this.diffAreaOfId[diffareaid]
// if (ctrlKZone.type !== 'CtrlKZone') return
// const { _mountInfo } = ctrlKZone
// const instructions = _mountInfo?.textAreaRef.current?.value ?? ''
// // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else:
// const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine })
// // if (isOllamaFIM) {
// // messages = {
// // type: 'ollamaFIM',
// // prefix,
// // suffix,
// // }
// // }
// // else {
// const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
// const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language })
// // type: 'messages',
// messages = [
// { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), },
// { role: 'user', content: userContent, }
// ]
// // }
// }
// else { throw new Error(`featureName ${from} is invalid`) }
// const onDone = (hadError: boolean) => {
// diffZone._streamState = { isStreaming: false, }
// this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
// if (from === 'QuickEdit') {
// const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
// ctrlKZone._linkedStreamingDiffZone = null
// this._deleteCtrlKZone(ctrlKZone)
// }
// this._refreshStylesAndDiffsInURI(uri)
// onFinishEdit()
// // if had error, revert!
// if (hadError) {
// this._undoHistory(diffZone._URI)
// }
// }
// // refresh now in case onText takes a while to get 1st message
// this._refreshStylesAndDiffsInURI(uri)
// const extractText = (fullText: string, recentlyAddedTextLen: number) => {
// if (from === 'QuickEdit') {
// if (isOllamaFIM) return fullText
// return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
// }
// else if (from === 'Chat') {
// return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
// }
// throw 1
// }
// const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
// // state used in onText:
// let fullText = ''
// let prevIgnoredSuffix = ''
// streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
// messagesType: 'chatMessages',
// useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K',
// logging: { loggingName: `startApplying - ${from}` },
// messages,
// onText: ({ newText: newText_ }) => {
// const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
// fullText += prevIgnoredSuffix + newText
// const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length)
// this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo)
// this._refreshStylesAndDiffsInURI(uri)
// prevIgnoredSuffix = ignoredSuffix
// },
// onFinalMessage: ({ fullText }) => {
// // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine)
// // at the end, re-write whole thing to make sure no sync errors
// const [text, _] = extractText(fullText, 0)
// this._writeText(uri, text,
// { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
// { shouldRealignDiffAreas: true }
// )
// onDone(false)
// },
// onError: (e) => {
// const details = errorDetails(e.fullError)
// this._notificationService.notify({
// severity: Severity.Warning,
// message: `Void Error: ${e.message}`,
// actions: {
// secondary: [{
// id: 'void.onerror.opensettings',
// enabled: true,
// label: 'Open Void settings',
// tooltip: '',
// class: undefined,
// run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }
// }]
// },
// source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined
// })
// onDone(true)
// },
// })
// return diffZone
// }
private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined {
const { featureName } = opts
const { from } = opts
let startLine: number
let endLine: number
let uri: URI
if (featureName === 'Ctrl+L') {
if (from === 'Chat') {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
@@ -1231,7 +1442,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
endLine = numLines
}
else if (featureName === 'Ctrl+K') {
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@@ -1242,7 +1453,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
endLine = endLine_
}
else {
throw new Error(`Void: diff.type not recognized on: ${featureName}`)
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
const currentFileStr = this._readURI(uri)
@@ -1278,7 +1489,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@@ -1287,16 +1498,16 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
}
// now handle messages
let messages: LLMMessage[]
let messages: LLMChatMessage[]
if (featureName === 'Ctrl+L') {
const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri })
if (from === 'Chat') {
const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri })
messages = [
{ role: 'system', content: fastApply_systemMessage, },
{ role: 'system', content: fastApply_rewritewholething_systemMessage, },
{ role: 'user', content: userContent, }
]
}
else if (featureName === 'Ctrl+K') {
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@@ -1323,14 +1534,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
]
// }
}
else { throw new Error(`featureName ${featureName} is invalid`) }
else { throw new Error(`featureName ${from} is invalid`) }
const onDone = (hadError: boolean) => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
ctrlKZone._linkedStreamingDiffZone = null
@@ -1350,11 +1561,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
if (isOllamaFIM) return fullText
return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
}
else if (featureName === 'Ctrl+L') {
else if (from === 'Chat') {
return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
}
throw 1
@@ -1367,9 +1578,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
let prevIgnoredSuffix = ''
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
type: 'sendLLMMessage',
useProviderFor: featureName,
logging: { loggingName: `startApplying - ${featureName}` },
messagesType: 'chatMessages',
useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K',
logging: { loggingName: `startApplying - ${from}` },
messages,
onText: ({ newText: newText_ }) => {
@@ -7,8 +7,13 @@
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
import { VSReadFile } from '../helpers/readFile.js';
import { _VSReadModel, VSReadFile } from '../helpers/readFile.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
// this is just for ease of readability
const tripleTick = ['```', '```']
export const chat_systemMessage = `\
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\`.
@@ -29,7 +34,7 @@ Do not tell the user anything about the examples below.
## EXAMPLE 1
FILES
math.ts
\`\`\`typescript
${tripleTick[0]}typescript
const addNumbers = (a, b) => a + b
const multiplyNumbers = (a, b) => a * b
const subtractNumbers = (a, b) => a - b
@@ -58,21 +63,21 @@ const normalized = (vector: number[]) => {
const v2 = [...vector] // clone vector
return normalize(v2)
}
\`\`\`
${tripleTick[1]}
SELECTIONS
math.ts (lines 3:3)
\`\`\`typescript
${tripleTick[0]}typescript
const subtractNumbers = (a, b) => a - b
\`\`\`
${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
We can add the following code to the file:
\`\`\`typescript
${tripleTick[0]}typescript
// existing code...
const subtractNumbers = (a, b) => a - b
const exponentiateNumbers = (a, b) => Math.pow(a, b)
@@ -84,13 +89,13 @@ const raiseAll = (vector: number[], power: number) => {
vector[i] = exponentiateNumbers(vector[i], power)
return vector
}
\`\`\`
${tripleTick[1]}
## EXAMPLE 2
FILES
fib.ts
\`\`\`typescript
${tripleTick[0]}typescript
const dfs = (root) => {
if (!root) return;
@@ -102,20 +107,20 @@ const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
${tripleTick[1]}
SELECTIONS
fib.ts (lines 10:10)
\`\`\`typescript
${tripleTick[0]}typescript
return fib(n - 1) + fib(n - 2)
\`\`\`
${tripleTick[1]}
INSTRUCTIONS
memoize results
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:
\`\`\`typescript
${tripleTick[0]}typescript
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
@@ -123,7 +128,7 @@ const fib = (n, memo = {}) => {
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
${tripleTick[1]}
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
@@ -133,29 +138,29 @@ Store Result: After computing fib(n), the result is stored in memo for future re
`
type FileSelnLocal = FileSelection & { content: string }
const stringifyFileSelection = ({ fileURI, selectionStr, range, content }: FileSelnLocal) => {
type FileSelnLocal = { fileURI: URI, content: string }
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content}
\`\`\`
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${selectionStr}
\`\`\`
${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) => {
const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const content = await VSReadFile(modelService, sel.fileURI) ?? failToReadStr
const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
@@ -163,27 +168,64 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], modelSer
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n')
}
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_userMessage = async (instructions: string, selections: StagingSelectionItem[] | null, modelService: IModelService) => {
const fileSelections = selections?.filter(s => s.type === 'File') as FileSelection[]
const codeSelections = selections?.filter(s => s.type === 'Selection') as CodeSelection[]
const filesStr = await stringifyFileSelections(fileSelections, modelService)
const codeStr = stringifyCodeSelections(codeSelections)
const selnsStr = stringifySelectionNames(currSelns)
let str = ''
if (filesStr) str += `FILES\n${filesStr}\n`
if (codeStr) str += `SELECTIONS\n${codeStr}\n`
str += `INSTRUCTIONS\n${instructions}`
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
str += `\nINSTRUCTIONS\n${instructions}`
return str;
};
export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => {
// ADD IN FILES AT TOP
const allSelections = [...currSelns || [], ...prevSelns || []]
const codeSelections: CodeSelection[] = []
const fileSelections: FileSelection[] = []
const filesURIs = new Set<string>()
for (const selection of allSelections) {
if (selection.type === 'Selection') {
codeSelections.push(selection)
}
else if (selection.type === 'File') {
const fileSelection = selection
const path = fileSelection.fileURI.fsPath
if (!filesURIs.has(path)) {
filesURIs.add(path)
fileSelections.push(fileSelection)
}
}
}
const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService)
const selnsStr = stringifyCodeSelections(codeSelections)
// ACTUAL MESSAGE CONTENT
const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns)
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_systemMessage = `\
export const fastApply_rewritewholething_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:
@@ -195,7 +237,40 @@ Directions:
export const fastApply_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
return `\
ORIGINAL_FILE
${tripleTick[0]}${language}
${originalCode}
${tripleTick[1]}
CHANGE
${tripleTick[0]}
${applyStr}
${tripleTick[1]}
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.
`
}
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 }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
@@ -220,6 +295,9 @@ Please finish writing the new file by applying the change to the original file.
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
@@ -309,9 +387,9 @@ export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instruction
return `\
CURRENT SELECTION
\`\`\`${language}
${tripleTick[0]}${language}
<${midTag}>${selection}</${midTag}>
\`\`\`
${tripleTick[1]}
INSTRUCTIONS
${instructions}
@@ -319,8 +397,8 @@ ${instructions}
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
Return only the completion block of code (of the form \`\`\`${language}
Return only the completion block of code (of the form ${tripleTick[0]}${language}
<${midTag}>...new code</${midTag}>
\`\`\`).`
${tripleTick[1]}).`
};
@@ -7,12 +7,12 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IInlineDiffsService } from './inlineDiffsService.js';
import { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { IMetricsService } from '../common/metricsService.js';
export type QuickEditPropsType = {
@@ -6,7 +6,8 @@
import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { useAccessor } from '../util/services.js'
import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js'
import { ChatLocation, getApplyBoxId, } from '../../../searchAndReplaceService.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
@@ -18,7 +19,7 @@ enum CopyButtonState {
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ text }: { text: string }) => {
const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => {
const accessor = useAccessor()
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
@@ -36,22 +37,24 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
}, [copyButtonState])
const onCopy = useCallback(() => {
clipboardService.writeText(text)
clipboardService.writeText(applyStr)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
metricsService.capture('Copy Code', { length: text.length }) // capture the length only
metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only
}, [metricsService, clipboardService, text])
}, [metricsService, clipboardService, applyStr])
const onApply = useCallback(() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
applyStr: text,
})
metricsService.capture('Apply Code', { length: text.length }) // capture the length only
}, [metricsService, inlineDiffService, text])
const isSingleLine = !text.includes('\n')
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
@@ -84,7 +87,8 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
</code>
}
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tokenId = '', }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation, tokenId?: string, }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
@@ -94,10 +98,18 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
}
if (t.type === "code") {
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
const applyBoxId = getApplyBoxId({
threadId: chatLocation!.threadId,
messageIdx: chatLocation!.messageIdx,
codeblockId: tokenId,
})
return <BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />}
/>
}
@@ -183,21 +195,21 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} />
<RenderToken key={index} token={token} tokenId={`${tokenId}-${index}`} /> // assign a unique tokenId to nested components
))}
</>
if (nested)
return contents
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
if (nested) return contents
return <p className={`${noSpace ? '' : 'my-4'}`}>
{contents}
</p>
}
if (t.type === "html") {
return (
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
{`<html>`}
<p className={`${noSpace ? '' : 'my-4'}`}>
{t.raw}
{`</html>`}
</pre>
</p>
)
}
@@ -266,12 +278,12 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatLocation?: ChatLocation }) => {
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} />
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatLocation={chatLocation} />
))}
</>
)
@@ -7,11 +7,12 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'reac
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
export const QuickEditChat = ({
diffareaid,
@@ -42,21 +43,22 @@ export const QuickEditChat = ({
}, [onChangeHeight]);
const settingsState = useSettingsState()
// state of current message
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
const isDisabled = instructionsAreEmpty
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
const onSubmit = useCallback((e: FormEvent) => {
const onSubmit = useCallback(() => {
if (isDisabled) return
if (currStreamingDiffZoneRef.current !== null) return
textAreaFnsRef.current?.disable()
const instructions = textAreaRef.current?.value ?? ''
const id = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
from: 'QuickEdit',
diffareaid: diffareaid,
})
setCurrentlyStreamingDiffZone(id ?? null)
@@ -79,110 +81,45 @@ export const QuickEditChat = ({
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
const chatAreaRef = useRef<HTMLDivElement | null>(null)
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
<VoidChatArea
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
isStreaming={isStreaming}
isDisabled={isDisabled}
featureName="Ctrl+K"
className="py-2 w-full"
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative`}
>
<div className='flex flex-row items-center justify-between items-end gap-1'>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full`}
>
{/* text input */}
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
// if presses the esc key, X
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder={`Enter instructions...`}
// ${keybindingString} to select.
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
multiline={true}
/>
</div>
{/* X button */}
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={12}
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onX}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder="Enter instructions..."
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
return
}
</div>
</div>
</form>
}}
multiline={true}
/>
</VoidChatArea>
</div>
@@ -5,7 +5,8 @@
import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
import { useSettingsState } from '../util/services.js';
export const ErrorDisplay = ({
@@ -22,9 +23,9 @@ export const ErrorDisplay = ({
const [isExpanded, setIsExpanded] = useState(false);
const details = errorDetails(fullError)
const isExpandable = !!details
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + ''
const message = message_ + ''
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
@@ -45,7 +46,7 @@ export const ErrorDisplay = ({
</div>
<div className='flex gap-2'>
{details && (
{isExpandable && (
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={() => setIsExpanded(!isExpanded)}
>
@@ -3,10 +3,10 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
@@ -15,12 +15,17 @@ import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
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 { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { ChatLocation } from '../../../searchAndReplaceService.js';
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
@@ -132,6 +137,115 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
}
interface VoidChatAreaProps {
// Required
children: React.ReactNode; // This will be the input component
// Form controls
onSubmit: () => void;
onAbort: () => void;
isStreaming: boolean;
isDisabled?: boolean;
divRef?: React.RefObject<HTMLDivElement>;
// UI customization
featureName: FeatureName;
className?: string;
showModelDropdown?: boolean;
showSelections?: boolean;
showProspectiveSelections?: boolean;
selections?: StagingSelectionItem[]
setSelections?: (s: StagingSelectionItem[]) => void
// selections?: any[];
// onSelectionsChange?: (selections: any[]) => void;
onClickAnywhere?: () => void;
// Optional close button
onClose?: () => void;
}
export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
children,
onSubmit,
onAbort,
onClose,
onClickAnywhere,
divRef,
isStreaming = false,
isDisabled = false,
className = '',
showModelDropdown = true,
featureName,
showSelections = false,
showProspectiveSelections = true,
selections,
setSelections,
}) => {
return (
<div
ref={divRef}
className={`
flex flex-col gap-1 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
${className}
`}
onClick={(e) => {
onClickAnywhere?.()
}}
>
{/* Selections section */}
{showSelections && selections && setSelections && (
<SelectedFiles
type='staging'
selections={selections}
setSelections={setSelections}
showProspectiveSelections={showProspectiveSelections}
/>
)}
{/* Input section */}
<div className="relative w-full">
{children}
{/* Close button (X) if onClose is provided */}
{onClose && (
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={12}
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onClose}
/>
</div>
)}
</div>
{/* Bottom row */}
<div className='flex flex-row justify-between items-end gap-1'>
{showModelDropdown && (
<div className='max-w-[150px] @@[&_select]:!void-border-none @@[&_select]:!void-outline-none flex-grow'
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
<ModelDropdown featureName={featureName} />
</div>
)}
{isStreaming ? (
<ButtonStop onClick={onAbort} />
) : (
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
)}
</div>
</div>
);
};
const useResizeObserver = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
@@ -428,92 +542,211 @@ export const SelectedFiles = (
}
type ChatBubbleMode = 'display' | 'edit'
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
const role = chatMessage.role
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
// global state
let isBeingEdited = false
let setIsBeingEdited = (v: boolean) => { }
let stagingSelections: StagingSelectionItem[] = []
let setStagingSelections = (s: StagingSelectionItem[]) => { }
if (messageIdx !== undefined) {
const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx)
isBeingEdited = _state.isBeingEdited
setIsBeingEdited = (v) => _setState({ isBeingEdited: v })
stagingSelections = _state.stagingSelections
setStagingSelections = (s) => { _setState({ stagingSelections: s }) }
}
// local state
const mode: ChatBubbleMode = isBeingEdited ? 'edit' : 'display'
const [isFocused, setIsFocused] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const [isDisabled, setIsDisabled] = useState(false)
const [textAreaRefState, setTextAreaRef] = useState<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
// initialize on first render, and when edit was just enabled
const _mustInitialize = useRef(true)
const _justEnabledEdit = useRef(false)
useEffect(() => {
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
if (canInitialize && shouldInitialize) {
setStagingSelections(chatMessage.selections || [])
if (textAreaFnsRef.current)
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
textAreaRefState.focus();
_justEnabledEdit.current = false
_mustInitialize.current = false
}
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
const EditSymbol = mode === 'display' ? Pencil : X
const onOpenEdit = () => {
setIsBeingEdited(true)
chatThreadsService.setFocusedMessageIdx(messageIdx)
_justEnabledEdit.current = true
}
const onCloseEdit = () => {
setIsFocused(false)
setIsHovered(false)
setIsBeingEdited(false)
chatThreadsService.setFocusedMessageIdx(undefined)
}
// set chat bubble contents
let chatbubbleContents: React.ReactNode
if (role === 'user') {
if (mode === 'display') {
chatbubbleContents = <>
<SelectedFiles type='past' selections={chatMessage.selections || []} />
{chatMessage.displayContent}
</>
}
else if (mode === 'edit') {
const onSubmit = async () => {
if (isDisabled) return;
if (!textAreaRefState) return;
if (messageIdx === undefined) return;
// cancel any streams on this thread
const thread = chatThreadsService.getCurrentThread()
chatThreadsService.cancelStreaming(thread.id)
// reset state
setIsBeingEdited(false)
chatThreadsService.setFocusedMessageIdx(undefined)
// stream the edit
const userMessage = textAreaRefState.value;
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
}
const onAbort = () => {
const threadId = chatThreadsService.state.currentThreadId
chatThreadsService.cancelStreaming(threadId)
}
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Escape') {
onCloseEdit()
}
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
return null
}
chatbubbleContents = <>
<VoidChatArea
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={false}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={false}
featureName="Ctrl+L"
selections={stagingSelections}
setSelections={setStagingSelections}
>
<VoidInputBox2
ref={setTextAreaRef}
className='min-h-[81px] max-h-[500px] p-1'
placeholder="Edit your message..."
onChangeText={(text) => setIsDisabled(!text)}
onFocus={() => {
setIsFocused(true)
chatThreadsService.setFocusedMessageIdx(messageIdx);
}}
onBlur={() => {
setIsFocused(false)
}}
onKeyDown={onKeyDown}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
</>
}
}
else if (role === 'assistant') {
const thread = chatThreadsService.getCurrentThread()
const chatLocation: ChatLocation = {
threadId: thread.id,
messageIdx: messageIdx!,
}
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatLocation={chatLocation} />
}
return <div
// align chatbubble accoridng to role
className={`
relative
${isEditMode ? 'px-2 w-full max-w-full'
relative
${mode === 'edit' ? 'px-2 w-full max-w-full'
: 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)}
>
<div
// style chatbubble according to role
className={`
text-left rounded-lg
overflow-x-auto max-w-full
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
`}
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 === 'assistant' ? 'px-2 overflow-x-auto' : ''
}
`}
>
{children}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm px-2' />}
</div>
{/* edit button */}
{/* {role === 'user' &&
<Pencil
size={16}
{role === 'user' && <EditSymbol
size={18}
className={`
absolute top-0 right-2
absolute -top-1 right-1
translate-x-0 -translate-y-0
cursor-pointer z-1
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => { setIsEditMode(v => !v); }}
/>
} */}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>}
</div>
}
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
const role = chatMessage.role
// edit mode state
const [isEditMode, setIsEditMode] = useState(false)
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
return null
}
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles type='past' selections={chatMessage.selections || []} />
{chatMessage.displayContent}
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
{/* edit mode content */}
{/* TODO this should be the same input box as in the Sidebar */}
{/* <textarea
value={editModeText}
className={`
w-full max-w-full
h-auto min-h-[81px] max-h-[500px]
bg-void-bg-1 resize-none
`}
style={{ marginTop: 0 }}
hidden={!isEditMode}
/> */}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
}
return <ChatBubble_ role={role} isEditMode={isEditMode} isLoading={!!isLoading}>
{chatbubbleContents}
</ChatBubble_>
}
export const SidebarChat = () => {
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
@@ -522,15 +755,17 @@ export const SidebarChat = () => {
const accessor = useAccessor()
// const modelService = accessor.get('IModelService')
const commandService = accessor.get('ICommandService')
const chatThreadsService = accessor.get('IChatThreadService')
const settingsState = useSettingsState()
// ----- HIGHER STATE -----
// sidebar state
const sidebarStateService = accessor.get('ISidebarStateService')
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.blur() })
)
return () => disposables.forEach(d => d.dispose())
}, [sidebarStateService, textAreaRef])
@@ -539,11 +774,13 @@ export const SidebarChat = () => {
// threads state
const chatThreadsState = useChatThreadsState()
const chatThreadsService = accessor.get('IChatThreadService')
const currentThread = chatThreadsService.getCurrentThread()
const previousMessages = currentThread?.messages ?? []
const selections = chatThreadsState.currentStagingSelections
const [_state, _setState] = chatThreadsService._useCurrentThreadState()
const selections = _state.stagingSelections
const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) }
// stream state
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
@@ -556,16 +793,17 @@ export const SidebarChat = () => {
// state of current message
const initVal = ''
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
const isDisabled = instructionsAreEmpty
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState)
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [formRef, formDimensions] = useResizeObserver()
const [chatAreaRef, chatAreaDimensions] = useResizeObserver()
const [historyRef, historyDimensions] = useResizeObserver()
useScrollbarStyles(sidebarRef)
const onSubmit = async () => {
const onSubmit = useCallback(async () => {
if (isDisabled) return
if (isStreaming) return
@@ -574,11 +812,11 @@ export const SidebarChat = () => {
const userMessage = textAreaRef.current?.value ?? ''
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
chatThreadsService.setStaging([]) // clear staging
setSelections([]) // clear staging
textAreaFnsRef.current?.setValue('')
textAreaRef.current?.focus() // focus input after submit
}
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections])
const onAbort = () => {
const threadId = currentThread.id
@@ -599,7 +837,7 @@ export const SidebarChat = () => {
const prevMessagesHTML = useMemo(() => {
return previousMessages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
)
}, [previousMessages])
@@ -611,6 +849,7 @@ export const SidebarChat = () => {
</div>
const messagesHTML = <ScrollToBottomContainer
scrollContainerRef={scrollContainerRef}
className={`
@@ -619,8 +858,9 @@ export const SidebarChat = () => {
overflow-x-hidden
overflow-y-auto
py-4
${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
`}
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
>
{/* previous messages */}
{prevMessagesHTML}
@@ -639,83 +879,45 @@ export const SidebarChat = () => {
showDismiss={true}
/>
<WarningBox className='text-sm my-2 pl-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
<WarningBox className='text-sm my-2 mx-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
</div>
}
</ScrollToBottomContainer>
const inputBox = <div // this div is used to position the input box properly
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<div
ref={formRef}
className={`
flex flex-col gap-1 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
max-h-[80vh] overflow-y-auto
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
const onChangeText = useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
}, [setInstructionsAreEmpty])
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}, [onSubmit])
const inputForm = <div className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
<VoidChatArea
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={isStreaming}
isDisabled={isDisabled}
showSelections={true}
showProspectiveSelections={prevMessagesHTML.length === 0}
selections={selections}
setSelections={setSelections}
onClickAnywhere={() => { textAreaRef.current?.focus() }}
featureName="Ctrl+L"
>
{/* top row */}
<>
{/* selections */}
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0} />
</>
{/* middle row */}
<div>
{/* text input */}
<VoidInputBox2
className='min-h-[81px] p-1'
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none
flex-grow
'
>
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onAbort}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
}
</div>
</div>
<VoidInputBox2
className='min-h-[81px] p-1'
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
onFocus={() => { chatThreadsService.setFocusedMessageIdx(undefined) }}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidChatArea>
</div>
return <div ref={sidebarRef} className={`w-full h-full`}>
@@ -723,7 +925,7 @@ export const SidebarChat = () => {
{messagesHTML}
{inputBox}
{inputForm}
</div>
}
@@ -58,9 +58,11 @@ type InputBox2Props = {
className?: string;
onChangeText?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
onChangeHeight?: (newHeight: number) => void;
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
@@ -114,6 +116,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
onFocus={onFocus}
onBlur={onBlur}
disabled={!isEnabled}
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
@@ -298,9 +303,9 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
export const VoidCustomSelectBox = <T extends any>({
export const VoidCustomDropdownBox = <T extends any>({
options,
selectedOption: selectedOption_,
selectedOption,
onChangeOption,
getOptionDropdownName,
getOptionDisplayName,
@@ -311,7 +316,7 @@ export const VoidCustomSelectBox = <T extends any>({
gap = 0,
}: {
options: T[];
selectedOption?: T;
selectedOption: T | undefined;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDisplayName: (option: T) => string;
@@ -335,7 +340,7 @@ export const VoidCustomSelectBox = <T extends any>({
} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement:'bottom-start',
placement: 'bottom-start',
middleware: [
offset(gap),
@@ -367,17 +372,15 @@ export const VoidCustomSelectBox = <T extends any>({
}),
],
whileElementsMounted: autoUpdate,
strategy:'fixed',
strategy: 'fixed',
});
// if the selected option is null, use the 0th option
// if the selected option is null, set the selection to the 0th option
useEffect(() => {
if (!options[0]) return
if (!selectedOption_) {
onChangeOption(options[0]);
}
}, [selectedOption_, options])
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
if (options.length === 0) return
if (selectedOption) return
onChangeOption(options[0])
}, [selectedOption, onChangeOption, options])
// Handle clicks outside
useEffect(() => {
@@ -404,6 +407,9 @@ export const VoidCustomSelectBox = <T extends any>({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, refs.floating, refs.reference]);
if (!selectedOption)
return null
return (
<div className={`inline-block relative ${className}`}>
{/* Hidden measurement div */}
@@ -5,14 +5,14 @@
import React, { useState, useEffect } from 'react'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
@@ -25,9 +25,9 @@ import { IContextViewService, IContextMenuService } from '../../../../../../../p
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
import { 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 { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
@@ -46,7 +46,7 @@ import { IKeybindingService } from '../../../../../../../platform/keybinding/com
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
@@ -288,6 +288,17 @@ export const useChatThreadsState = () => {
return () => { chatThreadsStateListeners.delete(ss) }
}, [ss])
return s
// allow user to set state natively in react
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
// _ss(action)
// if (typeof action === 'function') {
// const newState = action(chatThreadsState)
// chatThreadsState = newState
// } else {
// chatThreadsState = action
// }
// }
// return [s, ss] as const
}
@@ -1,7 +1,6 @@
import { useEffect } from 'react';
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
@@ -12,90 +11,121 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivE
'[class*="overflow-y-auto"]'
].join(',');
// Get all matching elements within the container, including the container itself
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
// Function to initialize scrollbar styles for elements
const initializeScrollbarStyles = () => {
// Get all matching elements within the container, including the container itself
const scrollElements = [
...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || [])
];
// Apply styles and listeners to each scroll element
scrollElements.forEach(element => {
// Add the scrollable class directly to the overflow element
element.classList.add('void-scrollable-element');
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
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);
};
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;
});
return () => {
// Clean up all scroll elements
// Clean up existing elements first
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
// Apply styles and listeners to each scroll element
scrollElements.forEach(element => {
// Add the scrollable class directly to the overflow element
element.classList.add('void-scrollable-element');
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
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);
};
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;
});
};
// Initialize for the first time
initializeScrollbarStyles();
// Set up mutation observer
const observer = new MutationObserver((mutations) => {
initializeScrollbarStyles();
});
// Start observing the container for child changes
observer.observe(containerRef.current, {
childList: true,
subtree: true
});
return () => {
observer.disconnect();
// Your existing cleanup code...
if (containerRef.current) {
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
}
};
}, [containerRef]);
};
@@ -4,15 +4,14 @@
*--------------------------------------------------------------------------------------*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
@@ -27,13 +26,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
const voidSettingsService = accessor.get('IVoidSettingsService')
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection))! : options[0]
const onChangeOption = useCallback((newOption: ModelOption) => {
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
}, [voidSettingsService, featureName])
return <VoidCustomSelectBox
return <VoidCustomDropdownBox
options={options}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
@@ -75,10 +74,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
// />
// }
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
@@ -92,30 +94,6 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
}
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
return <div
className={`
text-void-warning brightness-90 opacity-90
text-xs text-ellipsis
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
flex items-center flex-nowrap
${className}
`}
onClick={onClick}
>
<IconWarning
size={14}
className='mr-1'
/>
<span>{text}</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
// onChangeSelection={() => { }}
// />
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
@@ -124,10 +102,14 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
return <>
{settingsState._modelOptions.length === 0 ?
<WarningBox onClick={openSettings} text='Provider required' />
: <MemoizedModelSelectBox featureName={featureName} />
}
</>
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
if (isDisabled)
return <WarningBox onClick={openSettings} text={
isDisabled === 'needToEnableModel' ? 'Enable a model'
: isDisabled === 'addModel' ? 'Add a model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} />
}
@@ -5,17 +5,18 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { WarningBox, ModelDropdown } from './ModelDropdown.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
@@ -79,7 +80,7 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
return <div key={providerName} className='pb-4'>
<RefreshModelButton providerName={providerName} />
</div>
@@ -112,7 +113,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
<div className='flex items-center gap-4'>
{/* provider */}
<VoidCustomSelectBox
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
@@ -199,7 +200,7 @@ export const ModelDump = () => {
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
// if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
}
// sort by hidden
@@ -223,7 +224,6 @@ export const ModelDump = () => {
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
{/* <span>{`${modelName} (${providerName})`}</span> */}
</div>
{/* right part is anything that fits */}
<div className='flex items-center gap-4'>
@@ -260,7 +260,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidMetricsService = accessor.get('IMetricsService')
let weChangedTextRef = false
@@ -284,25 +283,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
})
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
if (shouldEnable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
voidMetricsService.capture('Enable Provider', { providerName })
}
if (shouldDisable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
voidMetricsService.capture('Disable Provider', { providerName })
}
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
@@ -318,7 +300,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
// const voidSettingsState = useSettingsState()
const voidSettingsState = useSettingsState()
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
@@ -349,6 +334,12 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
{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).`} />
: null}
</div>
</div >
}
@@ -0,0 +1,26 @@
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
return <div
className={`
text-void-warning brightness-90 opacity-90 w-fit
text-xs text-ellipsis
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
flex items-center flex-nowrap
${className}
`}
onClick={onClick}
>
<IconWarning
size={14}
className='mr-1'
/>
<span>{text}</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
// onChangeSelection={() => { }}
// />
}
@@ -0,0 +1,76 @@
/*--------------------------------------------------------------------------------------
* 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);
@@ -17,7 +17,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
@@ -67,6 +67,13 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
}
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
return currentSelections?.findIndex(s =>
s.fileURI.fsPath === newSelection.fileURI.fsPath
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
)
}
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
@@ -124,26 +131,37 @@ registerAction2(class extends Action2 {
range: selectionRange,
}
// add selection to staging
// update the staging selections
const chatThreadService = accessor.get(IChatThreadService)
const currentStaging = chatThreadService.state.currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
chatThreadService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
const [state, setState] = chatThreadService._useCurrentThreadState()
selections = state.stagingSelections
setSelections = (s) => setState({ stagingSelections: s })
} else {
const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx)
selections = state.stagingSelections
setSelections = (s) => setState({ stagingSelections: s })
}
// if matches with existing selection, overwrite (since text may change)
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
setSelections([
...selections!.slice(0, matchingStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
])
}
// if no match, add
// if no match, add it
else {
chatThreadService.setStaging([...(currentStaging ?? []), selection])
setSelections([...(selections ?? []), selection])
}
}
@@ -21,6 +21,10 @@ import './chatThreadService.js'
// register Autocomplete
import './autocompleteService.js'
// register Context services
// import './contextGatheringService.js'
// import './contextUserChangesService.js'
// settings pane
import './voidSettingsPane.js'
@@ -29,3 +33,26 @@ import './media/void.css'
// update (frontend part, also see platform/)
import './voidUpdateActions.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
import '../common/llmMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
// updates
import '../common/voidUpdateService.js'
// tools
import '../common/toolsService.js'
@@ -9,8 +9,8 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { localize2 } from '../../../../nls.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
@@ -4,14 +4,16 @@
*--------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
@@ -90,10 +92,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
// end early if no provider
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (modelSelection === null) {
onError({ message: 'Please add a Provider in Settings!', fullError: null })
if (isDisabled || modelSelection === null) {
let message: string
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void 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}` : ''}.`
else
message = 'Please add a provider in Void Settings.'
onError({ message, fullError: null })
return null
}
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -11,6 +11,7 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
else if (typeof fullError === 'object') {
if (Object.keys(fullError).length === 0) return null
return JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
@@ -24,28 +25,28 @@ export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMMessage = {
export type LLMChatMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
}
export type _InternalLLMMessage = {
export type _InternalLLMChatMessage = {
role: 'user' | 'assistant';
content: string;
}
type _InternalOllamaFIMMessages = {
type _InternalSendFIMMessage = {
prefix: string;
suffix: string;
stopTokens: string[];
}
type SendLLMType = {
type: 'sendLLMMessage';
messages: LLMMessage[];
messagesType: 'chatMessages';
messages: LLMChatMessage[];
} | {
type: 'ollamaFIM';
messages: _InternalOllamaFIMMessages;
messagesType: 'FIMMessage';
messages: _InternalSendFIMMessage;
}
// service types
@@ -54,7 +55,7 @@ export type ServiceSendLLMMessageParams = {
onFinalMessage: OnFinalMessage;
onError: OnError;
logging: { loggingName: string, };
useProviderFor: 'Ctrl+K' | 'Ctrl+L' | 'Autocomplete';
useProviderFor: FeatureName;
} & SendLLMType
// params to the true sendLLMMessage function
@@ -85,7 +86,7 @@ export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0]
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
export type _InternalSendLLMMessageFnType = (
export type _InternalSendLLMChatMessageFnType = (
params: {
onText: OnText;
onFinalMessage: OnFinalMessage;
@@ -95,11 +96,11 @@ export type _InternalSendLLMMessageFnType = (
modelName: string;
_setAborter: (aborter: () => void) => void;
messages: _InternalLLMMessage[];
messages: _InternalLLMChatMessage[];
}
) => void
export type _InternalOllamaFIMMessageFnType = (
export type _InternalSendLLMFIMMessageFnType = (
params: {
onText: OnText;
onFinalMessage: OnFinalMessage;
@@ -109,7 +110,7 @@ export type _InternalOllamaFIMMessageFnType = (
modelName: string;
_setAborter: (aborter: () => void) => void;
messages: _InternalOllamaFIMMessages;
messages: _InternalSendFIMMessage;
}
) => void
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { Action2, registerAction2 } from '../../actions/common/actions.js';
import { localize2 } from '../../../nls.js';
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
import { INotificationService } from '../../notification/common/notification.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { localize2 } from '../../../../nls.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { 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 { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -44,8 +44,8 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
ollama: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
@@ -95,7 +95,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.startRefreshingModels(providerName, autoOptions)
// every time providerName.enabled changes, refresh models too, like a useEffect
@@ -175,7 +175,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
this._setRefreshState(providerName, 'finished', options)
autoPoll()
@@ -0,0 +1,182 @@
import { CancellationToken } from '../../../../base/common/cancellation.js'
import { URI } from '../../../../base/common/uri.js'
import { IFileService, IFileStat } 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'
// tool use for AI
// we do this using Anthropic's style and convert to OpenAI style later
export type InternalToolInfo = {
description: string,
params: {
[paramName: string]: { type: string, description: string | undefined } // name -> type
},
required: string[], // required paramNames
}
// helper
const pagination = {
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 = {
read_file: {
description: 'Returns file contents of a given URI.',
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
list_dir: {
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
params: {
uri: { type: 'string', description: undefined },
...pagination.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}`,
params: {
query: { type: 'string', description: undefined },
...pagination.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}`,
params: {
query: { type: 'string', description: undefined },
...pagination.param,
},
required: ['query'],
},
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} as const satisfies { [name: string]: InternalToolInfo }
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>)
}
// 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 stat = await fileService.resolve(rootURI, { resolveMetadata: false });
// kickstart recursion
output += `${stat.name}\n`;
traverseChildren(stat.children ?? [], 1);
return output;
}
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('(uri was not a string)')
const uri = URI.file(uriStr)
return uri
}
export interface IToolService {
readonly _serviceBrand: undefined;
callContextTool: <T extends ContextToolName>(toolName: T, params: ContextToolParams<T>) => Promise<string>
}
export const IToolService = createDecorator<IToolService>('ToolService');
export class ToolService implements IToolService {
readonly _serviceBrand: undefined;
contextToolCallFns: AllContextToolCallFns
constructor(
@IFileService fileService: IFileService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.contextToolCallFns = {
read_file: async ({ uri: uriStr }) => {
const uri = validateURI(uriStr)
const fileContents = await _VSReadFileRaw(fileService, uri)
return fileContents ?? '(could not read file)'
},
list_dir: async ({ uri: uriStr }) => {
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 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 data = await searchService.textSearch(query, CancellationToken.None);
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
return str
},
}
}
callContextTool: IToolService['callContextTool'] = (toolName, params) => {
return this.contextToolCallFns[toolName](params)
}
}
registerSingleton(IToolService, ToolService, InstantiationType.Eager);
@@ -3,15 +3,15 @@
* 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 { deepClone } from '../../../base/common/objects.js';
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
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, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.settingsServiceStorage'
@@ -42,8 +42,8 @@ export type VoidSettingsState = {
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
// type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
// type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
export interface IVoidSettingsService {
@@ -51,7 +51,7 @@ export interface IVoidSettingsService {
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
@@ -64,26 +64,76 @@ export interface IVoidSettingsService {
}
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute _didFillInProviderSettings
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (!providerConfig._enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
const settingsAtProvider = newSettingsOfProvider[providerName]
const didFillInProviderSettings = Object.keys(defaultProviderSettings[providerName]).every(key => !!settingsAtProvider[key as keyof typeof settingsAtProvider])
if (didFillInProviderSettings === settingsAtProvider._didFillInProviderSettings) continue
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
...settingsAtProvider,
_didFillInProviderSettings: didFillInProviderSettings,
},
}
}
return 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
if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options
for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) {
if (isHidden) continue
newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } })
}
}
// now that model options are updated, make sure the selection is valid
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
let newModelSelectionOfFeature = state.modelSelectionOfFeature
for (const featureName of featureNames) {
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
if (selnIdx !== -1) continue
newModelSelectionOfFeature = {
...newModelSelectionOfFeature,
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
}
}
const newState = {
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
return newState
}
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null },
globalSettings: deepClone(defaultGlobalSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
_modelOptions: [], // computed later
}
return d
}
@@ -93,8 +143,8 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // await this if you need a valid state initially
@@ -118,39 +168,47 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._readState().then(readS => {
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
readS = {
...readS,
settingsOfProvider: {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
...{ mistral: defaultSettingsOfProvider.mistral },
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
...{ mistral: defaultSettingsOfProvider.mistral },
...readS.settingsOfProvider,
...readS.settingsOfProvider,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
},
modelSelectionOfFeature: {
// A HACK BECAUSE WE ADDED FastApply
...{ 'FastApply': null },
...readS.modelSelectionOfFeature,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
}
this.state = readS
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'FastApply': null },
...readS.modelSelectionOfFeature,
}
readS = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
}
this.state = _updatedValidatedState(readS)
resolver()
this._onDidChangeState.fire('all')
this._onDidChangeState.fire()
})
}
private async _readState(): Promise<VoidSettingsState> {
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
@@ -172,7 +230,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newSettingsOfProvider = {
const newSettingsOfProvider: SettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
...this.state.settingsOfProvider[providerName],
@@ -182,38 +240,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newGlobalSettings = this.state.globalSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
_modelOptions: newModelsList,
}
// this must go above this.setanythingelse()
this.state = newState
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
if (modelsListChanged) {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
}
}
this.state = _updatedValidatedState(newState)
await this._storeState()
this._onDidChangeState.fire('settingsOfProvider')
this._onDidChangeState.fire()
}
@@ -227,7 +264,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire(['globalSettings', settingName])
this._onDidChangeState.fire()
}
@@ -247,7 +284,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return
await this._storeState()
this._onDidChangeState.fire('modelSelectionOfFeature')
this._onDidChangeState.fire()
}
@@ -256,23 +293,23 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const { models } = this.state.settingsOfProvider[providerName]
const old_names = models.map(m => m.modelName)
const oldModelNames = models.map(m => m.modelName)
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
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
]
this.setSettingOfProvider(providerName, 'models', newModels)
this.setSettingOfProvider(providerName, 'models', newModelInfo)
// if the models changed, log it
const new_names = newModels.map(m => m.modelName)
if (!(old_names.length === new_names.length
&& old_names.every((_, i) => old_names[i] === new_names[i])
)) {
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
const new_names = newModelInfo.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 })
}
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
@@ -4,28 +4,28 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
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
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 modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
export const modelInfoOfDefaultModelNames = (defaultModelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
const { isAutodetected, existingModels } = options ?? {}
if (!existingModels) { // default settings
return modelNames.map((modelName, i) => ({
return defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
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)
@@ -35,7 +35,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto
existingModelsMap[existingModel.modelName] = existingModel
}
return modelNames.map((modelName, i) => ({
return defaultModelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
@@ -47,7 +47,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto
}
// https://docs.anthropic.com/en/docs/about-claude/models
export const defaultAnthropicModels = modelInfoOfDefaultNames([
export const defaultAnthropicModels = modelInfoOfDefaultModelNames([
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
@@ -57,9 +57,10 @@ export const defaultAnthropicModels = modelInfoOfDefaultNames([
// https://platform.openai.com/docs/models/gp
export const defaultOpenAIModels = modelInfoOfDefaultNames([
'o1-preview',
export const defaultOpenAIModels = modelInfoOfDefaultModelNames([
'o1',
'o1-mini',
'o3-mini',
'gpt-4o',
'gpt-4o-mini',
// 'gpt-4o-2024-05-13',
@@ -78,22 +79,23 @@ export const defaultOpenAIModels = modelInfoOfDefaultNames([
])
// https://platform.openai.com/docs/models/gp
export const defaultDeepseekModels = modelInfoOfDefaultNames([
export const defaultDeepseekModels = modelInfoOfDefaultModelNames([
'deepseek-chat',
'deepseek-reasoner',
])
// https://console.groq.com/docs/models
export const defaultGroqModels = modelInfoOfDefaultNames([
"distil-whisper-large-v3-en",
export const defaultGroqModels = modelInfoOfDefaultModelNames([
"llama3-70b-8192",
"llama-3.3-70b-versatile",
"llama-3.1-8b-instant",
"gemma2-9b-it"
"gemma2-9b-it",
"mixtral-8x7b-32768"
])
export const defaultGeminiModels = modelInfoOfDefaultNames([
export const defaultGeminiModels = modelInfoOfDefaultModelNames([
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
@@ -102,7 +104,7 @@ export const defaultGeminiModels = modelInfoOfDefaultNames([
'learnlm-1.5-pro-experimental'
])
export const defaultMistralModels = modelInfoOfDefaultNames([
export const defaultMistralModels = modelInfoOfDefaultModelNames([
"codestral-latest",
"open-codestral-mamba",
"open-mistral-nemo",
@@ -186,20 +188,22 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
}
type CommonProviderSettings = {
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
[providerName in ProviderName]: SettingsAtProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
export type SettingName = keyof SettingsAtProvider<ProviderName>
@@ -223,7 +227,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
}
else if (providerName === 'deepseek') {
return {
title: 'DeepSeek',
title: 'DeepSeek.com API',
}
}
else if (providerName === 'openRouter') {
@@ -244,17 +248,17 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
}
else if (providerName === 'gemini') {
return {
title: 'Gemini',
title: 'Gemini API',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq',
title: 'Groq.com API',
}
}
else if (providerName === 'mistral') {
return {
title: 'Mistral',
title: 'Mistral API',
}
}
@@ -308,7 +312,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
undefined,
}
}
else if (settingName === '_enabled') {
else if (settingName === '_didFillInProviderSettings') {
return {
title: '(never)',
placeholder: '(never)',
@@ -372,56 +376,56 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
openAI: {
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
deepseek: {
...defaultCustomSettings,
...defaultProviderSettings.deepseek,
...voidInitModelOptions.deepseek,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
mistral: {
...defaultCustomSettings,
...defaultProviderSettings.mistral,
...voidInitModelOptions.mistral,
_enabled: undefined,
}
_didFillInProviderSettings: undefined,
},
groq: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_didFillInProviderSettings: undefined,
},
openRouter: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_didFillInProviderSettings: undefined,
},
openAICompatible: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_didFillInProviderSettings: undefined,
},
ollama: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_didFillInProviderSettings: undefined,
},
}
@@ -440,20 +444,16 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
if (featureName === 'Autocomplete')
return 'Autocomplete'
else if (featureName === 'Ctrl+K')
return 'Quick Edit'
return 'Quick-Edit'
else if (featureName === 'Ctrl+L')
return 'Sidebar Chat'
return 'Chat'
else if (featureName === 'FastApply')
return 'Fast Apply'
return 'Apply'
else
throw new Error(`Feature Name ${featureName} not allowed`)
}
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
@@ -463,6 +463,45 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
// use this in isFeatuerNameDissbled
export const isProviderNameDisabled = (providerName: ProviderName, settingsState: VoidSettingsState) => {
const settingsAtProvider = settingsState.settingsOfProvider[providerName]
const isAutodetected = (refreshableProviderNames as string[]).includes(providerName)
const isDisabled = settingsAtProvider.models.length === 0
if (isDisabled) {
return isAutodetected ? 'providerNotAutoDetected' : (!settingsAtProvider._didFillInProviderSettings ? 'notFilledIn' : 'addModel')
}
return false
}
export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: VoidSettingsState) => {
// if has a selected provider, check if it's enabled
const selectedProvider = settingsState.modelSelectionOfFeature[featureName]
if (selectedProvider) {
const { providerName } = selectedProvider
return isProviderNameDisabled(providerName, settingsState)
}
// if there are any models they can turn on, tell them that
const canTurnOnAModel = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName].models.filter(m => m.isHidden).length !== 0)
if (canTurnOnAModel) return 'needToEnableModel'
// if there are any providers filled in, then they just need to add a model
const anyFilledIn = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName]._didFillInProviderSettings)
if (anyFilledIn) return 'addModel'
return 'addProvider'
}
export type GlobalSettings = {
@@ -478,3 +517,88 @@ export type GlobalSettingName = keyof GlobalSettings
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
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>';
}
@@ -3,10 +3,10 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
@@ -4,10 +4,31 @@
*--------------------------------------------------------------------------------------*/
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
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
@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------*/
import { Content, GoogleGenerativeAI } from '@google/generative-ai';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
// Gemini
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = ''
@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------*/
import Groq from 'groq-sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
// Groq
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = settingsOfProvider.groq
@@ -22,8 +22,6 @@ export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onT
messages: messages,
model: modelName,
stream: true,
// temperature: 0.7,
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())
@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------*/
import { Mistral } from '@mistralai/mistralai';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
// Mistral
export const sendMistralMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = settingsOfProvider.mistral;
@@ -21,8 +21,6 @@ export const sendMistralMsg: _InternalSendLLMMessageFnType = async ({ messages,
messages: messages,
model: modelName,
stream: true,
// temperature: 0.7,
// maxTokens: 2048,
})
.then(async response => {
// Mistral has a really nonstandard API - no interrupt and weird stream types
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalOllamaFIMMessageFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
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 }) => {
@@ -38,7 +38,7 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async (
}
export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
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
@@ -54,10 +54,11 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex
suffix: messages.suffix,
options: {
stop: messages.stopTokens,
num_predict: 300, // max tokens
// repeat_penalty: 1,
},
raw: true,
stream: true,
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
})
.then(async stream => {
_setAborter(() => stream.abort())
@@ -77,7 +78,7 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex
// Ollama
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
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
@@ -4,12 +4,75 @@
*--------------------------------------------------------------------------------------*/
import OpenAI from 'openai';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
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 })
@@ -20,8 +83,7 @@ export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ on
}
try {
const thisConfig = settingsOfProvider.openAICompatible
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
const openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider })
openai.models.list()
.then(async (response) => {
@@ -44,47 +106,28 @@ export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ on
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 sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (providerName === 'openAI') {
const thisConfig = settingsOfProvider.openAI
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider.openRouter
openai = 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.
},
});
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'deepseek') {
const thisConfig = settingsOfProvider.deepseek
openai = new OpenAI({
baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
});
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else {
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
throw new Error(`providerName was invalid: ${providerName}`)
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
@@ -93,7 +136,11 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText,
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
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 });
}
@@ -3,18 +3,19 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js';
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMChatMessage, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendOllamaFIM, sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGroqMsg } from './groq.js';
import { sendMistralMsg } from './mistral.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 cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => {
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() }))
@@ -26,7 +27,7 @@ const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => {
// remove all system messages
const noSystemMessages = messages
.filter(msg => msg.role !== 'system') as _InternalLLMMessage[]
.filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[]
// add system mesasges to first message (should be a user message)
if (systemMessage && (noSystemMessages.length !== 0)) {
@@ -49,7 +50,7 @@ const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => {
export const sendLLMMessage = ({
type,
messagesType,
aiInstructions,
messages: messages_,
onText: onText_,
@@ -64,23 +65,29 @@ export const sendLLMMessage = ({
metricsService: IMetricsService
) => {
// messages.unshift({ role: 'system', content: aiInstructions })
const messagesArr = type === 'sendLLMMessage' ? cleanMessages(messages_) : []
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,
...type === 'sendLLMMessage' ? {
...messagesType === 'chatMessages' ? {
numMessages: messagesArr?.length,
messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })),
origNumMessages: messages_?.length,
origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : type === 'ollamaFIM' ? {
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
...extras,
@@ -108,6 +115,11 @@ export const sendLLMMessage = ({
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
// 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.`
captureLLMEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
@@ -125,28 +137,27 @@ export const sendLLMMessage = ({
try {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'deepseek':
case 'openAICompatible':
sendOpenAIMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
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 (type === 'ollamaFIM')
sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
else
sendOllamaMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
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':
sendGroqMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'mistral':
sendMistralMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
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 })
@@ -6,8 +6,8 @@
// registered in app.ts
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
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 { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
@@ -66,7 +66,7 @@ export class LLMMessageChannel implements IServerChannel {
}
}
// browser uses this to call
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'sendLLMMessage') {
@@ -3,14 +3,13 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { IProductService } from '../../product/common/productService.js';
import { StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { StorageTarget, StorageScope } from '../../../../platform/storage/common/storage.js';
import { IApplicationStorageMainService } from '../../../../platform/storage/electron-main/storageMainService.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
@@ -0,0 +1,13 @@
/*
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
}
*/
@@ -3,10 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { IProductService } from '../../product/common/productService.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
@@ -17,7 +17,6 @@ import './browser/workbench.contribution.js';
//#region --- Void
// Void added this:
import './contrib/void/browser/void.contribution.js';
import '../platform/void/browser/void.contribution.js';
//#endregion