diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index b60563b0..ac4608c6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -616,12 +616,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { delete this._currentlyRunningToolInterruptor[threadId]; } toolResult = await result // ts is bad... await is needed + + if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here } catch (error) { - if (interrupted) { - // the tool result is added when we stop running - return { interrupted: true } - } + if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here + + const errorMessage = getErrorMessage(error) this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, }) return {} 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 87983026..6ae68be1 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 @@ -678,7 +678,8 @@ const ToolHeaderWrapper = ({ onClick, isOpen, isRejected, -}: ToolHeaderParams) => { + className, // applies to the main content +}: ToolHeaderParams & { className?: string }) => { const [isOpen_, setIsOpen] = useState(false); const isExpanded = isOpen !== undefined ? isOpen : isOpen_ @@ -687,7 +688,7 @@ const ToolHeaderWrapper = ({ const isClickable = !!(isDropdown || onClick) return (
-
+
{/* header */}
@@ -1346,17 +1347,18 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => if (lintErrors.length === 0) return null; + const [isOpen, setIsOpen] = useState(false); + return (
-
- + { setIsOpen(o => !o) }} >
{lintErrors.map((error, i) => (
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
))}
+
-
) 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 d8a5bb79..b7e31757 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 @@ -418,6 +418,7 @@ export const VoidSwitch = ({ onChange, size = 'md', disabled = false, + ...props }: { value: boolean; onChange: (value: boolean) => void; @@ -425,7 +426,7 @@ export const VoidSwitch = ({ size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md'; }) => { return ( -
+ } + content={ +
{ + // TODO make a fadeout effect + voidSettingsService.setGlobalSetting('isOnboardingComplete', true) + }} + + > + Enter the Void +
+ } + bottom={ + { setPageIndex(pageIndex - 1) }} + /> + } + />, + } + + + return
+ {contentOfIdx[pageIndex]} +
+ +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/index.tsx new file mode 100644 index 00000000..71de53d3 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/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 { VoidOnboarding } from './VoidOnboarding.js' + +export const mountVoidOnboarding = mountFnGenerator(VoidOnboarding) 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 e193ecd9..776bb72c 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 @@ -3,22 +3,19 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.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 React, { useCallback, useEffect, useMemo, useState } from 'react' +import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' -import { VoidButtonBgDarken, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' +import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.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 { X, RefreshCw, Loader2, Check, } from 'lucide-react' import { URI } from '../../../../../../../base/common/uri.js' import { env } from '../../../../../../../base/common/process.js' import { ModelDropdown } from './ModelDropdown.js' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { WarningBox } from './WarningBox.js' import { os } from '../../../../common/helpers/systemInfo.js' -import { IconLoading, IconX } from '../sidebar-tsx/SidebarChat.js' -import { getModelCapabilities, getProviderCapabilities, ollamaRecommendedModels, VoidStaticModelInfo } from '../../../../common/modelCapabilities.js' +import { IconLoading } from '../sidebar-tsx/SidebarChat.js' const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -99,7 +96,7 @@ const RefreshableModels = () => { -const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => { +export const AnimatedCheckmarkButton = ({ text, className }: { text?: string, className?: string }) => { const [dashOffset, setDashOffset] = useState(40); useEffect(() => { @@ -157,7 +154,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t // shows a providerName dropdown if no `providerName` is given -const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => { +export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => { const accessor = useAccessor() const settingsStateService = accessor.get('IVoidSettingsService') @@ -165,6 +162,7 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp const settingsState = useSettingsState() const [isOpen, setIsOpen] = useState(false) + const [showCheckmark, setShowCheckmark] = useState(false) // const providerNameRef = useRef(null) const [userChosenProviderName, setUserChosenProviderName] = useState('anthropic') @@ -176,6 +174,10 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp const numModels = settingsState.settingsOfProvider[providerName].models.length + if (showCheckmark) { + return + } + if (!isOpen) { return
{ + setShowCheckmark(false) + setIsOpen(false) + }, 1500) setErrorString('') setModelName('') }} @@ -284,7 +290,16 @@ export const ModelDump = () => { const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName + const providerTitle = displayInfoOfProviderName(providerName).title + const disabled = !providerEnabled + const value = disabled ? false : !isHidden + + const tooltipName = ( + disabled ? `Add ${providerTitle} to enable` + : value === true ? 'Enabled' + : 'Disabled' + ) return
{ > {/* left part is width:full */}
- {isNewProviderName ? displayInfoOfProviderName(providerName).title : ''} + {isNewProviderName ? providerTitle : ''} {modelName}
{/* right part is anything that fits */} @@ -307,10 +322,14 @@ export const ModelDump = () => { {isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'} { settingsStateService.toggleModelHidden(providerName, modelName) }} disabled={disabled} size='sm' + + data-tooltip-id='void-tooltip' + data-tooltip-place='right' + data-tooltip-content={tooltipName} />
@@ -405,7 +424,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider //
// } -const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => { +export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => { const voidSettingsState = useSettingsState() const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel' @@ -527,6 +546,29 @@ const FastApplyMethodDropdown = () => { } +export const ollamaSetupInstructions =
+
+
+
+
+
+
+ + +const RedoOnboardingButton = ({ className }: { className?: string }) => { + const accessor = useAccessor() + const voidSettingsService = accessor.get('IVoidSettingsService') + return
{ voidSettingsService.setGlobalSetting('isOnboardingComplete', false) }} + > + See onboarding screen? +
+ +} + + + export const FeaturesTab = () => { const voidSettingsState = useSettingsState() const accessor = useAccessor() @@ -537,7 +579,8 @@ export const FeaturesTab = () => {

Models

- + + @@ -822,7 +865,7 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit } -const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => { +export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => { const accessor = useAccessor() const fileService = accessor.get('IFileService') @@ -959,14 +1002,6 @@ export const Settings = () => { const [tab, setTab] = useState('models') - - const deleteme = true - if (deleteme) { - return
- -
- } - return
@@ -1013,669 +1048,3 @@ 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
- - - {/*
- -
*/} -
- - { 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 w-full justify-center p-6 rounded-md cursor-pointer md:aspect-[8/7] border-void-border-1 border relative overflow-hidden group" - > -
-
- 🧠 -

Intelligence

-

{basicDescOfWantToUseOption['smart']}

-
- -
{ setWantToUseOption('private'); setPageIndex(pageIndex + 1); }} - className="flex flex-col items-center w-full justify-center p-6 rounded-md cursor-pointer md:aspect-[8/7] border-void-border-1 border relative overflow-hidden group" - > -
-
- 🔒 -

Privacy

-

{basicDescOfWantToUseOption['private']}

-
- -
{ setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }} - className="flex flex-col items-center w-full justify-center p-6 rounded-md cursor-pointer md:aspect-[8/7] border-void-border-1 border relative overflow-hidden group" - > -
-
- 💵 -

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 index d86a753a..cc376fde 100644 --- 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 @@ -48,7 +48,7 @@ export const VoidTooltip = () => { font-size: 12px; padding: 0px 8px; border-radius: 6px; - z-index: 999; + z-index: 999999; } #void-tooltip { 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 be66a10b..cc03b53d 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -11,6 +11,7 @@ export default defineConfig({ './src2/sidebar-tsx/index.tsx', './src2/void-settings-tsx/index.tsx', './src2/void-tooltip/index.tsx', + './src2/void-onboarding/index.tsx', './src2/quick-edit-tsx/index.tsx', './src2/diff/index.tsx', ], diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ab3ca373..9bc53227 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -211,6 +211,14 @@ export class ToolsService implements IToolsService { return { queryStr, searchInFolder, isRegex, pageNumber } }, + read_lint_errors: (params: RawToolParamsObj) => { + const { + uri: uriUnknown, + } = params + const uri = validateURI(uriUnknown) + return { uri } + }, + // --- create_file_or_folder: (params: RawToolParamsObj) => { @@ -322,6 +330,12 @@ export class ToolsService implements IToolsService { return { result: { queryStr, uris, hasNextPage } } }, + read_lint_errors: async ({ uri }) => { + await timeout(1000) + const { lintErrors } = this._getLintErrors(uri) + return { result: { lintErrors } } + }, + // --- create_file_or_folder: async ({ uri, isFolder }) => { @@ -359,20 +373,11 @@ export class ToolsService implements IToolsService { editCodeService.interruptURIStreaming({ uri: diffZoneURI }) } + // at end, get lint errors const lintErrorsPromise = applyDonePromise.then(async () => { - await timeout(500) - - const lintErrors = this.markerService - .read({ resource: uri }) - .map(l => ({ - code: typeof l.code === 'string' ? l.code : l.code?.value || '', - message: l.message, - startLineNumber: l.startLineNumber, - endLineNumber: l.endLineNumber, - } satisfies LintErrorItem)) - - if (!lintErrors.length) return { lintErrors: null } - return { lintErrors, } + await timeout(2000) + const { lintErrors } = this._getLintErrors(uri) + return { lintErrors } }) return { result: lintErrorsPromise, interruptTool } @@ -386,7 +391,11 @@ export class ToolsService implements IToolsService { const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' - const lintErrorsStr = (lintErrors: LintErrorItem[]) => lintErrors.map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`).join('\n\n') + const stringifyLintErrors = (lintErrors: LintErrorItem[]) => { + return lintErrors + .map((e, i) => `Error ${i + 1}:\nLines Affected: ${e.startLineNumber}-${e.endLineNumber}\nError message:${e.message}`) + .join('\n\n') + } // given to the LLM after the call this.stringOfResult = { @@ -406,6 +415,11 @@ export class ToolsService implements IToolsService { search_files: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, + read_lint_errors: (params, result) => { + return result.lintErrors ? + stringifyLintErrors(result.lintErrors) + : 'No lint errors found.' + }, // --- create_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully created.` @@ -416,7 +430,7 @@ export class ToolsService implements IToolsService { edit_file: (params, result) => { const lintErrsString = ( this.voidSettingsService.state.globalSettings.includeToolLintErrors ? - (result.lintErrors ? ` Lint errors found after change:\n${lintErrorsStr(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` + (result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : ` No lint errors found.`) : '') @@ -454,6 +468,21 @@ export class ToolsService implements IToolsService { } + private _getLintErrors(uri: URI): { lintErrors: LintErrorItem[] | null } { + const lintErrors = this.markerService + .read({ resource: uri }) + .map(l => ({ + code: typeof l.code === 'string' ? l.code : l.code?.value || '', + message: l.message, + startLineNumber: l.startLineNumber, + endLineNumber: l.endLineNumber, + } satisfies LintErrorItem)) + + if (!lintErrors.length) return { lintErrors: null } + return { lintErrors, } + } + + } registerSingleton(IToolsService, ToolsService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 2576f097..30cdfc8b 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -52,6 +52,9 @@ import './voidSelectionHelperWidget.js' // register tooltip service import './tooltipService.js' +// register onboarding service +import './voidOnboardingService.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/voidOnboardingService.ts b/src/vs/workbench/contrib/void/browser/voidOnboardingService.ts new file mode 100644 index 00000000..010498f4 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidOnboardingService.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 { mountVoidOnboarding } from './react/out/void-onboarding/index.js' +import { h, getActiveWindow } from '../../../../base/browser/dom.js'; + +// Onboarding contribution that mounts the component at startup +export class OnboardingContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.voidOnboarding'; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.initialize(); + } + + private initialize(): 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) { + + const onboardingContainer = h('div.void-onboarding-container').root; + workbench.appendChild(onboardingContainer); + + // Mount the React component + this.instantiationService.invokeFunction((accessor: ServicesAccessor) => { + const result = mountVoidOnboarding(onboardingContainer, accessor); + if (result && typeof result.dispose === 'function') { + this._register(toDisposable(result.dispose)); + } + }); + + // Register cleanup for the DOM element + this._register(toDisposable(() => { + if (onboardingContainer.parentElement) { + onboardingContainer.parentElement.removeChild(onboardingContainer); + } + })); + } + } +} + +// Register the contribution to be initialized during the AfterRestored phase +registerWorkbenchContribution2(OnboardingContribution.ID, OnboardingContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index daef05ab..eaf61c38 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -78,15 +78,17 @@ export const defaultModelsOfProvider = { vLLM: [ // autodetected ], openRouter: [ // https://openrouter.ai/models - 'anthropic/claude-3.7-sonnet:thinking', + // 'anthropic/claude-3.7-sonnet:thinking', 'anthropic/claude-3.7-sonnet', 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-r1', 'deepseek/deepseek-r1-zero:free', - 'mistralai/codestral-2501', - 'qwen/qwen-2.5-coder-32b-instruct', + 'openrouter/quasar-alpha', + 'google/gemini-2.5-pro-preview-03-25', + // 'mistralai/codestral-2501', + // 'qwen/qwen-2.5-coder-32b-instruct', // 'mistralai/mistral-small-3.1-24b-instruct:free', - 'google/gemini-2.0-flash-lite-preview-02-05:free', + // 'google/gemini-2.0-flash-lite-preview-02-05:free', // 'google/gemini-2.0-pro-exp-02-05:free', // 'google/gemini-2.0-flash-exp:free', ], @@ -285,6 +287,12 @@ const openSourceModelOptions_assumingOAICompat = { contextWindow: 128_000, maxOutputTokens: 8_192, }, + 'quasar': { // openrouter/quasar-alpha + supportsFIM: false, + supportsSystemMessage: 'system-role', + reasoningCapabilities: false, + contextWindow: 1_000_000, maxOutputTokens: 32_000, + } } as const satisfies { [s: string]: Partial } @@ -305,9 +313,6 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = ( ...fallbackKnownValues } } - if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower)) - return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat]) - if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25']) if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) @@ -338,6 +343,8 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = ( if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], }) // max output unclear + if (lower.includes('quasar') || lower.includes('quaser')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['quasar'] }) + if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini']) if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o']) if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini']) @@ -345,6 +352,9 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = ( if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini']) // if (lower.includes('o3')) return toFallback(openAIModelOptions['o3']) + if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower)) + return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat]) + return toFallback(modelOptionsDefaults) } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d8462e54..c3e910dc 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -11,16 +11,17 @@ import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { ChatMode } from '../voidSettingsTypes.js'; -// this is just for ease of readability +// Triple backtick wrapper used throughout the prompts for code blocks export const tripleTick = ['```', '```'] +// Maximum limits for directory structure information export const MAX_DIRSTR_CHARS_TOTAL_BEGINNING = 20_000 export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000 export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100 export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100 - +// Maximum character limits for prefix and suffix context export const MAX_PREFIX_SUFFIX_CHARS = 20_000 @@ -70,7 +71,7 @@ export const voidTools = { read_file: { name: 'read_file', - description: `Returns file contents of a given URI.`, + description: `Returns full contents of a given file.`, params: { ...uriParam('file'), start_line: { description: 'Optional. Default is 1. Start reading on this line.' }, @@ -123,11 +124,19 @@ export const voidTools = { }, }, + read_lint_errors: { + name: 'read_lint_errors', + description: `Returns all lint errors on a given file.`, + params: { + ...uriParam('file'), + }, + }, + // --- editing (create/delete) --- create_file_or_folder: { name: 'create_file_or_folder', - description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash.`, + description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`, params: { ...uriParam('file or folder'), }, @@ -581,10 +590,16 @@ Instructions: ` } -export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: { - selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string, - isOllamaFIM: false, // we require this be false for clarity -}) => { +export const ctrlKStream_userMessage = ({ + selection, + prefix, + suffix, + instructions, + // isOllamaFIM: false, // Remove unused variable + fimTags, + language }: { + selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string, + }) => { const { preTag, sufTag, midTag } = fimTags // prompt the model artifically on how to do FIM diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index f8fe0951..891db752 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -28,6 +28,7 @@ export type ToolCallParams = { 'get_dir_structure': { rootURI: URI }, 'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number }, 'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number }, + 'read_lint_errors': { uri: URI }, // --- 'edit_file': { uri: URI, changeDescription: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, @@ -43,6 +44,7 @@ export type ToolResultType = { 'get_dir_structure': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 'search_files': { uris: URI[], hasNextPage: boolean }, + 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index bf54f61a..3501b399 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -357,6 +357,7 @@ export type GlobalSettings = { autoApprove: boolean; showInlineSuggestions: boolean; includeToolLintErrors: boolean; + isOnboardingComplete: boolean; } export const defaultGlobalSettings: GlobalSettings = { @@ -369,6 +370,7 @@ export const defaultGlobalSettings: GlobalSettings = { autoApprove: false, showInlineSuggestions: true, includeToolLintErrors: true, + isOnboardingComplete: true, } export type GlobalSettingName = keyof GlobalSettings