Merge pull request #393 from voideditor/model-selection

Quasar + misc fixes before 1.0.3
This commit is contained in:
Andrew Pareles 2025-04-12 14:07:11 -07:00 committed by GitHub
commit 5d7f8fc91f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 975 additions and 731 deletions

View file

@ -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 {}

View file

@ -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 (<div className=''>
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
<div className={`w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ${className}`}>
{/* header */}
<div className={`select-none flex items-center min-h-[24px] ${!isDropdown ? 'mx-1' : ''}`}>
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
@ -1346,17 +1347,18 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) =>
if (lintErrors.length === 0) return null;
const [isOpen, setIsOpen] = useState(false);
return (
<div className="w-full px-2">
<div className="w-full border-l border-r border-b border-void-border-2 rounded bg-void-bg-3 overflow-hidden">
<ToolHeaderWrapper className='!border-t-0' title={'Lint errors'} desc1={''} isOpen={isOpen} onClick={() => { setIsOpen(o => !o) }} >
<div className="text-xs text-void-fg-4 opacity-80 border-l-2 border-void-warning px-2 py-0.5 flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap">
{lintErrors.map((error, i) => (
<div key={i}>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</div>
</ToolHeaderWrapper>
</div>
</div>
)

View file

@ -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 (
<label className="inline-flex items-center">
<label className="inline-flex items-center" {...props}>
<div
onClick={() => !disabled && onChange(!value)}
className={`

View file

@ -0,0 +1,745 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useEffect, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Check, ExternalLink, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
export const VoidOnboarding = () => {
const voidSettingsState = useSettingsState()
const isOnboardingComplete = voidSettingsState.globalSettings.isOnboardingComplete
const isDark = useIsDark()
return (
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
>
<VoidOnboardingContent />
</div>
</div>
)
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => {
setOpacity(1)
}, delayMs)
return () => clearTimeout(timeout)
}, [setOpacity, delayMs])
return (
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
{children}
</div>
)
}
// 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<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 bg-[#0e70c0] enabled:hover:bg-[#1177cb] disabled:opacity-50 disabled:cursor-not-allowed rounded text-white duration-300 transition-all"
{...props.disabled && {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': 'Disabled (Please enter all required fields or choose another Provider)',
'data-tooltip-place': 'top',
}}
{...props}
>
Next
</button>
)
}
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2 duration-300 transition-all"
{...props}
>
Skip
</button>
)
}
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded text-void-fg-3 opacity-80 hover:brightness-110 duration-300 transition-all"
{...props}
>
Back
</button>
)
}
const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, className = '', }: {
top?: React.ReactNode,
bottom?: React.ReactNode,
content?: React.ReactNode,
hasMaxWidth?: boolean,
className?: string,
}) => {
return (
<div className={`min-h-full flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<FadeIn className='w-full pt-16'>{top}</FadeIn>
<FadeIn className='w-full my-auto'>{content}</FadeIn>
<div className='w-full pb-8'>{bottom}</div>
</div>
)
}
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // 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);
// }}
// />
// </div>
// }
// return <></>
}
const YesNoText = ({ val }: { val: boolean | null }) => {
return <div
className={
val === true ? "text text-green-500"
: val === false ? 'text-red-500'
: "text text-yellow-500"
}
>
{
val === true ? "Yes"
: val === false ? 'No'
: "Yes*"
}
</div>
}
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<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
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 <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
const capabilities = getModelCapabilities(providerName, modelName)
const {
downloadable,
cost,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = capabilities
// TODO update this when tools work
const supportsTools = !!!((capabilities as unknown as any).supportsTools)
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsTools} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
type WantToUseOption = 'smart' | 'private' | 'cheap' | 'all'
const VoidOnboardingContent = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const [pageIndex, setPageIndex] = useState(0)
// page 1 state
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
// page 2 state
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(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 = <div className="max-w-[600px] w-full mx-auto flex flex-col items-end">
<div className="flex items-center gap-4">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Fully private and hosted on your computer/network.",
cheap: "Free and low-cost options.",
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 with Ollama or vLLM 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]);
// reset the page to page 0 if the user redos onboarding
useEffect(() => {
if (!voidSettingsState.globalSettings.isOnboardingComplete) {
setPageIndex(0)
}
}, [setPageIndex, voidSettingsState.globalSettings.isOnboardingComplete])
// TODO add a description next to the skip button saying (you can always restart the onboarding in Settings)
const contentOfIdx: { [pageIndex: number]: React.ReactNode } = {
// 0: <OnboardingPageShell
// top={
// <div className='bg-green-600 h-6 w-32' />
// }
// content={
// <div className='bg-red-600 h-[10000px] w-32' />
// }
// bottom={
// <div className='bg-blue-600 h-6 w-32' />
// }
// />,
0: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Welcome to Void</div>
}
content={
<FadeIn
delayMs={500}
className="text-center"
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</FadeIn>
}
bottom={
''
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={
<FadeIn className='flex flex-col items-center'>
<div className="text-5xl font-light text-center">AI Preferences</div>
<div className="mt-[10%] text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
<div className="flex justify-center w-full md:flex-nowrap md:max-w-[80%] max-w-[90%] gap-4">
<div
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🧠</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
</div>
<div
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🔒</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
</div>
<div
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="w-full max-w-[250px] h-full relative p-6 aspect-[8/7] border border-void-border-1 rounded-md group flex flex-col items-center justify-center cursor-pointer"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">💵</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Affordability</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
</div>
</div>
</FadeIn>
}
content={<></>}
/>,
2: <OnboardingPageShell
top={
<div className='flex flex-col items-center'>
{/* Title */}
<div className="text-5xl font-light text-center">Choose a Provider</div>
{/* Preference Selector */}
<div className="mt-6 mb-6 mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
<button
onClick={() => {
setWantToUseOption('smart');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'smart'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Intelligent
</button>
<button
onClick={() => {
setWantToUseOption('private');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'private'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Private
</button>
<button
onClick={() => {
setWantToUseOption('cheap');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'cheap'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Low-Cost
</button>
<button
onClick={() => {
setWantToUseOption('all')
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'all'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
All
</button>
</div>
{/* Provider Buttons */}
<div
key={wantToUseOption}
className="mb-2 flex flex-wrap items-center w-full"
>
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
const isSelected = selectedProviderName === providerName
return (
<button
key={providerName}
onClick={() => setSelectedProviderName(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
`}
>
{displayInfoOfProviderName(providerName).title}
</button>
)
})}
</div>
{/* Description */}
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8'>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
</div>
</div>}
</div>
}
bottom={
prevAndNextButtons
}
/>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
top={
<div>
<div className="text-5xl font-light text-center">Settings and Themes</div>
<div className="mt-8 text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="VS Code" />
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Cursor" />
<OneClickSwitchButton className='w-full px-4 py-2' fromEditor="Windsurf" />
</div>
</div>
}
bottom={prevAndNextButtons}
/>,
4: <OnboardingPageShell
top={
<div className="text-5xl font-light text-center">Jump in</div>
}
content={
<div
className="text-center"
onClick={() => {
// TODO make a fadeout effect
voidSettingsService.setGlobalSetting('isOnboardingComplete', true)
}}
>
Enter the Void
</div>
}
bottom={
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
}
/>,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
</div>
}

View file

@ -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)

View file

@ -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<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
@ -176,6 +174,10 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
const numModels = settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
}
if (!isOpen) {
return <div
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
@ -240,7 +242,11 @@ const AddModelInputBox = ({ providerName: permanentProviderName, className, comp
}
settingsStateService.addModel(providerName, modelName)
setIsOpen(false)
setShowCheckmark(true)
setTimeout(() => {
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 <div key={`${modelName}${providerName}`}
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
@ -292,7 +307,7 @@ export const ModelDump = () => {
>
{/* left part is width:full */}
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
</div>
{/* right part is anything that fits */}
@ -307,10 +322,14 @@ export const ModelDump = () => {
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
value={value}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
data-tooltip-id='void-tooltip'
data-tooltip-place='right'
data-tooltip-content={tooltipName}
/>
<div className={`w-5 flex items-center justify-center`}>
@ -405,7 +424,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
// </div >
// }
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 = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
const RedoOnboardingButton = ({ className }: { className?: string }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <div
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', false) }}
>
See onboarding screen?
</div>
}
export const FeaturesTab = () => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
@ -537,7 +579,8 @@ export const FeaturesTab = () => {
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelInputBox className='my-4' compact />
<AddModelInputBox className='mt-4' compact />
<RedoOnboardingButton className='mt-2 mb-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
@ -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<TabName>('models')
const deleteme = true
if (deleteme) {
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ width: '100%', height: '100%' }}>
<VoidOnboarding />
</div>
}
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
@ -1013,669 +1048,3 @@ export const Settings = () => {
</div>
}
const FADE_DURATION_MS = 2000
const FadeIn = ({ children, className, delayMs = 0, ...props }: { children: React.ReactNode, delayMs?: number, className?: string } & React.HTMLAttributes<HTMLDivElement>) => {
const [opacity, setOpacity] = useState(0)
useEffect(() => {
const timeout = setTimeout(() => {
setOpacity(1)
}, delayMs)
return () => clearTimeout(timeout)
}, [setOpacity, delayMs])
return (
<div className={className} style={{ opacity, transition: `opacity ${FADE_DURATION_MS}ms ease-in-out` }} {...props}>
{children}
</div>
)
}
// 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<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-accent hover:bg-void-accent/90 text-white"
{...props}
>
Next
</button>
)
}
const SkipButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3 text-void-fg-2"
{...props}
>
Skip
</button>
)
}
const PreviousButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return (
<button
onClick={onClick}
className="px-6 py-2 rounded bg-void-bg-2 hover:bg-void-bg-3"
{...props}
>
Previous
</button>
)
}
const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center text-void-fg-2 hover:text-void-fg-1"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // 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);
// }}
// />
// </div>
// }
// return <></>
}
const YesNoText = ({ val }: { val: boolean | null }) => {
return <div
className={
val === true ? "text text-green-500"
: val === false ? 'text-red-500'
: "text text-yellow-500"
}
>
{
val === true ? "Yes"
: val === false ? 'No'
: "Yes*"
}
</div>
}
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<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
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 <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{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 = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsTools || null} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
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 = <SkipButton onClick={() => { setPageIndex(pageIndex + 1) }} />
// page 1 state
const [wantToUseOption, setWantToUseOption] = useState<WantToUseOption>('smart')
// page 2 state
const [selectedProviderName, setSelectedProviderName] = useState<ProviderName | null>(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 = <div className="self-end flex items-center gap-1 pb-8">
<PreviousButton
onClick={() => { setPageIndex(pageIndex - 1) }}
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
// cannot be md
const basicDescOfWantToUseOption: { [wantToUseOption in WantToUseOption]: string } = {
smart: "Models with the best performance on benchmarks.",
private: "Fully private and hosted on your computer/network.",
cheap: "Free and 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: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn >
<div className="text-5xl font-light mb-6 mt-12 text-center">Welcome to Void</div>
{/* <div className="w-8 h-8 mb-2">
<VoidImage className='h-full w-full' />
</div> */}
</FadeIn>
<FadeIn delayMs={1000} className="text-center pb-8" onClick={() => { setPageIndex(pageIndex + 1) }}>
Get Started
</FadeIn>
</div>,
1: <div className="max-w-full w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn>
<div className="text-5xl font-light mb-6 mt-12 text-center">AI Preferences</div>
<div className="flex flex-col items-center w-full mx-auto">
<div className="text-base text-void-fg-2 mb-8 text-center">What are you looking for in an AI model?</div>
<div className="flex md:flex-nowrap gap-4 w-full md:max-w-[80%] max-w-[90%]">
<div
onClick={() => { 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"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🧠</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Intelligence</h3>
<p className="text-center text-root text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['smart']}</p>
</div>
<div
onClick={() => { 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"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">🔒</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Privacy</h3>
<p className="text-center text-sm text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['private']}</p>
</div>
<div
onClick={() => { 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"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/15 via-[#0e70c0]/5 to-transparent dark:from-[#0e70c0]/20 dark:via-[#0e70c0]/10 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-100"></div>
<div className="absolute inset-0 bg-gradient-to-br from-[#0e70c0]/25 via-[#0e70c0]/10 to-[#0e70c0]/5 dark:from-[#0e70c0]/30 dark:via-[#0e70c0]/15 dark:to-[#0e70c0]/5 transition-opacity duration-300 ease-in-out opacity-0 group-hover:opacity-100"></div>
<span className="text-5xl mb-4 relative z-10">💵</span>
<h3 className="text-xl font-medium mb-3 relative z-10">Low-Cost</h3>
<p className="text-center text-sm text-void-fg-2 relative z-10">{basicDescOfWantToUseOption['cheap']}</p>
</div>
</div>
</div>
</FadeIn>
<div className="max-w-[600px] w-full flex flex-col items-center justify-between">
{prevAndNextButtons}
</div>
</div>,
2: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn className="flex flex-col gap-2 w-full">
<div className="text-5xl font-light mb-6 mt-12 text-center">Choose a Provider</div>
<div className="mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md">
<button
onClick={() => {
setWantToUseOption('smart');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'smart'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Intelligent
</button>
<button
onClick={() => {
setWantToUseOption('private');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'private'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Private
</button>
<button
onClick={() => {
setWantToUseOption('cheap');
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'cheap'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
Low-Cost
</button>
<button
onClick={() => {
setWantToUseOption('all')
}}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors
${wantToUseOption === 'all'
? 'bg-zinc-700/10 dark:bg-zinc-300/10 text-white font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}
`}
>
All
</button>
</div>
{/* Provider Buttons */}
<div
key={wantToUseOption}
className="flex flex-wrap items-center mt-4 min-h-[37px] w-full"
>
{(wantToUseOption === 'all' ? providerNames : providerNamesOfWantToUseOption[wantToUseOption]).map((providerName) => {
const isSelected = selectedProviderName === providerName
return (
<button
key={providerName}
onClick={() => setSelectedProviderName(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-colors duration-150 border
${isSelected ? 'bg-[#0e70c0] text-white shadow-sm border-[#0e70c0]/80' : 'bg-[#0e70c0]/10 text-void-fg-3 hover:bg-[#0e70c0]/30 border-[#0e70c0]/20'}
`}
>
{displayInfoOfProviderName(providerName).title}
</button>
)
})}
</div>
{/* Description */}
<div className="text-left text-sm text-void-fg-3 px-2 py-1">
<div className='pl-4 select-text'>
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</div>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8'>
<div className='text-base font-semibold'>
Add {displayInfoOfProviderName(selectedProviderName).title}
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <div className="mt-2"><AnimatedCheckmarkButton text='Added' /></div>}
</div>
</div>}
</FadeIn>
{prevAndNextButtons}
</div>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn>
<div className="text-5xl font-light mb-6 mt-12">Settings and Themes</div>
<div className="text-center flex flex-col items-center gap-4 w-full max-w-md mx-auto">
<h4 className="text-void-fg-3 mb-4">Transfer your settings from an existing editor?</h4>
<OneClickSwitchButton className='w-full' fromEditor="VS Code" />
<OneClickSwitchButton className='w-full' fromEditor="Cursor" />
<OneClickSwitchButton className='w-full' fromEditor="Windsurf" />
</div>
</FadeIn>
{prevAndNextButtons}
</div>,
4: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
<FadeIn className="text-5xl font-light mb-6 mt-12">
Jump in
</FadeIn>
<FadeIn className="text-center">
Enter the Void
</FadeIn>
{prevAndNextButtons}
</div>,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-auto flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
</div>
}

View file

@ -48,7 +48,7 @@ export const VoidTooltip = () => {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
z-index: 999;
z-index: 999999;
}
#void-tooltip {

View file

@ -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',
],

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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<VoidStaticModelInfo> }
@ -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)
}

View file

@ -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

View file

@ -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': {},

View file

@ -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