feat: add fusion for multi-run sessions

Adds Run fusion for multi-run-like sessions
Combines sibling outputs into a new fusion session
Adds configurable Fusion prompts in Magic Prompts settings
This commit is contained in:
Bohdan Triapitsyn
2026-05-15 13:26:46 +03:00
parent 713bb6dfb1
commit 3e53d7e071
24 changed files with 548 additions and 13 deletions
@@ -0,0 +1,31 @@
import type { SVGProps } from 'react';
export function FusionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
{...props}
>
<path
d="M12 4.5v6.25M12 10.75v3.5M12 10.75H7.25A3.25 3.25 0 0 0 4 14v1.25M12 10.75h4.75A3.25 3.25 0 0 1 20 14v1.25"
stroke="currentColor"
strokeWidth="1.65"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.25 7.25 12 4.5l2.75 2.75"
stroke="currentColor"
strokeWidth="1.65"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="4" cy="17.25" r="1.55" stroke="currentColor" strokeWidth="1.65" />
<circle cx="12" cy="17.25" r="1.55" stroke="currentColor" strokeWidth="1.65" />
<circle cx="20" cy="17.25" r="1.55" stroke="currentColor" strokeWidth="1.65" />
</svg>
);
}
@@ -22,6 +22,8 @@ export interface AgentSelectorProps {
disabled?: boolean;
/** ID for accessibility */
id?: string;
/** Portal menu to body instead of nearest dialog. */
portalToBody?: boolean;
}
/**
@@ -34,6 +36,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
className,
disabled,
id,
portalToBody,
}) => {
const { t } = useI18n();
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
@@ -95,7 +98,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
>
<SelectValue placeholder={t('multirun.agentSelector.placeholder')} />
</SelectTrigger>
<SelectContent fitContent>
<SelectContent fitContent portalToBody={portalToBody}>
{selectableAgents.length > 0 && (
<SelectGroup>
{selectableAgents.map((agent) => (
@@ -100,6 +100,14 @@ export interface ModelMultiSelectProps {
maxModels?: number;
/** Optional className for add model trigger button */
addButtonClassName?: string;
/** Direction for the model picker popup. Multi-run launcher opens upward near the footer. */
dropdownSide?: 'top' | 'bottom';
/** Optional className for the picker popup. */
dropdownClassName?: string;
/** Optional className for the trigger/dropdown positioning container. */
containerClassName?: string;
/** Optional trigger icon override. */
triggerIcon?: React.ReactNode;
}
/**
@@ -115,6 +123,10 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
showChips = true,
maxModels,
addButtonClassName,
dropdownSide = 'top',
dropdownClassName,
containerClassName,
triggerIcon,
}) => {
const { t } = useI18n();
const providers = useConfigStore((state) => state.providers);
@@ -128,7 +140,8 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
const dropdownRef = React.useRef<HTMLDivElement>(null);
const triggerRef = React.useRef<HTMLButtonElement>(null);
const itemRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
const canAddModel = maxModels === undefined || selectedModels.length < maxModels;
const isSingleSelect = maxModels === 1;
const canAddModel = maxModels === undefined || selectedModels.length < maxModels || isSingleSelect;
// Count occurrences of each model for display purposes
const modelCounts = React.useMemo(() => {
@@ -208,12 +221,21 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
const hasResults = filteredFavorites.length > 0 || filteredRecents.length > 0 || filteredProviders.length > 0;
// Calculate available height: space above trigger within visible area
// Calculate available height: multi-run opens upward inside a scroller; fusion opens downward and may extend past the dialog.
React.useEffect(() => {
if (!isOpen || !triggerRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect();
if (dropdownSide === 'bottom') {
const viewportHeight = window.visualViewport?.height ?? document.documentElement.clientHeight ?? window.innerHeight;
const spaceBelow = viewportHeight - triggerRect.bottom - 16;
// availableHeight is only the scrollable model list; reserve room for search + keyboard hint chrome.
const listSpaceBelow = spaceBelow - 112;
setAvailableHeight(Math.max(160, Math.min(320, listSpaceBelow)));
return;
}
// Find the nearest dialog or overflow ancestor to constrain within
let container: HTMLElement | null = triggerRef.current.parentElement;
while (container) {
@@ -231,7 +253,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
const spaceAbove = triggerRect.top - topBound - 16;
// Cap: min 150, max 300
setAvailableHeight(Math.max(150, Math.min(300, spaceAbove)));
}, [isOpen]);
}, [dropdownSide, isOpen]);
// Focus search input when opened
React.useEffect(() => {
@@ -292,12 +314,27 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
type="button"
disabled={!canAddModel}
onClick={() => {
onAdd({
const nextModel = {
providerID,
modelID,
displayName: (model.name as string) || modelID,
instanceId: generateInstanceId(),
};
if (isSingleSelect && selectedModels.length > 0 && onUpdate) {
onUpdate(0, nextModel);
setIsOpen(false);
setSearchQuery('');
setSelectedIndex(0);
return;
}
onAdd({
...nextModel,
});
if (isSingleSelect) {
setIsOpen(false);
setSearchQuery('');
setSelectedIndex(0);
}
// Don't close dropdown - allow selecting multiple
}}
onMouseEnter={() => setSelectedIndex(flatIndex)}
@@ -333,7 +370,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<div className="space-y-2">
<div className="flex flex-wrap gap-1.5 items-center">
{/* Add model button (dropdown trigger) */}
<div className="relative" ref={dropdownRef}>
<div className={cn('relative', containerClassName)} ref={dropdownRef}>
<Button
ref={triggerRef}
type="button"
@@ -349,7 +386,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
setIsOpen(!isOpen);
}}
>
<Icon name="add" className="h-3.5 w-3.5 mr-1" />
{triggerIcon ?? <Icon name="add" className="h-3.5 w-3.5 mr-1" />}
{addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
</Button>
@@ -398,12 +435,22 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
e.stopPropagation();
const selectedItem = flatModelList[selectedIndex];
if (selectedItem && canAddModel) {
onAdd({
const nextModel = {
providerID: selectedItem.providerID,
modelID: selectedItem.modelID,
displayName: (selectedItem.model.name as string) || selectedItem.modelID,
instanceId: generateInstanceId(),
});
};
if (isSingleSelect && selectedModels.length > 0 && onUpdate) {
onUpdate(0, nextModel);
} else {
onAdd(nextModel);
}
if (isSingleSelect) {
setIsOpen(false);
setSearchQuery('');
setSelectedIndex(0);
}
}
} else if (e.key === 'Escape') {
e.preventDefault();
@@ -418,7 +465,11 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
return (
<div
className="absolute bottom-full left-0 mb-1 z-50 w-[min(380px,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] flex flex-col overflow-hidden rounded-xl border border-border/50 shadow-lg"
className={cn(
'absolute left-0 z-50 w-[min(420px,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] flex flex-col overflow-hidden rounded-xl border border-border/50 shadow-lg',
dropdownSide === 'top' ? 'bottom-full mb-1' : 'top-full mt-1',
dropdownClassName,
)}
style={{
background: 'linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))',
}}
@@ -0,0 +1,259 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2/client';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Icon } from '@/components/icon/Icon';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { useI18n } from '@/lib/i18n';
import { opencodeClient } from '@/lib/opencode/client';
import { useConfigStore } from '@/stores/useConfigStore';
import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useAllLiveSessions } from '@/sync/sync-context';
import { getSyncMessages, getSyncParts } from '@/sync/sync-refs';
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
import { getFusionSessionTitle, parseMultiRunSessionTitle } from '@/lib/multirun/title';
import { renderMagicPrompt } from '@/lib/magicPrompts';
import { AgentSelector } from './AgentSelector';
import { ModelMultiSelect, generateInstanceId, type ModelSelectionWithId } from './ModelMultiSelect';
type FusionSource = {
session: Session;
directory: string | null;
projectDirectory: string | null;
};
const buildSourcePart = (source: FusionSource, text: string, index: number): string => {
const title = source.session.title?.trim() || source.session.id;
return `\n\n--- RESULT ${index + 1}: ${title} ---\n${text.trim()}\n--- END RESULT ${index + 1} ---`;
};
const getSessionProjectDirectory = (sessionId: string, directory: string | null): string | null => {
const metadata = useSessionUIStore.getState().getWorktreeMetadata(sessionId);
return metadata?.projectDirectory ?? directory;
};
const getLastAssistantText = async (source: FusionSource): Promise<string> => {
const directory = source.directory ?? undefined;
const messages = getSyncMessages(source.session.id, directory);
if (messages.length === 0 && source.directory) {
const result = await opencodeClient.withDirectory(source.directory, () =>
opencodeClient.getSdkClient().session.messages({
sessionID: source.session.id,
directory: source.directory ?? undefined,
limit: 50,
})
);
const records = result.data ?? [];
for (let index = records.length - 1; index >= 0; index -= 1) {
const record = records[index] as { info?: { role?: string }; parts?: unknown[] };
if (record.info?.role !== 'assistant') continue;
return flattenAssistantTextParts((record.parts ?? []) as Parameters<typeof flattenAssistantTextParts>[0]).trim();
}
return '';
}
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message.role !== 'assistant') continue;
return flattenAssistantTextParts(getSyncParts(message.id, directory)).trim();
}
return '';
};
export function MultiRunFusionDialog({
session,
open,
onOpenChange,
}: {
session: Session;
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useI18n();
const liveSessions = useAllLiveSessions();
const activeSessions = useGlobalSessionsStore((state) => state.activeSessions);
const archivedSessions = useGlobalSessionsStore((state) => state.archivedSessions);
const providers = useConfigStore((state) => state.providers);
const currentProviderId = useConfigStore((state) => state.currentProviderId);
const currentModelId = useConfigStore((state) => state.currentModelId);
const currentAgentName = useConfigStore((state) => state.currentAgentName);
const [providerID, setProviderID] = React.useState(currentProviderId ?? '');
const [modelID, setModelID] = React.useState(currentModelId ?? '');
const [selectedModelSelection, setSelectedModelSelection] = React.useState<ModelSelectionWithId[]>(() => (
currentProviderId && currentModelId
? [{ providerID: currentProviderId, modelID: currentModelId, instanceId: generateInstanceId() }]
: []
));
const [variant, setVariant] = React.useState<string>('');
const [agent, setAgent] = React.useState(currentAgentName ?? '');
const [sources, setSources] = React.useState<FusionSource[]>([]);
const [isStarting, setIsStarting] = React.useState(false);
const parsed = React.useMemo(() => parseMultiRunSessionTitle(session.title), [session.title]);
const allSessions = React.useMemo(() => {
const byId = new Map<string, Session>();
for (const candidate of liveSessions) byId.set(candidate.id, candidate);
for (const candidate of activeSessions) byId.set(candidate.id, candidate);
for (const candidate of archivedSessions) byId.set(candidate.id, candidate);
if (session.id) byId.set(session.id, session);
return Array.from(byId.values());
}, [activeSessions, archivedSessions, liveSessions, session]);
React.useEffect(() => {
if (!open || !parsed) return;
const currentDirectory = useSessionUIStore.getState().getDirectoryForSession(session.id);
const currentProjectDirectory = getSessionProjectDirectory(session.id, currentDirectory);
const nextSources = allSessions
.map((candidate): FusionSource | null => {
const candidateParsed = parseMultiRunSessionTitle(candidate.title);
if (!candidateParsed || candidateParsed.groupSlug !== parsed.groupSlug || candidateParsed.fusion) return null;
const directory = useSessionUIStore.getState().getDirectoryForSession(candidate.id)
?? resolveGlobalSessionDirectory(candidate);
const projectDirectory = getSessionProjectDirectory(candidate.id, directory);
if (currentProjectDirectory && projectDirectory && currentProjectDirectory !== projectDirectory) return null;
return { session: candidate, directory, projectDirectory };
})
.filter((source): source is FusionSource => source !== null)
.sort((a, b) => (a.session.time?.created ?? 0) - (b.session.time?.created ?? 0));
setSources(nextSources);
}, [allSessions, open, parsed, session.id]);
const selectedProvider = providers.find((provider) => provider.id === providerID);
const selectedProviderModel = selectedProvider?.models.find((model) => model.id === modelID) as { variants?: Record<string, unknown> } | undefined;
const variantKeys = selectedProviderModel?.variants ? Object.keys(selectedProviderModel.variants) : [];
const canStart = Boolean(parsed && providerID && modelID && sources.length > 0 && !isStarting);
const handleModelSelect = React.useCallback((model: ModelSelectionWithId) => {
setSelectedModelSelection([model]);
setProviderID(model.providerID);
setModelID(model.modelID);
setVariant('');
}, []);
const selectedModelLabel = selectedModelSelection[0]?.displayName || selectedModelSelection[0]?.modelID || t('multirun.fusion.model.placeholder');
const handleStart = async () => {
if (!parsed || !providerID || !modelID) return;
setIsStarting(true);
try {
const sourceTexts = await Promise.all(sources.map((source) => getLastAssistantText(source)));
const usableSources = sources
.map((source, index) => ({ source, text: sourceTexts[index] ?? '' }))
.filter((item) => item.text.trim().length > 0);
if (usableSources.length === 0) {
toast.error(t('multirun.fusion.toast.noOutputs'));
return;
}
const directory = sources[0]?.projectDirectory ?? sources[0]?.directory ?? null;
const fusionTitle = getFusionSessionTitle(parsed.groupSlug, providerID, modelID);
const [visiblePrompt, instructionsPrompt] = await Promise.all([
renderMagicPrompt('session.fusion.visible'),
renderMagicPrompt('session.fusion.instructions'),
]);
const fusionSession = await useSessionUIStore.getState().createSession(fusionTitle, directory, null);
if (!fusionSession) throw new Error('Failed to create fusion session');
useSessionUIStore.getState().setCurrentSession(fusionSession.id, directory);
onOpenChange(false);
await opencodeClient.withDirectory(directory ?? opencodeClient.getDirectory(), () =>
opencodeClient.sendMessage({
id: fusionSession.id,
providerID,
modelID,
variant: variant || undefined,
agent: agent || undefined,
text: visiblePrompt,
additionalParts: [
{ text: instructionsPrompt, synthetic: true },
...usableSources.map((item, index) => ({ text: buildSourcePart(item.source, item.text, index), synthetic: true })),
{ text: '\n\n--- FUSION INPUTS END ---\nNow write the final fused answer.', synthetic: true },
],
})
);
} catch (error) {
console.error('[MultiRunFusion] Failed to start fusion', error);
toast.error(t('multirun.fusion.toast.failed'));
} finally {
setIsStarting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-xl overflow-visible">
<DialogHeader>
<DialogTitle>{t('multirun.fusion.title')}</DialogTitle>
<DialogDescription>{t('multirun.fusion.description')}</DialogDescription>
</DialogHeader>
<div className="flex flex-wrap items-center gap-2">
<div className="max-w-full">
<ModelMultiSelect
selectedModels={selectedModelSelection}
onAdd={handleModelSelect}
onUpdate={(_, model) => handleModelSelect(model)}
onRemove={() => {
setSelectedModelSelection([]);
setProviderID('');
setModelID('');
setVariant('');
}}
maxModels={1}
addButtonLabel={selectedModelLabel}
showChips={false}
addButtonClassName="h-8 w-fit max-w-[min(28rem,calc(100vw-8rem))] justify-start rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] px-3 py-1.5"
dropdownSide="bottom"
dropdownClassName="w-[min(28rem,calc(100vw-8rem))]"
triggerIcon={providerID ? <ProviderLogo providerId={providerID} className="h-3.5 w-3.5 mr-1" /> : undefined}
/>
</div>
{variantKeys.length > 0 ? (
<Select value={variant || '__default__'} onValueChange={(value) => setVariant(value === '__default__' ? '' : value)}>
<SelectTrigger size="lg" className="h-8 w-fit rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] !border-border/80 !bg-[var(--surface-subtle)] hover:!bg-[var(--interactive-hover)]/70 typography-meta font-medium text-foreground px-3 py-1.5">
<Icon name="brain-ai-3" className="h-3.5 w-3.5 text-muted-foreground" />
<SelectValue>{(value) => value === '__default__' ? t('multirun.modelMultiSelect.variant.default') : value}</SelectValue>
</SelectTrigger>
<SelectContent fitContent portalToBody>
<SelectItem value="__default__">{t('multirun.modelMultiSelect.variant.default')}</SelectItem>
{variantKeys.map((key) => <SelectItem key={key} value={key}>{key}</SelectItem>)}
</SelectContent>
</Select>
) : null}
<AgentSelector value={agent} onChange={setAgent} portalToBody className="h-8 rounded-[9px] [corner-shape:squircle] supports-[corner-shape:squircle]:rounded-[50px] px-3 py-1.5" />
</div>
<div className="space-y-2">
<div className="typography-meta font-medium text-foreground">{t('multirun.fusion.sources.label', { count: sources.length })}</div>
<div className="max-h-56 space-y-1 overflow-auto rounded-lg border border-[var(--interactive-border)] p-1">
{sources.map((source) => (
<div key={source.session.id} className="flex items-center gap-2 rounded-md px-2 py-1.5 typography-meta">
<ProviderLogo providerId={parseMultiRunSessionTitle(source.session.title)?.providerID ?? ''} className="h-4 w-4" />
<span className="min-w-0 flex-1 truncate">{source.session.title || source.session.id}</span>
<button type="button" onClick={() => setSources((prev) => prev.filter((item) => item.session.id !== source.session.id))} className="text-muted-foreground hover:text-foreground">
<Icon name="close" className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => onOpenChange(false)}>{t('multirun.fusion.actions.cancel')}</Button>
<Button onClick={handleStart} disabled={!canStart}>{isStarting ? t('multirun.fusion.actions.starting') : t('multirun.fusion.actions.start')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -140,6 +140,14 @@ const PROMPT_PAGE_MAP: Record<string, PromptPageConfig> = {
{ id: 'session.review.instructions', titleKey: 'settings.magicPrompts.page.block.instructions' },
],
},
'session.fusion': {
titleKey: 'settings.magicPrompts.page.group.sessionFusion.title',
descriptionKey: 'settings.magicPrompts.page.group.sessionFusion.description',
blocks: [
{ id: 'session.fusion.visible', titleKey: 'settings.magicPrompts.page.block.visiblePrompt' },
{ id: 'session.fusion.instructions', titleKey: 'settings.magicPrompts.page.block.instructions' },
],
},
};
const hasOwn = (input: Record<string, string>, key: string) => Object.prototype.hasOwnProperty.call(input, key);
@@ -47,6 +47,7 @@ export const MagicPromptsSidebar: React.FC<MagicPromptsSidebarProps> = ({ onItem
items: [
{ id: 'session.summary', titleKey: 'settings.magicPrompts.sidebar.item.sessionSummary' },
{ id: 'session.review', titleKey: 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview' },
{ id: 'session.fusion', titleKey: 'settings.magicPrompts.sidebar.item.sessionFusion' },
],
},
] as const;
@@ -29,6 +29,9 @@ import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useSessionUnseenCount } from '@/sync/notification-store';
import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore';
import { useI18n } from '@/lib/i18n';
import { parseMultiRunSessionTitle } from '@/lib/multirun/title';
import { MultiRunFusionDialog } from '@/components/multirun/MultiRunFusionDialog';
import { FusionIcon } from '@/components/icons/FusionIcon';
type Folder = { id: string; name: string; sessionIds: string[] };
@@ -319,6 +322,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const sessionUpdatedLabel = formatSessionDateLabel(sessionTimestamp);
const sessionCompactUpdatedLabel = formatSessionCompactDateLabel(sessionTimestamp);
const isMenuOpen = openSidebarMenuKey === menuInstanceKey;
const isMultiRunLikeSession = React.useMemo(() => parseMultiRunSessionTitle(resolvedSession.title) !== null, [resolvedSession.title]);
const [fusionDialogOpen, setFusionDialogOpen] = React.useState(false);
const descendantCount = React.useMemo(() => collectNodeDescendantIds(node).length, [collectNodeDescendantIds, node]);
@@ -670,6 +675,12 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<Icon name="download" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.exportMarkdown')}
</DropdownMenuItem>
{isMultiRunLikeSession ? (
<DropdownMenuItem onClick={() => setFusionDialogOpen(true)} className="[&>svg]:mr-1">
<FusionIcon className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.runFusion')}
</DropdownMenuItem>
) : null}
{sessionDirectory && !archivedBucket ? (() => {
const scopeFolders = getFoldersForScope(sessionDirectory);
@@ -980,6 +991,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
</DialogFooter>
</DialogContent>
</Dialog>
{isMultiRunLikeSession ? (
<MultiRunFusionDialog
session={resolvedSession}
open={fusionDialogOpen}
onOpenChange={setFusionDialogOpen}
/>
) : null}
</React.Fragment>
);
}
+4 -2
View File
@@ -155,6 +155,7 @@ function SelectTrigger({
type SelectContentExtra = {
position?: "popper" | "item-aligned";
fitContent?: boolean;
portalToBody?: boolean;
sideOffset?: number;
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
@@ -165,6 +166,7 @@ function SelectContent({
children,
position = "popper",
fitContent = false,
portalToBody = false,
sideOffset,
side,
align,
@@ -175,13 +177,13 @@ function SelectContent({
const portalContainer = portalContext?.portalContainer ?? null;
return (
<BaseSelect.Portal container={portalContainer || undefined}>
<BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}>
<BaseSelect.Positioner
alignItemWithTrigger={alignItemWithTrigger}
sideOffset={sideOffset}
side={side}
align={align}
className="z-[120] pointer-events-auto"
className="absolute z-[120] pointer-events-auto"
>
<BaseSelect.Popup
data-slot="select-content"