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 31167102..b05b95ce 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 @@ -17,7 +17,7 @@ import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdow import { URI } from '../../../../../../../base/common/uri.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; -import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; +import { TextAreaFns, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; @@ -29,7 +29,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; import { ToolName } from '../../../toolsService.js'; - +import { getModelCapabilities } from '../../../../common/modelCapabilities.js'; @@ -146,6 +146,58 @@ export const IconLoading = ({ className = '' }: { className?: string }) => { const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`; + + +const ReasoningOptionDropdown = () => { + const accessor = useAccessor() + + const voidSettingsService = accessor.get('IVoidSettingsService') + const voidSettingsState = useSettingsState() + + const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L'] + if (!modelSelection) return null + + const { modelName, providerName } = modelSelection + const { canToggleReasoning, reasoningBudgetOptions } = getModelCapabilities(providerName, modelName).supportsReasoningOutput || {} + + const defaultEnabledVal = canToggleReasoning ? true : false + const isEnabled = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEnabled ?? defaultEnabledVal + + let toggleButton: React.ReactNode = null + if (canToggleReasoning) { + toggleButton = { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: newVal }) }} + /> + } + + let slider: React.ReactNode = null + if (isEnabled && reasoningBudgetOptions?.type === 'slider') { + const { min, max, default: defaultVal } = reasoningBudgetOptions + const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + slider = { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningBudget: newVal }) }} + label={value + ''} + /> + } + + return <> + {toggleButton} + {slider} + + +} + + + + interface VoidChatAreaProps { // Required children: React.ReactNode; // This will be the input component @@ -248,6 +300,8 @@ export const VoidChatArea: React.FC = ({ )} + + {isStreaming ? ( ) : ( 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 e4a2e4c6..c76b46d2 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 @@ -214,6 +214,151 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac + +export const VoidSlider = ({ + value, + onChange, + size = 'md', + label, + disabled = false, + min = 0, + max = 7, + step = 1, + className = '', + width = 200, +}: { + value: number; + onChange: (value: number) => void; + label?: string; + disabled?: boolean; + size?: 'xs' | 'sm' | 'sm+' | 'md'; + min?: number; + max?: number; + step?: number; + className?: string; + width?: number; +}) => { + // Calculate percentage for position + const percentage = ((value - min) / (max - min)) * 100; + + // Handle track click + const handleTrackClick = (e: React.MouseEvent) => { + if (disabled) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const clickPosition = e.clientX - rect.left; + const trackWidth = rect.width; + + // Calculate new value + const newPercentage = Math.max(0, Math.min(1, clickPosition / trackWidth)); + const rawValue = min + newPercentage * (max - min); + + // Special handling to ensure max value is always reachable + if (rawValue >= max - step / 2) { + onChange(max); + return; + } + + // Normal step calculation + const steppedValue = Math.round((rawValue - min) / step) * step + min; + const clampedValue = Math.max(min, Math.min(max, steppedValue)); + + onChange(clampedValue); + }; + + // Helper function to handle thumb dragging that respects steps and max + const handleThumbDrag = (moveEvent: MouseEvent, track: Element) => { + if (!track) return; + + const rect = (track as HTMLElement).getBoundingClientRect(); + const movePosition = moveEvent.clientX - rect.left; + const trackWidth = rect.width; + + // Calculate new value + const newPercentage = Math.max(0, Math.min(1, movePosition / trackWidth)); + const rawValue = min + newPercentage * (max - min); + + // Special handling to ensure max value is always reachable + if (rawValue >= max - step / 2) { + onChange(max); + return; + } + + // Normal step calculation + const steppedValue = Math.round((rawValue - min) / step) * step + min; + const clampedValue = Math.max(min, Math.min(max, steppedValue)); + + onChange(clampedValue); + }; + + return ( +
+
+ {/* Track */} +
+ {/* Filled part of track */} +
+
+ + {/* Thumb */} +
{ + if (disabled) return; + + const track = e.currentTarget.previousElementSibling; + + const handleMouseMove = (moveEvent: MouseEvent) => { + handleThumbDrag(moveEvent, track as Element); + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + document.body.style.userSelect = 'none'; + document.body.style.cursor = 'grabbing'; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + + e.preventDefault(); + }} + /> +
+ + {label && ( + + {label} + + )} +
+ ); +}; + + + + + export const VoidSwitch = ({ value, onChange, diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 1389fad5..623f0dcb 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -21,7 +21,12 @@ type ModelOptions = { supportsReasoningOutput: false | { // you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing) // if it's open source, put the think tags here so we parse them out in e.g. ollama - readonly openSourceThinkTags?: [string, string] + readonly openSourceThinkTags?: [string, string]; + + // reasoning options + readonly canToggleReasoning?: boolean; // whether or not the user can enable reasoning mode (or if the model only supports reasoning) + readonly maxOutputTokens?: number; + readonly reasoningBudgetOptions?: { type: 'slider'; min: number; max: number; default: number }; }; } @@ -153,12 +158,16 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN const anthropicModelOptions = { 'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table contextWindow: 200_000, - maxOutputTokens: 8_192, // TODO!!! 64_000 for extended thinking, can bump it to 128_000 with output-128k-2025-02-19 + maxOutputTokens: 8_192, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoningOutput: {}, + supportsReasoningOutput: { + canToggleReasoning: true, + maxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19 + reasoningBudgetOptions: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000 + }, }, 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, @@ -520,12 +529,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting // ---------------- exports ---------------- -export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; didNotFindModel?: boolean } => { +export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => { const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] - if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] } + if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false } const result = modelOptionsFallback(modelName) - if (result) return result - return { modelName, ...modelOptionsDefaults, didNotFindModel: true } + if (result) return { ...result, isUnrecognizedModel: false } + return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true } } // non-model settings diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 279af6e3..0df1ec3b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -57,8 +57,6 @@ export interface IVoidSettingsService { readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state readonly waitForInitState: Promise; - readAndInitializeState: (providedState?: VoidSettingsState) => Promise; - onDidChangeState: Event; setSettingOfProvider: SetSettingOfProviderFn; @@ -213,48 +211,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this.readAndInitializeState() } - async readAndInitializeState(providedState?: VoidSettingsState) { - // If providedState is given, use it instead of reading from storage - const readS = providedState || await this._readState(); + async readAndInitializeState() { + const readS = await this._readState(); // the stored data structure might be outdated, so we need to update it here - const newSettingsOfProvider = { - // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) - ...{ deepseek: defaultSettingsOfProvider.deepseek }, - - // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) - ...{ xAI: defaultSettingsOfProvider.xAI }, - - // A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS) - ...{ vLLM: defaultSettingsOfProvider.vLLM }, - - ...readS.settingsOfProvider, - - // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) - gemini: { - ...readS.settingsOfProvider.gemini, - models: [ - ...readS.settingsOfProvider.gemini.models, - ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) - ] - } - }; - - const newModelSelectionOfFeature = { - // A HACK BECAUSE WE ADDED FastApply - ...{ 'Apply': null }, - ...readS.modelSelectionOfFeature, - }; - - const finalState = { - ...readS, - settingsOfProvider: newSettingsOfProvider, - modelSelectionOfFeature: newModelSelectionOfFeature, - }; - + const finalState = readS this.state = _validatedState(finalState); - this._resolver(); this._onDidChangeState.fire(); } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index ae508148..e70c929c 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -462,7 +462,8 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe export type ModelSelectionOptions = { - reasoningSelection?: { budget: number } + reasoningEnabled?: boolean; + reasoningBudget?: number; } -export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions } }> +export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }>