diff --git a/product.json b/product.json index a2f96552..1cc09ea6 100644 --- a/product.json +++ b/product.json @@ -39,6 +39,7 @@ "linkProtectionTrustedDomains": [ "https://voideditor.com", "https://voideditor.dev", - "https://github.com/voideditor/void" + "https://github.com/voideditor/void", + "https://ollama.com" ] } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 83063dae..49a6dd09 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -356,11 +356,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { const threadId = this.state.currentThreadId const thread = this.state.allThreads[threadId] if (!thread) return - console.log('awaiting') const s = await thread.state.mountedInfo?.whenMounted - console.log('got!', s) if (!this.isCurrentlyFocusingMessage()) { - console.log('running focus!') s?.textAreaRef.current?.focus() } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 8c968c62..7bf94b47 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -6,10 +6,9 @@ import { useEffect, useRef, useState } from 'react'; import { useAccessor, useIsDark, useSettingsState } from '../util/services.js'; import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react'; -import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js'; -import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js'; +import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName } from '../../../../common/voidSettingsTypes.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; -import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js'; +import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js'; import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'; import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'; import { isLinux } from '../../../../../../../base/common/platform.js'; @@ -27,9 +26,10 @@ export const VoidOnboarding = () => {
@@ -90,6 +90,126 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch } // Onboarding + +// ============================================= +// New AddProvidersPage Component and helpers +// ============================================= + +const tabNames = ['Free', 'Paid', 'Local'] as const; + +type TabName = typeof tabNames[number] | 'Cloud/Other'; + +// Data for cloud providers tab +const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible']; + +// Data structures for provider tabs +const providerNamesOfTab: Record = { + Free: ['gemini', 'openRouter'], + Local: localProviderNames, + Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[], + 'Cloud/Other': cloudProviders, +}; + +const descriptionOfTab: Record = { + Free: `Providers with a 100% free tier. Add as many as you'd like!`, + Paid: `Connect directly with any provider (bring your own key).`, + Local: `Add as many local providers as you'd like! Running providers should appear automatically.`, + 'Cloud/Other': `Reach out for custom configuration requests.`, +}; + +const subtextMdOfTab: Record = { + Free: ` +Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. +We recommend using models down the line as you run out of free credits. More information [here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits). + +OpenRouter offers 50 free messages a day, and that increases to 1000 if you deposit $10. Only applies to models labeled \`:free\`. More information [here](https://openrouter.ai/docs/api-reference/limits). +`, + Paid: null, + Local: null, + 'Cloud/Other': null, +}; + +const featureNameMap: { display: string, featureName: FeatureName }[] = [ + { display: 'Chat', featureName: 'Chat' }, + { display: 'Quick Edit', featureName: 'Ctrl+K' }, + { display: 'Autocomplete', featureName: 'Autocomplete' }, + { display: 'Fast Apply', featureName: 'Apply' }, +]; + +const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => { + const [currentTab, setCurrentTab] = useState('Free'); + const settingsState = useSettingsState(); + + return ( +
+ {/* Left Column - Fixed */} +
+ {/* Tab Selector */} +
+ {[...tabNames, 'Cloud/Other'].map(tab => ( + + ))} +
+ + {/* Feature Checklist */} +
+ {featureNameMap.map(({ display, featureName }) => { + const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null; + return ( +
+ {hasModel ? : } + {display} +
+ ); + })} +
+
+ + {/* Right Column */} +
+
{currentTab}
+
{descriptionOfTab[currentTab]}
+ {subtextMdOfTab[currentTab] ?
+ +
: null} + + {providerNamesOfTab[currentTab].map((providerName) => ( +
+
Add {displayInfoOfProviderName(providerName).title}
+ + {providerName === 'ollama' && } +
+ ))} + + {(currentTab === 'Local' || currentTab === 'Cloud/Other') && ( +
+
Models
+ {currentTab === 'Local' && } + {currentTab === 'Cloud/Other' && } +
+ )} + + {/* Navigation buttons in right column */} +
+
+ setPageIndex(pageIndex - 1)} /> + setPageIndex(pageIndex + 1)} /> +
+
+
+
+ ); +}; +// ============================================= // OnboardingPage // title: // div @@ -179,7 +299,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa className?: string, }) => { return ( -
+
{top && {top}} {content && {content}} {bottom &&
{bottom}
} @@ -188,8 +308,6 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa } 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 <> - - } @@ -306,114 +354,7 @@ const abbreviateNumber = (num: number): string => { } } -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.type !== 'custom', - 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] = { - isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false, - showAsDefault: true, - } - } - } - - return - - - - - - - - - {/* */} - {isDetectableLocally && } - {providerName === 'ollama' && } - - - - {Object.keys(infoOfModelName).map(modelName => { - const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {} - - - const capabilities = getModelCapabilities(providerName, modelName, undefined) - const { - downloadable, - cost, - supportsFIM, - reasoningCapabilities, - contextWindow, - - isUnrecognizedModel, - reservedOutputTokenSpace, - supportsSystemMessage, - } = capabilities - - // TODO update this when tools work - - const removeModelButton = - - - - return ( - - - - - - - - {/* */} - {isDetectableLocally && } - {providerName === 'ollama' && } - - - ) - })} - - - - - -
Models OfferedCost/MContextChatAgentAutotabReasoningDetectedDownload
- {!showAsDefault && removeModelButton} - {modelName} - ${cost.output ?? ''}{contextWindow ? abbreviateNumber(contextWindow) : ''}{!!isDownloaded ? : <>} - -
- - - -
-} @@ -431,22 +372,16 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil ${ringSize === 'xl' ? ` gap-2 px-16 py-8 - hover:ring-8 active:ring-8 transition-all duration-300 ease-in-out ` : ringSize === 'screen' ? ` gap-2 px-16 py-8 - ring-[3000px] transition-all duration-1000 ease-in-out `: ringSize === undefined ? ` gap-1 px-4 py-2 - hover:ring-2 active:ring-2 transition-all duration-300 ease-in-out `: ''} - hover:ring-black/90 dark:hover:ring-white/90 - active:ring-black/90 dark:active:ring-white/90 - rounded-lg group ${className} @@ -534,7 +469,6 @@ const VoidOnboardingContent = () => { /> { setPageIndex(pageIndex + 1) }} - disabled={pageIndex === 2 && !didFillInSelectedProviderSettings} />
@@ -612,7 +546,7 @@ const VoidOnboardingContent = () => { delayMs={1000} > { setPageIndex(pageIndex + 1) }} + onClick={() => { setPageIndex(1) }} > Get Started @@ -621,255 +555,13 @@ const VoidOnboardingContent = () => {
} />, - 1: } - content={
- {/*
AI Preferences
*/} - -
Model Preferences
- - -
- - - - - - -
- - -
} - bottom={ -
- { setPageIndex(pageIndex - 1) }} /> -
+ 1: } />, 2: - {/* Title */} - -
Choose a Provider
- - {/* Preference Selector */} - -
- {[ - { id: 'smart', label: 'Intelligent' }, - { id: 'private', label: 'Private' }, - { id: 'cheap', label: 'Affordable' }, - { id: 'all', label: 'All' } - ].map(option => ( - - - - ))} -
- - - - {/* Provider Buttons - Modified to use separate components for each tab */} -
- {/* Intelligent tab */} - -
- {providerNamesOfWantToUseOption['smart'].map((providerName) => { - const isSelected = selectedIntelligentProvider === providerName; - return ( - - ); - })} -
-
- - - {/* Private tab */} - -
- {providerNamesOfWantToUseOption['private'].map((providerName) => { - const isSelected = selectedPrivateProvider === providerName; - return ( - - ); - })} -
-
- - - {/* Affordable tab */} - - -
- {providerNamesOfWantToUseOption['cheap'].map((providerName) => { - const isSelected = selectedAffordableProvider === providerName; - return ( - - ); - })} -
-
- - - {/* All tab */} - -
- {providerNames.map((providerName) => { - const isSelected = selectedAllProvider === providerName; - return ( - - ); - })} -
-
-
- - {/* Description */} - -
- -
-
- - - {/* ModelsTable and ProviderFields */} - {selectedProviderName &&
- {/* Models Table */} - - - - - - {/* Add provider section - simplified styling */} - -
- -
- Add {displayInfoOfProviderName(selectedProviderName).title} - -
- {selectedProviderName === 'ollama' ? : ''} -
- -
-
- - - {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

- : } -
-
- -
} - - } - - bottom={ - - - {prevAndNextButtons} - - - } - - />, - - // 2.5:
- // - //
Autocomplete
- - //
- //

