diff --git a/package-lock.json b/package-lock.json
index 966a06e81..bc0458914 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
+ "@floating-ui/react": "^0.27.3",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
@@ -1551,6 +1552,65 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.13",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.3.tgz",
+ "integrity": "sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.9",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/react/node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
+ },
"node_modules/@google/generative-ai": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
diff --git a/package.json b/package.json
index c8f0159b4..a4ee38bb4 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
+ "@floating-ui/react": "^0.27.3",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts
index c92810abe..811f43371 100644
--- a/src/vs/platform/void/common/voidSettingsService.ts
+++ b/src/vs/platform/void/common/voidSettingsService.ts
@@ -81,7 +81,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
- modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
+ modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null },
globalSettings: deepClone(defaultGlobalSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
}
@@ -137,6 +137,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
+ },
+ modelSelectionOfFeature: {
+ // A HACK BECAUSE WE ADDED FastApply
+ ...{ 'FastApply': null },
+ ...readS.modelSelectionOfFeature,
}
}
diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts
index f0f2624a2..43a29f5b7 100644
--- a/src/vs/platform/void/common/voidSettingsTypes.ts
+++ b/src/vs/platform/void/common/voidSettingsTypes.ts
@@ -432,14 +432,22 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
}
// this is a state
-export type ModelSelectionOfFeature = {
- 'Ctrl+L': ModelSelection | null,
- 'Ctrl+K': ModelSelection | null,
- 'Autocomplete': ModelSelection | null,
-}
+export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'FastApply'] as const
+export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
export type FeatureName = keyof ModelSelectionOfFeature
-export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
+export const displayInfoOfFeatureName = (featureName: FeatureName) => {
+ if (featureName === 'Autocomplete')
+ return 'Autocomplete'
+ else if (featureName === 'Ctrl+K')
+ return 'Quick Edit'
+ else if (featureName === 'Ctrl+L')
+ return 'Sidebar Chat'
+ else if (featureName === 'FastApply')
+ return 'Fast Apply'
+ else
+ throw new Error(`Feature Name ${featureName} not allowed`)
+}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx
index 43abd5b1d..4c2cbea48 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx
@@ -13,10 +13,9 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
return (
<>
-
+
{buttonsOnHover === null ? null : (
-
+
{buttonsOnHover}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
index 8cb4864fb..0ea5febba 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -436,7 +436,7 @@ const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMess
className={`
relative
${isEditMode ? 'px-2 w-full max-w-full'
- : role === 'user' ? `px-2 self-end w-fit max-w-full`
+ : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
}
`}
@@ -444,7 +444,7 @@ const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMess
{
)
}, [previousMessages])
- return
- {/* thread selector */}
-
-
-
+
+
- {/* previous messages + current stream */}
-
- {/* previous messages */}
- {prevMessagesHTML}
- {/* message stream */}
-
+ const messagesHTML =
+ {/* previous messages */}
+ {prevMessagesHTML}
+ {/* message stream */}
+
- {/* error message */}
- {latestError === undefined ? null :
-
- { chatThreadsService.dismissStreamError(currentThread.id) }}
- showDismiss={true}
- />
- { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
-
- }
+ {/* error message */}
+ {latestError === undefined ? null :
+
+ { chatThreadsService.dismissStreamError(currentThread.id) }}
+ showDismiss={true}
+ />
-
+ { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
+
+ }
+
- {/* input box */}
- 0 ? 'absolute bottom-0' : ''}`}
+ const inputBox =
0 ? 'absolute bottom-0' : ''}`}
+ >
+
{
+ textAreaRef.current?.focus()
+ }}
>
+ {/* top row */}
+ <>
+ {/* selections */}
+
+ >
+
+ {/* middle row */}
+
+
+ {/* text input */}
+ { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ onSubmit()
+ }
+ }}
+ ref={textAreaRef}
+ fnsRef={textAreaFnsRef}
+ multiline={true}
+ />
+
+
+ {/* bottom row */}
{
- textAreaRef.current?.focus()
- }}
+ className='flex flex-row justify-between items-end gap-1'
>
- {/* top row */}
- <>
- {/* selections */}
-
- >
-
- {/* middle row */}
-
-
- {/* text input */}
-
{ setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
- onKeyDown={(e) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- onSubmit()
- }
- }}
- ref={textAreaRef}
- fnsRef={textAreaFnsRef}
- multiline={true}
- />
+ {/* submit options */}
+
+
- {/* bottom row */}
-
- {/* submit options */}
-
-
-
+ {/* submit / stop button */}
+ {isStreaming ?
+ // stop button
+
+ :
+ // submit button (up arrow)
+
+ }
+
+
+
- {/* submit / stop button */}
- {isStreaming ?
- // stop button
-
- :
- // submit button (up arrow)
-
- }
-
+ return
+ {threadSelector}
+ {messagesHTML}
-
-
-
+ {inputBox}
+
+
}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx
index b21df36fc..357640c08 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx
@@ -15,6 +15,7 @@ import { useAccessor } from './services.js';
import { ITextModel } from '../../../../../../../editor/common/model.js';
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
+import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
// type guard
@@ -296,6 +297,7 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
+
export const VoidCustomSelectBox =
({
options,
selectedOption: selectedOption_,
@@ -306,7 +308,6 @@ export const VoidCustomSelectBox = ({
className,
arrowTouchesText = true,
matchInputWidth = false,
- isMenuPositionFixed = true,
gap = 0,
}: {
options: T[];
@@ -318,18 +319,58 @@ export const VoidCustomSelectBox = ({
className?: string;
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
- isMenuPositionFixed?: boolean;
gap?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
- const [readyToShow, setReadyToShow] = useState(false);
- const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
- const containerRef = useRef(null);
- const buttonRef = useRef(null);
- const measureRef = useRef(null);
-
-
- // if the selected option is null, use the 0th option as the selected, and set the option to options[0]
+ const measureRef = useRef(null);
+
+ // Replace manual positioning with floating-ui
+ const {
+ x,
+ y,
+ strategy,
+ refs,
+ middlewareData,
+ update
+ } = useFloating({
+ open: isOpen,
+ onOpenChange: setIsOpen,
+ placement:'bottom-start',
+
+ middleware: [
+ offset(gap),
+ flip({
+ boundary: document.body,
+ padding: 8
+ }),
+ shift({
+ boundary: document.body,
+ padding: 8,
+ }),
+ size({
+ apply({ availableHeight, elements, rects }) {
+ const maxHeight = Math.min(availableHeight)
+
+ Object.assign(elements.floating.style, {
+ maxHeight: `${maxHeight}px`,
+ overflowY: 'auto',
+ // Ensure the width isn't constrained by the parent
+ width: `${Math.max(
+ rects.reference.width,
+ measureRef.current?.offsetWidth ?? 0
+ )}px`
+ });
+ },
+ padding: 8,
+ // Use viewport as boundary instead of any parent element
+ boundary: document.body,
+ }),
+ ],
+ whileElementsMounted: autoUpdate,
+ strategy:'fixed',
+ });
+
+ // if the selected option is null, use the 0th option
useEffect(() => {
if (!options[0]) return
if (!selectedOption_) {
@@ -338,84 +379,33 @@ export const VoidCustomSelectBox = ({
}, [selectedOption_, options])
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
-
- const updatePosition = useCallback(() => {
- if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
-
- const buttonRect = buttonRef.current.getBoundingClientRect();
- const containerRect = containerRef.current.getBoundingClientRect();
- const containerWidth = containerRef.current.offsetWidth;
- const viewportHeight = window.innerHeight;
- const spaceBelow = viewportHeight - buttonRect.bottom;
- const spaceNeeded = options.length * 28;
- const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
-
- // Calculate the menu width
- let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
-
- // If not matchInputWidth, calculate content width from measurement div
- if (!matchInputWidth) {
- const contentWidth = measureRef.current.offsetWidth;
- menuWidth = Math.max(buttonRect.width, contentWidth);
- }
-
- if (isMenuPositionFixed) {
- // Fixed positioning (relative to viewport)
- setPosition({
- top: showAbove
- ? buttonRect.top - spaceNeeded
- : buttonRect.bottom + gap,
- left: buttonRect.left,
- width: menuWidth,
- });
- } else {
- // Absolute positioning (relative to parent container)
- setPosition({
- top: showAbove
- ? -(spaceNeeded + gap)
- : buttonRect.height + gap,
- left: 0,
- width: menuWidth,
- });
- }
-
- setReadyToShow(true);
- }, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
-
+ // Handle clicks outside
useEffect(() => {
- if (isOpen) {
- setReadyToShow(false);
- updatePosition();
- window.addEventListener('scroll', updatePosition, true);
- window.addEventListener('resize', updatePosition);
-
- return () => {
- window.removeEventListener('scroll', updatePosition, true);
- window.removeEventListener('resize', updatePosition);
- };
- } else {
- setReadyToShow(false);
- }
- }, [isOpen, updatePosition]);
+ if (!isOpen) return;
- useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ const target = event.target as Node;
+ const floating = refs.floating.current;
+ const reference = refs.reference.current;
+
+ // Check if reference is an HTML element before using contains
+ const isReferenceHTMLElement = reference && 'contains' in reference;
+
+ if (
+ floating &&
+ (!isReferenceHTMLElement || !reference.contains(target)) &&
+ !floating.contains(target)
+ ) {
setIsOpen(false);
}
};
- if (isOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }
- }, [isOpen]);
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [isOpen, refs.floating, refs.reference]);
return (
-
+
{/* Hidden measurement div */}
({
{/* Select Button */}
{/* Dropdown Menu */}
- {isOpen && readyToShow && (
+ {isOpen && (
{options.map((option) => {
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
index 67a9a9c86..d6ca33b28 100644
--- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
+++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx
@@ -42,7 +42,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className={`text-xs text-void-fg-3 px-1`}
matchInputWidth={false}
- isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
+ // isMenuPositionFixed={false}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
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 62de3bed8..7c5751d70 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
@@ -5,7 +5,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
-import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
+import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
@@ -14,7 +14,7 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
-import { WarningBox } from './ModelDropdown.js'
+import { WarningBox, ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
@@ -392,7 +392,7 @@ export const AIInstructionsBox = () => {
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
return
{
+
+
Model Selection
+ {featureNames.map(featureName =>
+
+
{displayInfoOfFeatureName(featureName)}
+
+
+ )}
+
>
}