Merge remote-tracking branch 'origin/main' into pr/Dylan-86/403

This commit is contained in:
Andrew Pareles 2025-04-16 04:29:58 -07:00
commit bd53772523
30 changed files with 1108 additions and 892 deletions

View file

@ -86,7 +86,7 @@ function prepareDebPackage(arch) {
const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch);
gulp.src('resources/linux/debian/control.template', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
.pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`))
.pipe(replace('@@ARCHITECTURE@@', debArch))
.pipe(replace('@@DEPENDS@@', dependencies.join(', ')))
.pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', ')))
@ -201,7 +201,7 @@ function prepareRpmPackage(arch) {
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@NAME_LONG@@', product.nameLong))
.pipe(replace('@@ICON@@', product.linuxIconName))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
.pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`))
.pipe(replace('@@ARCHITECTURE@@', rpmArch))
.pipe(replace('@@LICENSE@@', product.licenseName))
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
@ -277,7 +277,7 @@ function prepareSnapPackage(arch) {
const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
.pipe(replace('@@VERSION@@', `${product.voidVersion}.${packageJson.release}`))
// Possible run-on values https://snapcraft.io/docs/architectures
.pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch))
.pipe(rename('snap/snapcraft.yaml'));

View file

@ -1,109 +0,0 @@
#!/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..."
cp -v ./void "$APP_DIR/usr/bin/"
# 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!"

View file

@ -1,7 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.2.1",
"voidVersion": "1.2.3",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",

View file

@ -1,3 +1,16 @@
# README
This is a community-made AppImage creation script.
There are some reported bugs with it.
To generate an AppImage yourself, feel free to look at
stable-linux.yml in the separate `void-builder/` repo,
which runs a GitHub Action that builds the AppImage you see on our website.
# Void AppImage Creation Script
This script automates the process of creating an AppImage for the Void Editor using Docker. It works on macOS and Linux platforms.

View file

@ -22,10 +22,12 @@ import { ColorScheme } from '../../web.api.js';
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
import { splitRecentLabel } from '../../../../base/common/labels.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
/* eslint-disable */ // Void
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
/* eslint-enable */
// interface WatermarkEntry {
@ -98,6 +100,7 @@ export class EditorGroupWatermark extends Disposable {
@ICommandService private readonly commandService: ICommandService,
@IHostService private readonly hostService: IHostService,
@ILabelService private readonly labelService: ILabelService,
@IViewsService private readonly viewsService: IViewsService,
) {
super();
@ -182,14 +185,21 @@ export class EditorGroupWatermark extends Disposable {
// Void - if the workbench is empty, show open
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Create a flex container for buttons with vertical direction
const buttonContainer = $('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.flexDirection = 'column'; // Change to column for vertical stacking
buttonContainer.style.alignItems = 'center'; // Center the buttons horizontally
buttonContainer.style.gap = '8px'; // Reduce gap between buttons from 16px to 8px
buttonContainer.style.marginBottom = '16px';
voidIconBox.appendChild(buttonContainer);
// Open a folder
const openFolderButton = h('button')
openFolderButton.root.classList.add('void-watermark-button')
openFolderButton.root.classList.add('void-openfolder-button')
openFolderButton.root.style.display = 'block'
openFolderButton.root.style.marginLeft = 'auto'
openFolderButton.root.style.marginRight = 'auto'
openFolderButton.root.style.marginBottom = '16px'
openFolderButton.root.textContent = 'Open a folder'
openFolderButton.root.style.width = '124px' // Set width to 124px as requested
openFolderButton.root.textContent = 'Open Folder'
openFolderButton.root.onclick = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
@ -198,7 +208,19 @@ export class EditorGroupWatermark extends Disposable {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
voidIconBox.appendChild(openFolderButton.root);
buttonContainer.appendChild(openFolderButton.root);
// Open SSH button
const openSSHButton = h('button')
openSSHButton.root.classList.add('void-openssh-button')
openSSHButton.root.style.display = 'block'
openSSHButton.root.style.backgroundColor = '#5a5a5a' // Made darker than the default gray
openSSHButton.root.style.width = '124px' // Set width to 124px as requested
openSSHButton.root.textContent = 'Open SSH'
openSSHButton.root.onclick = () => {
this.viewsService.openViewContainer(REMOTE_EXPLORER_VIEWLET_ID);
}
buttonContainer.appendChild(openSSHButton.root);
// Recents
@ -244,6 +266,9 @@ export class EditorGroupWatermark extends Disposable {
const dirSpan = $('span');
dirSpan.style.paddingLeft = '4px';
dirSpan.style.whiteSpace = 'nowrap';
dirSpan.style.overflow = 'hidden';
dirSpan.style.maxWidth = '300px';
dirSpan.innerText = parentPath;
dirSpan.title = fullPath;

View file

@ -50,6 +50,7 @@
}
/* light */
.void-void-icon,
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
width: 100%;
max-height: 100%;
@ -60,14 +61,17 @@
background-repeat: no-repeat;
}
.void-void-icon,
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}
.void-void-icon,
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}
.void-void-icon,
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./void_cube_noshadow.png'); /* // Void */
}

View file

@ -792,7 +792,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
const aiInstructions = this._settingsService.state.globalSettings.aiInstructions
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
@ -804,8 +803,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
prefix: llmPrefix,
suffix: llmSuffix,
stopTokens: stopTokens,
},
aiInstructions
}
}),
modelSelection,
modelSelectionOptions,

View file

