diff --git a/package-lock.json b/package-lock.json index d753857a..6688a29b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "posthog-node": "^4.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-tooltip": "^5.28.0", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", "vscode-html-languageservice": "^5.3.1", @@ -6621,6 +6622,12 @@ "node": ">=0.10.0" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -18000,6 +18007,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-tooltip": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", + "integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index f1faef9f..6ce28572 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "posthog-node": "^4.8.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-tooltip": "^5.28.0", "tas-client-umd": "0.2.0", "v8-inspect-profiler": "^0.1.1", "vscode-html-languageservice": "^5.3.1", diff --git a/src/bootstrap-fork.ts b/src/bootstrap-fork.ts index a92290a2..d9f424af 100644 --- a/src/bootstrap-fork.ts +++ b/src/bootstrap-fork.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// This bootstrap-fork module handles the initialization of a forked process in VS Code. +// It sets up logging, exception handling, and loads the ESM module system. + import * as performance from './vs/base/common/performance.js'; import { removeGlobalNodeJsModuleLookupPaths, devInjectNodeModuleLookupPath } from './bootstrap-node.js'; import { bootstrapESM } from './bootstrap-esm.js'; diff --git a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts index 520e71d2..931f2dc5 100644 --- a/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts @@ -121,6 +121,93 @@ export class SmartSelectController implements IEditorContribution { } this._state = this._state.map(state => state.mov(forward)); const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition())); + + // Void changed this to skip over added whitespace when using smartSelect + // // Store the original selections for comparison + // const originalSelections = selections; + + // // Keep skipping while we're only adding/removing whitespace + // let keepSkipping = true; + // let skipCount = 0; + // const MAX_SKIPS = 5; // Avoid infinite loops by setting a reasonable limit + + // while (keepSkipping && skipCount < MAX_SKIPS) { + // keepSkipping = false; // Reset for each iteration + + // // Check if all selections only added/removed whitespace + // if (originalSelections.length === newSelections.length) { + // for (let i = 0; i < originalSelections.length; i++) { + // const oldSel = originalSelections[i]; + // const newSel = newSelections[i]; + + // if (forward) { // For expanding (^+Shift+Right) + // // Skip if only whitespace was added + // const oldText = model.getValueInRange(oldSel).trim(); + // const newText = model.getValueInRange(newSel).trim(); + // const onlyWhitespaceAdded = oldText === newText && oldText.length > 0; + + // if (onlyWhitespaceAdded) { + // console.log(`SMART SELECT - SKIPPING (EXPAND) [${skipCount + 1}]:`, { + // reason: 'only whitespace added', + // oldText: model.getValueInRange(oldSel), + // newText: model.getValueInRange(newSel) + // }); + // keepSkipping = true; + // break; + // } + // } else { // For shrinking (^+Shift+Left) + // // Skip if only whitespace was removed + // const oldText = model.getValueInRange(oldSel).trim(); + // const newText = model.getValueInRange(newSel).trim(); + // const onlyWhitespaceRemoved = oldText === newText && newText.length > 0; + + // if (onlyWhitespaceRemoved) { + // console.log(`SMART SELECT - SKIPPING (SHRINK) [${skipCount + 1}]:`, { + // reason: 'only whitespace removed', + // oldText: model.getValueInRange(oldSel), + // newText: model.getValueInRange(newSel) + // }); + // keepSkipping = true; + // break; + // } + // } + // } + // } + + // // If we need to skip, move one more time + // if (keepSkipping) { + // skipCount++; + + // // Try to move to the next range + // const prevState = this._state; + // this._state = this._state.map(state => state.mov(forward)); + + // // Check if we've reached the end of available ranges + // const stateUnchanged = this._state.every((state, idx) => + // state.index === prevState[idx].index + // ); + + // if (stateUnchanged) { + // // We can't move any further, so stop skipping + // keepSkipping = false; + // } else { + // // Update selections for the next iteration + // newSelections = this._state.map(state => Selection.fromPositions( + // state.ranges[state.index].getStartPosition(), + // state.ranges[state.index].getEndPosition() + // )); + // } + // } + // } + + // // Print AFTER selection (before actually setting it) + // console.log('SMART SELECT - AFTER:', newSelections.map(s => { + // return { + // range: `(${s.startLineNumber},${s.startColumn}) -> (${s.endLineNumber},${s.endColumn})`, + // text: model.getValueInRange(s) + // }; + // })); + this._ignoreSelection = true; try { this._editor.setSelections(newSelections); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 16d645a8..39492ef3 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -612,7 +612,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."), // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. // On Mac, the delay is 1500. - 'default': isMacintosh ? 1500 : 500, + 'default': 300, // Void changed this from isMacintosh ? 1500 : 500, 'minimum': 0 }, 'workbench.reduceMotion': { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 10effb06..efd70d7e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -71,6 +71,21 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); const numLinesOfStr = (str: string) => str.split('\n').length + +export const getLengthOfTextPx = ({ tabWidth, spaceWidth, content }: { tabWidth: number, spaceWidth: number, content: string }) => { + let lengthOfTextPx = 0; + for (const char of content) { + if (char === '\t') { + lengthOfTextPx += tabWidth + } else { + lengthOfTextPx += spaceWidth; + } + } + + return lengthOfTextPx +} + + const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -94,16 +109,14 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth; const tabWidth = numSpacesInTab * spaceWidth; - let paddingLeft = 0; - for (const char of leadingWhitespace) { - if (char === '\t') { - paddingLeft += tabWidth - } else if (char === ' ') { - paddingLeft += spaceWidth; - } - } + const leftWhitespacePx = getLengthOfTextPx({ + tabWidth, + spaceWidth, + content: leadingWhitespace + }); - return paddingLeft; + + return leftWhitespacePx; }; @@ -1584,7 +1597,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined - const N_RETRIES = 5 + const N_RETRIES = 2 // allowed to throw errors - this is called inside a promise that handles everything const runSearchReplace = async () => { diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index 3a420737..ac5ff5f3 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -76,93 +76,107 @@ opacity: 80%; } +/* styles for all containers used by void */ +.void-scope { + --scrollbar-vertical-width: 8px; + --scrollbar-horizontal-height: 6px; +} +/* Target both void-scope and all its descendants with scrollbars */ +.void-scope, +.void-scope * { + scrollbar-width: thin !important; + scrollbar-color: var(--void-bg-1) var(--void-bg-3) !important; /* For Firefox */ +} +.void-scope::-webkit-scrollbar, +.void-scope *::-webkit-scrollbar { + width: var(--scrollbar-vertical-width) !important; + height: var(--scrollbar-horizontal-height) !important; + background-color: var(--void-bg-3) !important; +} +.void-scope::-webkit-scrollbar-thumb, +.void-scope *::-webkit-scrollbar-thumb { + background-color: var(--void-bg-1) !important; + border-radius: 4px !important; + border: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} + +.void-scope::-webkit-scrollbar-thumb:hover, +.void-scope *::-webkit-scrollbar-thumb:hover { + background-color: var(--void-bg-1) !important; + filter: brightness(1.1) !important; +} + +.void-scope::-webkit-scrollbar-thumb:active, +.void-scope *::-webkit-scrollbar-thumb:active { + background-color: var(--void-bg-1) !important; + filter: brightness(1.2) !important; +} + +.void-scope::-webkit-scrollbar-track, +.void-scope *::-webkit-scrollbar-track { + background-color: var(--void-bg-3) !important; + border: none !important; +} + +.void-scope::-webkit-scrollbar-corner, +.void-scope *::-webkit-scrollbar-corner { + background-color: var(--void-bg-3) !important; +} + +/* Add void-scrollable-element styles to match */ +.void-scrollable-element { + background-color: var(--vscode-editor-background); + --scrollbar-vertical-width: 14px; + --scrollbar-horizontal-height: 6px; + 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 */ +} .void-scrollable-element::-webkit-scrollbar, .void-scrollable-element *::-webkit-scrollbar { - width: 14px !important; - height: 4px !important; -} - -.void-scrollable-element::-webkit-scrollbar-track, -.void-scrollable-element *::-webkit-scrollbar-track { - background: transparent !important; + width: var(--scrollbar-vertical-width) !important; + height: var(--scrollbar-horizontal-height) !important; + background-color: var(--void-bg-3) !important; } .void-scrollable-element::-webkit-scrollbar-thumb, .void-scrollable-element *::-webkit-scrollbar-thumb { - background-color: transparent !important; - border-radius: 0px !important; + background-color: var(--void-bg-1) !important; + border-radius: 4px !important; + border: none !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; } .void-scrollable-element::-webkit-scrollbar-thumb:hover, .void-scrollable-element *::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground) !important; + background-color: var(--void-bg-1) !important; + filter: brightness(1.1) !important; } .void-scrollable-element::-webkit-scrollbar-thumb:active, .void-scrollable-element *::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground) !important; + background-color: var(--void-bg-1) !important; + filter: brightness(1.2) !important; +} + +.void-scrollable-element::-webkit-scrollbar-track, +.void-scrollable-element *::-webkit-scrollbar-track { + background-color: var(--void-bg-3) !important; + border: none !important; } .void-scrollable-element::-webkit-scrollbar-corner, .void-scrollable-element *::-webkit-scrollbar-corner { - background-color: transparent !important; -} - -.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb { - background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important; -} - -.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb, -.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb { - background-color: var(--vscode-scrollbarSlider-background) !important; + background-color: var(--void-bg-3) !important; } 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 72f963da..9f364ba0 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 @@ -11,6 +11,7 @@ import { URI } from '../../../../../../../base/common/uri.js' import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js' +import { PlacesType, VariantType } from 'react-tooltip' enum CopyButtonText { Idle = 'Copy', @@ -20,30 +21,28 @@ enum CopyButtonText { type IconButtonProps = { - onClick: () => void; Icon: LucideIcon - disabled?: boolean - className?: string } -export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => ( +export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes) => ( @@ -94,13 +93,14 @@ export const CopyButton = ({ codeStr }: { codeStr: string }) => { return } -export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { +export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & React.ButtonHTMLAttributes) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -110,6 +110,8 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => { onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + {...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })} + {...props} /> ) return jumpToFileButton @@ -122,7 +124,6 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => { ) } @@ -163,10 +164,11 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u rerender(c => c + 1) console.log('rerendering....') } - }, [applyBoxId, applyBoxId, uri])) + }, [applyBoxId, uri])) const currStreamState = getStreamState() + return { getStreamState, isDisabled, @@ -175,22 +177,61 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u } -export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => { +type IndicatorColor = 'green' | 'orange' | 'dark' | 'yellow' | null +export const StatusIndicator = ({ indicatorColor, title, className, ...props }: { indicatorColor: IndicatorColor, title?: React.ReactNode, className?: string } & React.HTMLAttributes) => { + return ( +
+ {title && {title}} +
+
+ ); +}; + +const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position = 'top', offset = undefined }: { tooltipName: string, color?: IndicatorColor, position?: PlacesType, offset?: number }) => ({ + 'data-tooltip-id': color === 'orange' ? `void-tooltip-orange` : color === 'green' ? 'void-tooltip-green' : 'void-tooltip', + 'data-tooltip-place': position as PlacesType, + 'data-tooltip-content': `${tooltipName}`, + 'data-tooltip-offset': offset, +}) + + +export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes) => { + const { currStreamState } = useApplyButtonState({ applyBoxId, uri }) - return
-
-
+ const color = ( + currStreamState === 'idle-no-changes' ? 'dark' : + currStreamState === 'streaming' ? 'orange' : + currStreamState === 'idle-has-changes' ? 'green' : + null + ) + + const tooltipName = ( + currStreamState === 'idle-no-changes' ? 'Done' : + currStreamState === 'streaming' ? 'Applying' : + currStreamState === 'idle-has-changes' ? 'Done' : // also 'Done'? 'Applied' looked bad + '' + ) + + const statusIndicatorHTML = + return statusIndicatorHTML } + export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -254,11 +295,22 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co if (currStreamState === 'streaming') { - return + return } if (currStreamState === 'idle-no-changes') { - return + + return } if (currStreamState === 'idle-has-changes') { @@ -270,19 +322,18 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co } } - export const BlockCodeApplyWrapper = ({ children, initValue, @@ -317,7 +368,7 @@ export const BlockCodeApplyWrapper = ({ {/* header */}
- + {name} diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index d541dc43..2a531815 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -10,7 +10,6 @@ import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; -import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; export const QuickEditChat = ({ @@ -89,8 +88,6 @@ export const QuickEditChat = ({ editCodeService.removeCtrlKZone({ diffareaid }) }, [editCodeService, diffareaid]) - useScrollbarStyles(sizerRef) - const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() const chatAreaRef = useRef(null) 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 4eaed58c..041cebfe 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 @@ -15,17 +15,18 @@ import { ErrorDisplay } from './ErrorDisplay.js'; import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; -import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react'; +import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolCallParams } from '../../../../common/toolsServiceTypes.js'; -import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; +import { ToolCallParams, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { PlacesType } from 'react-tooltip'; import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; @@ -351,7 +352,7 @@ export const VoidChatArea: React.FC = ({
-
+
{featureName === 'Chat' && }
@@ -392,6 +393,9 @@ export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Re ${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'} ${className} `} + // data-tooltip-id='void-tooltip' + // data-tooltip-content={'Send'} + // data-tooltip-place='left' {...props} > @@ -681,23 +685,26 @@ const ToolHeaderWrapper = ({ return (
{/* header */} -
{ - if (isDropdown) { setIsOpen(v => !v); } - if (onClick) { onClick(); } - }} - > - {isDropdown && ( - - )} +
{/* left */} -
+
{ + if (isDropdown) { setIsOpen(v => !v); } + if (onClick) { onClick(); } + }} + > + {isDropdown && ()} {title} - {desc1} + {desc1}
{/* right */} @@ -953,25 +960,32 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> + +
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
+
@@ -1024,6 +1038,7 @@ const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { prose-blockquote:pl-2 prose-blockquote:my-2 + prose-code:text-void-fg-3 prose-code:text-[12px] prose-code:before:content-none prose-code:after:content-none @@ -1237,7 +1252,7 @@ const ToolRequestAcceptRejectButtons = () => { +) + +export const RejectAllButtonWrapper = ({ text, onClick, className }: { text: string, onClick: () => void, className?: string }) => ( + +) + + + const CommandBarInChat = () => { - const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - const [isExpanded, setIsExpanded] = useState(false) + const { stateOfURI: commandBarStateOfURI, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + const numFilesChanged = sortedCommandBarURIs.length const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') const commandService = accessor.get('ICommandService') + const chatThreadsState = useChatThreadsState() + const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) { - return null - } + const [fileDetailsOpenedState, setFileDetailsOpenedState] = useState<'auto-opened' | 'auto-closed' | 'user-opened' | 'user-closed'>('auto-closed'); + const isFileDetailsOpened = fileDetailsOpenedState === 'auto-opened' || fileDetailsOpenedState === 'user-opened'; + + + useEffect(() => { + // close the file details if there are no files + // this converts 'user-closed' to 'auto-closed' + if (numFilesChanged === 0) { + setFileDetailsOpenedState('auto-closed') + } + // open the file details if it hasnt been closed + if (numFilesChanged > 0 && fileDetailsOpenedState !== 'user-closed') { + setFileDetailsOpenedState('auto-opened') + } + }, [fileDetailsOpenedState, setFileDetailsOpenedState, numFilesChanged]) + + + const isFinishedMakingThreadChanges = numFilesChanged !== 0 && (chatThreadsStreamState ? !chatThreadsStreamState.isRunning : true) + + // ======== status of agent ======== + // This icon answers the question "is the LLM doing work on this thread?" + // assume it is single threaded for now + // green = Running + // orange = Requires action + // dark = Done + + const threadStatus = ( + chatThreadsStreamState?.isRunning === 'awaiting_user' ? { title: 'Needs Approval', color: 'yellow', } as const + : chatThreadsStreamState?.isRunning ? { title: 'Running', color: 'orange', } as const + : { title: 'Done', color: 'dark', } as const + ) + + + const threadStatusHTML = + + + // ======== info about changes ======== + // num files changed + // acceptall + rejectall + // popup info about each change (each with num changes + acceptall + rejectall of their own) + + const numFilesChangedStr = numFilesChanged === 0 ? 'No files with changes' + : `${sortedCommandBarURIs.length} file${numFilesChanged === 1 ? '' : 's'} with changes` + + + + + const acceptRejectAllButtons =
+ { + sortedCommandBarURIs.forEach(uri => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "reject", + _addToHistory: true, + }); + }); + }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Reject all' + /> + + { + sortedCommandBarURIs.forEach(uri => { + editCodeService.acceptOrRejectAllDiffAreas({ + uri, + removeCtrlKs: true, + behavior: "accept", + _addToHistory: true, + }); + }); + }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Accept all' + /> + + + +
+ + + // !select-text cursor-auto + const fileDetailsContent =
+ {sortedCommandBarURIs.map((uri, i) => { + const basename = getBasename(uri.fsPath) + + const { sortedDiffIds, isStreaming } = commandBarStateOfURI[uri.fsPath] ?? {} + const isFinishedMakingFileChanges = !isStreaming + + const numDiffs = sortedDiffIds?.length || 0 + + const fileStatus = (isFinishedMakingFileChanges + ? { title: 'Done', color: 'dark', } as const + : { title: 'Running', color: 'orange', } as const + ) + + const fileNameHTML =
commandService.executeCommand('vscode.open', uri, { preview: true })} + > + {/* */} + {basename} +
+ + + + + const detailsContent =
+ {numDiffs} diff{numDiffs !== 1 ? 's' : ''} +
+ + const acceptRejectButtons =
+ + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "reject", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Reject file' + + /> + { editCodeService.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: "accept", _addToHistory: true, }); }} + data-tooltip-id='void-tooltip' + data-tooltip-place='top' + data-tooltip-content='Accept file' + /> + +
+ + const fileStatusHTML = + + return ( + // name, details +
+
+ {fileNameHTML} + {detailsContent} +
+
+ {acceptRejectButtons} + {fileStatusHTML} +
+
+ ) + })} +
+ + const fileDetailsButton = ( + + ) return ( - - {sortedCommandBarURIs.map((uri, i) => ( - { commandService.executeCommand('vscode.open', uri, { preview: true }) }} - /> - ))} - + <> + {/* file details */} +
+
+ {fileDetailsContent} +
+
+ {/* main content */} +
+
+ {fileDetailsButton} +
+
+ {acceptRejectAllButtons} + {threadStatusHTML} +
+
+ ) } @@ -2030,8 +2310,6 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - useScrollbarStyles(sidebarRef) - const onSubmit = useCallback(async () => { if (isDisabled) return @@ -2167,33 +2445,40 @@ export const SidebarChat = () => { } }, [onSubmit, onAbort, isRunning]) - const inputForm =
- { textAreaRef.current?.focus() }} + const inputForm =
+
+ {previousMessages.length > 0 && + + } +
+
- { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} - ref={textAreaRef} - fnsRef={textAreaFnsRef} - multiline={true} - /> + { textAreaRef.current?.focus() }} + > + { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} + ref={textAreaRef} + fnsRef={textAreaFnsRef} + multiline={true} + /> - + +
return ( 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 d2417f44..eb515ccb 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 @@ -154,12 +154,13 @@ export const VoidInputBox2 = forwardRef(fun }) -export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, ...inputProps }: { +export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, compact, ...inputProps }: { value: string; onChangeValue: (value: string) => void; placeholder: string; className?: string; disabled?: boolean; + compact?: boolean; passwordBlur?: boolean; } & React.InputHTMLAttributes) => { @@ -169,7 +170,11 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam onChange={(e) => onChangeValue(e.target.value)} placeholder={placeholder} disabled={disabled} - className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm + // className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root' + // className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm + className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 + ${compact ? 'py-1 px-2' : 'py-2 px-4 '} + rounded ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`} style={{ @@ -636,7 +641,7 @@ export const VoidCustomDropdownBox = >({ className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full" onClick={() => setIsOpen(!isOpen)} > - + {getOptionDisplayName(selectedOption)} void }) => { +export const VoidButtonBgDarken = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => { return } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 6cd180a0..6574c5e1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -350,9 +350,9 @@ export const useCommandBarURIListener = (listener: (uri: URI) => void) => { export const useCommandBarState = () => { const accessor = useAccessor() const commandBarService = accessor.get('IVoidCommandBarService') - const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); + const [s, ss] = useState({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); const listener = useCallback(() => { - ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); + ss({ stateOfURI: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); }, [commandBarService]) useCommandBarURIListener(listener) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx index 1ba69c24..1e20f692 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx @@ -1,128 +1,130 @@ -import { useEffect } from 'react'; +// Get rid of this as it was causing lag -export const useScrollbarStyles = (containerRef: React.MutableRefObject) => { - useEffect(() => { - if (!containerRef.current) return; +// import { useEffect } from 'react'; - // Create selector for specific overflow classes - const overflowSelector = [ - '[class*="overflow-auto"]', - '[class*="overflow-x-auto"]', - '[class*="overflow-y-auto"]' - ].join(','); +// export const useScrollbarStyles = (containerRef: React.RefObject) => { +// useEffect(() => { +// if (!containerRef.current) return; - // Function to initialize scrollbar styles for elements - const initializeScrollbarStyles = () => { - // Get all matching elements within the container, including the container itself - const scrollElements = [ - ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []), - ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || []) - ]; +// // Create selector for specific overflow classes +// const overflowSelector = [ +// '[class*="overflow-auto"]', +// '[class*="overflow-x-auto"]', +// '[class*="overflow-y-auto"]' +// ].join(','); - // Apply basic styling to all elements - scrollElements.forEach(element => { - element.classList.add('void-scrollable-element'); - }); +// // Function to initialize scrollbar styles for elements +// const initializeScrollbarStyles = () => { +// // Get all matching elements within the container, including the container itself +// const scrollElements = [ +// ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []), +// ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || []) +// ]; - // Only initialize fade effects for elements that haven't been initialized yet - scrollElements.forEach(element => { - if (!(element as any).__scrollbarCleanup) { - let fadeTimeout: NodeJS.Timeout | null = null; - let fadeInterval: NodeJS.Timeout | null = null; +// // Apply basic styling to all elements +// scrollElements.forEach(element => { +// element.classList.add('void-scrollable-element'); +// }); - const fadeIn = () => { - if (fadeInterval) clearInterval(fadeInterval); +// // Only initialize fade effects for elements that haven't been initialized yet +// scrollElements.forEach(element => { +// if (!(element as any).__scrollbarCleanup) { +// let fadeTimeout: NodeJS.Timeout | null = null; +// let fadeInterval: NodeJS.Timeout | null = null; - let step = 0; - fadeInterval = setInterval(() => { - if (step <= 10) { - element.classList.remove(`show-scrollbar-${step - 1}`); - element.classList.add(`show-scrollbar-${step}`); - step++; - } else { - clearInterval(fadeInterval!); - } - }, 10); - }; +// const fadeIn = () => { +// if (fadeInterval) clearInterval(fadeInterval); - const fadeOut = () => { - if (fadeInterval) clearInterval(fadeInterval); +// let step = 0; +// fadeInterval = setInterval(() => { +// if (step <= 10) { +// element.classList.remove(`show-scrollbar-${step - 1}`); +// element.classList.add(`show-scrollbar-${step}`); +// step++; +// } else { +// clearInterval(fadeInterval!); +// } +// }, 10); +// }; - let step = 10; - fadeInterval = setInterval(() => { - if (step >= 0) { - element.classList.remove(`show-scrollbar-${step + 1}`); - element.classList.add(`show-scrollbar-${step}`); - step--; - } else { - clearInterval(fadeInterval!); - } - }, 60); - }; +// const fadeOut = () => { +// if (fadeInterval) clearInterval(fadeInterval); - const onMouseEnter = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - fadeIn(); - }; +// let step = 10; +// fadeInterval = setInterval(() => { +// if (step >= 0) { +// element.classList.remove(`show-scrollbar-${step + 1}`); +// element.classList.add(`show-scrollbar-${step}`); +// step--; +// } else { +// clearInterval(fadeInterval!); +// } +// }, 60); +// }; - const onMouseLeave = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - fadeTimeout = setTimeout(() => { - fadeOut(); - }, 10); - }; +// const onMouseEnter = () => { +// if (fadeTimeout) clearTimeout(fadeTimeout); +// if (fadeInterval) clearInterval(fadeInterval); +// fadeIn(); +// }; - element.addEventListener('mouseenter', onMouseEnter); - element.addEventListener('mouseleave', onMouseLeave); +// const onMouseLeave = () => { +// if (fadeTimeout) clearTimeout(fadeTimeout); +// fadeTimeout = setTimeout(() => { +// fadeOut(); +// }, 10); +// }; - // Store cleanup function - const cleanup = () => { - element.removeEventListener('mouseenter', onMouseEnter); - element.removeEventListener('mouseleave', onMouseLeave); - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - element.classList.remove('void-scrollable-element'); - // Remove any remaining show-scrollbar classes - for (let i = 0; i <= 10; i++) { - element.classList.remove(`show-scrollbar-${i}`); - } - }; +// element.addEventListener('mouseenter', onMouseEnter); +// element.addEventListener('mouseleave', onMouseLeave); - // Store the cleanup function on the element for later use - (element as any).__scrollbarCleanup = cleanup; - } - }); - }; +// // Store cleanup function +// const cleanup = () => { +// element.removeEventListener('mouseenter', onMouseEnter); +// element.removeEventListener('mouseleave', onMouseLeave); +// if (fadeTimeout) clearTimeout(fadeTimeout); +// if (fadeInterval) clearInterval(fadeInterval); +// element.classList.remove('void-scrollable-element'); +// // Remove any remaining show-scrollbar classes +// for (let i = 0; i <= 10; i++) { +// element.classList.remove(`show-scrollbar-${i}`); +// } +// }; - // Initialize for the first time - initializeScrollbarStyles(); +// // Store the cleanup function on the element for later use +// (element as any).__scrollbarCleanup = cleanup; +// } +// }); +// }; - // Set up mutation observer to do the same - const observer = new MutationObserver(() => { - initializeScrollbarStyles(); - }); +// // Initialize for the first time +// initializeScrollbarStyles(); - // Start observing the container for child changes - observer.observe(containerRef.current, { - childList: true, - subtree: true - }); +// // Set up mutation observer to do the same +// const observer = new MutationObserver(() => { +// initializeScrollbarStyles(); +// }); - return () => { - observer.disconnect(); - // Your existing cleanup code... - if (containerRef.current) { - const scrollElements = [ - ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []), - ...Array.from(containerRef.current.querySelectorAll(overflowSelector)) - ]; - scrollElements.forEach(element => { - if ((element as any).__scrollbarCleanup) { - (element as any).__scrollbarCleanup(); - } - }); - } - }; - }, [containerRef]); -}; +// // Start observing the container for child changes +// observer.observe(containerRef.current, { +// childList: true, +// subtree: true +// }); + +// return () => { +// observer.disconnect(); +// // Your existing cleanup code... +// if (containerRef.current) { +// const scrollElements = [ +// ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []), +// ...Array.from(containerRef.current.querySelectorAll(overflowSelector)) +// ]; +// scrollElements.forEach(element => { +// if ((element as any).__scrollbarCleanup) { +// (element as any).__scrollbarCleanup(); +// } +// }); +// } +// }; +// }, [containerRef]); +// }; 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 86% 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 index 99dbf770..b4940e9e 100644 --- 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 @@ -9,8 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js' import '../styles.css' import { useCallback, useEffect, useState, useRef } from 'react'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; -import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js'; import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; +import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js'; export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() @@ -39,7 +40,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const commandService = accessor.get('ICommandService') const commandBarService = accessor.get('IVoidCommandBarService') const voidModelService = accessor.get('IVoidModelService') - const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() // useEffect(() => { @@ -211,38 +212,47 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { if (!isADiffZoneInAnyFile) return null - const acceptAllButton = + + // const rejectAllButton = + + const acceptAllButton = - Accept File - + /> - - const rejectAllButton = + /> const acceptRejectAllButtons =
{acceptAllButton} @@ -273,7 +283,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
{upButton} {downButton} - + {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` : streamState === 'streaming' ? @@ -289,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { {/*
*/} {rightButton} {/*
*/} - + {currFileIdx !== null ? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` @@ -299,7 +309,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
- return
+ return
{showAcceptRejectAll && acceptRejectAllButtons} {leftRightUpDownButtons} 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..a737ccc7 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidSelectionHelper.tsx @@ -0,0 +1,170 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + + +import { useAccessor, useActiveURI, 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') + + useEffect(() => { + const disposable = commandService.onWillExecuteCommand(e => { + if (e.commandId === VOID_CTRL_L_ACTION_ID || e.commandId === VOID_CTRL_K_ACTION_ID) { + setClickState('clickedOption') + } + }); + + return () => { + disposable.dispose(); + }; + }, [commandService, setClickState]); + + + // 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? +
+ + {dividerHTML} + +
{ + setClickState('init'); + }} + > + +
+ + + 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 6fe499e7..860ee06b 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,12 +5,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' -import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' +import { VoidButtonBgDarken, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' -import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react' -import { useScrollbarStyles } from '../util/useScrollbarStyles.js' +import { X, RefreshCw, Loader2, Check, MoveRight, PlusCircle, MinusCircle, Download, Trash, StopCircle, Square, ExternalLink } from 'lucide-react' import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js' import { URI } from '../../../../../../../base/common/uri.js' import { env } from '../../../../../../../base/common/process.js' @@ -18,11 +17,13 @@ import { ModelDropdown } from './ModelDropdown.js' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { WarningBox } from './WarningBox.js' import { os } from '../../../../common/helpers/systemInfo.js' -import { IconX } from '../sidebar-tsx/SidebarChat.js' +import { IconLoading, IconX } from '../sidebar-tsx/SidebarChat.js' +import { getModelCapabilities, getProviderCapabilities, ollamaRecommendedModels, VoidStaticModelInfo } from '../../../../common/modelCapabilities.js' + const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { - return
+ return
{leftButton ? leftButton : null} {text} @@ -98,58 +99,134 @@ const RefreshableModels = () => { -const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () => void }) => { +const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => { + const [dashOffset, setDashOffset] = useState(40); + + useEffect(() => { + const startTime = performance.now(); + const duration = 500; // 500ms animation + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const newOffset = 40 - (progress * 40); + + setDashOffset(newOffset); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + const animationId = requestAnimationFrame(animate); + return () => cancelAnimationFrame(animationId); + }, []); + + return
+ + + + {text} +
+} + + +const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, text?: React.ReactNode } & React.ButtonHTMLAttributes) => { + + return + +} + + +// shows a providerName dropdown if no `providerName` is given +const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => { const accessor = useAccessor() const settingsStateService = accessor.get('IVoidSettingsService') const settingsState = useSettingsState() + const [isOpen, setIsOpen] = useState(false) + // const providerNameRef = useRef(null) - const [providerName, setProviderName] = useState(null) + const [userChosenProviderName, setUserChosenProviderName] = useState(null) - const modelNameRef = useRef(null) + const providerName = permanentProviderName ?? userChosenProviderName; + const [modelName, setModelName] = useState('') const [errorString, setErrorString] = useState('') + if (!providerName) { return null; } + + const numModels = settingsState.settingsOfProvider[providerName].models.length + + if (!isOpen) { + return
setIsOpen(true)} + + > +
+ {numModels > 0 ? `Add a different model?` : `Add a model`} +
+
+ } + return <> -
+
- {/* provider */} - setProviderName(pn)} - getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} - getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} - getOptionsEqual={(a, b) => a === b} - className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root + {/* X button + */} + + {/* provider input */} + {!permanentProviderName && + setUserChosenProviderName(pn)} + getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} + getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} + getOptionsEqual={(a, b) => a === b} + className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px] `} - arrowTouchesText={false} - /> - {/* <_VoidSelectBox - onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state - onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])} - options={providerOptions} - /> */} - - {/* model */} -
- -
+ } - {/* button */} - { - const modelName = modelNameRef.current?.value + {/* model input */} + + {/* add button */} + { if (providerName === null) { setErrorString('Please select a provider.') return @@ -160,17 +237,20 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () } // if model already exists here if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) { - setErrorString(`This model already exists under ${providerName}.`) + // setErrorString(`This model already exists under ${providerName}.`) + setErrorString(`This model already exists.`) return } settingsStateService.addModel(providerName, modelName) - onSubmit() + setIsOpen(false) + setErrorString('') + setModelName('') }} - >Add model + /> - -
+ + {!errorString ? null :
{errorString} @@ -180,17 +260,6 @@ const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () } -const AddModelMenuFull = () => { - const [open, setOpen] = useState(false) - - return
- {open ? - setOpen(false)} onClose={() => setOpen(false)} /> - : setOpen(true)}>Add Model - } -
-} - export const ModelDump = () => { @@ -200,7 +269,7 @@ export const ModelDump = () => { const settingsState = useSettingsState() // a dump of all the enabled providers' models - const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] + const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] for (let providerName of providerNames) { const providerSettings = settingsState.settingsOfProvider[providerName] // if (!providerSettings.enabled) continue @@ -277,6 +346,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider // placeholder={`${providerTitle} ${settingTitle} (${placeholder})`} placeholder={`${settingTitle} (${placeholder})`} passwordBlur={isPasswordField} + compact={true} /> {subTextMd === undefined ? null :
@@ -286,7 +356,52 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider } -const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { +// const OldSettingsForProvider = ({ providerName, showProviderTitle }: { providerName: ProviderName, showProviderTitle: boolean }) => { +// const voidSettingsState = useSettingsState() + +// const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel' + +// // const accessor = useAccessor() +// // const voidSettingsService = accessor.get('IVoidSettingsService') + +// // const { enabled } = voidSettingsState.settingsOfProvider[providerName] +// const settingNames = customSettingNamesOfProvider(providerName) + +// const { title: providerTitle } = displayInfoOfProviderName(providerName) + +// return
+ +//
+// {showProviderTitle &&

{providerTitle}

} + +// {/* enable provider switch */} +// {/* { +// const enabledRef = voidSettingsService.state.settingsOfProvider[providerName].enabled +// voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabledRef) +// }, [voidSettingsService, providerName])} +// size='sm+' +// /> */} +//
+ +//
+// {/* settings besides models (e.g. api key) */} +// {settingNames.map((settingName, i) => { +// return +// })} + +// {needsModel ? +// providerName === 'ollama' ? +// +// : +// : null} +//
+//
+// } + +const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => { const voidSettingsState = useSettingsState() const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel' @@ -299,10 +414,10 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = const { title: providerTitle } = displayInfoOfProviderName(providerName) - return
+ return
-

{providerTitle}

+ {showProviderTitle &&

{providerTitle}

} {/* enable provider switch */} {/* })} - {needsModel ? + {showProviderSuggestions && needsModel ? providerName === 'ollama' ? : @@ -335,7 +450,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = export const VoidProviderSettings = ({ providerNames }: { providerNames: ProviderName[] }) => { return <> {providerNames.map(providerName => - + )} } @@ -418,7 +533,7 @@ export const FeaturesTab = () => {

Models

- + @@ -429,14 +544,7 @@ export const FeaturesTab = () => { {/*

{`Instructions:`}

*/} {/*

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

*/}

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

-
- - - - - - {/* TODO we should create UI for downloading models without user going into terminal */} -
+ {ollamaSetupInstructions} @@ -533,9 +641,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'} +
+
+
@@ -547,41 +672,91 @@ export const FeaturesTab = () => { } - +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 type TransferFilesInfo = { from: URI, to: URI }[] -const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => { +const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => { if (os === null) throw new Error(`One-click switch is not possible in this environment.`) if (os === 'mac') { const homeDir = env['HOME'] if (!homeDir) throw new Error(`$HOME not found`) - return [{ - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), - }] + + if (fromEditor === 'VS Code') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Cursor') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Windsurf') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } } if (os === 'linux') { const homeDir = env['HOME'] if (!homeDir) throw new Error(`variable for $HOME location not found`) - return [{ - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), - }] + + if (fromEditor === 'VS Code') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Cursor') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Windsurf') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } } if (os === 'windows') { @@ -590,73 +765,115 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): Transfe const userprofile = env['USERPROFILE'] if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`) - return [{ - from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), - }, { - from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), - }] + if (fromEditor === 'VS Code') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Cursor') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + }] + } else if (fromEditor === 'Windsurf') { + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + }] + } } - throw new Error(`os '${os}' not recognized`) + throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`) } -let transferTheseFiles: TransferFilesInfo = [] -let transferError: string | null = null - -try { transferTheseFiles = transferTheseFilesOfOS(os) } -catch (e) { transferError = e + '' } - -const OneClickSwitchButton = () => { +const OneClickSwitchButton = ({ fromEditor = 'VS Code' }: { fromEditor?: TransferEditorType }) => { const accessor = useAccessor() const fileService = accessor.get('IFileService') - const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' }) + const [transferState, setTransferState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' }) + + let transferTheseFiles: TransferFilesInfo = []; + let editorError: string | null = null; + + try { + transferTheseFiles = transferTheseFilesOfOS(os, fromEditor) + } catch (e) { + editorError = e + '' + } if (transferTheseFiles.length === 0) return <> - + - - const onClick = async () => { + if (transferState.type !== 'done') return - if (state.type !== 'done') return - - setState({ type: 'loading' }) + setTransferState({ type: 'loading' }) let errAcc = '' for (let { from, to } of transferTheseFiles) { console.log('transferring', from, to) - // not sure if this can fail, just wrapping it with try/catch for now - try { await fileService.copy(from, to, true) } - catch (e) { errAcc += e + '\n' } + // Check if the source file exists before attempting to copy + try { + const exists = await fileService.exists(from) + if (exists) { + // Ensure the destination directory exists + const toParent = URI.joinPath(to, '..') + const toParentExists = await fileService.exists(toParent) + if (!toParentExists) { + await fileService.createFolder(toParent) + } + await fileService.copy(from, to, true) + } else { + console.log(`Skipping file that doesn't exist: ${from.toString()}`) + } + } + catch (e) { + console.error('Error copying file:', e) + errAcc += `Error copying ${from.toString()}: ${e}\n` + } } + + // Even if some files were missing, consider it a success if no actual errors occurred const hadError = !!errAcc if (hadError) { - setState({ type: 'done', error: errAcc }) + setTransferState({ type: 'done', error: errAcc }) } else { - setState({ type: 'justfinished' }) - setTimeout(() => { setState({ type: 'done' }); }, 3000) + setTransferState({ type: 'justfinished' }) + setTimeout(() => { setTransferState({ type: 'done' }); }, 3000) } } return <> - - {state.type === 'done' ? 'Transfer my Settings' - : state.type === 'loading' ? 'Transferring...' - : state.type === 'justfinished' ? 'Success!' + + {transferState.type === 'done' ? `Transfer from ${fromEditor}` + : transferState.type === 'loading' ? Transferring + : transferState.type === 'justfinished' ? : null } - - {state.type === 'done' && state.error ? : null} + + {transferState.type === 'done' && transferState.error ? : null} } @@ -668,12 +885,15 @@ const GeneralTab = () => { const nativeHostService = accessor.get('INativeHostService') return <> - -

One-Click Switch

-

{`Transfer your settings from VS Code to Void in one click.`}

- +

{`Transfer your settings from another editor to Void in one click.`}

+ +
+ + + +
@@ -683,24 +903,24 @@ const GeneralTab = () => {

{`IDE settings, keyboard settings, and theme customization.`}

- { commandService.executeCommand('workbench.action.openSettings') }}> + { commandService.executeCommand('workbench.action.openSettings') }}> General Settings - +
- { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}> + { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}> Keyboard Settings - +
- { commandService.executeCommand('workbench.action.selectTheme') }}> + { commandService.executeCommand('workbench.action.selectTheme') }}> Theme Settings - +
- { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}> + { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}> Open Logs - +
@@ -722,11 +942,16 @@ export const Settings = () => { const [tab, setTab] = useState('models') - const containerRef = useRef(null) - useScrollbarStyles(containerRef) + + const deleteme = true + if (deleteme) { + return
+ +
+ } return
-
+
@@ -739,10 +964,10 @@ export const Settings = () => { {/* tabs */}
- -
@@ -770,3 +995,664 @@ export const Settings = () => {
} + + +const FADE_DURATION_MS = 2000 + + +const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes) => { + const [opacity, setOpacity] = useState(0) + + useEffect(() => { + + const timeout = setTimeout(() => { + setOpacity(1) + }, delayMs) + + return () => clearTimeout(timeout) + }, [setOpacity, delayMs]) + + + return ( +
+ {children} +
+ ) +} + +// Onboarding +// OnboardingPage +// title: +// div +// "Welcome to Void" +// image +// content:<> +// title +// content +// prev/next + +// OnboardingPage +// title: +// div +// "How would you like to use Void?" +// content: +// ModelQuestionContent +// | +// div +// "I want to:" +// div +// "Use the smartest models" +// "Keep my data fully private" +// "Save money" +// "I don't know" +// | div +// | div +// "We recommend using " +// "Set API" +// | div +// "" +// | div +// +// title +// content +// prev/next +// +// OnboardingPage +// title +// content +// prev/next + + +const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes) => { + return ( + + ) +} + +const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes) => { + return ( + + ) +} + +const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes) => { + return ( + + ) +} + + +const ollamaSetupInstructions =
+
+
+
+
+
+
+ +const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => { + + + // for now just link to the ollama download page + return + + + + // if (isModelInstalled) { + // return
+ + // Uninstall + + // { + + // setIsModelInstalling(false); + // }} + // /> + + //
+ // } + + + + // else if (isModelInstalling) { + // return
+ + // {`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`} + + // { + // // abort() + + // // TODO!!!!!!!!!!! don't do this + // setIsModelInstalling(false); + // }} + // /> + + //
+ // } + + + // else if (!isModelInstalled) { + + // return
+ + // Download ({sizeGb} Gb) + + // { + // // this is a check for whether the model was installed: + + // if (isModelInstalling) return + + + // // TODO!!!!!! don't do this + + + // // install(modelname), callback = setIsModelInstalling(false); + + // setIsModelInstalling(true); + // }} + // /> + + //
+ + // } + + // return <> + + +} + + +const YesNoText = ({ val }: { val: boolean | null }) => { + + return
+ { + val === true ? "Yes" + : val === false ? 'No' + : "Yes*" + } +
+ +} + + + +const abbreviateNumber = (num: number): string => { + if (num >= 1000000) { + // For millions + return Math.floor(num / 1000000) + 'M'; + } else if (num >= 1000) { + // For thousands + return Math.floor(num / 1000) + 'K'; + } else { + // For numbers less than 1000 + return num.toString(); + } +} + +const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => { + + const accessor = useAccessor() + const voidSettingsService = accessor.get('IVoidSettingsService') + const voidSettingsState = useSettingsState() + const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName) + // const providerCapabilities = getProviderCapabilities(providerName) + + + // info used to show the table + const infoOfModelName: Record = {} + + voidSettingsState.settingsOfProvider[providerName].models.forEach(m => { + infoOfModelName[m.modelName] = { + showAsDefault: m.isDefault, + isDownloaded: true + } + }) + + // special case columns for ollama; show recommended models as default + if (providerName === 'ollama') { + for (const modelName of ollamaRecommendedModels) { + if (modelName in infoOfModelName) continue + infoOfModelName[modelName] = { + ...infoOfModelName[modelName], + showAsDefault: true, + } + } + } + + return + + + + + + + + + {/* */} + {isDetectableLocally && } + {providerName === 'ollama' && } + + + + {Object.keys(infoOfModelName).map(modelName => { + const { showAsDefault, isDownloaded } = infoOfModelName[modelName] + + const { + downloadable, + cost, + supportsTools, + supportsFIM, + reasoningCapabilities, + contextWindow, + + isUnrecognizedModel, + maxOutputTokens, + supportsSystemMessage, + } = getModelCapabilities(providerName, modelName) + + + const removeModelButton = + + + + return ( + + + + + + + + {/* */} + {isDetectableLocally && } + {providerName === 'ollama' && } + + + ) + })} + + + + + +
Models OfferedCost/MContextChatAgentAutotabReasoningDetectedDownload
+ {!showAsDefault && removeModelButton} + {modelName} + ${cost.output ?? ''}{contextWindow ? abbreviateNumber(contextWindow) : ''}{!!isDownloaded ? : <>} + +
+ +
+} + + + + +type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all' + +const VoidOnboarding = () => { + + const accessor = useAccessor() + const voidSettingsService = accessor.get('IVoidSettingsService') + + const voidSettingsState = useSettingsState() + const isOnboardingComplete = false // voidSettingsService._isOnboardingComplete + + if (isOnboardingComplete) { + return null + } + + const [pageIndex, setPageIndex] = useState(0) + + + const skipButton = { setPageIndex(pageIndex + 1) }} /> + + + // page 1 state + const [wantToUseOption, setWantToUseOption] = useState('smart') + + // page 2 state + const [selectedProviderName, setSelectedProviderName] = useState(null) + + 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 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 + + const didFillInSelectedProviderSettings = !!(didFillInProviderSettings && isApiKeyLongEnoughIfApiKeyExists && isAtLeastOneModel) + + const prevAndNextButtons =
+ { setPageIndex(pageIndex - 1) }} + /> + { setPageIndex(pageIndex + 1) }} + disabled={pageIndex === 2 && !didFillInSelectedProviderSettings} + /> +
+ + + // 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 affordable options.", + all: "", + } + + // can be md + 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 for free.", + all: "", + } + + // set the selected provider name appropriately + useEffect(() => { + if (wantToUseOption && providerNamesOfWantToUseOption[wantToUseOption].length > 0) { + setSelectedProviderName(providerNamesOfWantToUseOption[wantToUseOption][0]); + } else { + setSelectedProviderName(null); + } + }, [wantToUseOption]); + + // set wantToUseOption to smart when page changes + useEffect(() => { + setWantToUseOption(wantToUseOption); + }, [pageIndex]); + + + // 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:
+ + Welcome to Void + + Image +
+ Void Logo +
+
+ + { setPageIndex(pageIndex + 1) }}> + Get Started + +
, + 1:
+ + + +
AI Preferences
+ +
+ +
What are you looking for in an AI model?
+ +
+
{ setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }} + className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5" + > + 🧠 +

Intelligence

+

{basicDescOfWantToUseOption['smart']}

+
+ +
{ setWantToUseOption('private'); setPageIndex(pageIndex + 1); }} + className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5" + > + 🔒 +

Privacy

+

{basicDescOfWantToUseOption['private']}

+
+ +
{ setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }} + className="flex flex-col items-center justify-center p-6 rounded-md transition-all duration-300 cursor-pointer md:aspect-[8/7] border-void-border-1 border bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent hover:from-[#0e70c0]/25 hover:via-[#0e70c0]/10 hover:to-[#0e70c0]/5 dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 dark:hover:from-[#0e70c0]/30 dark:hover:via-[#0e70c0]/15 dark:hover:to-[#0e70c0]/5" + > + 💵 +

Low-Cost

+

{basicDescOfWantToUseOption['cheap']}

+
+
+
+ +
+ +
+ {prevAndNextButtons} +
+ +
, + 2:
+ + +
Choose a Provider
+ +
+ + + + +
+ + {/* Provider Buttons */} +
+ + {(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => { + const isSelected = selectedProviderName === providerName + + return ( + + ) + })} + +
+ + {/* Description */} +
+ +
+ +
+ +
+ + + {/* ModelsTable and ProviderFields */} + {selectedProviderName &&
+ + + {/* Models Table */} + + + + {/* Add provider section - simplified styling */} +
+
+ Add {displayInfoOfProviderName(selectedProviderName).title} + + + {selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''} + +
+ + {selectedProviderName && + + } + + {/* Button and status indicators */} + {!didFillInProviderSettings ?

Please fill in all fields to continue

+ : !isAtLeastOneModel ?

Please add a model to continue

+ : !isApiKeyLongEnoughIfApiKeyExists ?

Please enter a valid API key

+ :
} +
+ + +
} + +
+ + {prevAndNextButtons} +
, + // 2.5:
+ // + //
Autocomplete
+ + //
+ //

Void offers free autocomplete with locally hosted models

+ //

[have buttons for Ollama install Qwen2.5coder3b and memory requirements]

+ + //
+ //
+ + // {prevAndNextButtons} + //
, + 3:
+ +
Settings and Themes
+ +
+

Transfer your settings from an existing editor?

+ + + +
+ +
+ + {prevAndNextButtons} +
, + 4:
+ + Jump in + + + + Enter the Void + + + {prevAndNextButtons} +
, + } + + + return
+ {contentOfIdx[pageIndex]} +
+} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx new file mode 100644 index 00000000..d86a753a --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/VoidTooltip.tsx @@ -0,0 +1,97 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Tooltip } from 'react-tooltip'; +import 'react-tooltip/dist/react-tooltip.css'; +import { useIsDark } from '../util/services.js'; + +/** + * Creates a configured global tooltip component with consistent styling + * To use: + * 1. Mount a Tooltip with some id eg id='void-tooltip' + * 2. Add data-tooltip-id="void-tooltip" and data-tooltip-content="Your tooltip text" to any element + */ +export const VoidTooltip = () => { + + + const isDark = useIsDark() + + return ( + + // use native colors so we don't have to worry about @@void-scope styles + // --void-bg-1: var(--vscode-input-background); + // --void-bg-1-alt: var(--vscode-badge-background); + // --void-bg-2: var(--vscode-sideBar-background); + // --void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%); + // --void-bg-3: var(--vscode-editor-background); + + // --void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%); + // --void-fg-1: var(--vscode-editor-foreground); + // --void-fg-2: var(--vscode-input-foreground); + // --void-fg-3: var(--vscode-input-placeholderForeground); + // /* --void-fg-4: var(--vscode-tab-inactiveForeground); */ + // --void-fg-4: var(--vscode-list-deemphasizedForeground); + + // --void-warning: var(--vscode-charts-yellow); + + // --void-border-1: var(--vscode-commandCenter-activeBorder); + // --void-border-2: var(--vscode-commandCenter-border); + // --void-border-3: var(--vscode-commandCenter-inactiveBorder); + // --void-border-4: var(--vscode-editorGroup-border); + + <> + + + + + + + + ); +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/index.tsx new file mode 100644 index 00000000..f51adfe6 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-tooltip/index.tsx @@ -0,0 +1,9 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { VoidTooltip } from './VoidTooltip.js' + +export const mountVoidTooltip = mountFnGenerator(VoidTooltip) 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..be66a10b 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -7,9 +7,10 @@ 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/void-tooltip/index.tsx', './src2/quick-edit-tsx/index.tsx', './src2/diff/index.tsx', ], diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index ad453853..7bcb0ff0 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -189,7 +189,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/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts index 1fe2e88a..803b2b8b 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarPane.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts @@ -108,7 +108,7 @@ export const VOID_VIEW_ID = VOID_VIEW_CONTAINER_ID const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const container = viewContainerRegistry.registerViewContainer({ id: VOID_VIEW_CONTAINER_ID, - title: nls.localize2('voidContainer', 'Void Chat'), // this is used to say "Void" (Ctrl + L) + title: nls.localize2('voidContainer', 'Chat'), // this is used to say "Void" (Ctrl + L) ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VOID_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true, orientation: Orientation.HORIZONTAL, diff --git a/src/vs/workbench/contrib/void/browser/tooltipService.ts b/src/vs/workbench/contrib/void/browser/tooltipService.ts new file mode 100644 index 00000000..2bd64251 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/tooltipService.ts @@ -0,0 +1,55 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { mountVoidTooltip } from './react/out/void-tooltip/index.js'; +import { h, getActiveWindow } from '../../../../base/browser/dom.js'; + +// Tooltip contribution that mounts the component at startup +export class TooltipContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.voidTooltip'; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.initializeTooltip(); + } + + private initializeTooltip(): void { + // Get the active window reference for multi-window support + const targetWindow = getActiveWindow(); + + // Find the monaco-workbench element using the proper window reference + const workbench = targetWindow.document.querySelector('.monaco-workbench'); + + if (workbench) { + // Create a container element for the tooltip using h function + const tooltipContainer = h('div.void-tooltip-container').root; + workbench.appendChild(tooltipContainer); + + // Mount the React component + this.instantiationService.invokeFunction((accessor: ServicesAccessor) => { + const result = mountVoidTooltip(tooltipContainer, accessor); + if (result && typeof result.dispose === 'function') { + this._register(toDisposable(result.dispose)); + } + }); + + // Register cleanup for the DOM element + this._register(toDisposable(() => { + if (tooltipContainer.parentElement) { + tooltipContainer.parentElement.removeChild(tooltipContainer); + } + })); + } + } +} + +// Register the contribution to be initialized during the AfterRestored phase +registerWorkbenchContribution2(TooltipContribution.ID, TooltipContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 9054450b..2576f097 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -46,6 +46,12 @@ import './metricsPollService.js' // helper services import './helperServices/consistentItemService.js' +// register selection helper +import './voidSelectionHelperWidget.js' + +// register tooltip service +import './tooltipService.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 529680da..02eadbde 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..cb26f9b7 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidSelectionHelperWidget.ts @@ -0,0 +1,282 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { getLengthOfTextPx } from './editCodeService.js'; + + +const minDistanceFromRightPx = 400; +const minLeftPx = 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 + root.style.pointerEvents = 'none'; + root.style.marginLeft = '16px'; + + // 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; + } + + if (this._editor.getModel().uri.scheme !== 'file') { + 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()!; + + // get the longest length of the nearest neighbors of the target + const { tabSize: numSpacesInTab } = model.getFormattingOptions(); + const spaceWidth = this._editor.getOption(EditorOption.fontInfo).spaceWidth; + const tabWidth = numSpacesInTab * spaceWidth; + const numLinesModel = model.getLineCount() + + // Calculate right edge of visible editor area + const editorWidthPx = this._editor.getLayoutInfo().width; + const maxLeftPx = editorWidthPx - minDistanceFromRightPx + + // returns the position where the box should go on the targetLine + const getBoxPosition = (targetLine: number): { top: number, left: number } => { + + const targetPosition = this._editor.getScrolledVisiblePosition({ lineNumber: targetLine, column: 1 }) ?? { left: 0, top: 0 }; + + const { top: targetTop, left: targetLeft } = targetPosition + + let targetWidth = 0; + for (let i = targetLine; i <= targetLine + 1; i++) { + + // if not in range, continue + if (!(i >= 1) || !(i <= numLinesModel)) continue; + + const content = model.getLineContent(i); + const currWidth = getLengthOfTextPx({ + tabWidth, + spaceWidth, + content + }) + + targetWidth = Math.max(targetWidth, currWidth); + } + + return { + top: targetTop, + left: targetLeft + targetWidth, + }; + + } + + + // Calculate the middle line of the selection + const startLine = selection.startLineNumber; + const endLine = selection.endLineNumber; + // const middleLine = Math.floor(startLine + (endLine - startLine) / 2); + const targetLine = endLine - startLine + 1 <= 2 ? startLine : startLine + 2; + + let boxPos = getBoxPosition(targetLine); + + // if the position of the box is too far to the right, keep searching for a good position + const lineDeltasToTry = [-1, -2, -3, 1, 2, 3]; + + if (boxPos.left > maxLeftPx) { + for (const lineDelta of lineDeltasToTry) { + + boxPos = getBoxPosition(targetLine + lineDelta); + if (boxPos.left <= maxLeftPx) { + break; + } + } + } + if (boxPos.left > maxLeftPx) { // if still not found, make it 2 lines before + boxPos = getBoxPosition(targetLine - 2) + } + + + // Position the helper element at the end of the middle line but ensure it's visible + const xPosition = Math.max(Math.min(boxPos.left, maxLeftPx), minLeftPx); + const yPosition = boxPos.top; + + // Update the React component position + this._rootHTML.style.left = `${xPosition}px`; + this._rootHTML.style.top = `${yPosition}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/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index a8ade5f7..899fb94f 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -6,6 +6,46 @@ import { FeatureName, ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js'; + + + +export const defaultProviderSettings = { + anthropic: { + apiKey: '', + }, + openAI: { + apiKey: '', + }, + deepseek: { + apiKey: '', + }, + ollama: { + endpoint: 'http://127.0.0.1:11434', + }, + vLLM: { + endpoint: 'http://localhost:8000', + }, + openRouter: { + apiKey: '', + }, + openAICompatible: { + endpoint: '', + apiKey: '', + }, + gemini: { + apiKey: '', + }, + groq: { + apiKey: '', + }, + xAI: { + apiKey: '' + }, +} as const + + + + export const defaultModelsOfProvider = { openAI: [ // https://platform.openai.com/docs/models/gp 'o3-mini', @@ -68,19 +108,22 @@ export const defaultModelsOfProvider = { - - - -type ModelOptions = { +export type VoidStaticModelInfo = { // not stateful contextWindow: number; // input tokens maxOutputTokens: number | null; // output tokens, defaults to 4092 - cost: { // <-- UNUSED + cost: { // <-- UNUSED input: number; output: number; cache_read?: number; cache_write?: number; } - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; + + downloadable: false | { + sizeGb: number | 'not-known' + } + + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special parameter + // supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style'; supportsFIM: boolean; reasoningCapabilities: false | { @@ -108,18 +151,19 @@ type ProviderReasoningIOSettings = { | { nameOfFieldInDelta?: undefined, needsManualParse?: true, }; } -type ProviderSettings = { +type VoidStaticProviderInfo = { // doesn't change (not stateful) providerReasoningIOSettings?: ProviderReasoningIOSettings; // input/output settings around thinking (allowed to be empty) - only applied if the model supports reasoning output - modelOptions: { [key: string]: ModelOptions }; - modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null; + modelOptions: { [key: string]: VoidStaticModelInfo }; + modelOptionsFallback: (modelName: string, fallbackKnownValues?: Partial) => (VoidStaticModelInfo & { modelName: string }) | null; } -const modelOptionsDefaults: ModelOptions = { +const modelOptionsDefaults: VoidStaticModelInfo = { contextWindow: 32_000, maxOutputTokens: 4_096, cost: { input: 0, output: 0 }, + downloadable: false, supportsSystemMessage: false, supportsFIM: false, reasoningCapabilities: false, @@ -242,21 +286,24 @@ const openSourceModelOptions_assumingOAICompat = { contextWindow: 128_000, maxOutputTokens: 8_192, }, -} as const satisfies { [s: string]: Omit } +} as const satisfies { [s: string]: Partial } -const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { +const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = (modelName, fallbackKnownValues) => { + const lower = modelName.toLowerCase() - const toFallback = (opts: Omit): ModelOptions & { modelName: string } => { + const toFallback = (opts: Omit): VoidStaticModelInfo & { modelName: string } => { return { modelName, ...opts, supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false, cost: { input: 0, output: 0 }, + downloadable: false, + ...fallbackKnownValues } } if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower)) @@ -313,6 +360,7 @@ const anthropicModelOptions = { contextWindow: 200_000, maxOutputTokens: 8_192, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'separated', reasoningCapabilities: { @@ -327,6 +375,7 @@ const anthropicModelOptions = { contextWindow: 200_000, maxOutputTokens: 8_192, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'separated', reasoningCapabilities: false, @@ -335,6 +384,7 @@ const anthropicModelOptions = { contextWindow: 200_000, maxOutputTokens: 8_192, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'separated', reasoningCapabilities: false, @@ -343,20 +393,22 @@ const anthropicModelOptions = { contextWindow: 200_000, maxOutputTokens: 4_096, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'separated', reasoningCapabilities: false, }, 'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + downloadable: false, maxOutputTokens: 4_096, supportsFIM: false, supportsSystemMessage: 'separated', reasoningCapabilities: false, } -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const anthropicSettings: ProviderSettings = { +const anthropicSettings: VoidStaticProviderInfo = { providerReasoningIOSettings: { input: { includeInPayload: (reasoningInfo) => { @@ -388,6 +440,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing contextWindow: 128_000, maxOutputTokens: 100_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'developer-role', reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it @@ -396,6 +449,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing contextWindow: 200_000, maxOutputTokens: 100_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'developer-role', reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, @@ -404,6 +458,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing contextWindow: 128_000, maxOutputTokens: 16_384, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -412,6 +467,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing contextWindow: 128_000, maxOutputTokens: 65_536, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + downloadable: false, supportsFIM: false, supportsSystemMessage: false, // does not support any system reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, @@ -420,14 +476,15 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing contextWindow: 128_000, maxOutputTokens: 16_384, cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', // ?? reasoningCapabilities: false, }, -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const openAISettings: ProviderSettings = { +const openAISettings: VoidStaticProviderInfo = { modelOptions: openAIModelOptions, modelOptionsFallback: (modelName) => { const lower = modelName.toLowerCase() @@ -446,13 +503,14 @@ const xAIModelOptions = { contextWindow: 131_072, maxOutputTokens: null, // 131_072, cost: { input: 2.00, output: 10.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, }, -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const xAISettings: ProviderSettings = { +const xAISettings: VoidStaticProviderInfo = { modelOptions: xAIModelOptions, modelOptionsFallback: (modelName) => { const lower = modelName.toLowerCase() @@ -470,6 +528,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 1_048_576, maxOutputTokens: 8_192, cost: { input: 0, output: 0 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -478,6 +537,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 1_048_576, maxOutputTokens: 8_192, // 8_192, cost: { input: 0.10, output: 0.40 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -486,6 +546,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 1_048_576, maxOutputTokens: 8_192, // 8_192, cost: { input: 0.075, output: 0.30 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -494,6 +555,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 1_048_576, maxOutputTokens: 8_192, // 8_192, cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -502,6 +564,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 2_097_152, maxOutputTokens: 8_192, cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -510,13 +573,14 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing contextWindow: 1_048_576, maxOutputTokens: 8_192, cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, }, -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const geminiSettings: ProviderSettings = { +const geminiSettings: VoidStaticProviderInfo = { modelOptions: geminiModelOptions, modelOptionsFallback: (modelName) => { return null } } @@ -530,17 +594,19 @@ const deepseekModelOptions = { contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing maxOutputTokens: 8_000, // 8_000, cost: { cache_read: .07, input: .27, output: 1.10, }, + downloadable: false, }, 'deepseek-reasoner': { ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 64_000, maxOutputTokens: 8_000, // 8_000, cost: { cache_read: .14, input: .55, output: 2.19, }, + downloadable: false, }, -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const deepseekSettings: ProviderSettings = { +const deepseekSettings: VoidStaticProviderInfo = { modelOptions: deepseekModelOptions, providerReasoningIOSettings: { // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model @@ -555,6 +621,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq contextWindow: 128_000, maxOutputTokens: 32_768, // 32_768, cost: { input: 0.59, output: 0.79 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -563,6 +630,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq contextWindow: 128_000, maxOutputTokens: 8_192, cost: { input: 0.05, output: 0.08 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -571,6 +639,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq contextWindow: 128_000, maxOutputTokens: null, // not specified? cost: { input: 0.79, output: 0.79 }, + downloadable: false, supportsFIM: false, // unfortunately looks like no FIM support on groq supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -579,12 +648,13 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq contextWindow: 128_000, maxOutputTokens: null, // not specified? cost: { input: 0.29, output: 0.39 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags }, -} as const satisfies { [s: string]: ModelOptions } -const groqSettings: ProviderSettings = { +} as const satisfies { [s: string]: VoidStaticModelInfo } +const groqSettings: VoidStaticProviderInfo = { providerReasoningIOSettings: { input: { includeInPayload: (reasoningInfo) => { @@ -600,23 +670,67 @@ const groqSettings: ProviderSettings = { modelOptionsFallback: (modelName) => { return null } } +const ollamaModelOptions = { + 'qwen2.5-coder:3b': { + contextWindow: 32_000, + maxOutputTokens: null, + cost: { input: 0, output: 0 }, + downloadable: { sizeGb: 1.9 }, + supportsFIM: true, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, + 'qwen2.5-coder': { + contextWindow: 128_000, + maxOutputTokens: null, + cost: { input: 0, output: 0 }, + downloadable: { sizeGb: 4.7 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + }, + 'qwq': { + contextWindow: 128_000, + maxOutputTokens: 32_000, + cost: { input: 0, output: 0 }, + downloadable: { sizeGb: 20 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, + }, + 'deepseek-r1': { + contextWindow: 128_000, + maxOutputTokens: null, + cost: { input: 0, output: 0 }, + downloadable: { sizeGb: 4.7 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, + }, + +} as const satisfies Record + +export const ollamaRecommendedModels = ['qwen2.5-coder:3b', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[] + + // ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ---------------- -const vLLMSettings: ProviderSettings = { + +const vLLMSettings: VoidStaticProviderInfo = { // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, }, - modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), - modelOptions: {}, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), + modelOptions: {}, // TODO } -const ollamaSettings: ProviderSettings = { +const ollamaSettings: VoidStaticProviderInfo = { // reasoning: we need to filter out reasoning tags manually providerReasoningIOSettings: { output: { needsManualParse: true }, }, - modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), - modelOptions: {}, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }), + modelOptions: ollamaModelOptions, } -const openaiCompatible: ProviderSettings = { +const openaiCompatible: VoidStaticProviderInfo = { // reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), modelOptions: {}, @@ -629,6 +743,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 128_000, maxOutputTokens: null, cost: { input: 0, output: 0 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -637,6 +752,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 1_048_576, maxOutputTokens: null, cost: { input: 0, output: 0 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -645,6 +761,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 1_048_576, maxOutputTokens: null, cost: { input: 0, output: 0 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -653,6 +770,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 1_048_576, maxOutputTokens: null, cost: { input: 0, output: 0 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -662,11 +780,13 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 128_000, maxOutputTokens: null, cost: { input: 0.8, output: 2.4 }, + downloadable: false, }, 'anthropic/claude-3.7-sonnet:thinking': { contextWindow: 200_000, maxOutputTokens: null, cost: { input: 3.00, output: 15.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: { // same as anthropic, see above @@ -681,6 +801,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 200_000, maxOutputTokens: null, cost: { input: 3.00, output: 15.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking @@ -689,6 +810,7 @@ const openRouterModelOptions_assumingOpenAICompat = { contextWindow: 200_000, maxOutputTokens: null, cost: { input: 3.00, output: 15.00 }, + downloadable: false, supportsFIM: false, supportsSystemMessage: 'system-role', reasoningCapabilities: false, @@ -699,22 +821,25 @@ const openRouterModelOptions_assumingOpenAICompat = { maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, reasoningCapabilities: false, + downloadable: false, }, 'qwen/qwen-2.5-coder-32b-instruct': { ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 33_000, maxOutputTokens: null, cost: { input: 0.07, output: 0.16 }, + downloadable: false, }, 'qwen/qwq-32b': { ...openSourceModelOptions_assumingOAICompat['qwq'], contextWindow: 33_000, maxOutputTokens: null, cost: { input: 0.07, output: 0.16 }, + downloadable: false, } -} as const satisfies { [s: string]: ModelOptions } +} as const satisfies { [s: string]: VoidStaticModelInfo } -const openRouterSettings: ProviderSettings = { +const openRouterSettings: VoidStaticProviderInfo = { // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models providerReasoningIOSettings: { input: { @@ -741,7 +866,7 @@ const openRouterSettings: ProviderSettings = { // ---------------- model settings of everything above ---------------- -const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = { +const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProviderInfo } = { openAI: openAISettings, anthropic: anthropicSettings, xAI: xAISettings, @@ -767,8 +892,10 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting // ---------------- exports ---------------- // returns the capabilities and the adjusted modelName if it was a fallback -export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => { +export const getModelCapabilities = (providerName: ProviderName, modelName: string): VoidStaticModelInfo & { modelName: string; isUnrecognizedModel: boolean } => { + const lowercaseModelName = modelName.toLowerCase() + const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] // search model options object directly first diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index ded53adc..1301cdbc 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -92,6 +92,10 @@ export const voidTools = { } }, + // pathname_search: { + // name: 'pathname_search', + // description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`, + search_pathnames_only: { name: 'search_pathnames_only', description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`, @@ -102,6 +106,8 @@ export const voidTools = { }, }, + + search_files: { name: 'search_files', description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 28fb0fb7..b4ddb7ab 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IMetricsService } from './metricsService.js'; -import { getModelCapabilities } from './modelCapabilities.js'; +import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js'; import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js'; // name is the name in the dropdown @@ -71,10 +71,10 @@ export interface IVoidSettingsService { -const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { +const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[] }) => { const { existingModels } = options - const existingModelsMap: Record = {} + const existingModelsMap: Record = {} for (const existingModel of existingModels) { existingModelsMap[existingModel.modelName] = existingModel } @@ -363,7 +363,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const modelIdx = models.findIndex(m => m.modelName === modelName) if (modelIdx === -1) return const newIsHidden = !models[modelIdx].isHidden - const newModels: VoidModelInfo[] = [ + const newModels: VoidStatefulModelInfo[] = [ ...models.slice(0, modelIdx), { ...models[modelIdx], isHidden: newIsHidden }, ...models.slice(modelIdx + 1, Infinity) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index bbb274a4..47c1f515 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -4,49 +4,13 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { defaultModelsOfProvider } from './modelCapabilities.js'; +import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js'; import { VoidSettingsState } from './voidSettingsService.js' type UnionOfKeys = T extends T ? keyof T : never; -export const defaultProviderSettings = { - anthropic: { - apiKey: '', - }, - openAI: { - apiKey: '', - }, - deepseek: { - apiKey: '', - }, - ollama: { - endpoint: 'http://127.0.0.1:11434', - }, - vLLM: { - endpoint: 'http://localhost:8000', - }, - openRouter: { - apiKey: '', - }, - openAICompatible: { - endpoint: '', - apiKey: '', - }, - gemini: { - apiKey: '', - }, - groq: { - apiKey: '', - }, - xAI: { - apiKey: '' - }, -} as const - - - export type ProviderName = keyof typeof defaultProviderSettings export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] @@ -64,7 +28,7 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => { -export type VoidModelInfo = { // <-- STATEFUL +export type VoidStatefulModelInfo = { // <-- STATEFUL modelName: string, isDefault: boolean, // whether or not it's a default for its provider isHidden: boolean, // whether or not the user is hiding it (switched off) @@ -75,7 +39,7 @@ export type VoidModelInfo = { // <-- STATEFUL type CommonProviderSettings = { _didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields - models: VoidModelInfo[], + models: VoidStatefulModelInfo[], } export type SettingsAtProvider = CustomProviderSettings & CommonProviderSettings @@ -227,7 +191,7 @@ const defaultCustomSettings: Record = { } -const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => { +const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidStatefulModelInfo[] } => { return { models: defaultModelNames.map((modelName, i) => ({ modelName, @@ -334,6 +298,8 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { export const refreshableProviderNames = localProviderNames export type RefreshableProviderName = typeof refreshableProviderNames[number] +// models that come with download buttons +export const hasDownloadButtonsOnModelsProviderNames = ['ollama'] as const satisfies ProviderName[] @@ -389,6 +355,7 @@ export type GlobalSettings = { enableFastApply: boolean; chatMode: ChatMode; autoApprove: boolean; + showInlineSuggestions: boolean; } export const defaultGlobalSettings: GlobalSettings = { @@ -399,6 +366,7 @@ export const defaultGlobalSettings: GlobalSettings = { enableFastApply: true, chatMode: 'agent', autoApprove: false, + showInlineSuggestions: true, } export type GlobalSettingName = keyof GlobalSettings diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index f3c532ac..4f1d3661 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -195,6 +195,192 @@ const prepareMessages_addSystemInstructions = ({ return { messages: newMessages, separateSystemMessageStr } } + + + + +// // convert messages as if about to send to openai +// /* +// reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps +// openai MESSAGE (role=assistant): +// "tool_calls":[{ +// "type": "function", +// "id": "call_12345xyz", +// "function": { +// "name": "get_weather", +// "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" +// }] + +// openai RESPONSE (role=user): +// { "role": "tool", +// "tool_call_id": tool_call.id, +// "content": str(result) } + +// also see +// openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +// openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command +// */ + +// type PrepareMessagesToolsOpenAI = ( +// Exclude | { +// role: 'assistant', +// content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; +// tool_calls?: { +// type: 'function'; +// id: string; +// function: { +// name: string; +// arguments: string; +// } +// }[] +// } | { +// role: 'tool', +// tool_call_id: string; +// content: string; +// } +// )[] +// const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => { + +// const newMessages: PrepareMessagesToolsOpenAI = []; + +// for (let i = 0; i < messages.length; i += 1) { +// const currMsg = messages[i] + +// if (currMsg.role !== 'tool') { +// newMessages.push(currMsg) +// continue +// } + +// // edit previous assistant message to have called the tool +// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined +// if (prevMsg?.role === 'assistant') { +// prevMsg.tool_calls = [{ +// type: 'function', +// id: currMsg.id, +// function: { +// name: currMsg.name, +// arguments: JSON.stringify(currMsg.params) +// } +// }] +// } + +// // add the tool +// newMessages.push({ +// role: 'tool', +// tool_call_id: currMsg.id, +// content: currMsg.content || EMPTY_TOOL_CONTENT, +// }) +// } +// return { messages: newMessages } + +// } + + +// // convert messages as if about to send to anthropic +// /* +// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +// anthropic MESSAGE (role=assistant): +// "content": [{ +// "type": "text", +// "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." +// }, { +// "type": "tool_use", +// "id": "toolu_01A09q90qw90lq917835lq9", +// "name": "get_weather", +// "input": { "location": "San Francisco, CA", "unit": "celsius" } +// }] +// anthropic RESPONSE (role=user): +// "content": [{ +// "type": "tool_result", +// "tool_use_id": "toolu_01A09q90qw90lq917835lq9", +// "content": "15 degrees" +// }] +// */ + +// type PrepareMessagesToolsAnthropic = ( +// Exclude | { +// role: 'assistant', +// content: string | ( +// | AnthropicReasoning +// | { +// type: 'text'; +// text: string; +// } +// | { +// type: 'tool_use'; +// name: string; +// input: Record; +// id: string; +// })[] +// } | { +// role: 'user', +// content: string | ({ +// type: 'text'; +// text: string; +// } | { +// type: 'tool_result'; +// tool_use_id: string; +// content: string; +// })[] +// } +// )[] +// /* +// Converts: + +// assistant: ...content +// tool: (id, name, params) +// -> +// assistant: ...content, call(name, id, params) +// user: ...content, result(id, content) +// */ +// const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => { +// const newMessages: PrepareMessagesToolsAnthropic = messages; + + +// for (let i = 0; i < newMessages.length; i += 1) { +// const currMsg = newMessages[i] + +// if (currMsg.role !== 'tool') continue + +// const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + +// if (prevMsg?.role === 'assistant') { +// if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] +// prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) +// } + +// // turn each tool into a user message with tool results at the end +// newMessages[i] = { +// role: 'user', +// content: [ +// ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const, +// ] +// } +// } +// return { messages: newMessages } +// } + + + + +// type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI + +// const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'TODO-yes-but-we-handle-it-manually' | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => { +// if (!supportsTools) { +// return { messages: messages } +// } +// else if (supportsTools === 'anthropic-style') { +// return prepareMessages_tools_anthropic({ messages }) +// } +// else if (supportsTools === 'openai-style') { +// return prepareMessages_tools_openai({ messages }) +// } +// else { +// throw new Error(`supportsTools type not recognized`) +// } +// } + + // remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent const prepareMessages_anthropicReasoning = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => { const newMessages: InternalLLMChatMessage[] = [] diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index edcc1a9a..d3d5a527 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -8,9 +8,9 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; -import { ChatMode, defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; -import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; +import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractToolsWrapper } from './extractGrammar.js'; @@ -66,7 +66,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } else if (providerName === 'gemini') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else if (providerName === 'deepseek') { const thisConfig = settingsOfProvider[providerName]