diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js
index 26b5bc37..9507aa59 100755
--- a/src/vs/workbench/contrib/void/browser/react/build.js
+++ b/src/vs/workbench/contrib/void/browser/react/build.js
@@ -74,7 +74,7 @@ function saveStylesFile() {
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
- }, 4000);
+ }, 6000);
}
const args = process.argv.slice(2);
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx
index bc68e66f..856049ea 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx
@@ -274,7 +274,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
null
)
- const statusIndicatorHTML =
+ const statusIndicatorHTML =
return {
statusIndicatorHTML,
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx
similarity index 100%
rename from src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx
rename to src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidSelectionHelper.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidSelectionHelper.tsx
new file mode 100644
index 00000000..1e6e89ba
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidSelectionHelper.tsx
@@ -0,0 +1,145 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+
+import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
+
+import '../styles.css'
+import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
+import { Circle, MoreVertical } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
+import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
+
+
+export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
+
+ const isDark = useIsDark()
+
+ return
+
+
+}
+
+
+
+const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
+
+
+ const accessor = useAccessor()
+ const keybindingService = accessor.get('IKeybindingService')
+ const commandService = accessor.get('ICommandService')
+
+ const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
+ const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
+
+ const dividerHTML =
+
+ const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
+ const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
+
+ // rerender when the key changes
+ if (reactRerenderCount !== rerenderKey) {
+ setReactRerenderKey(rerenderKey)
+ setClickState('init')
+ }
+ // useEffect(() => {
+ // }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
+
+ // if the user selected an option, close
+ if (clickState === 'clickedOption') {
+ return null
+ }
+
+ const defaultHTML = <>
+ {ctrlLKeybind &&
+ {
+ commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
+ setClickState('clickedOption');
+ }}
+ >
+ Add to Chat
+
+ {ctrlLKeybind.getLabel()}
+
+
+ }
+ {ctrlLKeybind && ctrlKKeybind &&
+ dividerHTML
+ }
+ {ctrlKKeybind &&
+ {
+ commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
+ setClickState('clickedOption');
+ }}
+ >
+ Edit Inline
+
+ {ctrlKKeybind.getLabel()}
+
+
+ }
+
+ {dividerHTML}
+
+ {
+ setClickState('clickedMore');
+ }}
+ >
+
+
+ >
+
+
+ const moreOptionsHTML = <>
+ {
+ commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
+ setClickState('clickedOption');
+ }}
+ >
+ Disable Suggestions?
+
+ >
+
+ return
+ {clickState === 'init' ? defaultHTML
+ : clickState === 'clickedMore' ? moreOptionsHTML
+ : <>>
+ }
+
+}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/index.tsx
similarity index 77%
rename from src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx
rename to src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/index.tsx
index 5b185788..61663502 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/index.tsx
@@ -5,5 +5,9 @@
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidCommandBarMain } from './VoidCommandBar.js'
+import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
+
+export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
+
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
index 3b97463a..92c5dffc 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx
@@ -534,9 +534,26 @@ export const FeaturesTab = () => {
+
+
+
Editor
+
{`Settings that control the visibility of suggestions and widgets in the code editor.`}
+
+
+ {/* Auto Accept Switch */}
+
+ voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
+ />
+ {voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}
+
+
+
diff --git a/src/vs/workbench/contrib/void/browser/react/tsup.config.js b/src/vs/workbench/contrib/void/browser/react/tsup.config.js
index ab3bd525..82dae1d9 100644
--- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js
+++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js
@@ -7,7 +7,7 @@ import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
- './src2/void-command-bar-tsx/index.tsx',
+ './src2/void-editor-widgets-tsx/index.tsx',
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
index 503981c6..b997dc11 100644
--- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts
+++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts
@@ -175,7 +175,7 @@ registerAction2(class extends Action2 {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
- title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
+ title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension
diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts
index 9054450b..f0dcc477 100644
--- a/src/vs/workbench/contrib/void/browser/void.contribution.ts
+++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts
@@ -46,6 +46,9 @@ import './metricsPollService.js'
// helper services
import './helperServices/consistentItemService.js'
+// register selection helper
+import './voidSelectionHelperWidget.js'
+
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts
index 7711068d..c7780301 100644
--- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts
+++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts
@@ -11,7 +11,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
-import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
+import { mountVoidCommandBar } from './react/out/void-editor-widgets-tsx/index.js'
import { deepClone } from '../../../../base/common/objects.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
diff --git a/src/vs/workbench/contrib/void/browser/voidSelectionHelperWidget.ts b/src/vs/workbench/contrib/void/browser/voidSelectionHelperWidget.ts
new file mode 100644
index 00000000..b3cd5ead
--- /dev/null
+++ b/src/vs/workbench/contrib/void/browser/voidSelectionHelperWidget.ts
@@ -0,0 +1,229 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
+import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
+import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
+import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';
+import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
+import { Selection } from '../../../../editor/common/core/selection.js';
+import { RunOnceScheduler } from '../../../../base/common/async.js';
+import * as dom from '../../../../base/browser/dom.js';
+import { mountVoidSelectionHelper } from './react/out/void-editor-widgets-tsx/index.js';
+import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
+import { IVoidSettingsService } from '../common/voidSettingsService.js';
+
+
+const minDistanceFromRightPx = 400;
+const minDistanceFromLeftPx = 60;
+
+
+export type VoidSelectionHelperProps = {
+ rerenderKey: number // alternates between 0 and 1
+}
+
+
+export class SelectionHelperContribution extends Disposable implements IEditorContribution, IOverlayWidget {
+ public static readonly ID = 'editor.contrib.voidSelectionHelper';
+ // react
+ private _rootHTML: HTMLElement;
+ private _rerender: (props?: any) => void = () => { };
+ private _rerenderKey: number = 0;
+ private _reactComponentDisposable: IDisposable | null = null;
+
+ // internal
+ private _isVisible = false;
+ private _showScheduler: RunOnceScheduler;
+ private _lastSelection: Selection | null = null;
+
+ constructor(
+ private readonly _editor: ICodeEditor,
+ @IInstantiationService private readonly _instantiationService: IInstantiationService,
+ @IVoidSettingsService private readonly _voidSettingsService: IVoidSettingsService
+ ) {
+ super();
+
+ // Create the container element for React component
+ const { root, content } = dom.h('div@root', [
+ dom.h('div@content', [])
+ ]);
+
+ // Set styles for container
+ root.style.position = 'absolute';
+ root.style.display = 'none'; // Start hidden
+
+ // Initialize React component
+ this._instantiationService.invokeFunction(accessor => {
+ if (this._reactComponentDisposable) {
+ this._reactComponentDisposable.dispose();
+ }
+ const res = mountVoidSelectionHelper(content, accessor);
+ if (!res) return;
+
+ this._reactComponentDisposable = res;
+ this._rerender = res.rerender;
+
+ this._register(this._reactComponentDisposable);
+
+
+ });
+
+ this._rootHTML = root;
+
+ // Register as overlay widget
+ this._editor.addOverlayWidget(this);
+
+ // Use scheduler to debounce showing widget
+ this._showScheduler = new RunOnceScheduler(() => {
+ if (this._lastSelection) {
+ this._showHelperForSelection(this._lastSelection);
+ }
+ }, 50);
+
+ // Register event listeners
+ this._register(this._editor.onDidChangeCursorSelection(e => this._onSelectionChange(e)));
+
+ // Add a flag to track if mouse is over the widget
+ let isMouseOverWidget = false;
+ this._rootHTML.addEventListener('mouseenter', () => {
+ isMouseOverWidget = true;
+ });
+ this._rootHTML.addEventListener('mouseleave', () => {
+ isMouseOverWidget = false;
+ });
+
+ // Only hide helper when text editor loses focus and mouse is not over the widget
+ this._register(this._editor.onDidBlurEditorText(() => {
+ if (!isMouseOverWidget) {
+ this._hideHelper();
+ }
+ }));
+
+ this._register(this._editor.onDidScrollChange(() => this._updatePositionIfVisible()));
+ this._register(this._editor.onDidLayoutChange(() => this._updatePositionIfVisible()));
+ }
+
+ // IOverlayWidget implementation
+ public getId(): string {
+ return SelectionHelperContribution.ID;
+ }
+
+ public getDomNode(): HTMLElement {
+ return this._rootHTML;
+ }
+
+ public getPosition(): IOverlayWidgetPosition | null {
+ return null; // We position manually
+ }
+
+ private _onSelectionChange(e: ICursorSelectionChangedEvent): void {
+ if (!this._editor.hasModel()) {
+ return;
+ }
+
+ const selection = this._editor.getSelection();
+
+ if (!selection || selection.isEmpty()) {
+ this._hideHelper();
+ return;
+ }
+
+ // Get selection text to check if it's worth showing the helper
+ const text = this._editor.getModel()!.getValueInRange(selection);
+ if (text.length < 3) {
+ this._hideHelper();
+ return;
+ }
+
+ // Store selection
+ this._lastSelection = new Selection(
+ selection.startLineNumber,
+ selection.startColumn,
+ selection.endLineNumber,
+ selection.endColumn
+ );
+
+ this._showScheduler.schedule();
+ }
+
+ // Update the _showHelperForSelection method to work with the React component
+ private _showHelperForSelection(selection: Selection): void {
+ if (!this._editor.hasModel()) {
+ return;
+ }
+
+ const model = this._editor.getModel()!;
+
+ // Calculate the middle line of the selection
+ const startLine = selection.startLineNumber;
+ const endLine = selection.endLineNumber;
+ const middleLineNumber = Math.floor(startLine + (endLine - startLine) / 2);
+
+ // Get the content of the middle line
+ const lineContent = model.getLineContent(middleLineNumber);
+
+ // Find the position at the end of the middle line
+ const endOfLinePos = this._editor.getScrolledVisiblePosition({
+ lineNumber: middleLineNumber,
+ column: lineContent.length + 1 // +1 because columns are 1-based
+ });
+
+ if (!endOfLinePos) {
+ this._hideHelper();
+ return;
+ }
+
+ // Calculate right edge of visible editor area
+ const editorWidth = this._editor.getLayoutInfo().width;
+
+ // Position the helper element at the end of the middle line but ensure it's visible
+ const xPosition = Math.max(Math.min(endOfLinePos.left, editorWidth - minDistanceFromRightPx), minDistanceFromLeftPx);
+
+ // Update the React component position
+ this._rootHTML.style.left = `${xPosition}px`;
+ this._rootHTML.style.top = `${endOfLinePos.top}px`;
+ this._rootHTML.style.display = 'flex'; // Show the container
+
+ this._isVisible = true;
+
+ // rerender
+ const enabled = this._voidSettingsService.state.globalSettings.showInlineSuggestions
+ && this._editor.hasTextFocus() // needed since VS Code counts unfocused selections as selections, which causes this to rerender when it shouldnt (bad ux)
+
+ if (enabled) {
+ this._rerender({ rerenderKey: this._rerenderKey } satisfies VoidSelectionHelperProps)
+ this._rerenderKey = (this._rerenderKey + 1) % 2;
+ // this._reactComponentRerender();
+ }
+
+ }
+
+ private _hideHelper(): void {
+ this._rootHTML.style.display = 'none';
+ this._isVisible = false;
+ this._lastSelection = null;
+ }
+
+ private _updatePositionIfVisible(): void {
+ if (!this._isVisible || !this._lastSelection || !this._editor.hasModel()) {
+ return;
+ }
+
+ this._showHelperForSelection(this._lastSelection);
+ }
+
+ override dispose(): void {
+ this._hideHelper();
+ if (this._reactComponentDisposable) {
+ this._reactComponentDisposable.dispose();
+ }
+ this._editor.removeOverlayWidget(this);
+ this._showScheduler.dispose();
+ super.dispose();
+ }
+}
+
+// Register the contribution
+registerEditorContribution(SelectionHelperContribution.ID, SelectionHelperContribution, EditorContributionInstantiation.Eager);
diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
index acfe57f0..a056a84a 100644
--- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
+++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts
@@ -389,6 +389,7 @@ export type GlobalSettings = {
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: boolean;
+ showInlineSuggestions: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
@@ -399,6 +400,7 @@ export const defaultGlobalSettings: GlobalSettings = {
enableFastApply: true,
chatMode: 'agent',
autoApprove: false,
+ showInlineSuggestions: true,
}
export type GlobalSettingName = keyof GlobalSettings