@ -25,8 +25,6 @@ import { Position } from '../../../../editor/common/core/position.js';
import { IMetricsService } from '../common/metricsService.js';
import { shorten } from '../../../../base/common/labels.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { findLast, findLastIdx } from '../../../../base/common/arraysFind.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
@ -246,8 +244,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@IMetricsService private readonly _metricsService: IMetricsService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@INotificationService private readonly _notificationService: INotificationService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
@ -266,9 +262,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// always be in a thread
this.openNewThread()
// when the user changes files, automatically add the new file as a stagingSelection
this._register(this._editorService.onDidActiveEditorChange(() => this._addCurrentFileAsStagingSelectionDuringFileChange()));
// keep track of user-modified files
// const disposablesOfModelId: { [modelId: string]: IDisposable[] } = {}
@ -288,40 +281,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// add the current file to the thread being edited
private _addCurrentFileAsStagingSelectionDuringFileChange() {
const newModel = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null
if (!newModel) { return }
const isCurrentlyFocusing = this.isCurrentlyFocusingMessage()
if (isCurrentlyFocusing) return
// only add if the user hasn't sent a message yet
if (this.getCurrentThread().messages.length !== 0) return
const newStagingSelection: StagingSelectionItem = {
type: 'File',
uri: newModel.uri,
language: newModel.getLanguageId(),
state: { wasAddedAsCurrentFile: true }
}
const oldStagingSelections = this.getCurrentThreadState().stagingSelections || [];
// remove all old selectons that are marked as `wasAddedAsCurrentFile`
const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => s.state && !s.state.wasAddedAsCurrentFile)
const fileIsAlreadyHere = oldStagingSelections.some(s => s.type === 'File' && s.uri.fsPath === newStagingSelection.uri.fsPath)
if (!fileIsAlreadyHere) {
newStagingSelections.push(newStagingSelection)
}
this.setCurrentThreadState({ stagingSelections: newStagingSelections });
}
// !!! this is important for properly restoring URIs from storage
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
@ -390,36 +349,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
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 currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
// 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)
// re-add the message and stream it
this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
}
private _currentModelSelectionProps = () => {
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
const featureName: FeatureName = 'Chat'
@ -924,7 +853,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const [_, toIdx] = c
if (toIdx === fromIdx) return
// console.log(`going from ${fromIdx} to ${toIdx}`)
console.log(`going from ${fromIdx} to ${toIdx}`)
// update the user's checkpoint
this._addUserModificationsToCurrCheckpoint({ threadId })
@ -936,15 +865,14 @@ A,B,C are all files.
x means a checkpoint where the file changed.
A B C D E F G H I
x x x x x x x x x
| | | | | | | | |
x | | | | | | | x
---x-|-|-|-x-|-x-|----- <-- to
x | | | | | x
| | x x |
| | | |
-------x-|---x-x------- <-- from
x
x x x x x x <-- you can't always go up to find the "before" version; sometimes you need to go down
| | | | | | x
--x-|-|-|-x---x-|----- <-- to
| | | | x x
| | x x |
| | | |
----x-|---x-x------- <-- from
x
We need to revert anything that happened between to+1 and from.
**We do this by finding the last x from 0...`to` for each file and applying those contents.**
@ -952,9 +880,19 @@ We only need to do it for files that were edited since `to`, ie files between to
*/
if (toIdx < fromIdx) {
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: toIdx + 1, hiIdx: fromIdx })
const idxes = function* () {
for (let k = toIdx; k >= 0; k -= 1) { // first go up
yield k
}
for (let k = toIdx + 1; k < thread.messages.length; k += 1) { // then go down
yield k
}
}
for (const fsPath in lastIdxOfURI) {
// apply lowest down content for each uri (or original if not found)
for (let k = toIdx; k >= 0; k -= 1) {
// find the first instance of this file starting at toIdx (go up to latest file; if there is none, go down)
for (const k of idxes()) {
const message = thread.messages[k]
if (message.role !== 'checkpoint') continue
const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
@ -970,16 +908,16 @@ We only need to do it for files that were edited since `to`, ie files between to
/*
if redoing
A B C D E F G H I
x x x x x x x x x
| | | | | | | | |
x | | | | | | | x
---x-|-|-|-x-|-x-|----- <-- from
x | | | | | x
| | x x |
| | | |
-------x-|---x-x------- <-- to
x
A B C D E F G H I J
x x x x x x x
| | | | | | x x x
--x-|-|-|-x---x-|-|--- <-- from
| | | | x x
| | x x |
| | | |
----x-|---x-x-----|--- <-- to
x x
We need to apply latest change for anything that happened between from+1 and to.
We only need to do it for files that were edited since `from`, ie files between from+1...to.
@ -995,7 +933,6 @@ We only need to do it for files that were edited since `from`, ie files between
if (!res) continue
const { voidFileSnapshot } = res
if (!voidFileSnapshot) continue
this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot)
break
}
@ -1044,11 +981,15 @@ We only need to do it for files that were edited since `from`, ie files between
})
}
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined }, 'merge')
}
private async _addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') {
// if about to call the other LLM, just wait for it by stopping right now
@ -1057,14 +998,11 @@ We only need to do it for files that were edited since `from`, ie files between
// stop it (this simply resolves the promise to free up space)
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
// add dummy before this message to keep checkpoint before user message idea consistent
if (thread.messages.length === 0) {
this._addUserCheckpoint({ threadId })
}
const { chatMode } = this._settingsService.state.globalSettings
// add user's message to chat history
@ -1084,11 +1022,61 @@ We only need to do it for files that were edited since `from`, ie files between
)
}
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined }, 'merge')
async addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId }: { userMessage: string, _chatSelections?: StagingSelectionItem[], threadId: string }) {
const thread = this.state.allThreads[threadId];
if (!thread) return
// if there's a current checkpoint, delete all messages after it
if (thread.state.currCheckpointIdx !== null) {
const checkpointIdx = thread.state.currCheckpointIdx;
const newMessages = thread.messages.slice(0, checkpointIdx + 1);
// Update the thread with truncated messages
const newThreads = {
...this.state.allThreads,
[threadId]: {
...thread,
lastModified: new Date().toISOString(),
messages: newMessages,
}
};
this._storeAllThreads(newThreads);
this._setState({ allThreads: newThreads }, true);
}
// Now call the original method to add the user message and stream the response
await this._addUserMessageAndStreamResponse({ userMessage, _chatSelections, threadId });
}
editUserMessageAndStreamResponse: IChatThreadService['editUserMessageAndStreamResponse'] = async ({ userMessage, messageIdx, threadId }) => {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
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 currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
// 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)
// re-add the message and stream it
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
}
// ---------- the rest ----------
@ -1386,21 +1374,6 @@ We only need to do it for files that were edited since `from`, ie files between
// switch to the thread
this.switchToThread(threadId)
// add the current file as a staging selection
const model = this._codeEditorService.getActiveCodeEditor()?.getModel()
if (model) {
this._setThreadState(this.state.currentThreadId, {
stagingSelections: [{
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: {
wasAddedAsCurrentFile: true
}
}]
})
}
return;
}
}
// otherwise, start a new thread

View file

@ -7,12 +7,14 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
import { toolCallXMLStr, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.js';
@ -143,13 +145,19 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA
// add anthropic reasoning
if (currMsg.role === 'assistant') {
if (currMsg.anthropicReasoning && supportsAnthropicReasoning) {
const content = currMsg.content
newMessages[i] = {
role: 'assistant',
content: content ? [...currMsg.anthropicReasoning, { type: 'text' as const, text: content }] : currMsg.anthropicReasoning
}
}
else {
newMessages[i] = {
role: 'assistant',
content: currMsg.content,
// strip away anthropicReasoning
}
}
continue
}
@ -199,7 +207,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content: LLMChatMessage['content'] = c.content
if (next?.role === 'tool') {
content = `${content}\n\n${toolCallXMLStr(next.name, next.rawParams)}`
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
}
// anthropic reasoning
@ -406,9 +414,9 @@ const prepareMessages = ({
export interface IConvertToLLMMessageService {
readonly _serviceBrand: undefined;
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined };
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
prepareFIMMessage(opts: { messages: LLMFIMMessage, aiInstructions: string, }): { prefix: string, suffix: string, stopTokens: string[] }
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
}
@ -425,10 +433,35 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@IVoidModelService private readonly voidModelService: IVoidModelService,
) {
super()
}
// Read .voidinstructions files from workspace folders
private _getVoidInstructionsFileContents(): string {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
let voidInstructions = '';
for (const folder of workspaceFolders) {
const uri = URI.joinPath(folder.uri, '.voidinstructions')
const { model } = this.voidModelService.getModel(uri)
if (!model) continue
voidInstructions += model.getValue() + '\n\n';
}
return voidInstructions.trim();
}
// Get combined AI instructions from settings and .voidinstructions files
private _getCombinedAIInstructions(): string {
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
const voidInstructionsFileContent = this._getVoidInstructionsFileContents();
const ans: string[] = []
if (globalAIInstructions) ans.push(globalAIInstructions)
if (voidInstructionsFileContent) ans.push(voidInstructionsFileContent)
return ans.join('\n\n')
}
// system message
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
@ -442,7 +475,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
`...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const includeXMLToolDefinitions = specialToolFormat === undefined
const includeXMLToolDefinitions = !specialToolFormat
const runningTerminalIds = this.terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
@ -496,7 +529,9 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
} = getModelCapabilities(providerName, modelName)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
@ -512,7 +547,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
@ -525,7 +559,9 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
@ -542,19 +578,20 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
// --- FIM ---
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages, aiInstructions }) => {
prepareFIMMessage: IConvertToLLMMessageService['prepareFIMMessage'] = ({ messages }) => {
// Get combined AI instructions with the provided aiInstructions as the base
const combinedInstructions = this._getCombinedAIInstructions();
let prefix = `\
${!aiInstructions ? '' : `\
${!combinedInstructions ? '' : `\
// Instructions:
// Do not output an explanation. Try to avoid outputting comments. Only output the middle code.
${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
${combinedInstructions.split('\n').map(line => `//${line}`).join('\n')}`}
${messages.prefix}`
@ -567,7 +604,6 @@ ${messages.prefix}`
}
// pick one and delete the other:
registerSingleton(IConvertToLLMMessageService, ConvertToLLMMessageService, InstantiationType.Eager);

View file

@ -0,0 +1,37 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IVoidModelService } from '../common/voidModelService.js';
class ConvertContribWorkbenchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.convertcontrib'
_serviceBrand: undefined;
constructor(
@IVoidModelService private readonly voidModelService: IVoidModelService,
@IWorkspaceContextService private readonly workspaceContext: IWorkspaceContextService,
) {
super()
const initializeURI = (uri: URI) => {
this.workspaceContext.getWorkspace()
const voidInstrsURI = URI.joinPath(uri, '.voidinstructions')
this.voidModelService.initializeModel(voidInstrsURI)
}
// call
this._register(this.workspaceContext.onDidChangeWorkspaceFolders((e) => {
[...e.changed, ...e.added].forEach(w => { initializeURI(w.uri) })
}))
this.workspaceContext.getWorkspace().folders.forEach(w => { initializeURI(w.uri) })
}
}
registerWorkbenchContribution2(ConvertContribWorkbenchContribution.ID, ConvertContribWorkbenchContribution, WorkbenchPhase.BlockRestore);

View file

@ -258,6 +258,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._realignAllDiffAreasLines(uri, change.text, change.range)
}
this._refreshStylesAndDiffsInURI(uri)
// if diffarea has no diffs after a user edit, delete it
const diffAreasToDelete: DiffZone[] = []
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] ?? []) {
const diffArea = this.diffAreaOfId[diffareaid] ?? null
const shouldDelete = diffArea?.type === 'DiffZone' && Object.keys(diffArea._diffOfId).length === 0
if (shouldDelete) {
diffAreasToDelete.push(diffArea)
}
}
if (diffAreasToDelete.length !== 0) {
const { onFinishEdit } = this._addToHistory(uri)
diffAreasToDelete.forEach(da => this._deleteDiffZone(da))
onFinishEdit()
}
}
@ -1562,20 +1578,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const errContentOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === `Not unique` ?
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
: str === 'Has overlap' ?
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error.'
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
const errMsg = `${descStr}\n${soFarStr}`
return errMsg
@ -1610,7 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
diffZone._streamState.line = 1
const N_RETRIES = 2
const N_RETRIES = 4
// allowed to throw errors - this is called inside a promise that handles everything
const runSearchReplace = async () => {
@ -1684,17 +1702,25 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
// if error
if (typeof originalBounds === 'string') {
// Check for overlap with existing modified ranges
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
return !hasNoOverlap
});
if (typeof originalBounds === 'string' || hasOverlap) {
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
console.log('--------------Error finding text in code:')
console.log('originalFileCode', { originalFileCode })
console.log('fullText', { fullText })
console.log('error:', originalBounds)
console.log('error:', errorMessage)
console.log('block.orig:', block.orig)
console.log('---------')
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
const content = errContentOfInvalidStr(errorMessage, block.orig)
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
}
@ -23,10 +23,30 @@
background-color: var(--vscode-void-redBG);
}
.void-watermark-button {
margin: 8px 0;
/* Renamed from void-watermark-button to void-openfolder-button */
.void-openfolder-button {
padding: 8px 20px;
background-color: #3b82f6;
background-color: #306dce;
color: white;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-openfolder-button:hover {
background-color: #2563eb;
}
.void-openfolder-button:active {
background-color: #2563eb;
}
/* Added for Open SSH button with slightly darker color */
.void-openssh-button {
padding: 8px 20px;
background-color: #656565; /* Slightly darker than the #5a5a5a in the TS file */
color: white;
border: none;
border-radius: 4px;
@ -35,43 +55,41 @@
cursor: pointer;
transition: background-color 0.2s ease;
}
.void-watermark-button:hover {
background-color: #2563eb;
.void-openssh-button:hover {
background-color: #474747; /* Darker on hover */
}
.void-watermark-button:active {
background-color: #2563eb;
.void-openssh-button:active {
background-color: #474747;
}
.void-settings-watermark-button {
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
margin: 8px 0;
padding: 8px 20px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
outline: none !important;
border: none;
border-radius: 4px;
outline: none !important;
box-shadow: none !important;
cursor: pointer;
transition: all 0.2s ease;
cursor: pointer;
transition: all 0.2s ease;
}
.void-settings-watermark-button:hover {
filter: brightness(1.1);
}
.void-settings-watermark-button:active {
filter: brightness(1.1);
}
.void-link {
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
}
.void-link:hover {
opacity: 80%;
}
@ -86,7 +104,8 @@
.void-scope,
.void-scope * {
scrollbar-width: thin !important;
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
/* For Firefox */
}
.void-scope::-webkit-scrollbar,
@ -133,13 +152,16 @@
background-color: var(--vscode-editor-background);
--scrollbar-vertical-width: 14px;
--scrollbar-horizontal-height: 6px;
overflow: auto; /* Ensure scrollbars are shown when needed */
overflow: auto;
/* Ensure scrollbars are shown when needed */
}
.void-scrollable-element,
.void-scrollable-element * {
scrollbar-width: thin !important; /* For Firefox */
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */
scrollbar-width: thin !important;
/* For Firefox */
scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important;
/* For Firefox */
}
.void-scrollable-element::-webkit-scrollbar,

View file

@ -5,6 +5,7 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
interface Props {
children: ReactNode;
@ -51,11 +52,12 @@ class ErrorBoundary extends Component<Props, State> {
// Use ErrorDisplay component as the default error UI
return (
<ErrorDisplay
message={this.state.error + ''}
fullError={this.state.error}
onDismiss={this.props.onDismiss || null}
/>
<WarningBox text={this.state.error + ''} />
// <ErrorDisplay
// message={this.state.error + ''}
// fullError={this.state.error}
// onDismiss={this.props.onDismiss || null}
// />
);
}

View file

@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
import ErrorBoundary from './ErrorBoundary.js';
@ -1364,12 +1365,12 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) =>
style={{ background: 'none' }}
>
<ChevronRight
className={`mr-1 h-4 w-4 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
className={`mr-1 h-3 w-3 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
/>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100 py-1' : 'max-h-0 opacity-0'} text-xs pl-5`}
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100' : 'max-h-0 opacity-0'} text-xs pl-4`}
>
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{lintErrors.map((error, i) => (
@ -1949,10 +1950,14 @@ const Checkpoint = ({ message, threadId, messageIdx, isCheckpointGhost, threadIs
style={{ position: 'relative', display: 'inline-block' }} // allow absolute icon
onClick={() => {
if (threadIsRunning) return
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
chatThreadService.jumpToCheckpointBeforeMessageIdx({
threadId,
messageIdx,
jumpToUserModified: messageIdx === (chatThreadService.state.allThreads[threadId]?.messages.length ?? 0) - 1
})
}}
>
Checkpoint
Checkpoint
</div>
</div>
}
@ -2479,7 +2484,7 @@ export const SidebarChat = () => {
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity)
const previousMessagesHTML = useMemo(() => {
const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
// tool request shows up as Editing... if in progress
return previousMessages.map((message, i) => {
return <ChatBubble
@ -2616,14 +2621,20 @@ export const SidebarChat = () => {
<div ref={sidebarRef} className='w-full h-full flex flex-col overflow-hidden'>
{/* History selector */}
<div className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}>
<SidebarThreadSelector />
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div>
<div className='flex-1 flex flex-col overflow-hidden'>
<div className={`flex-1 overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
{messagesHTML}
<ErrorBoundary>
{messagesHTML}
</ErrorBoundary>
</div>
{inputForm}
<ErrorBoundary>
{inputForm}
</ErrorBoundary>
</div>
</div>
)

View file

@ -11,7 +11,7 @@
--void-bg-1: var(--vscode-input-background);
--void-bg-1-alt: var(--vscode-badge-background);
--void-bg-2: var(--vscode-sideBar-background);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
--void-bg-3: var(--vscode-editor-background);
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);

View file

@ -664,57 +664,60 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
{isOpen && (
<div
ref={refs.setFloating}
className="z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg"
className="z-10 bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: matchInputWidth
width: (matchInputWidth
? (refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0)
: Math.max(
(refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0),
(measureRef.current instanceof HTMLElement ? measureRef.current.offsetWidth : 0)
),
))
}}
>
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
onWheel={(e) => e.stopPropagation()}
><div className='overflow-auto max-h-80'>
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
const optionDetail = getOptionDropdownDetail?.(option) || '';
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
bg-void-bg-1
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
${thisOptionIsSelected ? 'bg-void-bg-2' : 'bg-void-bg-2-alt hover:bg-void-bg-2'}
`}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
<span className="flex justify-between w-full">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
);
})}
);
})}
</div>
</div>
)}
</div>

View file

@ -111,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
}
}

View file

@ -3,15 +3,16 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Check, ExternalLink, X } from 'lucide-react';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
const OVERRIDE_VALUE = true
const OVERRIDE_VALUE = false
export const VoidOnboarding = () => {
@ -24,7 +25,6 @@ export const VoidOnboarding = () => {
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
hidden
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
@ -35,14 +35,39 @@ export const VoidOnboarding = () => {
)
}
const VoidIcon = () => {
const accessor = useAccessor()
const themeService = accessor.get('IThemeService')
const divRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
// void icon style
const updateTheme = () => {
const theme = themeService.getColorTheme().type
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
if (divRef.current) {
divRef.current.style.maxWidth = '220px'
divRef.current.style.opacity = '50%'
divRef.current.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
}
}
updateTheme()
const d = themeService.onDidColorThemeChange(updateTheme)
return () => d.dispose()
}, [])
return <div ref={divRef} className='@@void-void-icon' />
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { children: React.ReactNode, delayMs?: number, durationMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
const effectiveDurationMs = durationMs ?? FADE_DURATION_MS
useEffect(() => {
const timeout = setTimeout(() => {
@ -54,7 +79,7 @@ const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: Reac
return (
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
<div className={className} style={{ opacity, transition: `opacity ${effectiveDurationMs}ms ease-in-out` }} {...props}>
{children}
</div>
)
@ -102,15 +127,14 @@ const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: Reac
// content
// prev/next
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 bg-[#0e70c0] enabled:hover:bg-[#1177cb] disabled:opacity-50 disabled:cursor-not-allowed rounded text-white duration-300 transition-all"
className="px-6 py-2 bg-zinc-100 enabled:hover:bg-zinc-100 disabled:bg-zinc-100/40 disabled:cursor-not-allowed rounded text-black duration-600 transition-all"
{...props.disabled && {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
'data-tooltip-content': 'Please enter all required fields or choose another provider',
'data-tooltip-place': 'top',
}}
{...props}
@ -120,23 +144,11 @@ const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.Butto
)
}
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2 duration-300 transition-all"
{...props}
>
Skip
</button>
)
}
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-110 duration-300 transition-all"
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-115 duration-600 transition-all"
{...props}
>
Back
@ -154,10 +166,10 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
className?: string,
}) => {
return (
<div className={`min-h-full flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<FadeIn className='w-full pt-16'>{top}</FadeIn>
<FadeIn className='w-full my-auto'>{content}</FadeIn>
<div className='w-full pb-8'>{bottom}</div>
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
{bottom && <div className='w-full pb-8'>{bottom}</div>}
</div>
)
}
@ -170,7 +182,7 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
href={`https://ollama.com/library/${modelName}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
className="flex items-center justify-center text-void-fg-2 hover:text-void-fg-1"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
@ -252,9 +264,9 @@ const YesNoText = ({ val }: { val: boolean | null }) => {
return <div
className={
val === true ? "text text-green-500"
: val === false ? 'text-red-500'
: "text text-yellow-500"
val === true ? "text text-emerald-500"
: val === false ? 'text-rose-600'
: "text text-amber-300"
}
>
{
@ -344,7 +356,6 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
} = capabilities
// TODO update this when tools work
const supportsTools = !!!((capabilities as unknown as any).supportsTools)
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
@ -364,10 +375,10 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsTools} /></td>
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
@ -390,6 +401,57 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
const PrimaryActionButton = ({ children, className, ringSize, ...props }: { children: React.ReactNode, ringSize?: undefined | 'xl' | 'screen' } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
type='button'
className={`
flex items-center justify-center
text-white dark:text-black
bg-black/90 dark:bg-white/90
${ringSize === 'xl' ? `
gap-2 px-16 py-8
hover:ring-8 active:ring-8
transition-all duration-300 ease-in-out
`
: ringSize === 'screen' ? `
gap-2 px-16 py-8
ring-[3000px]
transition-all duration-1000 ease-in-out
`: ringSize === undefined ? `
gap-1 px-4 py-2
hover:ring-2 active:ring-2
transition-all duration-300 ease-in-out
`: ''}
hover:ring-black/90 dark:hover:ring-white/90
active:ring-black/90 dark:active:ring-white/90
rounded-lg
group
${className}
`}
{...props}
>
{children}
<ChevronRight
className={`
transition-all duration-300 ease-in-out
transform
group-hover:translate-x-1
group-active:translate-x-1
`}
/>
</button>
)
}
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
const VoidOnboardingContent = () => {
@ -406,18 +468,42 @@ const VoidOnboardingContent = () => {
// page 1 state
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
// page 2 state
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(null)
// Replace the single selectedProviderName with four separate states
// page 2 state - each tab gets its own state
const [selectedIntelligentProvider, setSelectedIntelligentProvider] = useState<ProviderName>('anthropic');
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
// Helper function to get the current selected provider based on active tab
const getSelectedProvider = (): ProviderName => {
switch (wantToUseOption) {
case 'smart': return selectedIntelligentProvider;
case 'private': return selectedPrivateProvider;
case 'cheap': return selectedAffordableProvider;
case 'all': return selectedAllProvider;
}
}
// Helper function to set the selected provider for the current tab
const setSelectedProvider = (provider: ProviderName) => {
switch (wantToUseOption) {
case 'smart': setSelectedIntelligentProvider(provider); break;
case 'private': setSelectedPrivateProvider(provider); break;
case 'cheap': setSelectedAffordableProvider(provider); break;
case 'all': setSelectedAllProvider(provider); break;
}
}
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
private: ['ollama', 'vLLM', 'openAICompatible'],
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
all: providerNames,
// TODO allow user to redo onboarding
}
const selectedProviderName = getSelectedProvider();
const didFillInProviderSettings = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName]._didFillInProviderSettings
const isApiKeyLongEnoughIfApiKeyExists = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].apiKey ? voidSettingsState.settingsOfProvider[selectedProviderName].apiKey.length > 15 : true
const isAtLeastOneModel = selectedProviderName && voidSettingsState.settingsOfProvider[selectedProviderName].models.length >= 1
@ -425,7 +511,7 @@ const VoidOnboardingContent = () => {
const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel)
const prevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
@ -437,11 +523,24 @@ const VoidOnboardingContent = () => {
</div>
const lastPagePrevAndNextButtons = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
<div className="flex items-center gap-2">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<PrimaryActionButton
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
>Enter the Void</PrimaryActionButton>
</div>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Fully private and hosted on your computer/network.",
cheap: "Free and low-cost options.",
private: "Host on your computer or local network for full data privacy.",
cheap: "Free and affordable options.",
all: "",
}
@ -449,24 +548,25 @@ const VoidOnboardingContent = () => {
const detailedDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Most intelligent and best for agent mode.",
private: "Private-hosted so your data never leaves your computer or network. [Email us](mailto:founders@voideditor.com) for help setting up at your company.",
cheap: "Great deals like Gemini 2.5 Pro or self-host a model with Ollama or vLLM for free.",
cheap: "Use great deals like Gemini 2.5 Pro, or self-host a model with Ollama or vLLM for free.",
all: "",
}
// set the selected provider name appropriately
// Modified: initialize separate provider states on initial render instead of watching wantToUseOption changes
useEffect(() => {
if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) {
setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]);
} else {
setSelectedProviderName(null);
if (selectedIntelligentProvider === undefined) {
setSelectedIntelligentProvider(providerNamesOfWantToUseOption['smart'][0]);
}
}, [wantToUseOption]);
// set wantToUseOption to smart when page changes
useEffect(() => {
setWantToUseOption(wantToUseOption);
}, [pageIndex]);
if (selectedPrivateProvider === undefined) {
setSelectedPrivateProvider(providerNamesOfWantToUseOption['private'][0]);
}
if (selectedAffordableProvider === undefined) {
setSelectedAffordableProvider(providerNamesOfWantToUseOption['cheap'][0]);
}
if (selectedAllProvider === undefined) {
setSelectedAllProvider(providerNamesOfWantToUseOption['all'][0]);
}
}, []);
// reset the page to page 0 if the user redos onboarding
useEffect(() => {
@ -476,168 +576,189 @@ const VoidOnboardingContent = () => {
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
// 0: <OnboardingPageShell
// top={
// <div className='bg-green-600 h-6 w-32' />
// }
// content={
// <div className='bg-red-600 h-[10000px] w-32' />
// }
// bottom={
// <div className='bg-blue-600 h-6 w-32' />
// }
// />,
0: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Welcome to Void</div>
}
content={
<FadeIn
delayMs={500}
className="text-center"
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</FadeIn>
}
bottom={
''
<div className='flex flex-col items-center gap-8'>
<div className="text-5xl font-light text-center">Welcome to Void</div>
{/* Slice of Void image */}
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
<VoidIcon />
</div>
<FadeIn
delayMs={1000}
>
<PrimaryActionButton
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</PrimaryActionButton>
</FadeIn>
</div>
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={
<FadeIn className='flex flex-col items-center'>
<div className="text-5xl font-light text-center">AI Preferences</div>
top={<></>}
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
<div className="mt-[10%] text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
<div className="flex justify-center w-full md:flex-nowrap md:max-w-[80%] max-w-[90%] gap-4">
<div
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🧠</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
<button
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Brain size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
</button>
<div
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🔒</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
<button
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Lock size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Private</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
</button>
<div
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">💵</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Affordability</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
<button
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<DollarSign size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
</button>
</div>
</FadeIn>
</div>}
bottom={
<div className='mx-auto w-full max-w-[800px]'>
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
</div>
}
content={<></>}
/>,
2: <OnboardingPageShell
top={
<div className='flex flex-col items-center'>
<>
{/* Title */}
<div className="text-5xl font-light text-center">Choose a Provider</div>
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
{/* Preference Selector */}
<div className="mt-6 mb-6 mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
<button
onClick={() => {
setWantToUseOption('smart');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'smart'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
<div
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
>
{[
{ id: 'smart', label: 'Intelligent' },
{ id: 'private', label: 'Private' },
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<button
key={option.id}
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Intelligent
</button>
<button
onClick={() => {
setWantToUseOption('private');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'private'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Private
</button>
<button
onClick={() => {
setWantToUseOption('cheap');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'cheap'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Low-Cost
</button>
<button
onClick={() => {
setWantToUseOption('all')
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'all'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
All
</button>
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
))}
</div>
{/* Provider Buttons */}
<div
key={wantToUseOption}
className="mb-2 flex flex-wrap items-center w-full"
>
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
const isSelected = selectedProviderName === providerName
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
return (
<button
key={providerName}
onClick={() => setSelectedProviderName(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
`}
>
{displayInfoOfProviderName(providerName).title}
</button>
)
})}
{/* Private tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
{/* Affordable tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
{/* All tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</div>
{/* Description */}
@ -647,7 +768,7 @@ const VoidOnboardingContent = () => {
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4'>
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
@ -655,12 +776,13 @@ const VoidOnboardingContent = () => {
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8'>
<div className='mb-5 mt-8 mx-auto'>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
<div className='my-4'>
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
</div>
@ -672,17 +794,18 @@ const VoidOnboardingContent = () => {
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</div>
</div>}
</div>
</>
}
bottom={
prevAndNextButtons
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
}
/>,
@ -701,7 +824,8 @@ const VoidOnboardingContent = () => {
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
top={
content={
<div>
<div className="text-5xl font-light text-center">Settings and Themes</div>
@ -713,34 +837,33 @@ const VoidOnboardingContent = () => {
</div>
</div>
}
bottom={prevAndNextButtons}
/>,
4: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Jump in</div>
}
content={
<div
className="text-center"
onClick={() => {
// TODO make a fadeout effect
voidSettingsService.setGlobalSetting('isOnboardingComplete', true)
}}
>
Enter the Void
</div>
}
bottom={
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
}
bottom={lastPagePrevAndNextButtons}
// bottom={prevAndNextButtons}
/>,
// 4: <OnboardingPageShell
// content={
// <>
// <div
// className='flex justify-center'
// >
// <PrimaryActionButton
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
// className='text-4xl'
// >Enter the Void</PrimaryActionButton>
// </div>
// </>
// }
// bottom={
// <PreviousButton
// onClick={() => { setPageIndex(pageIndex - 1) }}
// />
// }
// />,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
</div>

View file

@ -12,6 +12,7 @@ import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
@ -44,37 +45,6 @@ const ModelSelectBox = ({ options, featureName, className }: { options: ModelOpt
matchInputWidth={false}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
// let weChangedText = false
// return <VoidSelectBox
// className='@@[&_select]:!void-text-xs text-void-fg-3'
// options={options}
// onChangeSelection={useCallback((newVal: ModelSelection) => {
// if (weChangedText) return
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
// }, [voidSettingsService, featureName])}
// // we are responsible for setting the initial state here. always sync instance when state changes.
// onCreateInstance={useCallback((instance: SelectBox) => {
// const syncInstance = () => {
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
// weChangedText = true
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
// weChangedText = false
// }
// syncInstance()
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
// return [disposable]
// }, [voidSettingsService, featureName])}
// />
// }
const MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
@ -123,5 +93,7 @@ export const ModelDropdown = ({ featureName, className }: { featureName: Feature
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} className={className} />
return <ErrorBoundary>
<MemoizedModelDropdown featureName={featureName} className={className} />
</ErrorBoundary>
}

View file

@ -121,7 +121,7 @@ export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, cl
return <div
className={`flex items-center gap-1.5 w-fit
${className ? className : `px-2 py-0.5 text-xs text-white bg-[#0e70c0] rounded-sm`}
${className ? className : `px-2 py-0.5 text-xs text-zinc-900 bg-zinc-100 rounded-sm`}
`}
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -146,7 +146,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
return <button
disabled={disabled}
className={`bg-[#0e70c0] px-3 py-1 text-white rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
className={`bg-[#0e70c0] px-3 py-1 text-white dark:text-black rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
{...props}
>{text}</button>
@ -175,7 +175,7 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
const numModels = settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
}
if (!isOpen) {
@ -549,7 +549,12 @@ export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:lis
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
<div
className='pl-6 flex items-center w-fit'
data-tooltip-id='void-tooltip-ollama-settings'
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
</div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
@ -567,182 +572,6 @@ const RedoOnboardingButton = ({ className }: { className?: string }) => {
}
export const FeaturesTab = () => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelInputBox className='mt-4' compact />
<RedoOnboardingButton className='mt-2 mb-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
{ollamaSetupInstructions}
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
<ErrorBoundary>
{/* L1 */}
<div className='flex items-start justify-around mt-4 my-4 gap-x-8'>
{/* FIM */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
<span>
Experimental. Only works with FIM models.
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
data-tooltip-class-name='void-max-w-[20px]'
>
*
</span>
</div>
<div className='my-2'>
{/* Enable Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.enableAutocomplete}
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${!voidSettingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
</div>
{/* Apply */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.syncApplyToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${voidSettingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
<div className='my-2'>
{/* Fast Apply Method Dropdown */}
<div className='flex items-center gap-x-2 my-2'>
<FastApplyMethodDropdown />
</div>
</div>
</div>
</div>
{/* L2 */}
<div className='flex items-start justify-around my-4 gap-x-8'>
{/* Tools Section */}
<div className='w-full'>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.autoApprove}
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
</div>
{/* Tool Lint Errors Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Don't fix lint errors`}</span>
</div>
</div>
</div>
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.showInlineSuggestions}
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
</div>
</div>
</div>
</div>
<div className='py-8' />
</ErrorBoundary>
</>
}
type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
@ -949,113 +778,264 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
}
const GeneralTab = () => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const environmentService = accessor.get('IEnvironmentService')
const nativeHostService = accessor.get('INativeHostService')
return <>
<div className=''>
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
<div className='flex flex-col gap-4'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
</div>
</div>
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
</VoidButtonBgDarken>
</div>
</div>
<div className='mt-12 max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`Instructions to include on all AI requests.`}</h4>
<AIInstructionsBox />
</div>
</>
}
// full settings
export const Settings = () => {
const isDark = useIsDark()
const [tab, setTab] = useState<TabName>('models')
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const environmentService = accessor.get('IEnvironmentService')
const nativeHostService = accessor.get('INativeHostService')
const settingsState = useSettingsState()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
<div className='max-w-5xl mx-auto'>
<div className='max-w-xl mx-auto'>
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
{/* separator */}
<div className='w-full h-[1px] my-4' />
<div className='flex items-stretch'>
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ModelDump />
<AddModelInputBox className='mt-4' compact />
<RedoOnboardingButton className='mt-2 mb-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
{/* tabs */}
<div className='flex flex-col w-full max-w-32'>
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('general') }}
>General</button>
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
{ollamaSetupInstructions}
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
{/* L1 */}
<div className='flex items-start justify-around my-4 gap-x-8'>
<ErrorBoundary>
{/* FIM */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
<span>
Experimental.{' '}
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
data-tooltip-class-name='void-max-w-[20px]'
>
Only works with FIM models.*
</span>
</div>
<div className='my-2'>
{/* Enable Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.enableAutocomplete}
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
</div>
</ErrorBoundary>
{/* Model Dropdown */}
<ErrorBoundary>
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</ErrorBoundary>
</div>
</div>
</ErrorBoundary>
{/* Apply */}
<ErrorBoundary>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.syncApplyToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
<div className='my-2'>
{/* Fast Apply Method Dropdown */}
<div className='flex items-center gap-x-2 my-2'>
<FastApplyMethodDropdown />
</div>
</div>
</div>
</ErrorBoundary>
</div>
{/* L2 */}
<div className='flex items-start justify-around my-4 gap-x-8'>
{/* Tools Section */}
<div className='w-full'>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.autoApprove}
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
</div>
</ErrorBoundary>
{/* Tool Lint Errors Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
</div>
</ErrorBoundary>
</div>
</div>
{/* separator */}
<div className='w-[1px] mx-4' />
{/* content */}
<div className='w-full min-w-[550px]'>
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<FeaturesTab />
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.showInlineSuggestions}
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
</div>
</ErrorBoundary>
</div>
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
<GeneralTab />
</div>
</div>
</div>
{/* General section (formerly GeneralTab) */}
<div className='mt-12'>
<ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>One-Click Switch</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
<div className='flex flex-col gap-4'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
</div>
</ErrorBoundary>
</div>
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<ErrorBoundary>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
</VoidButtonBgDarken>
</div>
</ErrorBoundary>
</div>
<div className='mt-12 max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
System instructions to include with all AI requests.
Alternatively, place a \`.voidinstructions\` file in the root of your workspace.
`} chatMessageLocation={undefined} />
</h4>
<ErrorBoundary>
<AIInstructionsBox />
</ErrorBoundary>
</div>
</div>
</div>
</div>
}

View file

@ -3,6 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import '../styles.css'
import { Tooltip } from 'react-tooltip';
import 'react-tooltip/dist/react-tooltip.css';
import { useIsDark } from '../util/services.js';
@ -44,7 +45,7 @@ export const VoidTooltip = () => {
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green {
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
@ -66,6 +67,11 @@ export const VoidTooltip = () => {
color: white;
}
#void-tooltip-ollama-settings {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
.react-tooltip-arrow {
z-index: -1 !important; /* Keep arrow behind content (somehow this isnt done automatically) */
}
@ -92,6 +98,28 @@ export const VoidTooltip = () => {
opacity={1}
delayShow={50}
/>
<Tooltip
id="void-tooltip-ollama-settings"
border='1px solid rgba(100,100,100,.2)'
opacity={1}
openEvents={{ mouseover: true, click: true, focus: true }}
place='right'
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11 }}
>
<div style={{ padding: '8px 10px' }}>
<div style={{ opacity: 0.8, textAlign: 'center', fontWeight: 'bold', marginBottom: 8 }}>
Good starter models
</div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
</div>
<div>
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
</div>
</div>
</Tooltip>
</>
);
};

View file

@ -32,6 +32,7 @@ import './media/void.css'
// update (frontend part, also see platform/)
import './voidUpdateActions.js'
import './convertToLLMMessageWorkbenchContrib.js'
// tools
import './toolsService.js'

View file

@ -362,8 +362,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :(
// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore);
export type VoidCommandBarProps = {
uri: URI | null;

View file

@ -99,7 +99,7 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
secondary: [{
id: 'void.updater.close',
enabled: true,
label: `Keep Void outdated`,
label: `Keep current version`,
tooltip: '',
class: undefined,
run: () => {

View file

@ -783,7 +783,7 @@ const groqSettings: VoidStaticProviderInfo = {
}
const ollamaModelOptions = {
'qwen2.5-coder:3b': {
'qwen2.5-coder:1.5b': {
contextWindow: 32_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
@ -792,6 +792,15 @@ const ollamaModelOptions = {
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'llama3.1': {
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 4.9 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'qwen2.5-coder': {
contextWindow: 128_000,
maxOutputTokens: null,
@ -822,7 +831,7 @@ const ollamaModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
export const ollamaRecommendedModels = ['qwen2.5-coder:3b', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]

View file

@ -205,7 +205,7 @@ export const availableTools = (chatMode: ChatMode) => {
return tools
}
const availableXMLToolsStr = (tools: InternalToolInfo[]) => {
const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
return `${tools.map((t, i) => {
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
return `\
@ -217,7 +217,7 @@ Format:
}).join('\n\n')}`
}
export const toolCallXMLStr = (toolName: ToolName, toolParams: RawToolParamsObj) => {
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolName}>${!params ? '' : `\n${params}`}
@ -234,12 +234,12 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const toolXMLDefinitions = (`\
Available tools:
${availableXMLToolsStr(tools)}`)
${toolCallDefinitionsXMLString(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- Once you write a tool call, you must STOP and WAIT for the result.
- To call a tool, write its name and parameters in one of the XML formats specified above at the BOTTOM of your response.
- To call a tool, write its name and parameters in one of the XML formats specified above.
- After you write the tool call, you must STOP and WAIT for the result.
- All parameters are REQUIRED unless noted otherwise.
- You are only allowed to output ONE tool call, and it must be at the END of your response.
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
@ -341,9 +341,9 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
const ansStrs: string[] = []
ansStrs.push(header)
ansStrs.push(sysInfo)
ansStrs.push(fsInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push(fsInfo)
const fullSystemMsgStr = ansStrs
.join('\n\n\n')

View file

@ -245,6 +245,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
// the stored data structure might be outdated, so we need to update it here
readS = {
...readS,
settingsOfProvider: {
...defaultSettingsOfProvider,
...readS.settingsOfProvider,
mistral: { // we added mistral
...defaultSettingsOfProvider.mistral,
...readS.settingsOfProvider.mistral,
},
} // we added mistral
}
this.state = readS
this.state = _stateWithUpdatedDefaultModels(this.state)
this.state = _validatedModelState(this.state);

View file

@ -70,7 +70,8 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
}
else if (providerName === 'deepseek') {
return {
title: 'DeepSeek.com API',
// title: 'DeepSeek.com API',
title: 'DeepSeek',
}
}
else if (providerName === 'openRouter') {
@ -95,22 +96,26 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
}
else if (providerName === 'gemini') {
return {
title: 'Gemini API',
// title: 'Gemini API',
title: 'Gemini',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq.com API',
// title: 'Groq.com API',
title: 'Groq',
}
}
else if (providerName === 'xAI') {
return {
title: 'Grok (xAI)',
// title: 'Grok (xAI)',
title: 'xAI',
}
}
else if (providerName === 'mistral') {
return {
title: 'Mistral API',
// title: 'Mistral API',
title: 'Mistral',
}
}

View file

@ -3,12 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// disable foreign import complaints
/* eslint-disable */
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
/* eslint-enable */
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
@ -192,7 +194,9 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError,
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools } as const : {}
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
{ tools: potentialTools } as const
: {}
// instance
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
@ -374,7 +378,9 @@ const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onE
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
const nativeToolsObj = potentialTools ? { tools: potentialTools, tool_choice: { type: 'auto' } } as const : {}
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
: {}
// instance

View file

@ -23,6 +23,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
super()
}
async check(explicit: boolean): Promise<VoidCheckUpdateRespose> {
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
@ -37,17 +38,17 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
if (this._updateService.state.type === StateType.Uninitialized) {
// The update service hasn't been initialized yet
return { message: explicit ? 'Not yet checking for updates...' : null, action: explicit ? 'reinstall' : undefined } as const
return { message: explicit ? 'Checking for updates soon...' : null, action: explicit ? 'reinstall' : undefined } as const
}
if (this._updateService.state.type === StateType.Idle) {
// No updates currently available
return { message: explicit ? 'No update found!' : null, action: explicit ? 'reinstall' : undefined } as const
return { message: explicit ? 'No updates found!' : null, action: explicit ? 'reinstall' : undefined } as const
}
if (this._updateService.state.type === StateType.CheckingForUpdates) {
// Currently checking for updates
return { message: explicit ? 'No updates found!' : null } as const
return { message: explicit ? 'Checking for updates...' : null } as const
}
if (this._updateService.state.type === StateType.AvailableForDownload) {
@ -62,7 +63,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
if (this._updateService.state.type === StateType.Downloaded) {
// Update has been downloaded but not yet ready
return { message: explicit ? 'Got download, need to apply...' : null, action: 'apply' } as const
return { message: explicit ? 'An update is ready to be applied!' : null, action: 'apply' } as const
}
if (this._updateService.state.type === StateType.Updating) {
@ -76,28 +77,69 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
}
if (this._updateService.state.type === StateType.Disabled) {
try {
const res = await fetch(`https://updates.voideditor.dev/api/v0/${this._productService.commit}`)
const resJSON = await res.json()
return await this._manualCheckGHTagIfDisabled(explicit)
}
return null
}
if (!resJSON) return null // null means error
const { hasUpdate, downloadMessage } = resJSON ?? {}
if (hasUpdate === undefined)
return null
const after = (downloadMessage || '') + ''
if (hasUpdate)
return { message: after, action: 'reinstall' } as const
return { message: 'Void is up-to-date!' } as const
private async _manualCheckGHTagIfDisabled(explicit: boolean): Promise<VoidCheckUpdateRespose> {
try {
const response = await fetch('https://api.github.com/repos/voideditor/binaries/releases/latest');
const data = await response.json();
const version = data.tag_name;
const myVersion = `${this._productService.voidVersion}.${this._productService.release}`
const latestVersion = version
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok
let message: string | null
let action: 'reinstall' | undefined
// explicit
if (explicit) {
if (response.ok) {
if (!isUpToDate) {
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
action = 'reinstall'
}
else {
message = 'Void is up-to-date!'
}
}
else {
message = `An error occurred when fetching the latest GitHub release tag. Please try again in ~5 minutes, or reinstall.`
action = 'reinstall'
}
}
catch (e) {
return null
// not explicit
else {
if (response.ok && !isUpToDate) {
message = 'A new version of Void is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
action = 'reinstall'
}
else {
message = null
}
}
return { message, action } as const
}
catch (e) {
if (explicit) {
return {
message: `An error occurred when fetching the latest GitHub release tag: ${e}. Please try again in ~5 minutes.`,
action: 'reinstall',
}
}
else {
return { message: null } as const
}
}
return null
}
}