feat(core): Vanish Fix with Assistant banner when AIA fixes error (#39977)

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Anne Aguirre 2026-09-30 13:10:54 +00:00 committed by GitHub
parent bd9e1d8947
commit 0f11660414
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 429 additions and 31 deletions

View File

@ -517,6 +517,12 @@ afterEach(async () => {
for (const wrapper of mountedWrappers.splice(0)) {
wrapper.unmount();
}
const fixCalloutKeys: string[] = [];
for (let index = 0; index < sessionStorage.length; index++) {
const key = sessionStorage.key(index);
if (key?.startsWith('N8N_AGENT_PREVIEW_FIX_CALLOUT:')) fixCalloutKeys.push(key);
}
for (const key of fixCalloutKeys) sessionStorage.removeItem(key);
await flushPromises();
});
@ -620,6 +626,7 @@ const commonStubs = {
'effectiveSessionId',
'initialPrompt',
'canSendToAssistant',
'dismissedFixToolCallIds',
'beforeSend',
'canDeleteSession',
'isDeletingSession',
@ -1458,6 +1465,49 @@ describe('AgentBuilderView — preview routing', { timeout: 60_000 }, () => {
);
});
it('dismisses handed-off tool calls after the assistant writes the agent', async () => {
handoffMock.mockReturnValueOnce(true);
localStorage.setItem('N8N_AGENT_PREVIEW_OPEN:p1:a1', 'true');
routeQuery.continueSessionId = 'thread-1';
fetchedSessionThreads.push({ id: 'thread-1', updatedAt: '2026-01-01T00:00:00.000Z' });
const wrapper = await renderView();
const preview = wrapper.findComponent({ name: 'AgentPreviewDock' });
expect(preview.props('dismissedFixToolCallIds')).toEqual([]);
preview.vm.$emit('send-to-assistant', fixEvent);
await flushPromises();
expect(
wrapper.findComponent({ name: 'AgentPreviewDock' }).props('dismissedFixToolCallIds'),
).toEqual([]);
agentsEventBus.emit('agentUpdated', { agentId: 'a1', source: 'instance-ai' });
await nextTick();
expect(
wrapper.findComponent({ name: 'AgentPreviewDock' }).props('dismissedFixToolCallIds'),
).toEqual(['call-1', 'call-2']);
});
it('keeps handed-off tool calls when the assistant refuses the hand-off', async () => {
handoffMock.mockReturnValueOnce(false);
localStorage.setItem('N8N_AGENT_PREVIEW_OPEN:p1:a1', 'true');
routeQuery.continueSessionId = 'thread-1';
fetchedSessionThreads.push({ id: 'thread-1', updatedAt: '2026-01-01T00:00:00.000Z' });
const wrapper = await renderView();
wrapper.findComponent({ name: 'AgentPreviewDock' }).vm.$emit('send-to-assistant', fixEvent);
await flushPromises();
expect(handoffMock).toHaveBeenCalled();
agentsEventBus.emit('agentUpdated', { agentId: 'a1', source: 'instance-ai' });
await nextTick();
expect(
wrapper.findComponent({ name: 'AgentPreviewDock' }).props('dismissedFixToolCallIds'),
).toEqual([]);
});
it('keeps an artifact on the selected preview session and stages the handoff in its Assistant thread', async () => {
fetchedSessionThreads.push(
{

View File

@ -100,7 +100,11 @@ vi.mock('../composables/useSubAgentNames', () => ({
function mountSteps(
toolCalls: ToolCall[],
extra: { canFixWithAssistant?: boolean; executionId?: string } = {},
extra: {
canFixWithAssistant?: boolean;
executionId?: string;
dismissedToolCallIds?: string[];
} = {},
) {
return mount(AgentChatToolSteps, {
props: { toolCalls, projectId: 'project-1', ...extra },
@ -304,6 +308,68 @@ describe('AgentChatToolSteps', () => {
]);
});
it('hides dismissed tool errors and emits only the ones still shown', async () => {
const stillShown = mountSteps(
[
{
tool: 'search_nodes',
toolCallId: 'tc-gone',
state: TOOL_CALL_STATE.ERROR,
output: 'Old failure',
},
{
tool: 'http_request',
toolCallId: 'tc-stay',
state: TOOL_CALL_STATE.ERROR,
output: 'Still broken',
},
],
{
canFixWithAssistant: true,
executionId: 'exec-1',
dismissedToolCallIds: ['tc-gone'],
},
);
const callout = stillShown.find('[data-test-id="agent-chat-tool-fix-with-assistant-callout"]');
expect(callout.exists()).toBe(true);
expect(callout.text()).toContain('Still broken');
expect(callout.text()).not.toContain('Old failure');
await stillShown.find('[data-test-id="agent-chat-tool-fix-with-assistant"]').trigger('click');
expect(stillShown.emitted('fixWithAssistant')).toEqual([
[
[
{
toolCallId: 'tc-stay',
toolName: 'http_request',
toolDisplayName: 'Http request',
error: 'Still broken',
},
],
],
]);
const allDismissed = mountSteps(
[
{
tool: 'search_nodes',
toolCallId: 'tc-gone',
state: TOOL_CALL_STATE.ERROR,
output: 'Old failure',
},
],
{
canFixWithAssistant: true,
executionId: 'exec-1',
dismissedToolCallIds: ['tc-gone'],
},
);
expect(
allDismissed.find('[data-test-id="agent-chat-tool-fix-with-assistant-callout"]').exists(),
).toBe(false);
});
it('shows a generic error when the failed tool output is empty', () => {
const wrapper = mountSteps([
{

View File

@ -43,6 +43,7 @@ const props = defineProps<{
agentId?: string;
sessionId?: string;
canSendToAssistant?: boolean;
dismissedFixToolCallIds?: string[];
}>();
const emit = defineEmits<{
@ -461,6 +462,7 @@ watch(
:tool-calls="group.toolCalls"
:project-id="projectId"
:can-fix-with-assistant="canSendToAssistant"
:dismissed-tool-call-ids="dismissedFixToolCallIds"
:execution-id="group.executionId"
@fix-with-assistant="onFixWithAssistant(group, $event)"
/>
@ -551,6 +553,7 @@ watch(
:tool-calls="group.message.toolCalls"
:project-id="projectId"
:can-fix-with-assistant="canSendToAssistant"
:dismissed-tool-call-ids="dismissedFixToolCallIds"
:execution-id="group.message.executionId"
@fix-with-assistant="onFixWithAssistant(group, $event)"
/>

View File

@ -66,6 +66,7 @@ const props = withDefaults(
connectedTriggers: string[];
canEditAgent?: boolean;
canSendToAssistant?: boolean;
dismissedFixToolCallIds?: string[];
beforeSend?: () => Promise<void> | void;
inputDraft?: string;
backgroundJobsActive?: boolean;
@ -77,6 +78,7 @@ const props = withDefaults(
newSession: false,
canEditAgent: true,
canSendToAssistant: false,
dismissedFixToolCallIds: () => [],
beforeSend: undefined,
inputDraft: undefined,
backgroundJobsActive: false,
@ -761,6 +763,7 @@ onBeforeUnmount(() => {
:agent-id="agentId"
:session-id="continueSessionId"
:can-send-to-assistant="canSendToAssistant"
:dismissed-fix-tool-call-ids="dismissedFixToolCallIds"
@resume="resume"
@send-to-assistant="emit('send-to-assistant', $event)"
/>

View File

@ -33,6 +33,7 @@ const props = defineProps<{
toolCalls: ToolCall[];
projectId?: string;
canFixWithAssistant?: boolean;
dismissedToolCallIds?: string[];
executionId?: string;
}>();
@ -44,11 +45,15 @@ const i18n = useI18n();
const showFix = computed(() => Boolean(props.canFixWithAssistant && props.executionId));
const dismissedToolCallIds = computed(() => new Set(props.dismissedToolCallIds ?? []));
const fixableFailures = computed<AgentFixWithAssistantFailure[]>(() => {
if (!showFix.value) return [];
const dismissed = dismissedToolCallIds.value;
const failures: AgentFixWithAssistantFailure[] = [];
for (const toolCall of props.toolCalls) {
if (dismissed.has(toolCall.toolCallId)) continue;
if (toolCall.state !== TOOL_CALL_STATE.ERROR) continue;
const error = toolStepError(toolCall)?.trim();
@ -334,29 +339,31 @@ function hasActiveToolCall(): boolean {
</template>
</template>
<N8nCallout
v-if="fixableErrorTexts.length > 0"
theme="danger"
data-test-id="agent-chat-tool-fix-with-assistant-callout"
>
<template v-if="fixableErrorTexts.length === 1">
{{ fixableErrorTexts[0] }}
</template>
<ul v-else :class="$style.errorList">
<li v-for="error in fixableErrorTexts" :key="error">{{ error }}</li>
</ul>
<template #trailingContent>
<N8nButton
size="small"
variant="subtle"
data-test-id="agent-chat-tool-fix-with-assistant"
@click="emitFixWithAssistant"
>
<template #icon><N8nIcon icon="sparkles" size="small" /></template>
{{ i18n.baseText('agents.builder.preview.fixWithAssistant') }}
</N8nButton>
</template>
</N8nCallout>
<Transition name="fix-callout">
<N8nCallout
v-if="fixableErrorTexts.length > 0"
theme="danger"
data-test-id="agent-chat-tool-fix-with-assistant-callout"
>
<template v-if="fixableErrorTexts.length === 1">
{{ fixableErrorTexts[0] }}
</template>
<ul v-else :class="$style.errorList">
<li v-for="error in fixableErrorTexts" :key="error">{{ error }}</li>
</ul>
<template #trailingContent>
<N8nButton
size="small"
variant="subtle"
data-test-id="agent-chat-tool-fix-with-assistant"
@click="emitFixWithAssistant"
>
<template #icon><N8nIcon icon="sparkles" size="small" /></template>
{{ i18n.baseText('agents.builder.preview.fixWithAssistant') }}
</N8nButton>
</template>
</N8nCallout>
</Transition>
</div>
</template>
@ -432,3 +439,13 @@ function hasActiveToolCall(): boolean {
user-select: text;
}
</style>
<style lang="scss">
.fix-callout-leave-active {
transition: opacity var(--duration--snappy) var(--easing--ease-in);
}
.fix-callout-leave-to {
opacity: 0;
}
</style>

View File

@ -23,10 +23,11 @@ const props = withDefaults(
newSession?: boolean;
initialPrompt?: string;
canSendToAssistant?: boolean;
dismissedFixToolCallIds?: string[];
beforeSend?: () => Promise<void> | void;
layout?: 'page' | 'dock';
}>(),
{ visible: true, newSession: false, layout: 'dock' },
{ visible: true, newSession: false, layout: 'dock', dismissedFixToolCallIds: () => [] },
);
const emit = defineEmits<{
@ -83,6 +84,7 @@ defineExpose({ focusInput, getConversationMarkdown });
:agent-status="deriveAgentStatus(agent)"
:connected-triggers="connectedTriggers"
:can-send-to-assistant="canSendToAssistant"
:dismissed-fix-tool-call-ids="dismissedFixToolCallIds"
:before-send="beforeSend"
@continue-loaded="emit('continue-loaded', $event)"
@session-created="emit('session-created', $event)"

View File

@ -55,11 +55,17 @@ const props = withDefaults(
newSession?: boolean;
initialPrompt?: string;
canSendToAssistant?: boolean;
dismissedFixToolCallIds?: string[];
canDeleteSession?: boolean;
beforeSend?: () => Promise<void> | void;
isDeletingSession?: boolean;
}>(),
{ newSession: false, canDeleteSession: false, isDeletingSession: false },
{
newSession: false,
canDeleteSession: false,
isDeletingSession: false,
dismissedFixToolCallIds: () => [],
},
);
const emit = defineEmits<{
@ -294,6 +300,7 @@ useKeybindings({
:new-session="props.newSession"
:initial-prompt="props.initialPrompt"
:can-send-to-assistant="props.canSendToAssistant"
:dismissed-fix-tool-call-ids="props.dismissedFixToolCallIds"
:before-send="props.beforeSend"
@continue-loaded="emit('continue-loaded', $event)"
@session-created="emit('session-created', $event)"

View File

@ -0,0 +1,109 @@
import { defineComponent, ref } from 'vue';
import { mount } from '@vue/test-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { agentsEventBus } from '../../agents.eventBus';
import { useFixWithAssistantCalloutDismissal } from '../useFixWithAssistantCalloutDismissal';
const STORAGE_PREFIX = 'N8N_AGENT_PREVIEW_FIX_CALLOUT:';
function clearFixCalloutStorage() {
const keys: string[] = [];
for (let index = 0; index < sessionStorage.length; index++) {
const key = sessionStorage.key(index);
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
}
for (const key of keys) sessionStorage.removeItem(key);
}
function readStored(sessionId: string) {
const raw = sessionStorage.getItem(`${STORAGE_PREFIX}${sessionId}`);
return raw
? (JSON.parse(raw) as { agentId: string; pending: string[]; dismissed: string[] })
: null;
}
function mountDismissal(initialSessionId?: string) {
const sessionId = ref(initialSessionId);
let api!: ReturnType<typeof useFixWithAssistantCalloutDismissal>;
const wrapper = mount(
defineComponent({
setup() {
api = useFixWithAssistantCalloutDismissal(sessionId);
return () => null;
},
}),
);
return { wrapper, api, sessionId };
}
describe('useFixWithAssistantCalloutDismissal', () => {
beforeEach(() => {
clearFixCalloutStorage();
});
afterEach(() => {
clearFixCalloutStorage();
});
it('keeps accepted ids pending until the assistant writes that agent', () => {
const { wrapper, api } = mountDismissal('session-1');
api.trackAcceptedFixHandoff('session-1', 'agent-1', ['tc-1']);
api.trackAcceptedFixHandoff('session-1', 'agent-1', ['tc-1', 'tc-2']);
expect(readStored('session-1')).toEqual({
agentId: 'agent-1',
pending: ['tc-1', 'tc-2'],
dismissed: [],
});
expect(api.dismissedToolCallIds.value).toEqual([]);
agentsEventBus.emit('agentUpdated', { agentId: 'agent-1', source: 'agent-builder' });
agentsEventBus.emit('agentUpdated', { agentId: 'agent-other', source: 'instance-ai' });
expect(readStored('session-1')?.pending).toEqual(['tc-1', 'tc-2']);
agentsEventBus.emit('agentUpdated', { agentId: 'agent-1', source: 'instance-ai' });
expect(readStored('session-1')).toEqual({
agentId: 'agent-1',
pending: [],
dismissed: ['tc-1', 'tc-2'],
});
expect(api.dismissedToolCallIds.value).toEqual(['tc-1', 'tc-2']);
wrapper.unmount();
const remounted = mountDismissal('session-1');
expect(remounted.api.dismissedToolCallIds.value).toEqual(['tc-1', 'tc-2']);
remounted.wrapper.unmount();
});
it('does not write when the session has no pending ids', () => {
sessionStorage.setItem(
`${STORAGE_PREFIX}session-1`,
JSON.stringify({ agentId: 'agent-1', pending: [], dismissed: ['tc-kept'] }),
);
const setItem = vi.spyOn(Storage.prototype, 'setItem');
const { wrapper } = mountDismissal('session-1');
agentsEventBus.emit('agentUpdated', { agentId: 'agent-1', source: 'instance-ai' });
expect(setItem).not.toHaveBeenCalled();
expect(readStored('session-1')?.dismissed).toEqual(['tc-kept']);
wrapper.unmount();
setItem.mockRestore();
});
it('dismisses every stored session for the written agent', () => {
const { wrapper, api } = mountDismissal('session-1');
api.trackAcceptedFixHandoff('session-1', 'agent-1', ['tc-a']);
api.trackAcceptedFixHandoff('session-2', 'agent-1', ['tc-b']);
api.trackAcceptedFixHandoff('session-3', 'agent-2', ['tc-c']);
agentsEventBus.emit('agentUpdated', { agentId: 'agent-1', source: 'instance-ai' });
expect(readStored('session-1')?.dismissed).toEqual(['tc-a']);
expect(readStored('session-1')?.pending).toEqual([]);
expect(readStored('session-2')?.dismissed).toEqual(['tc-b']);
expect(readStored('session-3')?.pending).toEqual(['tc-c']);
wrapper.unmount();
});
});

View File

@ -0,0 +1,108 @@
import { computed, onBeforeUnmount, ref, type Ref } from 'vue';
import { isRecord } from '@n8n/utils/is-record';
import { agentsEventBus, type AgentUpdatedEvent } from '../agents.eventBus';
/**
* The preview message list unmounts when the dock closes, then history reload
* brings the same tool errors back. Remember handed-off ids here so the
* callout stays gone after that remount.
*/
const STORAGE_PREFIX = 'N8N_AGENT_PREVIEW_FIX_CALLOUT:';
interface FixCalloutRecord {
agentId: string;
pending: string[];
dismissed: string[];
}
function storageKey(sessionId: string): string {
return `${STORAGE_PREFIX}${sessionId}`;
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
function parseRecord(raw: string | null): FixCalloutRecord | null {
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
if (!isRecord(parsed)) return null;
if (typeof parsed.agentId !== 'string') return null;
if (!isStringArray(parsed.pending) || !isStringArray(parsed.dismissed)) return null;
return {
agentId: parsed.agentId,
pending: parsed.pending,
dismissed: parsed.dismissed,
};
} catch {
return null;
}
}
function readRecord(sessionId: string): FixCalloutRecord | null {
return parseRecord(sessionStorage.getItem(storageKey(sessionId)));
}
function listStorageKeys(): string[] {
const keys: string[] = [];
for (let index = 0; index < sessionStorage.length; index++) {
const key = sessionStorage.key(index);
if (key?.startsWith(STORAGE_PREFIX)) keys.push(key);
}
return keys;
}
function unique(ids: string[]): string[] {
return [...new Set(ids)];
}
export function useFixWithAssistantCalloutDismissal(sessionId: Ref<string | undefined>) {
// Bumped when storage changes so the computed re-reads the current session.
const storageVersion = ref(0);
const dismissedToolCallIds = computed(() => {
void storageVersion.value;
const id = sessionId.value;
if (!id) return [];
return readRecord(id)?.dismissed ?? [];
});
function trackAcceptedFixHandoff(
targetSessionId: string,
agentId: string,
toolCallIds: string[],
): void {
if (toolCallIds.length === 0) return;
const existing = readRecord(targetSessionId) ?? {
agentId,
pending: [],
dismissed: [],
};
existing.agentId = agentId;
existing.pending = unique([...existing.pending, ...toolCallIds]);
sessionStorage.setItem(storageKey(targetSessionId), JSON.stringify(existing));
}
function onAgentUpdated(event?: AgentUpdatedEvent): void {
if (event?.source !== 'instance-ai' || !event.agentId) return;
let changed = false;
for (const key of listStorageKeys()) {
const record = parseRecord(sessionStorage.getItem(key));
if (!record || record.agentId !== event.agentId || record.pending.length === 0) continue;
record.dismissed = unique([...record.dismissed, ...record.pending]);
record.pending = [];
sessionStorage.setItem(key, JSON.stringify(record));
changed = true;
}
if (changed) storageVersion.value++;
}
agentsEventBus.on('agentUpdated', onAgentUpdated);
onBeforeUnmount(() => {
agentsEventBus.off('agentUpdated', onAgentUpdated);
});
return { dismissedToolCallIds, trackAcceptedFixHandoff };
}

View File

@ -136,6 +136,7 @@ import { useAgentCollaborationStore } from '../stores/agentCollaboration.store';
import { useActivityDetection } from '@/app/composables/useActivityDetection';
import { buildAgentChangeRequestPrompt } from '../utils/agent-change-request';
import { buildAgentFixWithAssistantPrompt } from '../utils/fix-with-assistant';
import { useFixWithAssistantCalloutDismissal } from '../composables/useFixWithAssistantCalloutDismissal';
import { hasBlockingIssues } from '../utils/validationIssues';
const props = withDefaults(
@ -326,12 +327,14 @@ const aiPanelRef = useTemplateRef<InstanceType<typeof InstanceAiChatPanel>>('aiP
const queuedAiHandoff = ref<{
context: InstanceAiHandoffContext;
initialDraft?: PendingComposerDraft;
onAccepted?: () => void;
} | null>(null);
watch(aiPanelRef, (panel) => {
if (!panel || !queuedAiHandoff.value) return;
const { context, initialDraft } = queuedAiHandoff.value;
const { context, initialDraft, onAccepted } = queuedAiHandoff.value;
queuedAiHandoff.value = null;
panel.handoff(context, initialDraft);
const handed = panel.handoff(context, initialDraft);
if (handed) onAccepted?.();
});
const aiPanelWidth = useStorage('N8N_AGENT_AI_PANEL_WIDTH', 400);
type SidePanel = 'assistant' | 'preview';
@ -505,9 +508,25 @@ watch(
{ immediate: true },
);
// Assigned once `effectiveSessionId` exists. Clicks happen after setup.
let trackAcceptedFixHandoff: (
sessionId: string,
agentId: string,
toolCallIds: string[],
) => void = () => {};
async function onSendPreviewToAssistant(event?: AgentSendToAssistantEvent) {
const threadId = effectiveSessionId.value;
if (!threadId || !agentId.value || !projectId.value) return;
const targetAgentId = agentId.value;
const acceptFixHandoff = () => {
if (!event || !('failures' in event) || event.failures.length === 0) return;
trackAcceptedFixHandoff(
threadId,
targetAgentId,
event.failures.map((failure) => failure.toolCallId),
);
};
const session = currentSession.value;
const sessionTitle = session?.title?.trim() || currentSessionTitle.value || undefined;
const sessionNumber = session?.sessionNumber;
@ -552,7 +571,7 @@ async function onSendPreviewToAssistant(event?: AgentSendToAssistantEvent) {
if (isArtifactMode.value) {
// The host closes the dock — only it knows whether the hand-off went
// through (it refuses one while its composer holds a draft).
emit('assistant-handoff', params);
emit('assistant-handoff', { ...params, onAccepted: acceptFixHandoff });
return;
}
@ -572,6 +591,7 @@ async function onSendPreviewToAssistant(event?: AgentSendToAssistantEvent) {
if (aiPanelRef.value) {
const handed = aiPanelRef.value.handoff(context, params.initialDraft);
if (!handed) return;
acceptFixHandoff();
// Close the preview once the assistant has the request: coming back to an
// open preview chat beside the assistant reads as two places to ask.
closePreviewDock();
@ -579,7 +599,11 @@ async function onSendPreviewToAssistant(event?: AgentSendToAssistantEvent) {
// Standalone preview route: the panel isn't mounted here. Queue the
// hand-off and close the dock — on this route that navigates back to
// the builder, which mounts the panel and applies the queue.
queuedAiHandoff.value = { context, initialDraft: params.initialDraft };
queuedAiHandoff.value = {
context,
initialDraft: params.initialDraft,
onAccepted: acceptFixHandoff,
};
closePreviewDock();
}
@ -659,6 +683,9 @@ const {
projectId,
agentId,
});
const { dismissedToolCallIds: dismissedFixToolCallIds, trackAcceptedFixHandoff: trackFixHandoff } =
useFixWithAssistantCalloutDismissal(effectiveSessionId);
trackAcceptedFixHandoff = trackFixHandoff;
const previewSessionsLoading = computed(
() => sessionsStore.loading || sessionsStore.previewLoading,
);
@ -2817,6 +2844,7 @@ useKeybindings({
:effective-session-id="effectiveSessionId"
:new-session="currentSessionIsEphemeral"
:can-send-to-assistant="instanceAiAvailable"
:dismissed-fix-tool-call-ids="dismissedFixToolCallIds"
:before-send="beforePreviewSend"
@continue-loaded="onContinueLoaded"
@session-created="markSessionCreated"
@ -2919,6 +2947,7 @@ useKeybindings({
:can-delete-session="canDeletePreviewSession"
:is-deleting-session="isDeletingSession"
:can-send-to-assistant="instanceAiAvailable"
:dismissed-fix-tool-call-ids="dismissedFixToolCallIds"
:before-send="beforePreviewSend"
@view-trace="viewPreviewTrace"
@new-session="startNewPreviewSession"

View File

@ -685,10 +685,12 @@ function handleAgentPreviewAssistantHandoff(params: AgentPreviewHandoffParams) {
// preview chat open reads as two places to ask the same thing.
isAgentPreviewDockOpen.value = false;
conversationRef.value?.applyHandoff(
if (!conversationRef.value) return;
conversationRef.value.applyHandoff(
buildInstanceAiAgentPreviewHandoffContext(params),
params.initialDraft,
);
params.onAccepted?.();
}
/**

View File

@ -10,4 +10,6 @@ export interface AgentPreviewHandoffParams {
sessionTitle?: string;
executionId?: string;
initialDraft?: PendingComposerDraft;
/** Runs only after the assistant composer accepts the hand-off. */
onAccepted?: () => void;
}