feat: add support for model variants across chat components and session management
This commit is contained in:
Generated
+1
-1
@@ -2858,7 +2858,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openchamber-desktop"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
@@ -145,7 +145,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ 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<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
currentAgentName,
|
||||
primaryAttachments,
|
||||
agentMentionName,
|
||||
additionalParts.length > 0 ? additionalParts : undefined
|
||||
additionalParts.length > 0 ? additionalParts : undefined,
|
||||
currentVariant
|
||||
).catch((error: unknown) => {
|
||||
const rawMessage =
|
||||
error instanceof Error
|
||||
|
||||
@@ -165,18 +165,21 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
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<ChatMessageProps> = ({
|
||||
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<string, unknown>) => (m as Record<string, unknown>).id === modelID) as
|
||||
| { variants?: Record<string, unknown> }
|
||||
| undefined;
|
||||
|
||||
const variants = model?.variants;
|
||||
return Boolean(variants && Object.keys(variants).length > 0);
|
||||
}, [isUser, modelID, providerID, providers]);
|
||||
|
||||
const displayAgentName = useStickyDisplayValue<string>(agentName);
|
||||
const displayProviderIDValue = useStickyDisplayValue<string>(providerID ?? undefined);
|
||||
const displayModelName = useStickyDisplayValue<string>(modelName);
|
||||
@@ -508,6 +528,22 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
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<ChatMessageProps> = ({
|
||||
providerID={headerProviderID}
|
||||
agentName={headerAgentName}
|
||||
modelName={headerModelName}
|
||||
variant={headerVariant}
|
||||
isDarkTheme={isDarkTheme}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -274,9 +274,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
providers,
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentVariant,
|
||||
currentAgentName,
|
||||
setProvider,
|
||||
setModel,
|
||||
setCurrentVariant,
|
||||
getCurrentModelVariants,
|
||||
setAgent,
|
||||
getCurrentProvider,
|
||||
getModelMetadata,
|
||||
@@ -294,6 +297,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
|
||||
getSessionAgentSelection,
|
||||
saveAgentModelForSession,
|
||||
getAgentModelForSession,
|
||||
saveAgentModelVariantForSession,
|
||||
getAgentModelVariantForSession,
|
||||
analyzeAndSaveExternalSessionChoices,
|
||||
getSessionAgentEditMode,
|
||||
setSessionAgentEditMode,
|
||||
@@ -308,7 +313,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ 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<ModelControlsProps> = ({ 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<ModelControlsProps> = ({ 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<ModelControlsProps> = ({ className }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileVariantPanel = () => {
|
||||
if (!isCompact || !hasVariants) return null;
|
||||
|
||||
const isDefault = !currentVariant;
|
||||
|
||||
const handleSelect = (variant: string | undefined) => {
|
||||
handleVariantSelect(variant);
|
||||
closeMobilePanel();
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[data-chat-input="true"]');
|
||||
textarea?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileOverlayPanel
|
||||
open={activeMobilePanel === 'variant'}
|
||||
onClose={closeMobilePanel}
|
||||
title="Thinking"
|
||||
>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-2 rounded-xl border px-2 py-1.5 text-left',
|
||||
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
||||
isDefault ? 'border-primary/30 bg-primary/10' : 'border-border/40'
|
||||
)}
|
||||
onClick={() => handleSelect(undefined)}
|
||||
>
|
||||
<span className="typography-meta font-medium text-foreground">Default</span>
|
||||
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</button>
|
||||
|
||||
{availableVariants.map((variant) => {
|
||||
const selected = currentVariant === variant;
|
||||
const label = variant.charAt(0).toUpperCase() + variant.slice(1);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={variant}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-2 rounded-xl border px-2 py-1.5 text-left',
|
||||
'focus:outline-none focus-visible:ring-1 focus-visible:ring-primary',
|
||||
selected ? 'border-primary/30 bg-primary/10' : 'border-border/40'
|
||||
)}
|
||||
onClick={() => handleSelect(variant)}
|
||||
>
|
||||
<span className="typography-meta font-medium text-foreground">{label}</span>
|
||||
{selected && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</MobileOverlayPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileAgentPanel = () => {
|
||||
if (!isCompact) return null;
|
||||
|
||||
|
||||
const primaryAgents = agents.filter(agent => isPrimaryMode(agent.mode));
|
||||
|
||||
|
||||
return (
|
||||
<MobileOverlayPanel
|
||||
open={activeMobilePanel === 'agent'}
|
||||
@@ -2152,6 +2281,94 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ 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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveMobilePanel('variant')}
|
||||
className={cn(
|
||||
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity min-w-0 focus:outline-none',
|
||||
buttonHeight,
|
||||
'cursor-pointer hover:opacity-70',
|
||||
)}
|
||||
>
|
||||
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
||||
<span className={cn('model-controls__variant-label', controlTextSize, 'font-medium truncate min-w-0', colorClass)}>
|
||||
{displayVariant}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={800}>
|
||||
<DropdownMenu>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'model-controls__variant-trigger flex items-center gap-1.5 transition-opacity cursor-pointer hover:opacity-70 min-w-0',
|
||||
buttonHeight,
|
||||
)}
|
||||
>
|
||||
<RiBrainAi3Line className={cn(controlIconSize, 'flex-shrink-0', colorClass)} />
|
||||
<span
|
||||
className={cn(
|
||||
'model-controls__variant-label',
|
||||
controlTextSize,
|
||||
'font-medium min-w-0 truncate',
|
||||
isDesktopRuntime ? 'max-w-[180px]' : undefined,
|
||||
colorClass,
|
||||
)}
|
||||
>
|
||||
{displayVariant}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(180px,calc(100vw-2rem))]">
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">Thinking</DropdownMenuLabel>
|
||||
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
|
||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">Default</span>
|
||||
{isDefault && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{availableVariants.length > 0 && <DropdownMenuSeparator />}
|
||||
{availableVariants.map((variant) => {
|
||||
const selected = currentVariant === variant;
|
||||
const label = variant.charAt(0).toUpperCase() + variant.slice(1);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={variant}
|
||||
className="typography-meta"
|
||||
onSelect={() => handleVariantSelect(variant)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||
<span className="typography-meta font-medium text-foreground truncate min-w-0">{label}</span>
|
||||
{selected && <RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<TooltipContent side="top">
|
||||
<p className="typography-meta">Thinking: {displayVariant}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAgentSelector = () => {
|
||||
if (!isCompact) {
|
||||
return (
|
||||
@@ -2344,7 +2561,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ 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<ModelControlsProps> = ({ className }) => {
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center min-w-0 flex-1 justify-end',
|
||||
inlineGapClass,
|
||||
isMobile && 'overflow-hidden'
|
||||
)}
|
||||
>
|
||||
{renderVariantSelector()}
|
||||
{renderModelSelector()}
|
||||
</div>
|
||||
<div className={cn('flex items-center min-w-0', inlineGapClass, isMobile && 'flex-shrink-0')}>
|
||||
{renderAgentSelector()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderMobileModelPanel()}
|
||||
{renderMobileVariantPanel()}
|
||||
{renderMobileAgentPanel()}
|
||||
{renderMobileModelTooltip()}
|
||||
{renderMobileAgentTooltip()}
|
||||
|
||||
@@ -10,10 +10,11 @@ interface MessageHeaderProps {
|
||||
providerID: string | null;
|
||||
agentName: string | undefined;
|
||||
modelName: string | undefined;
|
||||
variant?: string;
|
||||
isDarkTheme: boolean;
|
||||
}
|
||||
|
||||
const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agentName, modelName, isDarkTheme }) => {
|
||||
const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agentName, modelName, variant, isDarkTheme }) => {
|
||||
const { src: logoSrc, onError: handleLogoError, hasLogo } = useProviderLogo(providerID);
|
||||
|
||||
return (
|
||||
@@ -67,6 +68,25 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
|
||||
<span className="font-medium">{agentName}</span>
|
||||
</div>
|
||||
)}
|
||||
{!isUser && variant && (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-1.5 py-0 rounded',
|
||||
'agent-badge typography-meta',
|
||||
variant === 'Default' ? undefined : 'agent-info'
|
||||
)}
|
||||
style={
|
||||
variant === 'Default'
|
||||
? ({
|
||||
'--agent-color': 'var(--muted-foreground)',
|
||||
'--agent-color-bg': 'var(--muted-foreground)',
|
||||
} as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="font-medium">{variant.length > 0 ? variant[0].toLowerCase() + variant.slice(1) : variant}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ interface ContextState {
|
||||
|
||||
sessionAgentModelSelections: Map<string, Map<string, { providerId: string; modelId: string }>>;
|
||||
|
||||
// sessionId → agentName → "providerId/modelId" → variant
|
||||
sessionAgentModelVariantSelections: Map<string, Map<string, Map<string, string>>>;
|
||||
|
||||
currentAgentContext: Map<string, string>;
|
||||
|
||||
sessionContextUsage: Map<string, ContextUsage>;
|
||||
@@ -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<string, { info: any; parts: any[] }[]>) => Promise<Map<string, { providerId: string; modelId: string; timestamp: number }>>;
|
||||
|
||||
|
||||
getContextUsage: (sessionId: string, contextLimit: number, outputLimit: number, messages: Map<string, { info: any; parts: any[] }[]>) => ContextUsage | null;
|
||||
|
||||
updateSessionContextUsage: (sessionId: string, contextLimit: number, outputLimit: number, messages: Map<string, { info: any; parts: any[] }[]>) => void;
|
||||
@@ -71,6 +78,7 @@ export const useContextStore = create<ContextStore>()(
|
||||
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<ContextStore>()(
|
||||
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<string, { info: any; parts: any[] }[]>) => {
|
||||
const { saveAgentModelForSession } = get();
|
||||
const { saveAgentModelForSession, saveAgentModelVariantForSession } = get();
|
||||
|
||||
const agentLastChoices = new Map<
|
||||
string,
|
||||
@@ -212,6 +274,14 @@ export const useContextStore = create<ContextStore>()(
|
||||
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<ContextStore>()(
|
||||
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<ContextStore>()(
|
||||
});
|
||||
}
|
||||
|
||||
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<ContextStore>()(
|
||||
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,
|
||||
|
||||
@@ -20,9 +20,10 @@ import { useContextStore } from "./contextStore";
|
||||
|
||||
// Helper function to clean up pending user message metadata
|
||||
const cleanupPendingUserMessageMeta = (
|
||||
currentPending: Map<string, { mode?: string; providerID?: string; modelID?: string }>,
|
||||
currentPending: Map<string, { mode?: string; providerID?: string; modelID?: string; variant?: string }>,
|
||||
|
||||
sessionId: string
|
||||
): Map<string, { mode?: string; providerID?: string; modelID?: string }> => {
|
||||
): Map<string, { mode?: string; providerID?: string; modelID?: string; variant?: string }> => {
|
||||
const nextPending = new Map(currentPending);
|
||||
nextPending.delete(sessionId);
|
||||
return nextPending;
|
||||
@@ -338,12 +339,12 @@ interface MessageState {
|
||||
sessionCompactionUntil: Map<string, number>;
|
||||
sessionAbortFlags: Map<string, SessionAbortRecord>;
|
||||
pendingAssistantHeaderSessions: Set<string>;
|
||||
pendingUserMessageMetaBySession: Map<string, { mode?: string; providerID?: string; modelID?: string }>;
|
||||
pendingUserMessageMetaBySession: Map<string, { mode?: string; providerID?: string; modelID?: string; variant?: string }>;
|
||||
}
|
||||
|
||||
interface MessageActions {
|
||||
loadMessages: (sessionId: string) => Promise<void>;
|
||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, currentSessionId?: string, attachments?: AttachedFile[], agentMentionName?: string | null, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => Promise<void>;
|
||||
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<void>;
|
||||
abortCurrentOperation: (currentSessionId?: string) => Promise<void>;
|
||||
_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<MessageStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
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<MessageStore>()(
|
||||
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<MessageStore>()(
|
||||
modelID,
|
||||
text: effectiveContent,
|
||||
agent,
|
||||
variant,
|
||||
files: filePayloads.length > 0 ? filePayloads : undefined,
|
||||
additionalParts: additionalPartsPayload,
|
||||
agentMentions: agentMentionName ? [{ name: agentMentionName }] : undefined,
|
||||
|
||||
@@ -131,7 +131,7 @@ export interface SessionStore {
|
||||
unshareSession: (id: string) => Promise<Session | null>;
|
||||
setCurrentSession: (id: string | null) => void;
|
||||
loadMessages: (sessionId: string) => Promise<void>;
|
||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>) => Promise<void>;
|
||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string, additionalParts?: Array<{ text: string; attachments?: AttachedFile[] }>, variant?: string) => Promise<void>;
|
||||
abortCurrentOperation: () => Promise<void>;
|
||||
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<Map<string, { providerId: string; modelId: string; timestamp: number }>>;
|
||||
|
||||
|
||||
isOpenChamberCreatedSession: (sessionId: string) => boolean;
|
||||
|
||||
markSessionAsOpenChamberCreated: (sessionId: string) => void;
|
||||
|
||||
@@ -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<boolean>;
|
||||
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<ConfigStore>()(
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentVariant: undefined,
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
@@ -612,12 +617,12 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentModelId: modelId,
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
currentModelId: modelId,
|
||||
directoryScoped: {
|
||||
@@ -628,6 +633,46 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
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<string, unknown> } | 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;
|
||||
|
||||
@@ -290,7 +290,7 @@ export const useSessionStore = create<SessionStore>()(
|
||||
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<SessionStore>()(
|
||||
// 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<SessionStore>()(
|
||||
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<SessionStore>()(
|
||||
} 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<SessionStore>()(
|
||||
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<string, unknown>[]) => {
|
||||
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<string, unknown>[]) => {
|
||||
const messages = useMessageStore.getState().messages;
|
||||
return useContextStore.getState().analyzeAndSaveExternalSessionChoices(sessionId, agents, messages);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user