Void offers free autocomplete with locally hosted models

- //

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

- - //
- //
- - // {prevAndNextButtons} - //
, - 3: @@ -884,32 +576,11 @@ const VoidOnboardingContent = () => {
} bottom={lastPagePrevAndNextButtons} - // bottom={prevAndNextButtons} />, - // 4: - //
- // { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }} - // ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined} - // className='text-4xl' - // >Enter the Void - //
- // - // } - // bottom={ - // { setPageIndex(pageIndex - 1) }} - // /> - // } - // />, } - return
+ return
{contentOfIdx[pageIndex]} 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 9bef52b3..641d7370 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 @@ -361,127 +361,9 @@ const SimpleModelSettingsDialog = ({ }; -// shows a providerName dropdown if no `providerName` is given -export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => { - - const accessor = useAccessor() - const settingsStateService = accessor.get('IVoidSettingsService') - - const settingsState = useSettingsState() - - const [isOpen, setIsOpen] = useState(false) - const [showCheckmark, setShowCheckmark] = useState(false) - - // const providerNameRef = useRef(null) - const [userChosenProviderName, setUserChosenProviderName] = useState(null) - - const providerName = permanentProviderName ?? userChosenProviderName; - - const [modelName, setModelName] = useState('') - const [errorString, setErrorString] = useState('') - - const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length - - if (showCheckmark) { - return - } - - if (!isOpen) { - return
setIsOpen(true)} - - > -
- {numModels > 0 ? `Add a different model?` : `Add a model`} -
-
- } - return <> -
- - {/* X button - */} - - {/* provider input */} - - {!permanentProviderName && - setUserChosenProviderName(pn)} - getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} - getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} - getOptionsEqual={(a, b) => a === b} - // className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`} - className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`} - arrowTouchesText={false} - /> - } - - - - {/* model input */} - - - - - {/* add button */} - - { - if (providerName === null) { - setErrorString('Please select a provider.') - return - } - if (!modelName) { - setErrorString('Please enter a model name.') - return - } - // if model already exists here - if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) { - // setErrorString(`This model already exists under ${providerName}.`) - setErrorString(`This model already exists.`) - return - } - - settingsStateService.addModel(providerName, modelName) - setShowCheckmark(true) - setTimeout(() => { - setShowCheckmark(false) - setIsOpen(false) - }, 1500) - setErrorString('') - setModelName('') - }} - /> - - - -
- - {!errorString ? null :
- {errorString} -
} - - - -} - - - - -export const ModelDump = () => { +export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderName[] }) => { const accessor = useAccessor() const settingsStateService = accessor.get('IVoidSettingsService') const settingsState = useSettingsState() @@ -493,9 +375,20 @@ export const ModelDump = () => { type: 'autodetected' | 'custom' | 'default' } | null>(null); + // States for add model functionality + const [isAddModelOpen, setIsAddModelOpen] = useState(false); + const [showCheckmark, setShowCheckmark] = useState(false); + const [userChosenProviderName, setUserChosenProviderName] = useState(null); + const [modelName, setModelName] = useState(''); + const [errorString, setErrorString] = useState(''); + // a dump of all the enabled providers' models const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] - for (let providerName of providerNames) { + + // Use either filtered providers or all providers + const providersToShow = filteredProviders || providerNames; + + for (let providerName of providersToShow) { const providerSettings = settingsState.settingsOfProvider[providerName] // if (!providerSettings.enabled) continue modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings }))) @@ -506,6 +399,34 @@ export const ModelDump = () => { return Number(b.providerEnabled) - Number(a.providerEnabled) }) + // Add model handler + const handleAddModel = () => { + if (!userChosenProviderName) { + setErrorString('Please select a provider.'); + return; + } + if (!modelName) { + setErrorString('Please enter a model name.'); + return; + } + + // Check if model already exists + if (settingsState.settingsOfProvider[userChosenProviderName].models.find(m => m.modelName === modelName)) { + setErrorString(`This model already exists.`); + return; + } + + settingsStateService.addModel(userChosenProviderName, modelName); + setShowCheckmark(true); + setTimeout(() => { + setShowCheckmark(false); + setIsAddModelOpen(false); + setUserChosenProviderName(null); + setModelName(''); + }, 1500); + setErrorString(''); + }; + return
{modelDump.map((m, i) => { const { isHidden, type, modelName, providerName, providerEnabled } = m @@ -584,6 +505,82 @@ export const ModelDump = () => {
})} + {/* Add Model Section */} + {showCheckmark ? ( +
+ +
+ ) : isAddModelOpen ? ( +
+
+ + {/* Provider dropdown */} + + setUserChosenProviderName(pn)} + getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} + getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} + getOptionsEqual={(a, b) => a === b} + className="max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded" + arrowTouchesText={false} + /> + + + {/* Model name input */} + + + + + {/* Add button */} + + + + + {/* X button to cancel */} + +
+ + {errorString && ( +
+ {errorString} +
+ )} +
+ ) : ( +
setIsAddModelOpen(true)} + > +
+ + Add a model +
+
+ )} + {/* Model Settings Dialog */} { } -export const OllamaSetupInstructions = () => { +export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?: boolean }) => { return
@@ -815,7 +812,7 @@ export const OllamaSetupInstructions = () => { >
-
+ {sayWeAutoDetect &&
}
} @@ -1176,17 +1173,20 @@ export const Settings = () => {

{`Void's Settings`}

- {/* separator */} -
+
{/* Models section (formerly FeaturesTab) */} + + + + +
{/* Models section (formerly FeaturesTab) */}

Models

- - +
@@ -1196,7 +1196,7 @@ export const Settings = () => {

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

- +
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 9c2eec45..c4106432 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 @@ -112,11 +112,14 @@ export const VoidTooltip = () => {
For chat:{` `} - llama3.1 + gemma3
-
+
For autocomplete:{` `} - qwen2.5-coder:1.5b + qwen2.5-coder +
+
+ Use the largest version of these you can!