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; disabled?: boolean;
/** ID for accessibility */ /** ID for accessibility */
id?: string; id?: string;
/** Portal menu to body instead of nearest dialog. */
portalToBody?: boolean;
} }
/** /**
@@ -34,6 +36,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
className, className,
disabled, disabled,
id, id,
portalToBody,
}) => { }) => {
const { t } = useI18n(); const { t } = useI18n();
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents); const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
@@ -95,7 +98,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
> >
<SelectValue placeholder={t('multirun.agentSelector.placeholder')} /> <SelectValue placeholder={t('multirun.agentSelector.placeholder')} />
</SelectTrigger> </SelectTrigger>
<SelectContent fitContent> <SelectContent fitContent portalToBody={portalToBody}>
{selectableAgents.length > 0 && ( {selectableAgents.length > 0 && (
<SelectGroup> <SelectGroup>
{selectableAgents.map((agent) => ( {selectableAgents.map((agent) => (
@@ -100,6 +100,14 @@ export interface ModelMultiSelectProps {
maxModels?: number; maxModels?: number;
/** Optional className for add model trigger button */ /** Optional className for add model trigger button */
addButtonClassName?: string; 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, showChips = true,
maxModels, maxModels,
addButtonClassName, addButtonClassName,
dropdownSide = 'top',
dropdownClassName,
containerClassName,
triggerIcon,
}) => { }) => {
const { t } = useI18n(); const { t } = useI18n();
const providers = useConfigStore((state) => state.providers); const providers = useConfigStore((state) => state.providers);
@@ -128,7 +140,8 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
const dropdownRef = React.useRef<HTMLDivElement>(null); const dropdownRef = React.useRef<HTMLDivElement>(null);
const triggerRef = React.useRef<HTMLButtonElement>(null); const triggerRef = React.useRef<HTMLButtonElement>(null);
const itemRefs = 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 // Count occurrences of each model for display purposes
const modelCounts = React.useMemo(() => { 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; 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(() => { React.useEffect(() => {
if (!isOpen || !triggerRef.current) return; if (!isOpen || !triggerRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect(); 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 // Find the nearest dialog or overflow ancestor to constrain within
let container: HTMLElement | null = triggerRef.current.parentElement; let container: HTMLElement | null = triggerRef.current.parentElement;
while (container) { while (container) {
@@ -231,7 +253,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
const spaceAbove = triggerRect.top - topBound - 16; const spaceAbove = triggerRect.top - topBound - 16;
// Cap: min 150, max 300 // Cap: min 150, max 300
setAvailableHeight(Math.max(150, Math.min(300, spaceAbove))); setAvailableHeight(Math.max(150, Math.min(300, spaceAbove)));
}, [isOpen]); }, [dropdownSide, isOpen]);
// Focus search input when opened // Focus search input when opened
React.useEffect(() => { React.useEffect(() => {
@@ -292,12 +314,27 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
type="button" type="button"
disabled={!canAddModel} disabled={!canAddModel}
onClick={() => { onClick={() => {
onAdd({ const nextModel = {
providerID, providerID,
modelID, modelID,
displayName: (model.name as string) || modelID, displayName: (model.name as string) || modelID,
instanceId: generateInstanceId(), 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 // Don't close dropdown - allow selecting multiple
}} }}
onMouseEnter={() => setSelectedIndex(flatIndex)} onMouseEnter={() => setSelectedIndex(flatIndex)}
@@ -333,7 +370,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap gap-1.5 items-center"> <div className="flex flex-wrap gap-1.5 items-center">
{/* Add model button (dropdown trigger) */} {/* Add model button (dropdown trigger) */}
<div className="relative" ref={dropdownRef}> <div className={cn('relative', containerClassName)} ref={dropdownRef}>
<Button <Button
ref={triggerRef} ref={triggerRef}
type="button" type="button"
@@ -349,7 +386,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
setIsOpen(!isOpen); 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')} {addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
</Button> </Button>
@@ -398,12 +435,22 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
e.stopPropagation(); e.stopPropagation();
const selectedItem = flatModelList[selectedIndex]; const selectedItem = flatModelList[selectedIndex];
if (selectedItem && canAddModel) { if (selectedItem && canAddModel) {
onAdd({ const nextModel = {
providerID: selectedItem.providerID, providerID: selectedItem.providerID,
modelID: selectedItem.modelID, modelID: selectedItem.modelID,
displayName: (selectedItem.model.name as string) || selectedItem.modelID, displayName: (selectedItem.model.name as string) || selectedItem.modelID,
instanceId: generateInstanceId(), 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') { } else if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
@@ -418,7 +465,11 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
return ( return (
<div <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={{ style={{
background: 'linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))', 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' }, { 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); 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: [ items: [
{ id: 'session.summary', titleKey: 'settings.magicPrompts.sidebar.item.sessionSummary' }, { id: 'session.summary', titleKey: 'settings.magicPrompts.sidebar.item.sessionSummary' },
{ id: 'session.review', titleKey: 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview' }, { id: 'session.review', titleKey: 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview' },
{ id: 'session.fusion', titleKey: 'settings.magicPrompts.sidebar.item.sessionFusion' },
], ],
}, },
] as const; ] as const;
@@ -29,6 +29,9 @@ import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useSessionUnseenCount } from '@/sync/notification-store'; import { useSessionUnseenCount } from '@/sync/notification-store';
import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore'; import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore';
import { useI18n } from '@/lib/i18n'; 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[] }; type Folder = { id: string; name: string; sessionIds: string[] };
@@ -319,6 +322,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const sessionUpdatedLabel = formatSessionDateLabel(sessionTimestamp); const sessionUpdatedLabel = formatSessionDateLabel(sessionTimestamp);
const sessionCompactUpdatedLabel = formatSessionCompactDateLabel(sessionTimestamp); const sessionCompactUpdatedLabel = formatSessionCompactDateLabel(sessionTimestamp);
const isMenuOpen = openSidebarMenuKey === menuInstanceKey; 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]); 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" /> <Icon name="download" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.exportMarkdown')} {t('sessions.sidebar.session.menu.exportMarkdown')}
</DropdownMenuItem> </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 ? (() => { {sessionDirectory && !archivedBucket ? (() => {
const scopeFolders = getFoldersForScope(sessionDirectory); const scopeFolders = getFoldersForScope(sessionDirectory);
@@ -980,6 +991,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{isMultiRunLikeSession ? (
<MultiRunFusionDialog
session={resolvedSession}
open={fusionDialogOpen}
onOpenChange={setFusionDialogOpen}
/>
) : null}
</React.Fragment> </React.Fragment>
); );
} }
+4 -2
View File
@@ -155,6 +155,7 @@ function SelectTrigger({
type SelectContentExtra = { type SelectContentExtra = {
position?: "popper" | "item-aligned"; position?: "popper" | "item-aligned";
fitContent?: boolean; fitContent?: boolean;
portalToBody?: boolean;
sideOffset?: number; sideOffset?: number;
side?: "top" | "right" | "bottom" | "left"; side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end"; align?: "start" | "center" | "end";
@@ -165,6 +166,7 @@ function SelectContent({
children, children,
position = "popper", position = "popper",
fitContent = false, fitContent = false,
portalToBody = false,
sideOffset, sideOffset,
side, side,
align, align,
@@ -175,13 +177,13 @@ function SelectContent({
const portalContainer = portalContext?.portalContainer ?? null; const portalContainer = portalContext?.portalContainer ?? null;
return ( return (
<BaseSelect.Portal container={portalContainer || undefined}> <BaseSelect.Portal container={portalToBody ? undefined : portalContainer || undefined}>
<BaseSelect.Positioner <BaseSelect.Positioner
alignItemWithTrigger={alignItemWithTrigger} alignItemWithTrigger={alignItemWithTrigger}
sideOffset={sideOffset} sideOffset={sideOffset}
side={side} side={side}
align={align} align={align}
className="z-[120] pointer-events-auto" className="absolute z-[120] pointer-events-auto"
> >
<BaseSelect.Popup <BaseSelect.Popup
data-slot="select-content" data-slot="select-content"
@@ -164,6 +164,7 @@ export const settingsDict = {
'settings.magicPrompts.sidebar.item.planImplement': 'Implement Plan', 'settings.magicPrompts.sidebar.item.planImplement': 'Implement Plan',
'settings.magicPrompts.sidebar.item.sessionSummary': 'Session Summary', 'settings.magicPrompts.sidebar.item.sessionSummary': 'Session Summary',
'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': 'Workspace Review', 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': 'Workspace Review',
'settings.magicPrompts.sidebar.item.sessionFusion': 'Fusion',
'settings.remoteInstances.sidebar.title': 'Remote Instances', 'settings.remoteInstances.sidebar.title': 'Remote Instances',
'settings.remoteInstances.sidebar.total': 'Total {count}', 'settings.remoteInstances.sidebar.total': 'Total {count}',
'settings.remoteInstances.sidebar.newSshInstanceName': 'New SSH Instance', 'settings.remoteInstances.sidebar.newSshInstanceName': 'New SSH Instance',
@@ -1541,6 +1542,8 @@ export const settingsDict = {
'settings.magicPrompts.page.group.sessionSummary.description': 'Prompts used by the /summary slash command: visible user message + hidden instructions. Non-destructive - does not compact session history.', 'settings.magicPrompts.page.group.sessionSummary.description': 'Prompts used by the /summary slash command: visible user message + hidden instructions. Non-destructive - does not compact session history.',
'settings.magicPrompts.page.group.sessionWorkspaceReview.title': 'Workspace Review', 'settings.magicPrompts.page.group.sessionWorkspaceReview.title': 'Workspace Review',
'settings.magicPrompts.page.group.sessionWorkspaceReview.description': 'Prompts used by the /workspace-review slash command: visible user message + hidden instructions. Reviews current workspace changes for high-signal issues only.', 'settings.magicPrompts.page.group.sessionWorkspaceReview.description': 'Prompts used by the /workspace-review slash command: visible user message + hidden instructions. Reviews current workspace changes for high-signal issues only.',
'settings.magicPrompts.page.group.sessionFusion.title': 'Fusion',
'settings.magicPrompts.page.group.sessionFusion.description': 'Prompts used when combining multi-run outputs into one final answer: visible user message + hidden instructions before source results.',
'settings.magicPrompts.page.actions.resetting': 'Resetting...', 'settings.magicPrompts.page.actions.resetting': 'Resetting...',
'settings.magicPrompts.page.actions.resetAllOverrides': 'Reset All Overrides', 'settings.magicPrompts.page.actions.resetAllOverrides': 'Reset All Overrides',
'settings.magicPrompts.page.actions.resetToDefault': 'Reset to Default', 'settings.magicPrompts.page.actions.resetToDefault': 'Reset to Default',
+11
View File
@@ -180,6 +180,16 @@ export const dict = {
'multirun.modelMultiSelect.validation.minOnly': 'Select at least {min} models.', 'multirun.modelMultiSelect.validation.minOnly': 'Select at least {min} models.',
'multirun.modelMultiSelect.validation.minToMax': 'Select from {min} to {max} models.', 'multirun.modelMultiSelect.validation.minToMax': 'Select from {min} to {max} models.',
'multirun.agentSelector.placeholder': 'Select an agent', 'multirun.agentSelector.placeholder': 'Select an agent',
'multirun.fusion.title': 'Run fusion',
'multirun.fusion.description': 'Combine the selected multi-run outputs into one stronger answer.',
'multirun.fusion.provider.placeholder': 'Provider',
'multirun.fusion.model.placeholder': 'Model',
'multirun.fusion.sources.label': 'Sources ({count})',
'multirun.fusion.actions.cancel': 'Cancel',
'multirun.fusion.actions.start': 'Start fusion',
'multirun.fusion.actions.starting': 'Starting...',
'multirun.fusion.toast.noOutputs': 'No assistant outputs found to fuse.',
'multirun.fusion.toast.failed': 'Failed to start fusion.',
'sessions.sidebar.header.actions.searchSessions': 'Search sessions', 'sessions.sidebar.header.actions.searchSessions': 'Search sessions',
'sessions.sidebar.header.actions.exitSelection': 'Exit selection', 'sessions.sidebar.header.actions.exitSelection': 'Exit selection',
'sessions.sidebar.header.actions.selectSessions': 'Select sessions', 'sessions.sidebar.header.actions.selectSessions': 'Select sessions',
@@ -242,6 +252,7 @@ export const dict = {
'sessions.sidebar.session.menu.copyLink': 'Copy link', 'sessions.sidebar.session.menu.copyLink': 'Copy link',
'sessions.sidebar.session.menu.unshare': 'Unshare', 'sessions.sidebar.session.menu.unshare': 'Unshare',
'sessions.sidebar.session.menu.exportMarkdown': 'Export Markdown', 'sessions.sidebar.session.menu.exportMarkdown': 'Export Markdown',
'sessions.sidebar.session.menu.runFusion': 'Run fusion',
'sessions.sidebar.session.menu.openInSidePanel': 'Open in Side Panel', 'sessions.sidebar.session.menu.openInSidePanel': 'Open in Side Panel',
'sessions.sidebar.session.menu.betaBadge': 'beta', 'sessions.sidebar.session.menu.betaBadge': 'beta',
'sessions.sidebar.session.menu.label': 'Session menu', 'sessions.sidebar.session.menu.label': 'Session menu',
@@ -164,6 +164,7 @@ export const settingsDict = {
"settings.magicPrompts.sidebar.item.planImplement": "Implementar plan", "settings.magicPrompts.sidebar.item.planImplement": "Implementar plan",
"settings.magicPrompts.sidebar.item.sessionSummary": "Resumen de sesión", "settings.magicPrompts.sidebar.item.sessionSummary": "Resumen de sesión",
"settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Revisión del espacio de trabajo", "settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Revisión del espacio de trabajo",
"settings.magicPrompts.sidebar.item.sessionFusion": "Fusion",
"settings.remoteInstances.sidebar.title": "Instancias remotas", "settings.remoteInstances.sidebar.title": "Instancias remotas",
"settings.remoteInstances.sidebar.total": "Total {count}", "settings.remoteInstances.sidebar.total": "Total {count}",
"settings.remoteInstances.sidebar.newSshInstanceName": "Nueva instancia SSH", "settings.remoteInstances.sidebar.newSshInstanceName": "Nueva instancia SSH",
@@ -1541,6 +1542,8 @@ export const settingsDict = {
"settings.magicPrompts.page.group.sessionSummary.description": "Prompts usados por el comando /summary: mensaje visible del usuario + instrucciones ocultas. No destructivo: no compacta el historial de la sesión.", "settings.magicPrompts.page.group.sessionSummary.description": "Prompts usados por el comando /summary: mensaje visible del usuario + instrucciones ocultas. No destructivo: no compacta el historial de la sesión.",
"settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Revisión del espacio de trabajo", "settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Revisión del espacio de trabajo",
"settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Prompts usados por el comando /workspace-review: mensaje visible del usuario + instrucciones ocultas. Revisa los cambios actuales del espacio de trabajo solo para problemas de alta señal.", "settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Prompts usados por el comando /workspace-review: mensaje visible del usuario + instrucciones ocultas. Revisa los cambios actuales del espacio de trabajo solo para problemas de alta señal.",
"settings.magicPrompts.page.group.sessionFusion.title": "Fusion",
"settings.magicPrompts.page.group.sessionFusion.description": "Prompts usados para combinar salidas de multi-run en una respuesta final: mensaje visible del usuario + instrucciones ocultas antes de los resultados fuente.",
"settings.magicPrompts.page.actions.resetting": "Restableciendo...", "settings.magicPrompts.page.actions.resetting": "Restableciendo...",
"settings.magicPrompts.page.actions.resetAllOverrides": "Restablecer todas las sobrescrituras", "settings.magicPrompts.page.actions.resetAllOverrides": "Restablecer todas las sobrescrituras",
"settings.magicPrompts.page.actions.resetToDefault": "Restablecer valores predeterminados", "settings.magicPrompts.page.actions.resetToDefault": "Restablecer valores predeterminados",
+11
View File
@@ -181,6 +181,16 @@ export const dict: Record<I18nKey, string> = {
"multirun.modelMultiSelect.validation.minOnly": "Selecciona al menos {min} modelos.", "multirun.modelMultiSelect.validation.minOnly": "Selecciona al menos {min} modelos.",
"multirun.modelMultiSelect.validation.minToMax": "Selecciona entre {min} y {max} modelos.", "multirun.modelMultiSelect.validation.minToMax": "Selecciona entre {min} y {max} modelos.",
"multirun.agentSelector.placeholder": "Selecciona un agente", "multirun.agentSelector.placeholder": "Selecciona un agente",
"multirun.fusion.title": "Ejecutar fusion",
"multirun.fusion.description": "Combina los resultados seleccionados del multi-run en una respuesta más sólida.",
"multirun.fusion.provider.placeholder": "Proveedor",
"multirun.fusion.model.placeholder": "Modelo",
"multirun.fusion.sources.label": "Fuentes ({count})",
"multirun.fusion.actions.cancel": "Cancelar",
"multirun.fusion.actions.start": "Iniciar fusion",
"multirun.fusion.actions.starting": "Iniciando...",
"multirun.fusion.toast.noOutputs": "No se encontraron respuestas del asistente para fusionar.",
"multirun.fusion.toast.failed": "No se pudo iniciar fusion.",
"sessions.sidebar.header.actions.searchSessions": "Buscar sesiones", "sessions.sidebar.header.actions.searchSessions": "Buscar sesiones",
"sessions.sidebar.header.actions.exitSelection": "Salir de selección", "sessions.sidebar.header.actions.exitSelection": "Salir de selección",
"sessions.sidebar.header.actions.selectSessions": "Seleccionar sesiones", "sessions.sidebar.header.actions.selectSessions": "Seleccionar sesiones",
@@ -243,6 +253,7 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.menu.copyLink": "Copiar enlace", "sessions.sidebar.session.menu.copyLink": "Copiar enlace",
"sessions.sidebar.session.menu.unshare": "Dejar de compartir", "sessions.sidebar.session.menu.unshare": "Dejar de compartir",
"sessions.sidebar.session.menu.exportMarkdown": "Exportar Markdown", "sessions.sidebar.session.menu.exportMarkdown": "Exportar Markdown",
"sessions.sidebar.session.menu.runFusion": "Ejecutar fusion",
"sessions.sidebar.session.menu.openInSidePanel": "Abrir en panel lateral", "sessions.sidebar.session.menu.openInSidePanel": "Abrir en panel lateral",
"sessions.sidebar.session.menu.betaBadge": "beta", "sessions.sidebar.session.menu.betaBadge": "beta",
"sessions.sidebar.session.menu.label": "Menú de sesión", "sessions.sidebar.session.menu.label": "Menú de sesión",
@@ -164,6 +164,7 @@ export const settingsDict = {
'settings.magicPrompts.sidebar.item.planImplement': '계획 구현', 'settings.magicPrompts.sidebar.item.planImplement': '계획 구현',
'settings.magicPrompts.sidebar.item.sessionSummary': '세션 요약', 'settings.magicPrompts.sidebar.item.sessionSummary': '세션 요약',
'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': '워크스페이스 리뷰', 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': '워크스페이스 리뷰',
'settings.magicPrompts.sidebar.item.sessionFusion': 'Fusion',
'settings.remoteInstances.sidebar.title': '원격 인스턴스', 'settings.remoteInstances.sidebar.title': '원격 인스턴스',
'settings.remoteInstances.sidebar.total': '총 {count}개', 'settings.remoteInstances.sidebar.total': '총 {count}개',
'settings.remoteInstances.sidebar.newSshInstanceName': '새 SSH 인스턴스', 'settings.remoteInstances.sidebar.newSshInstanceName': '새 SSH 인스턴스',
@@ -1541,6 +1542,8 @@ export const settingsDict = {
'settings.magicPrompts.page.group.sessionSummary.description': '/summary slash command에서 사용하는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침. 비파괴적이며 세션 기록을 압축하지 않습니다.', 'settings.magicPrompts.page.group.sessionSummary.description': '/summary slash command에서 사용하는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침. 비파괴적이며 세션 기록을 압축하지 않습니다.',
'settings.magicPrompts.page.group.sessionWorkspaceReview.title': '워크스페이스 리뷰', 'settings.magicPrompts.page.group.sessionWorkspaceReview.title': '워크스페이스 리뷰',
'settings.magicPrompts.page.group.sessionWorkspaceReview.description': '/workspace-review slash command에서 사용하는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침. 현재 workspace 변경 사항에서 중요한 문제만 리뷰합니다.', 'settings.magicPrompts.page.group.sessionWorkspaceReview.description': '/workspace-review slash command에서 사용하는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침. 현재 workspace 변경 사항에서 중요한 문제만 리뷰합니다.',
'settings.magicPrompts.page.group.sessionFusion.title': 'Fusion',
'settings.magicPrompts.page.group.sessionFusion.description': 'multi-run 출력을 하나의 최종 답변으로 결합할 때 사용하는 프롬프트입니다: 보이는 사용자 메시지 + 소스 결과 앞의 숨겨진 지침.',
'settings.magicPrompts.page.actions.resetting': '초기화 중...', 'settings.magicPrompts.page.actions.resetting': '초기화 중...',
'settings.magicPrompts.page.actions.resetAllOverrides': '모든 오버라이드 초기화', 'settings.magicPrompts.page.actions.resetAllOverrides': '모든 오버라이드 초기화',
'settings.magicPrompts.page.actions.resetToDefault': '기본값으로 초기화', 'settings.magicPrompts.page.actions.resetToDefault': '기본값으로 초기화',
+11
View File
@@ -181,6 +181,16 @@ export const dict: Record<I18nKey, string> = {
'multirun.modelMultiSelect.validation.minOnly': '모델을 최소 {min}개 선택하세요.', 'multirun.modelMultiSelect.validation.minOnly': '모델을 최소 {min}개 선택하세요.',
'multirun.modelMultiSelect.validation.minToMax': '모델을 {min}~{max}개 선택하세요.', 'multirun.modelMultiSelect.validation.minToMax': '모델을 {min}~{max}개 선택하세요.',
'multirun.agentSelector.placeholder': '에이전트 선택', 'multirun.agentSelector.placeholder': '에이전트 선택',
'multirun.fusion.title': 'fusion 실행',
'multirun.fusion.description': '선택한 multi-run 출력을 하나의 더 강한 답변으로 결합합니다.',
'multirun.fusion.provider.placeholder': 'Provider',
'multirun.fusion.model.placeholder': 'Model',
'multirun.fusion.sources.label': '소스({count})',
'multirun.fusion.actions.cancel': '취소',
'multirun.fusion.actions.start': 'fusion 시작',
'multirun.fusion.actions.starting': '시작 중...',
'multirun.fusion.toast.noOutputs': 'fusion할 어시스턴트 응답을 찾지 못했습니다.',
'multirun.fusion.toast.failed': 'fusion을 시작하지 못했습니다.',
'sessions.sidebar.header.actions.searchSessions': '세션 검색', 'sessions.sidebar.header.actions.searchSessions': '세션 검색',
'sessions.sidebar.header.actions.exitSelection': '선택 종료', 'sessions.sidebar.header.actions.exitSelection': '선택 종료',
'sessions.sidebar.header.actions.selectSessions': '세션 선택', 'sessions.sidebar.header.actions.selectSessions': '세션 선택',
@@ -243,6 +253,7 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.menu.copyLink': '링크 복사', 'sessions.sidebar.session.menu.copyLink': '링크 복사',
'sessions.sidebar.session.menu.unshare': '공유 해제', 'sessions.sidebar.session.menu.unshare': '공유 해제',
'sessions.sidebar.session.menu.exportMarkdown': 'Markdown 내보내기', 'sessions.sidebar.session.menu.exportMarkdown': 'Markdown 내보내기',
'sessions.sidebar.session.menu.runFusion': 'fusion 실행',
'sessions.sidebar.session.menu.openInSidePanel': '사이드 패널에서 열기', 'sessions.sidebar.session.menu.openInSidePanel': '사이드 패널에서 열기',
'sessions.sidebar.session.menu.betaBadge': 'beta', 'sessions.sidebar.session.menu.betaBadge': 'beta',
'sessions.sidebar.session.menu.label': '세션 메뉴', 'sessions.sidebar.session.menu.label': '세션 메뉴',
@@ -280,6 +280,8 @@ export const settingsDict = {
'settings.magicPrompts.page.group.sessionSummary.title': 'Podsumowanie Sesji', 'settings.magicPrompts.page.group.sessionSummary.title': 'Podsumowanie Sesji',
'settings.magicPrompts.page.group.sessionWorkspaceReview.description': 'Prompty używane przez polecenie /workspace-review: widoczna wiadomość użytkownika + ukryte instrukcje. Przegląda zmiany w bieżącym obszarze roboczym tylko pod kątem istotnych problemów.', 'settings.magicPrompts.page.group.sessionWorkspaceReview.description': 'Prompty używane przez polecenie /workspace-review: widoczna wiadomość użytkownika + ukryte instrukcje. Przegląda zmiany w bieżącym obszarze roboczym tylko pod kątem istotnych problemów.',
'settings.magicPrompts.page.group.sessionWorkspaceReview.title': 'Przegląd obszaru roboczego', 'settings.magicPrompts.page.group.sessionWorkspaceReview.title': 'Przegląd obszaru roboczego',
'settings.magicPrompts.page.group.sessionFusion.title': 'Fusion',
'settings.magicPrompts.page.group.sessionFusion.description': 'Prompty używane do łączenia wyników multi-run w jedną końcową odpowiedź: widoczna wiadomość użytkownika + ukryte instrukcje przed wynikami źródłowymi.',
'settings.magicPrompts.page.loading.aria': 'Ładowanie', 'settings.magicPrompts.page.loading.aria': 'Ładowanie',
'settings.magicPrompts.page.loading.text': 'Ładowanie Magicznych Promptów...', 'settings.magicPrompts.page.loading.text': 'Ładowanie Magicznych Promptów...',
'settings.magicPrompts.page.placeholdersLabel': 'Placeholdery:', 'settings.magicPrompts.page.placeholdersLabel': 'Placeholdery:',
@@ -314,6 +316,7 @@ export const settingsDict = {
'settings.magicPrompts.sidebar.item.planTodo': 'Planowanie Todo', 'settings.magicPrompts.sidebar.item.planTodo': 'Planowanie Todo',
'settings.magicPrompts.sidebar.item.sessionSummary': 'Podsumowanie Sesji', 'settings.magicPrompts.sidebar.item.sessionSummary': 'Podsumowanie Sesji',
'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': 'Przegląd obszaru roboczego', 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': 'Przegląd obszaru roboczego',
'settings.magicPrompts.sidebar.item.sessionFusion': 'Fusion',
'settings.magicPrompts.sidebar.title': 'Magiczne Prompty', 'settings.magicPrompts.sidebar.title': 'Magiczne Prompty',
'settings.mcp.page.actions.authorize': 'Autoryzuj', 'settings.mcp.page.actions.authorize': 'Autoryzuj',
'settings.mcp.page.actions.clearAuth': 'Wyczyść autoryzację', 'settings.mcp.page.actions.clearAuth': 'Wyczyść autoryzację',
+11
View File
@@ -84,6 +84,7 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.menu.copyLink': 'Kopiuj link', 'sessions.sidebar.session.menu.copyLink': 'Kopiuj link',
'sessions.sidebar.session.menu.unshare': 'Cofnij udostępnienie', 'sessions.sidebar.session.menu.unshare': 'Cofnij udostępnienie',
'sessions.sidebar.session.menu.exportMarkdown': 'Eksportuj Markdown', 'sessions.sidebar.session.menu.exportMarkdown': 'Eksportuj Markdown',
'sessions.sidebar.session.menu.runFusion': 'Uruchom fusion',
'sessions.sidebar.session.menu.openInSidePanel': 'Otwórz w panelu bocznym', 'sessions.sidebar.session.menu.openInSidePanel': 'Otwórz w panelu bocznym',
'sessions.sidebar.session.menu.betaBadge': 'beta', 'sessions.sidebar.session.menu.betaBadge': 'beta',
'sessions.sidebar.session.menu.label': 'Menu sesji', 'sessions.sidebar.session.menu.label': 'Menu sesji',
@@ -256,6 +257,16 @@ export const dict: Record<I18nKey, string> = {
'multirun.modelMultiSelect.validation.minOnly': 'Wybierz przynajmniej {min} modele.', 'multirun.modelMultiSelect.validation.minOnly': 'Wybierz przynajmniej {min} modele.',
'multirun.modelMultiSelect.validation.minToMax': 'Wybierz od {min} do {max} modeli.', 'multirun.modelMultiSelect.validation.minToMax': 'Wybierz od {min} do {max} modeli.',
'multirun.agentSelector.placeholder': 'Wybierz agenta', 'multirun.agentSelector.placeholder': 'Wybierz agenta',
'multirun.fusion.title': 'Uruchom fusion',
'multirun.fusion.description': 'Połącz wybrane wyniki multi-run w jedną mocniejszą odpowiedź.',
'multirun.fusion.provider.placeholder': 'Provider',
'multirun.fusion.model.placeholder': 'Model',
'multirun.fusion.sources.label': 'Źródła ({count})',
'multirun.fusion.actions.cancel': 'Anuluj',
'multirun.fusion.actions.start': 'Uruchom fusion',
'multirun.fusion.actions.starting': 'Uruchamianie...',
'multirun.fusion.toast.noOutputs': 'Nie znaleziono odpowiedzi asystenta do fusion.',
'multirun.fusion.toast.failed': 'Nie udało się uruchomić fusion.',
'sessions.sidebar.session.export.dialog.includeSubtasks': 'Dołącz pod-agentów', 'sessions.sidebar.session.export.dialog.includeSubtasks': 'Dołącz pod-agentów',
'sessions.sidebar.session.export.dialog.confirm': 'Eksportuj', 'sessions.sidebar.session.export.dialog.confirm': 'Eksportuj',
'sessions.sidebar.session.status.active': 'Sesja aktywna', 'sessions.sidebar.session.status.active': 'Sesja aktywna',
@@ -164,6 +164,7 @@ export const settingsDict = {
"settings.magicPrompts.sidebar.item.planImplement": "Implementar plano", "settings.magicPrompts.sidebar.item.planImplement": "Implementar plano",
"settings.magicPrompts.sidebar.item.sessionSummary": "Resumo de sessão", "settings.magicPrompts.sidebar.item.sessionSummary": "Resumo de sessão",
"settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Revisão do workspace", "settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Revisão do workspace",
"settings.magicPrompts.sidebar.item.sessionFusion": "Fusion",
"settings.remoteInstances.sidebar.title": "Instâncias remotas", "settings.remoteInstances.sidebar.title": "Instâncias remotas",
"settings.remoteInstances.sidebar.total": "Total {count}", "settings.remoteInstances.sidebar.total": "Total {count}",
"settings.remoteInstances.sidebar.newSshInstanceName": "Nova instância SSH", "settings.remoteInstances.sidebar.newSshInstanceName": "Nova instância SSH",
@@ -1541,6 +1542,8 @@ export const settingsDict = {
"settings.magicPrompts.page.group.sessionSummary.description": "Prompts usados pelo comando /summary: mensagem visível do usuário + instruções ocultas. Não destrutivo: não compacta o histórico da sessão.", "settings.magicPrompts.page.group.sessionSummary.description": "Prompts usados pelo comando /summary: mensagem visível do usuário + instruções ocultas. Não destrutivo: não compacta o histórico da sessão.",
"settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Revisão do workspace", "settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Revisão do workspace",
"settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Prompts usados pelo comando /workspace-review: mensagem visível do usuário + instruções ocultas. Revise apenas problemas importantes nas alterações atuais do workspace.", "settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Prompts usados pelo comando /workspace-review: mensagem visível do usuário + instruções ocultas. Revise apenas problemas importantes nas alterações atuais do workspace.",
"settings.magicPrompts.page.group.sessionFusion.title": "Fusion",
"settings.magicPrompts.page.group.sessionFusion.description": "Prompts usados para combinar saídas de multi-run em uma resposta final: mensagem visível do usuário + instruções ocultas antes dos resultados de origem.",
"settings.magicPrompts.page.actions.resetting": "Redefinindo...", "settings.magicPrompts.page.actions.resetting": "Redefinindo...",
"settings.magicPrompts.page.actions.resetAllOverrides": "Reiniciar todas as sobrescrituras", "settings.magicPrompts.page.actions.resetAllOverrides": "Reiniciar todas as sobrescrituras",
"settings.magicPrompts.page.actions.resetToDefault": "Reiniciar valores padrões", "settings.magicPrompts.page.actions.resetToDefault": "Reiniciar valores padrões",
@@ -181,6 +181,16 @@ export const dict: Record<I18nKey, string> = {
"multirun.modelMultiSelect.validation.minOnly": "Selecione ao menos {min} modelos.", "multirun.modelMultiSelect.validation.minOnly": "Selecione ao menos {min} modelos.",
"multirun.modelMultiSelect.validation.minToMax": "Selecione entre {min} e {max} modelos.", "multirun.modelMultiSelect.validation.minToMax": "Selecione entre {min} e {max} modelos.",
"multirun.agentSelector.placeholder": "Selecione um agente", "multirun.agentSelector.placeholder": "Selecione um agente",
"multirun.fusion.title": "Executar fusion",
"multirun.fusion.description": "Combine os resultados selecionados do multi-run em uma resposta mais forte.",
"multirun.fusion.provider.placeholder": "Provedor",
"multirun.fusion.model.placeholder": "Modelo",
"multirun.fusion.sources.label": "Fontes ({count})",
"multirun.fusion.actions.cancel": "Cancelar",
"multirun.fusion.actions.start": "Iniciar fusion",
"multirun.fusion.actions.starting": "Iniciando...",
"multirun.fusion.toast.noOutputs": "Nenhuma resposta do assistente encontrada para fusion.",
"multirun.fusion.toast.failed": "Falha ao iniciar fusion.",
"sessions.sidebar.header.actions.searchSessions": "Pesquisar sessões", "sessions.sidebar.header.actions.searchSessions": "Pesquisar sessões",
"sessions.sidebar.header.actions.exitSelection": "Sair da seleção", "sessions.sidebar.header.actions.exitSelection": "Sair da seleção",
"sessions.sidebar.header.actions.selectSessions": "Selecionar sessões", "sessions.sidebar.header.actions.selectSessions": "Selecionar sessões",
@@ -243,6 +253,7 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.menu.copyLink": "Copiar link", "sessions.sidebar.session.menu.copyLink": "Copiar link",
"sessions.sidebar.session.menu.unshare": "Parar de compartilhar", "sessions.sidebar.session.menu.unshare": "Parar de compartilhar",
"sessions.sidebar.session.menu.exportMarkdown": "Exportar Markdown", "sessions.sidebar.session.menu.exportMarkdown": "Exportar Markdown",
"sessions.sidebar.session.menu.runFusion": "Executar fusion",
"sessions.sidebar.session.menu.openInSidePanel": "Abrir no painel lateral", "sessions.sidebar.session.menu.openInSidePanel": "Abrir no painel lateral",
"sessions.sidebar.session.menu.betaBadge": "beta", "sessions.sidebar.session.menu.betaBadge": "beta",
"sessions.sidebar.session.menu.label": "Menu da sessão", "sessions.sidebar.session.menu.label": "Menu da sessão",
@@ -164,6 +164,7 @@ export const settingsDict = {
"settings.magicPrompts.sidebar.item.planImplement": "Реалізувати план", "settings.magicPrompts.sidebar.item.planImplement": "Реалізувати план",
"settings.magicPrompts.sidebar.item.sessionSummary": "Підсумок сесії", "settings.magicPrompts.sidebar.item.sessionSummary": "Підсумок сесії",
"settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Огляд робочого простору", "settings.magicPrompts.sidebar.item.sessionWorkspaceReview": "Огляд робочого простору",
"settings.magicPrompts.sidebar.item.sessionFusion": "Fusion",
"settings.remoteInstances.sidebar.title": "Віддалені інстанси", "settings.remoteInstances.sidebar.title": "Віддалені інстанси",
"settings.remoteInstances.sidebar.total": "Усього {count}", "settings.remoteInstances.sidebar.total": "Усього {count}",
"settings.remoteInstances.sidebar.newSshInstanceName": "Новий інстанс SSH", "settings.remoteInstances.sidebar.newSshInstanceName": "Новий інстанс SSH",
@@ -1541,6 +1542,8 @@ export const settingsDict = {
"settings.magicPrompts.page.group.sessionSummary.description": "Промпти, які використовуються командою /summary: видиме повідомлення користувача + приховані інструкції. Неруйнівний – не стискає історію сесії.", "settings.magicPrompts.page.group.sessionSummary.description": "Промпти, які використовуються командою /summary: видиме повідомлення користувача + приховані інструкції. Неруйнівний – не стискає історію сесії.",
"settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Огляд робочого простору", "settings.magicPrompts.page.group.sessionWorkspaceReview.title": "Огляд робочого простору",
"settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Промпти, які використовуються командою /workspace-review: видиме повідомлення користувача + приховані інструкції. Переглядає поточні зміни робочого простору лише для проблем із сильним сигналом.", "settings.magicPrompts.page.group.sessionWorkspaceReview.description": "Промпти, які використовуються командою /workspace-review: видиме повідомлення користувача + приховані інструкції. Переглядає поточні зміни робочого простору лише для проблем із сильним сигналом.",
"settings.magicPrompts.page.group.sessionFusion.title": "Fusion",
"settings.magicPrompts.page.group.sessionFusion.description": "Промпти для обʼєднання результатів multi-run в одну фінальну відповідь: видиме повідомлення користувача + приховані інструкції перед результатами джерел.",
"settings.magicPrompts.page.actions.resetting": "Скидання...", "settings.magicPrompts.page.actions.resetting": "Скидання...",
"settings.magicPrompts.page.actions.resetAllOverrides": "Скинути всі перевизначення", "settings.magicPrompts.page.actions.resetAllOverrides": "Скинути всі перевизначення",
"settings.magicPrompts.page.actions.resetToDefault": "Скинути до замовчування", "settings.magicPrompts.page.actions.resetToDefault": "Скинути до замовчування",
+11
View File
@@ -181,6 +181,16 @@ export const dict: Record<I18nKey, string> = {
"multirun.modelMultiSelect.validation.minOnly": "Виберіть щонайменше моделей: {min}.", "multirun.modelMultiSelect.validation.minOnly": "Виберіть щонайменше моделей: {min}.",
"multirun.modelMultiSelect.validation.minToMax": "Виберіть від {min} до {max} моделей.", "multirun.modelMultiSelect.validation.minToMax": "Виберіть від {min} до {max} моделей.",
"multirun.agentSelector.placeholder": "Вибрати агента", "multirun.agentSelector.placeholder": "Вибрати агента",
"multirun.fusion.title": "Запустити fusion",
"multirun.fusion.description": "Обʼєднайте вибрані результати multi-run в одну сильнішу відповідь.",
"multirun.fusion.provider.placeholder": "Провайдер",
"multirun.fusion.model.placeholder": "Модель",
"multirun.fusion.sources.label": "Джерела ({count})",
"multirun.fusion.actions.cancel": "Скасувати",
"multirun.fusion.actions.start": "Запустити fusion",
"multirun.fusion.actions.starting": "Запуск...",
"multirun.fusion.toast.noOutputs": "Не знайдено відповідей асистента для fusion.",
"multirun.fusion.toast.failed": "Не вдалося запустити fusion.",
"sessions.sidebar.header.actions.searchSessions": "Пошук сесій", "sessions.sidebar.header.actions.searchSessions": "Пошук сесій",
"sessions.sidebar.header.actions.exitSelection": "Вийти з вибору", "sessions.sidebar.header.actions.exitSelection": "Вийти з вибору",
"sessions.sidebar.header.actions.selectSessions": "Вибрати сесії", "sessions.sidebar.header.actions.selectSessions": "Вибрати сесії",
@@ -243,6 +253,7 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.menu.copyLink": "Копіювати посилання", "sessions.sidebar.session.menu.copyLink": "Копіювати посилання",
"sessions.sidebar.session.menu.unshare": "Скасувати спільний доступ", "sessions.sidebar.session.menu.unshare": "Скасувати спільний доступ",
"sessions.sidebar.session.menu.exportMarkdown": "Експорт Markdown", "sessions.sidebar.session.menu.exportMarkdown": "Експорт Markdown",
"sessions.sidebar.session.menu.runFusion": "Запустити fusion",
"sessions.sidebar.session.menu.openInSidePanel": "Відкрити на бічній панелі", "sessions.sidebar.session.menu.openInSidePanel": "Відкрити на бічній панелі",
"sessions.sidebar.session.menu.betaBadge": "бета-версія", "sessions.sidebar.session.menu.betaBadge": "бета-версія",
"sessions.sidebar.session.menu.label": "Меню сесії", "sessions.sidebar.session.menu.label": "Меню сесії",
@@ -164,6 +164,7 @@ export const settingsDict = {
'settings.magicPrompts.sidebar.item.planImplement': '执行计划', 'settings.magicPrompts.sidebar.item.planImplement': '执行计划',
'settings.magicPrompts.sidebar.item.sessionSummary': '会话总结', 'settings.magicPrompts.sidebar.item.sessionSummary': '会话总结',
'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': '工作区审查', 'settings.magicPrompts.sidebar.item.sessionWorkspaceReview': '工作区审查',
'settings.magicPrompts.sidebar.item.sessionFusion': 'Fusion',
'settings.remoteInstances.sidebar.title': '远程实例', 'settings.remoteInstances.sidebar.title': '远程实例',
'settings.remoteInstances.sidebar.total': '总计 {count}', 'settings.remoteInstances.sidebar.total': '总计 {count}',
'settings.remoteInstances.sidebar.newSshInstanceName': '新建 SSH 实例', 'settings.remoteInstances.sidebar.newSshInstanceName': '新建 SSH 实例',
@@ -1541,6 +1542,8 @@ export const settingsDict = {
'settings.magicPrompts.page.group.sessionSummary.description': '由 /summary 斜杠命令使用的提示词:可见用户消息 + 隐藏说明。非破坏性,不会压缩会话历史。', 'settings.magicPrompts.page.group.sessionSummary.description': '由 /summary 斜杠命令使用的提示词:可见用户消息 + 隐藏说明。非破坏性,不会压缩会话历史。',
'settings.magicPrompts.page.group.sessionWorkspaceReview.title': '工作区审查', 'settings.magicPrompts.page.group.sessionWorkspaceReview.title': '工作区审查',
'settings.magicPrompts.page.group.sessionWorkspaceReview.description': '由 /workspace-review 斜杠命令使用的提示词:可见用户消息 + 隐藏说明。仅审查当前工作区的高信号问题。', 'settings.magicPrompts.page.group.sessionWorkspaceReview.description': '由 /workspace-review 斜杠命令使用的提示词:可见用户消息 + 隐藏说明。仅审查当前工作区的高信号问题。',
'settings.magicPrompts.page.group.sessionFusion.title': 'Fusion',
'settings.magicPrompts.page.group.sessionFusion.description': '用于将 multi-run 输出合并为一个最终答案的提示词:可见用户消息 + 源结果前的隐藏说明。',
'settings.magicPrompts.page.actions.resetting': '重置中...', 'settings.magicPrompts.page.actions.resetting': '重置中...',
'settings.magicPrompts.page.actions.resetAllOverrides': '重置全部覆盖', 'settings.magicPrompts.page.actions.resetAllOverrides': '重置全部覆盖',
'settings.magicPrompts.page.actions.resetToDefault': '重置为默认', 'settings.magicPrompts.page.actions.resetToDefault': '重置为默认',
@@ -181,6 +181,16 @@ export const dict: Record<I18nKey, string> = {
'multirun.modelMultiSelect.validation.minOnly': '请至少选择 {min} 个模型。', 'multirun.modelMultiSelect.validation.minOnly': '请至少选择 {min} 个模型。',
'multirun.modelMultiSelect.validation.minToMax': '请选择 {min} 到 {max} 个模型。', 'multirun.modelMultiSelect.validation.minToMax': '请选择 {min} 到 {max} 个模型。',
'multirun.agentSelector.placeholder': '选择智能体', 'multirun.agentSelector.placeholder': '选择智能体',
'multirun.fusion.title': '运行 fusion',
'multirun.fusion.description': '将选中的 multi-run 输出合并为一个更强的答案。',
'multirun.fusion.provider.placeholder': 'Provider',
'multirun.fusion.model.placeholder': 'Model',
'multirun.fusion.sources.label': '来源({count})',
'multirun.fusion.actions.cancel': '取消',
'multirun.fusion.actions.start': '开始 fusion',
'multirun.fusion.actions.starting': '正在开始...',
'multirun.fusion.toast.noOutputs': '未找到可用于 fusion 的助手输出。',
'multirun.fusion.toast.failed': '启动 fusion 失败。',
'sessions.sidebar.header.actions.searchSessions': '搜索会话', 'sessions.sidebar.header.actions.searchSessions': '搜索会话',
'sessions.sidebar.header.actions.exitSelection': '退出选择', 'sessions.sidebar.header.actions.exitSelection': '退出选择',
'sessions.sidebar.header.actions.selectSessions': '选择会话', 'sessions.sidebar.header.actions.selectSessions': '选择会话',
@@ -243,6 +253,7 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.menu.copyLink': '复制链接', 'sessions.sidebar.session.menu.copyLink': '复制链接',
'sessions.sidebar.session.menu.unshare': '取消分享', 'sessions.sidebar.session.menu.unshare': '取消分享',
'sessions.sidebar.session.menu.exportMarkdown': '导出 Markdown', 'sessions.sidebar.session.menu.exportMarkdown': '导出 Markdown',
'sessions.sidebar.session.menu.runFusion': '运行 fusion',
'sessions.sidebar.session.menu.openInSidePanel': '在侧边面板中打开', 'sessions.sidebar.session.menu.openInSidePanel': '在侧边面板中打开',
'sessions.sidebar.session.menu.betaBadge': 'beta', 'sessions.sidebar.session.menu.betaBadge': 'beta',
'sessions.sidebar.session.menu.label': '会话菜单', 'sessions.sidebar.session.menu.label': '会话菜单',
+23 -1
View File
@@ -26,7 +26,9 @@ export type MagicPromptId =
| 'session.summary.visible' | 'session.summary.visible'
| 'session.summary.instructions' | 'session.summary.instructions'
| 'session.review.visible' | 'session.review.visible'
| 'session.review.instructions'; | 'session.review.instructions'
| 'session.fusion.visible'
| 'session.fusion.instructions';
export interface MagicPromptDefinition { export interface MagicPromptDefinition {
id: MagicPromptId; id: MagicPromptId;
@@ -570,6 +572,26 @@ Output:
Keep the review concise and practical.`, Keep the review concise and practical.`,
}, },
{
id: 'session.fusion.visible',
title: 'Fusion Visible Prompt',
group: 'Session',
description: 'Visible user message for multi-run fusion sessions.',
template: 'Create the best combined answer from the multi-run results.',
},
{
id: 'session.fusion.instructions',
title: 'Fusion Instructions',
group: 'Session',
description: 'Hidden instructions used before multi-run source outputs in fusion sessions.',
template: `You are performing fusion over multiple model outputs from the same original task.
Goal: produce the strongest possible final answer by combining complementary information, resolving conflicts, removing duplicates, and preserving useful nuance.
Use the results below as source material. Do not mention that the inputs were hidden parts. If sources disagree, prefer the most specific, well-supported, and internally consistent answer.
--- FUSION INPUTS START ---`,
},
] as const; ] as const;
const MAGIC_PROMPT_DEFINITION_BY_ID = new Map<MagicPromptId, MagicPromptDefinition>( const MAGIC_PROMPT_DEFINITION_BY_ID = new Map<MagicPromptId, MagicPromptDefinition>(
+42
View File
@@ -0,0 +1,42 @@
export type ParsedMultiRunTitle = {
groupSlug: string;
providerID: string;
modelID: string;
index?: number;
fusion: boolean;
};
const GROUP_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/;
export const parseMultiRunSessionTitle = (title?: string | null): ParsedMultiRunTitle | null => {
if (!title) return null;
const segments = title.split('/');
if (segments.length !== 3 && segments.length !== 4) return null;
const [groupSlug, providerID, modelID, suffix] = segments;
if (!GROUP_SLUG_PATTERN.test(groupSlug)) return null;
if (!providerID?.trim() || !modelID?.trim()) return null;
if (providerID !== providerID.trim() || modelID !== modelID.trim()) return null;
if (segments.length === 3) {
return { groupSlug, providerID, modelID, fusion: false };
}
if (suffix === 'fusion') {
return { groupSlug, providerID, modelID, fusion: true };
}
if (!/^\d+$/.test(suffix)) return null;
const index = Number.parseInt(suffix, 10);
if (!Number.isSafeInteger(index) || index <= 0) return null;
return { groupSlug, providerID, modelID, index, fusion: false };
};
export const isMultiRunSessionTitle = (title?: string | null): boolean => {
return parseMultiRunSessionTitle(title) !== null;
};
export const getFusionSessionTitle = (groupSlug: string, providerID: string, modelID: string): string => {
return `${groupSlug}/${providerID}/${modelID}/fusion`;
};