diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 658ac3be..c7529903 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2858,7 +2858,7 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.4.3" +version = "1.4.4" dependencies = [ "anyhow", "axum", diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index e4138a3f..9fdee983 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -145,7 +145,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const consumePendingInputText = useSessionStore((state) => state.consumePendingInputText); const pendingInputText = useSessionStore((state) => state.pendingInputText); - const { currentProviderId, currentModelId, currentAgentName, setAgent, getVisibleAgents } = useConfigStore(); + const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore(); const agents = getVisibleAgents(); const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen } = useUIStore(); const { working } = useAssistantStatus(); @@ -473,7 +473,8 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo currentAgentName, primaryAttachments, agentMentionName, - additionalParts.length > 0 ? additionalParts : undefined + additionalParts.length > 0 ? additionalParts : undefined, + currentVariant ).catch((error: unknown) => { const rawMessage = error instanceof Error diff --git a/packages/ui/src/components/chat/ChatMessage.tsx b/packages/ui/src/components/chat/ChatMessage.tsx index 7e2b9228..7e523a0e 100644 --- a/packages/ui/src/components/chat/ChatMessage.tsx +++ b/packages/ui/src/components/chat/ChatMessage.tsx @@ -165,18 +165,21 @@ const ChatMessage: React.FC = ({ const mode = getMessageInfoProp(previousMessage.info, 'mode'); const providerID = getMessageInfoProp(previousMessage.info, 'providerID'); const modelID = getMessageInfoProp(previousMessage.info, 'modelID'); + const variant = getMessageInfoProp(previousMessage.info, 'variant'); const resolvedAgent = typeof mode === 'string' && mode.trim().length > 0 ? mode : undefined; const resolvedProvider = typeof providerID === 'string' && providerID.trim().length > 0 ? providerID : undefined; const resolvedModel = typeof modelID === 'string' && modelID.trim().length > 0 ? modelID : undefined; - - if (!resolvedAgent && !resolvedProvider && !resolvedModel) { + const resolvedVariant = typeof variant === 'string' && variant.trim().length > 0 ? variant : undefined; + + if (!resolvedAgent && !resolvedProvider && !resolvedModel && !resolvedVariant) { return null; } - + return { agentName: resolvedAgent, providerId: resolvedProvider, modelId: resolvedModel, + variant: resolvedVariant, }; }, [isUser, previousMessage]); @@ -267,6 +270,23 @@ const ChatMessage: React.FC = ({ return undefined; }, [isUser, providerID, modelID, providers]); + const modelHasVariants = React.useMemo(() => { + if (isUser) return false; + if (!providerID || !modelID) return false; + + const provider = providers.find((p) => p.id === providerID); + if (!provider?.models || !Array.isArray(provider.models)) { + return false; + } + + const model = provider.models.find((m: Record) => (m as Record).id === modelID) as + | { variants?: Record } + | undefined; + + const variants = model?.variants; + return Boolean(variants && Object.keys(variants).length > 0); + }, [isUser, modelID, providerID, providers]); + const displayAgentName = useStickyDisplayValue(agentName); const displayProviderIDValue = useStickyDisplayValue(providerID ?? undefined); const displayModelName = useStickyDisplayValue(modelName); @@ -508,6 +528,22 @@ const ChatMessage: React.FC = ({ return typeof body === 'string' && body.trim().length > 0 ? body : undefined; }); + const variantFromTurnStore = useMessageStore((state) => { + if (!userMessageIdForTurn) return undefined; + const sessionId = message.info.sessionID; + if (!sessionId) return undefined; + const sessionMessages = state.messages.get(sessionId); + if (!sessionMessages) return undefined; + const userMsg = sessionMessages.find((entry) => entry.info?.id === userMessageIdForTurn); + if (!userMsg) return undefined; + const variant = (userMsg.info as { variant?: unknown }).variant; + return typeof variant === 'string' && variant.trim().length > 0 ? variant : undefined; + }); + + const headerVariantRaw = !isUser ? (variantFromTurnStore ?? previousUserMetadata?.variant) : undefined; + + const headerVariant = !isUser && modelHasVariants ? (headerVariantRaw ?? 'Default') : undefined; + const assistantSummaryCandidate = typeof turnGroupingContext?.summaryBody === 'string' && turnGroupingContext.summaryBody.trim().length > 0 ? turnGroupingContext.summaryBody @@ -809,6 +845,7 @@ const ChatMessage: React.FC = ({ providerID={headerProviderID} agentName={headerAgentName} modelName={headerModelName} + variant={headerVariant} isDarkTheme={isDarkTheme} /> )} diff --git a/packages/ui/src/components/chat/ModelControls.tsx b/packages/ui/src/components/chat/ModelControls.tsx index e38d42a0..13af6f2d 100644 --- a/packages/ui/src/components/chat/ModelControls.tsx +++ b/packages/ui/src/components/chat/ModelControls.tsx @@ -274,9 +274,12 @@ export const ModelControls: React.FC = ({ className }) => { providers, currentProviderId, currentModelId, + currentVariant, currentAgentName, setProvider, setModel, + setCurrentVariant, + getCurrentModelVariants, setAgent, getCurrentProvider, getModelMetadata, @@ -294,6 +297,8 @@ export const ModelControls: React.FC = ({ className }) => { getSessionAgentSelection, saveAgentModelForSession, getAgentModelForSession, + saveAgentModelVariantForSession, + getAgentModelVariantForSession, analyzeAndSaveExternalSessionChoices, getSessionAgentEditMode, setSessionAgentEditMode, @@ -308,7 +313,7 @@ export const ModelControls: React.FC = ({ className }) => { const isVSCodeRuntime = useIsVSCodeRuntime(); // Only use mobile panels on actual mobile devices, VSCode uses desktop dropdowns const isCompact = isMobile; - const [activeMobilePanel, setActiveMobilePanel] = React.useState<'model' | 'agent' | null>(null); + const [activeMobilePanel, setActiveMobilePanel] = React.useState<'model' | 'agent' | 'variant' | null>(null); const [mobileTooltipOpen, setMobileTooltipOpen] = React.useState<'model' | 'agent' | null>(null); const [mobileModelQuery, setMobileModelQuery] = React.useState(''); const closeMobilePanel = React.useCallback(() => setActiveMobilePanel(null), []); @@ -498,6 +503,15 @@ export const ModelControls: React.FC = ({ className }) => { const inputModalityIcons = getModalityIcons(currentMetadata, 'input'); const outputModalityIcons = getModalityIcons(currentMetadata, 'output'); + const availableVariants = React.useMemo(() => { + const variantKey = `${currentProviderId}/${currentModelId}`; + if (!variantKey) { + return []; + } + return getCurrentModelVariants(); + }, [getCurrentModelVariants, currentProviderId, currentModelId]); + const hasVariants = availableVariants.length > 0; + const costRows = [ { label: 'Input', value: formatCost(currentMetadata?.cost?.input) }, { label: 'Output', value: formatCost(currentMetadata?.cost?.output) }, @@ -850,6 +864,62 @@ export const ModelControls: React.FC = ({ className }) => { handleAgentSwitch(); }, [currentAgentName, currentSessionId, getAgentModelForSession, tryApplyModelSelection, agents, contextHydrated]); + React.useEffect(() => { + if (!contextHydrated || !currentSessionId || !currentAgentName) { + setCurrentVariant(undefined); + return; + } + + if (!currentProviderId || !currentModelId) { + setCurrentVariant(undefined); + return; + } + + const savedVariant = getAgentModelVariantForSession( + currentSessionId, + currentAgentName, + currentProviderId, + currentModelId, + ); + + if (savedVariant && !availableVariants.includes(savedVariant)) { + setCurrentVariant(undefined); + return; + } + + setCurrentVariant(savedVariant); + }, [ + availableVariants, + contextHydrated, + currentSessionId, + currentAgentName, + currentProviderId, + currentModelId, + getAgentModelVariantForSession, + setCurrentVariant, + ]); + + const handleVariantSelect = React.useCallback((variant: string | undefined) => { + setCurrentVariant(variant); + + if (currentSessionId && currentAgentName && currentProviderId && currentModelId) { + saveAgentModelVariantForSession( + currentSessionId, + currentAgentName, + currentProviderId, + currentModelId, + variant, + ); + } + }, [ + currentAgentName, + currentModelId, + currentProviderId, + currentSessionId, + saveAgentModelVariantForSession, + setCurrentVariant, + ]); + const handleAgentChange = (agentName: string) => { try { setAgent(agentName); @@ -1464,11 +1534,70 @@ export const ModelControls: React.FC = ({ className }) => { ); }; + const renderMobileVariantPanel = () => { + if (!isCompact || !hasVariants) return null; + + const isDefault = !currentVariant; + + const handleSelect = (variant: string | undefined) => { + handleVariantSelect(variant); + closeMobilePanel(); + requestAnimationFrame(() => { + const textarea = document.querySelector('textarea[data-chat-input="true"]'); + textarea?.focus(); + }); + }; + + return ( + +
+ + + {availableVariants.map((variant) => { + const selected = currentVariant === variant; + const label = variant.charAt(0).toUpperCase() + variant.slice(1); + + return ( + + ); + })} +
+
+ ); + }; + const renderMobileAgentPanel = () => { if (!isCompact) return null; - + const primaryAgents = agents.filter(agent => isPrimaryMode(agent.mode)); - + return ( = ({ className }) => { ); }; + const renderVariantSelector = () => { + if (!hasVariants) { + return null; + } + + const displayVariant = currentVariant ?? 'Default'; + const isDefault = !currentVariant; + const colorClass = isDefault ? 'text-muted-foreground' : 'text-[color:var(--status-info)]'; + + if (isCompact) { + return ( + + ); + } + + return ( + + + + +
+ + + {displayVariant} + +
+
+
+ + Thinking + handleVariantSelect(undefined)}> +
+ Default + {isDefault && } +
+
+ {availableVariants.length > 0 && } + {availableVariants.map((variant) => { + const selected = currentVariant === variant; + const label = variant.charAt(0).toUpperCase() + variant.slice(1); + return ( + handleVariantSelect(variant)} + > +
+ {label} + {selected && } +
+
+ ); + })} +
+
+ +

Thinking: {displayVariant}

+
+
+ ); + }; + const renderAgentSelector = () => { if (!isCompact) { return ( @@ -2344,7 +2561,6 @@ export const ModelControls: React.FC = ({ className }) => { const inlineClassName = cn( '@container/model-controls flex items-center min-w-0', - inlineGapClass, // Only force full-width + truncation behaviors on true mobile layouts. // VS Code also uses "compact" mode, but should keep its right-aligned inline sizing. isMobile && 'w-full', @@ -2357,17 +2573,18 @@ export const ModelControls: React.FC = ({ className }) => {
+ {renderVariantSelector()} {renderModelSelector()} -
-
{renderAgentSelector()}
{renderMobileModelPanel()} + {renderMobileVariantPanel()} {renderMobileAgentPanel()} {renderMobileModelTooltip()} {renderMobileAgentTooltip()} diff --git a/packages/ui/src/components/chat/message/MessageHeader.tsx b/packages/ui/src/components/chat/message/MessageHeader.tsx index 044b5fc5..e871b677 100644 --- a/packages/ui/src/components/chat/message/MessageHeader.tsx +++ b/packages/ui/src/components/chat/message/MessageHeader.tsx @@ -10,10 +10,11 @@ interface MessageHeaderProps { providerID: string | null; agentName: string | undefined; modelName: string | undefined; + variant?: string; isDarkTheme: boolean; } -const MessageHeader: React.FC = ({ isUser, providerID, agentName, modelName, isDarkTheme }) => { +const MessageHeader: React.FC = ({ isUser, providerID, agentName, modelName, variant, isDarkTheme }) => { const { src: logoSrc, onError: handleLogoError, hasLogo } = useProviderLogo(providerID); return ( @@ -67,6 +68,25 @@ const MessageHeader: React.FC = ({ isUser, providerID, agent {agentName} )} + {!isUser && variant && ( +
+ {variant.length > 0 ? variant[0].toLowerCase() + variant.slice(1) : variant} +
+ )} diff --git a/packages/ui/src/lib/opencode/client.ts b/packages/ui/src/lib/opencode/client.ts index 2e5b0efe..d163d781 100644 --- a/packages/ui/src/lib/opencode/client.ts +++ b/packages/ui/src/lib/opencode/client.ts @@ -530,6 +530,7 @@ class OpencodeService { text: string; prefaceText?: string; agent?: string; + variant?: string; files?: Array<{ type: 'file'; mime: string; @@ -638,6 +639,7 @@ class OpencodeService { modelID: params.modelID }, agent: params.agent, + variant: params.variant, parts }); diff --git a/packages/ui/src/stores/contextStore.ts b/packages/ui/src/stores/contextStore.ts index 36897a2c..935c1ed0 100644 --- a/packages/ui/src/stores/contextStore.ts +++ b/packages/ui/src/stores/contextStore.ts @@ -24,6 +24,9 @@ interface ContextState { sessionAgentModelSelections: Map>; + // sessionId → agentName → "providerId/modelId" → variant + sessionAgentModelVariantSelections: Map>>; + currentAgentContext: Map; sessionContextUsage: Map; @@ -42,8 +45,12 @@ interface ContextActions { saveAgentModelForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => void; getAgentModelForSession: (sessionId: string, agentName: string) => { providerId: string; modelId: string } | null; + saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => void; + getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => string | undefined; + analyzeAndSaveExternalSessionChoices: (sessionId: string, agents: any[], messages: Map) => Promise>; + getContextUsage: (sessionId: string, contextLimit: number, outputLimit: number, messages: Map) => ContextUsage | null; updateSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number, messages: Map) => void; @@ -71,6 +78,7 @@ export const useContextStore = create()( sessionModelSelections: new Map(), sessionAgentSelections: new Map(), sessionAgentModelSelections: new Map(), + sessionAgentModelVariantSelections: new Map(), currentAgentContext: new Map(), sessionContextUsage: new Map(), sessionAgentEditModes: new Map(), @@ -129,8 +137,62 @@ export const useContextStore = create()( return agentMap.get(agentName) || null; }, + saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => { + set((state) => { + const newSelections = new Map(state.sessionAgentModelVariantSelections); + + let agentMap = newSelections.get(sessionId); + if (!agentMap) { + agentMap = new Map(); + } else { + agentMap = new Map(agentMap); + } + + let modelMap = agentMap.get(agentName); + if (!modelMap) { + modelMap = new Map(); + } else { + modelMap = new Map(modelMap); + } + + const modelKey = `${providerId}/${modelId}`; + + if (variant === undefined) { + modelMap.delete(modelKey); + + if (modelMap.size === 0) { + agentMap.delete(agentName); + + if (agentMap.size === 0) { + newSelections.delete(sessionId); + } else { + newSelections.set(sessionId, agentMap); + } + } else { + agentMap.set(agentName, modelMap); + newSelections.set(sessionId, agentMap); + } + } else { + modelMap.set(modelKey, variant); + agentMap.set(agentName, modelMap); + newSelections.set(sessionId, agentMap); + } + + return { sessionAgentModelVariantSelections: newSelections }; + }); + }, + + getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => { + const { sessionAgentModelVariantSelections } = get(); + const agentMap = sessionAgentModelVariantSelections.get(sessionId); + if (!agentMap) return undefined; + const modelMap = agentMap.get(agentName); + if (!modelMap) return undefined; + return modelMap.get(`${providerId}/${modelId}`); + }, + analyzeAndSaveExternalSessionChoices: async (sessionId: string, agents: any[], messages: Map) => { - const { saveAgentModelForSession } = get(); + const { saveAgentModelForSession, saveAgentModelVariantForSession } = get(); const agentLastChoices = new Map< string, @@ -212,6 +274,14 @@ export const useContextStore = create()( const agentName = extractAgentFromMessage(infoAny, assistantMessages.indexOf(message)); if (agentName && agents.find((a) => a.name === agentName)) { + const resolvedVariant = typeof infoAny.variant === 'string' && infoAny.variant.trim().length > 0 + ? infoAny.variant + : undefined; + + if (resolvedVariant) { + saveAgentModelVariantForSession(sessionId, agentName, infoAny.providerID, infoAny.modelID, resolvedVariant); + } + const choice = { providerId: infoAny.providerID, modelId: infoAny.modelID, @@ -474,6 +544,10 @@ export const useContextStore = create()( sessionModelSelections: Array.from(state.sessionModelSelections.entries()), sessionAgentSelections: Array.from(state.sessionAgentSelections.entries()), sessionAgentModelSelections: Array.from(state.sessionAgentModelSelections.entries()).map(([sessionId, agentMap]) => [sessionId, Array.from(agentMap.entries())]), + sessionAgentModelVariantSelections: Array.from(state.sessionAgentModelVariantSelections.entries()).map(([sessionId, agentMap]) => [ + sessionId, + Array.from(agentMap.entries()).map(([agentName, modelMap]) => [agentName, Array.from(modelMap.entries())]), + ]), currentAgentContext: Array.from(state.currentAgentContext.entries()), sessionContextUsage: Array.from(state.sessionContextUsage.entries()), sessionAgentEditModes: Array.from(state.sessionAgentEditModes.entries()).map(([sessionId, agentMap]) => [sessionId, Array.from(agentMap.entries())]), @@ -487,6 +561,17 @@ export const useContextStore = create()( }); } + const agentModelVariantSelections = new Map(); + if (persistedState?.sessionAgentModelVariantSelections) { + persistedState.sessionAgentModelVariantSelections.forEach(([sessionId, agentArray]: [string, any[]]) => { + const agentMap = new Map(); + agentArray.forEach(([agentName, modelArray]: [string, any[]]) => { + agentMap.set(agentName, new Map(modelArray)); + }); + agentModelVariantSelections.set(sessionId, agentMap); + }); + } + const agentEditModes = new Map(); if (persistedState?.sessionAgentEditModes) { persistedState.sessionAgentEditModes.forEach(([sessionId, agentArray]: [string, any[]]) => { @@ -500,6 +585,7 @@ export const useContextStore = create()( sessionModelSelections: new Map(persistedState?.sessionModelSelections || []), sessionAgentSelections: new Map(persistedState?.sessionAgentSelections || []), sessionAgentModelSelections: agentModelSelections, + sessionAgentModelVariantSelections: agentModelVariantSelections, currentAgentContext: new Map(persistedState?.currentAgentContext || []), sessionContextUsage: new Map(persistedState?.sessionContextUsage || []), sessionAgentEditModes: agentEditModes, diff --git a/packages/ui/src/stores/messageStore.ts b/packages/ui/src/stores/messageStore.ts index 4ffa04db..9bda2cc2 100644 --- a/packages/ui/src/stores/messageStore.ts +++ b/packages/ui/src/stores/messageStore.ts @@ -20,9 +20,10 @@ import { useContextStore } from "./contextStore"; // Helper function to clean up pending user message metadata const cleanupPendingUserMessageMeta = ( - currentPending: Map, + currentPending: Map, + sessionId: string -): Map => { +): Map => { const nextPending = new Map(currentPending); nextPending.delete(sessionId); return nextPending; @@ -338,12 +339,12 @@ interface MessageState { sessionCompactionUntil: Map; sessionAbortFlags: Map; pendingAssistantHeaderSessions: Set; - pendingUserMessageMetaBySession: Map; + pendingUserMessageMetaBySession: Map; } interface MessageActions { loadMessages: (sessionId: string) => Promise; - sendMessage: (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => Promise; + sendMessage: (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => Promise; abortCurrentOperation: (currentSessionId?: string) => Promise; _addStreamingPartImmediate: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => void; addStreamingPart: (sessionId: string, messageId: string, part: Part, role?: string, currentSessionId?: string) => void; @@ -546,7 +547,7 @@ export const useMessageStore = create()( }); }, - sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => { + sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => { if (!currentSessionId) { throw new Error("No session selected"); } @@ -663,6 +664,7 @@ export const useMessageStore = create()( mode: typeof agent === 'string' && agent.trim().length > 0 ? agent.trim() : undefined, providerID, modelID, + variant: typeof variant === 'string' && variant.trim().length > 0 ? variant : undefined, }); return { pendingAssistantHeaderSessions: next, pendingUserMessageMetaBySession: nextUserMeta }; }); @@ -684,6 +686,7 @@ export const useMessageStore = create()( modelID, text: effectiveContent, agent, + variant, files: filePayloads.length > 0 ? filePayloads : undefined, additionalParts: additionalPartsPayload, agentMentions: agentMentionName ? [{ name: agentMentionName }] : undefined, diff --git a/packages/ui/src/stores/types/sessionTypes.ts b/packages/ui/src/stores/types/sessionTypes.ts index aa21d923..873a4ba0 100644 --- a/packages/ui/src/stores/types/sessionTypes.ts +++ b/packages/ui/src/stores/types/sessionTypes.ts @@ -131,7 +131,7 @@ export interface SessionStore { unshareSession: (id: string) => Promise; setCurrentSession: (id: string | null) => void; loadMessages: (sessionId: string) => Promise; - sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => Promise; + sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => Promise; abortCurrentOperation: () => Promise; acknowledgeSessionAbort: (sessionId: string) => void; armAbortPrompt: (durationMs?: number) => number | null; @@ -172,8 +172,12 @@ export interface SessionStore { saveAgentModelForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => void; getAgentModelForSession: (sessionId: string, agentName: string) => { providerId: string; modelId: string } | null; + saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => void; + getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => string | undefined; + analyzeAndSaveExternalSessionChoices: (sessionId: string, agents: Array<{ name: string; [key: string]: unknown }>) => Promise>; + isOpenChamberCreatedSession: (sessionId: string) => boolean; markSessionAsOpenChamberCreated: (sessionId: string) => void; diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index fac0160c..880b8d60 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -345,6 +345,7 @@ interface ConfigStore { agents: Agent[]; currentProviderId: string; currentModelId: string; + currentVariant: string | undefined; currentAgentName: string | undefined; selectedProviderId: string; agentModelSelections: { [agentName: string]: { providerId: string; modelId: string } }; @@ -363,6 +364,9 @@ interface ConfigStore { loadAgents: (options?: { directory?: string | null }) => Promise; setProvider: (providerId: string) => void; setModel: (modelId: string) => void; + setCurrentVariant: (variant: string | undefined) => void; + cycleCurrentVariant: () => void; + getCurrentModelVariants: () => string[]; setAgent: (agentName: string | undefined) => void; setSelectedProvider: (providerId: string) => void; setSettingsDefaultModel: (model: string | undefined) => void; @@ -399,6 +403,7 @@ export const useConfigStore = create()( agents: [], currentProviderId: "", currentModelId: "", + currentVariant: undefined, currentAgentName: undefined, selectedProviderId: "", agentModelSelections: {}, @@ -612,12 +617,12 @@ export const useConfigStore = create()( agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, }; - + const nextSnapshot: DirectoryScopedConfig = { ...baseSnapshot, currentModelId: modelId, }; - + return { currentModelId: modelId, directoryScoped: { @@ -628,6 +633,46 @@ export const useConfigStore = create()( }); }, + setCurrentVariant: (variant: string | undefined) => { + set((state) => { + if (state.currentVariant === variant) { + return state; + } + return { currentVariant: variant }; + }); + }, + + getCurrentModelVariants: () => { + const model = get().getCurrentModel(); + const variants = (model as { variants?: Record } | undefined)?.variants; + if (!variants) { + return []; + } + return Object.keys(variants); + }, + + cycleCurrentVariant: () => { + const variantKeys = get().getCurrentModelVariants(); + if (variantKeys.length === 0) { + return; + } + + const current = get().currentVariant; + if (!current) { + set((state) => (state.currentVariant === variantKeys[0] ? state : { currentVariant: variantKeys[0] })); + return; + } + + const index = variantKeys.indexOf(current); + if (index === -1 || index === variantKeys.length - 1) { + set((state) => (state.currentVariant === undefined ? state : { currentVariant: undefined })); + return; + } + + const nextVariant = variantKeys[index + 1]; + set((state) => (state.currentVariant === nextVariant ? state : { currentVariant: nextVariant })); + }, + setSelectedProvider: (providerId: string) => { set((state) => { const directoryKey = state.activeDirectoryKey; diff --git a/packages/ui/src/stores/useSessionStore.ts b/packages/ui/src/stores/useSessionStore.ts index 6516384f..524b1f06 100644 --- a/packages/ui/src/stores/useSessionStore.ts +++ b/packages/ui/src/stores/useSessionStore.ts @@ -290,7 +290,7 @@ export const useSessionStore = create()( get().evictLeastRecentlyUsed(); }, loadMessages: (sessionId: string) => useMessageStore.getState().loadMessages(sessionId), - sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => { + sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => { const draft = get().newSessionDraft; const trimmedAgent = typeof agent === 'string' && agent.trim().length > 0 ? agent.trim() : undefined; @@ -340,15 +340,25 @@ export const useSessionStore = create()( // ignored } - if (draftProviderId && draftModelId) { - try { - useContextStore - .getState() - .saveAgentModelForSession(created.id, effectiveDraftAgent, draftProviderId, draftModelId); - } catch { - // ignored + if (draftProviderId && draftModelId) { + try { + useContextStore + .getState() + .saveAgentModelForSession(created.id, effectiveDraftAgent, draftProviderId, draftModelId); + } catch { + // ignored + } + + if (variant !== undefined) { + try { + useContextStore + .getState() + .saveAgentModelVariantForSession(created.id, effectiveDraftAgent, draftProviderId, draftModelId, variant); + } catch { + // ignored + } + } } - } } try { @@ -365,7 +375,7 @@ export const useSessionStore = create()( try { return await useMessageStore .getState() - .sendMessage(content, providerID, modelID, effectiveDraftAgent, created.id, attachments, agentMentionName, additionalParts); + .sendMessage(content, providerID, modelID, effectiveDraftAgent, created.id, attachments, agentMentionName, additionalParts, variant); } catch (error) { setIdlePhase(created.id); throw error; @@ -385,14 +395,24 @@ export const useSessionStore = create()( } catch { // ignored } - } + if (variant !== undefined) { + try { + useContextStore + .getState() + .saveAgentModelVariantForSession(currentSessionId, effectiveAgent, providerID, modelID, variant); + } catch { + // ignored + } + } + } + if (currentSessionId) { setBusyPhase(currentSessionId); } try { - return await useMessageStore.getState().sendMessage(content, providerID, modelID, effectiveAgent, currentSessionId || undefined, attachments, agentMentionName, additionalParts); + return await useMessageStore.getState().sendMessage(content, providerID, modelID, effectiveAgent, currentSessionId || undefined, attachments, agentMentionName, additionalParts, variant); } catch (error) { if (currentSessionId) { setIdlePhase(currentSessionId); @@ -473,7 +493,9 @@ export const useSessionStore = create()( getSessionAgentSelection: (sessionId: string) => useContextStore.getState().getSessionAgentSelection(sessionId), saveAgentModelForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => useContextStore.getState().saveAgentModelForSession(sessionId, agentName, providerId, modelId), getAgentModelForSession: (sessionId: string, agentName: string) => useContextStore.getState().getAgentModelForSession(sessionId, agentName), - analyzeAndSaveExternalSessionChoices: (sessionId: string, agents: Record[]) => { + saveAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string, variant: string | undefined) => useContextStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, variant), + getAgentModelVariantForSession: (sessionId: string, agentName: string, providerId: string, modelId: string) => useContextStore.getState().getAgentModelVariantForSession(sessionId, agentName, providerId, modelId), + analyzeAndSaveExternalSessionChoices: (sessionId: string, agents: Record[]) => { const messages = useMessageStore.getState().messages; return useContextStore.getState().analyzeAndSaveExternalSessionChoices(sessionId, agents, messages); },