diff --git a/packages/frontend/editor-ui/src/app/constants/localStorage.ts b/packages/frontend/editor-ui/src/app/constants/localStorage.ts index f403740be08..a68772a7161 100644 --- a/packages/frontend/editor-ui/src/app/constants/localStorage.ts +++ b/packages/frontend/editor-ui/src/app/constants/localStorage.ts @@ -25,6 +25,10 @@ export const LOCAL_STORAGE_CHAT_HUB_STATIC_SIDEBAR = (userId: string) => `${userId}_N8N_CHAT_HUB_STATIC_SIDEBAR`; export const LOCAL_STORAGE_CHAT_HUB_HAD_CONVERSATION_BEFORE = (userId: string) => `${userId}_N8N_CHAT_HUB_HAD_CONVERSATION_BEFORE`; +export const LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN = (threadId: string) => + `N8N_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN:${threadId}`; +export const LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO = + 'N8N_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO'; export const LOCAL_STORAGE_SIDEBAR_WIDTH = 'N8N_SIDEBAR_WIDTH'; export const LOCAL_STORAGE_BROWSER_NOTIFICATION_METADATA = 'N8N_BROWSER_NOTIFICATION_METADATA'; export const LOCAL_STORAGE_FLOATING_CHAT_WINDOW = 'N8N_FLOATING_CHAT_WINDOW'; diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue index 65fb0e1ae35..5f8bd035038 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue @@ -21,7 +21,15 @@ import { N8nTooltip, TOOLTIP_DELAY_MS, } from '@n8n/design-system'; -import { onClickOutside, useElementSize, useScroll, useWindowSize } from '@vueuse/core'; +import { + StorageSerializers, + onClickOutside, + useDebounceFn, + useElementSize, + useLocalStorage, + useScroll, + useWindowSize, +} from '@vueuse/core'; import { useI18n } from '@n8n/i18n'; import type { InstanceAiAgentAttachment, @@ -29,11 +37,17 @@ import type { InstanceAiHandoffContext, } from '@n8n/api-types'; import { useRootStore } from '@n8n/stores/useRootStore'; +import { + DEBOUNCE_TIME, + LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN, + LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO, +} from '@/app/constants'; import { useDocumentTitle } from '@/app/composables/useDocumentTitle'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; import { COLLAPSED_MAIN_SIDEBAR_WIDTH, useSidebarLayout } from '@/app/composables/useSidebarLayout'; // Experiment cleanup: remove with openWorkflowInAssistant. import { useOpenWorkflowInAssistantStore } from '@/experiments/openWorkflowInAssistant/stores/openWorkflowInAssistant.store'; +import { getDebounceTime } from '@n8n/composables/useDebounce'; import { useTelemetry } from '@n8n/composables/useTelemetry'; import { TELEMETRY_EVENT } from '@n8n/telemetry'; import { countAttachedNodes } from './utils/buildNodesAttachment'; @@ -284,11 +298,22 @@ watch( ); // --- Canvas / data table preview --- +// null = no preference yet, so the first artifact still opens the preview. +// Sync flush keeps a thread switch from exposing the old thread's value for a tick. +const persistedArtifactPreviewOpen = useLocalStorage( + () => LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN(props.threadId), + null, + { serializer: StorageSerializers.boolean, writeDefaults: false, flush: 'sync' }, +); const preview = useCanvasPreview({ thread, threadId: () => props.threadId, initialAgentId: () => getAgentBuilderTargetFromThreadMetadata(store.getThreadMetadata(props.threadId))?.agentId, + previewOpenState: () => persistedArtifactPreviewOpen.value ?? undefined, + onPreviewOpenChange: (open) => { + persistedArtifactPreviewOpen.value = open; + }, }); const activeAgentPreviewSessionId = computed(() => { const context = pendingComposerContext.value; @@ -445,10 +470,16 @@ const shouldSuppressContentLayoutTransitions = computed( () => !isPreviewPanelTransitionEnabled.value, ); const artifactsPanelSlotRef = useTemplateRef('artifactsPanelSlot'); -const preferredPreviewPanelWidth = ref(Math.round(threadAreaWidth.value / 2)); const isResizingPreview = ref(false); +const isThreadAreaResizing = ref(false); const isPreviewExpanded = ref(false); const isAgentPreviewDockOpen = ref(false); +const MIN_SPLIT_PANEL_WIDTH = 400; +const DEFAULT_CHAT_PANEL_CONTENT_WIDTH = 800; +// Share of the thread area, so the split survives window and sidebar resizes. -1 = no preference yet. +const chatPanelWidthRatio = useLocalStorage(LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO, -1, { + writeDefaults: false, +}); watch(preview.activeTabId, (activeTabId, previousActiveTabId) => { if (activeTabId !== previousActiveTabId) { @@ -456,11 +487,31 @@ watch(preview.activeTabId, (activeTabId, previousActiveTabId) => { } }); -const previewMaxWidth = computed(() => Math.round(threadAreaWidth.value * 0.7)); -// Preserve the default or manually selected width while temporarily -// constraining it to the available space. -const previewPanelWidth = computed(() => - Math.min(preferredPreviewPanelWidth.value, previewMaxWidth.value), +// Below two panel minimums the limits meet at half, so both panels share the space evenly. +const halfThreadAreaWidth = computed(() => Math.round(threadAreaWidth.value / 2)); +const previewMinWidth = computed(() => Math.min(MIN_SPLIT_PANEL_WIDTH, halfThreadAreaWidth.value)); +const previewMaxWidth = computed(() => + Math.max(threadAreaWidth.value - MIN_SPLIT_PANEL_WIDTH, halfThreadAreaWidth.value), +); +const previewPanelWidth = computed(() => { + const ratio = chatPanelWidthRatio.value; + const chatPanelWidth = + ratio >= 0 && ratio <= 1 ? threadAreaWidth.value * ratio : DEFAULT_CHAT_PANEL_CONTENT_WIDTH; + return Math.round( + Math.min( + Math.max(threadAreaWidth.value - chatPanelWidth, previewMinWidth.value), + previewMaxWidth.value, + ), + ); +}); +const isPreviewResizeEnabled = computed( + () => !isPreviewExpanded.value && previewMinWidth.value < previewMaxWidth.value, +); +const shouldAnimatePreviewLayout = computed( + () => + isPreviewPanelTransitionEnabled.value && + !isResizingPreview.value && + !isThreadAreaResizing.value, ); const AGENT_PREVIEW_CHAT_MIN_WIDTH = 320; const AGENT_PREVIEW_CHAT_PREFERRED_WIDTH = 480; @@ -497,7 +548,9 @@ function handleAgentPreviewDockOpenChange(open: boolean) { } function handlePreviewResize({ width }: { width: number }) { - preferredPreviewPanelWidth.value = width; + // The wrapper clamps the width, so an unchanged value means the drag hit a limit: keep the stored ratio. + if (Math.round(width) === previewPanelWidth.value) return; + chatPanelWidthRatio.value = (threadAreaWidth.value - width) / threadAreaWidth.value; } function handlePreviewPanelAfterEnter() { @@ -520,23 +573,34 @@ watch( isPreviewPanelTransitioning.value = isPreviewPanelTransitionEnabled.value; } - if (visible) { - isArtifactsPanelRevealed.value = false; - preferredPreviewPanelWidth.value = previewMaxWidth.value; - } else { + if (!visible) { isAgentPreviewDockOpen.value = false; + } else { + isArtifactsPanelRevealed.value = false; } }, { flush: 'sync' }, ); -// Late-initialize if the panel became visible before the ResizeObserver -// reported the container size (otherwise the panel would render at 0px). -watch(threadAreaWidth, (width) => { - if (width > 0 && preferredPreviewPanelWidth.value === 0 && preview.isPreviewVisible.value) { - preferredPreviewPanelWidth.value = previewMaxWidth.value; - } -}); +const finishThreadAreaResize = useDebounceFn(() => { + isThreadAreaResizing.value = false; +}, getDebounceTime(DEBOUNCE_TIME.UI.RESIZE)); + +watch( + threadAreaWidth, + (width, previousWidth) => { + if ( + typeof previousWidth === 'number' && + previousWidth > 0 && + width !== previousWidth && + preview.isPreviewVisible.value + ) { + isThreadAreaResizing.value = true; + void finishThreadAreaResize(); + } + }, + { immediate: true }, +); watch(isArtifactsPanelInLayout, (isInLayout) => { isArtifactsPanelRevealed.value = false; @@ -1100,10 +1164,10 @@ async function dismissComposerContextChip() { :class="[ $style.chatArea, { - [$style.agentPreviewLayoutTransition]: isPreviewPanelTransitionEnabled, + [$style.agentPreviewLayoutTransition]: shouldAnimatePreviewLayout, }, ]" - :data-layout-animated="isPreviewPanelTransitionEnabled" + :data-layout-animated="shouldAnimatePreviewLayout" data-test-id="instance-ai-builder-chat" >
@@ -1363,8 +1427,7 @@ async function dismissComposerContextChip() { $style.canvasArea, { [$style.canvasAreaExpanded]: isPreviewExpanded, - [$style.agentPreviewLayoutTransition]: - isPreviewPanelTransitionEnabled && !isResizingPreview, + [$style.agentPreviewLayoutTransition]: shouldAnimatePreviewLayout, }, ]" :style="agentPreviewPanelStyle" @@ -1373,10 +1436,10 @@ async function dismissComposerContextChip() { > { expect(thread.loadHistoricalMessages).toHaveBeenCalledWith(); }); - it('opens the artifacts panel from the header toggle when too narrow for pinned artifacts', async () => { + it('opens artifacts when narrow and restores them in the pinned layout', async () => { mockWindowSizeState.width.value = 900; thread.messages = [ { @@ -1549,6 +1553,12 @@ describe('InstanceAiThreadView', () => { await user.click(getByTestId('instance-ai-content-area')); expect(queryByTestId('instance-ai-artifacts-sidebar-slot')).not.toBeInTheDocument(); + + mockWindowSizeState.width.value = 1700; + await vi.waitFor(() => { + expect(getByTestId('instance-ai-artifacts-sidebar-slot')).toBeInTheDocument(); + }); + expect(store.updateThreadMetadata).not.toHaveBeenCalled(); }); it('keeps the artifacts panel toggle available when the panel is in the layout', async () => { @@ -1581,6 +1591,18 @@ describe('InstanceAiThreadView', () => { expect(getByTestId('instance-ai-artifacts-sidebar-slot')).toBeInTheDocument(); }); + it('restores an open preview when the artifact has no message attachment', async () => { + thread.producedArtifacts = new Map([ + ['workflow-1', { type: 'workflow', id: 'workflow-1', name: 'Lead enrichment workflow' }], + ]) as typeof thread.producedArtifacts; + localStorage.setItem(LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN('thread-1'), 'true'); + + const { findByTestId } = renderView({ props: { threadId: 'thread-1' } }); + + expect(await findByTestId('instance-ai-workflow-preview-stub')).toBeInTheDocument(); + expect(await findByTestId('instance-ai-preview-panel')).toBeVisible(); + }); + it('renders the agent artifact preview when an agent is created', async () => { thread.producedArtifacts = new Map([ ['agent-1', { type: 'agent', id: 'agent-1', projectId: 'proj-1', name: 'SEO Auditor' }], @@ -1649,8 +1671,8 @@ describe('InstanceAiThreadView', () => { const threadArea = getByTestId('instance-ai-thread-area'); const header = getByTestId('instance-ai-builder-chat-header'); const content = getByTestId('instance-ai-content-area'); - expect(previewPanel.style.width).toBe('600px'); - expect(previewPanel.style.getPropertyValue('--agent-preview-chat-column-width')).toBe('300px'); + expect(previewPanel.style.width).toBe('400px'); + expect(previewPanel.style.getPropertyValue('--agent-preview-chat-column-width')).toBe('200px'); await vi.waitFor(() => { expect(previewPanel).toHaveClass('agentPreviewLayoutTransition'); }); @@ -1658,20 +1680,20 @@ describe('InstanceAiThreadView', () => { await user.click(getByTestId('instance-ai-agent-preview-open-dock')); expect(threadArea).toHaveClass('agentPreviewDockOpen'); - expect(previewPanel.style.width).toBe('600px'); - expect(previewPanel.style.getPropertyValue('--agent-preview-chat-column-width')).toBe('300px'); + expect(previewPanel.style.width).toBe('400px'); + expect(previewPanel.style.getPropertyValue('--agent-preview-chat-column-width')).toBe('200px'); expect(queryByTestId('resize-handle')).toBeInTheDocument(); await user.click(getByTestId('instance-ai-agent-preview-close-dock')); expect(threadArea).not.toHaveClass('agentPreviewDockOpen'); - expect(previewPanel.style.width).toBe('600px'); + expect(previewPanel.style.width).toBe('400px'); expect(queryByTestId('resize-handle')).toBeInTheDocument(); await fireEvent.mouseDown(getByTestId('resize-handle'), { clientX: 0 }); expect(previewPanel).not.toHaveClass('agentPreviewLayoutTransition'); - await fireEvent.mouseMove(window, { clientX: 120 }); + await fireEvent.mouseMove(window, { clientX: -80 }); expect(previewPanel.style.width).toBe('480px'); expect(previewPanel.style.getPropertyValue('--agent-preview-chat-column-width')).toBe('240px'); @@ -1680,6 +1702,9 @@ describe('InstanceAiThreadView', () => { await vi.waitFor(() => { expect(previewPanel).toHaveClass('agentPreviewLayoutTransition'); }); + expect(localStorageState.store.get(LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO)).toBe( + '0.6', + ); await user.click(getByTestId('instance-ai-agent-preview-open-dock')); @@ -1701,26 +1726,54 @@ describe('InstanceAiThreadView', () => { const { getByTestId } = await renderAgentArtifact({ threadAreaWidth: 1200 }); const previewPanel = getByTestId('instance-ai-preview-panel'); - expect(previewPanel.style.width).toBe('600px'); + expect(previewPanel.style.width).toBe('400px'); mockThreadAreaSizeState.width.value = 800; - await vi.waitFor(() => expect(previewPanel.style.width).toBe('560px')); + await vi.waitFor(() => expect(previewPanel.style.width).toBe('400px')); mockThreadAreaSizeState.width.value = 1200; - await vi.waitFor(() => expect(previewPanel.style.width).toBe('600px')); + await vi.waitFor(() => expect(previewPanel.style.width).toBe('400px')); await fireEvent.mouseDown(getByTestId('resize-handle'), { clientX: 0 }); - await fireEvent.mouseMove(window, { clientX: 120 }); + await fireEvent.mouseMove(window, { clientX: -80 }); await fireEvent.mouseUp(window); expect(previewPanel.style.width).toBe('480px'); mockThreadAreaSizeState.width.value = 600; - await vi.waitFor(() => expect(previewPanel.style.width).toBe('420px')); + await vi.waitFor(() => expect(previewPanel.style.width).toBe('300px')); mockThreadAreaSizeState.width.value = 1200; await vi.waitFor(() => expect(previewPanel.style.width).toBe('480px')); }); + it('keeps the default ratio when dragging against the minimum width', async () => { + const { getByTestId } = await renderAgentArtifact({ threadAreaWidth: 1200 }); + const previewPanel = getByTestId('instance-ai-preview-panel'); + + expect(previewPanel.style.width).toBe('400px'); + + await fireEvent.mouseDown(getByTestId('resize-handle'), { clientX: 0 }); + await fireEvent.mouseMove(window, { clientX: 120 }); + await fireEvent.mouseUp(window); + + mockThreadAreaSizeState.width.value = 1600; + await vi.waitFor(() => expect(previewPanel.style.width).toBe('800px')); + }); + + it('restores the stored chat panel width ratio', async () => { + localStorageState.store.set(LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO, '0.6'); + const { getByTestId } = await renderAgentArtifact({ threadAreaWidth: 1200 }); + + expect(getByTestId('instance-ai-preview-panel').style.width).toBe('480px'); + }); + + it('falls back to the default chat panel width when the stored ratio is not a number', async () => { + localStorageState.store.set(LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO, 'null'); + const { getByTestId } = await renderAgentArtifact({ threadAreaWidth: 1200 }); + + expect(getByTestId('instance-ai-preview-panel').style.width).toBe('400px'); + }); + it('keeps expanded preview state independent from the agent dock', async () => { const { getByTestId, user } = await renderAgentArtifact(); const previewPanel = getByTestId('instance-ai-preview-panel'); @@ -1859,7 +1912,7 @@ describe('InstanceAiThreadView', () => { expect(getByTestId('instance-ai-thread-area')).not.toHaveClass('agentPreviewDockOpen'); }); - it('keeps the new-agent artifact accessible when closed and restores it after refresh', async () => { + it('keeps the new-agent artifact accessible and restores the closed preview after refresh', async () => { mockWindowSizeState.width.value = 1700; thread.producedArtifacts = new Map([ [ @@ -1889,6 +1942,9 @@ describe('InstanceAiThreadView', () => { expect(firstRender.queryByTestId('instance-ai-agent-preview-stub')).not.toBeInTheDocument(); expect(firstRender.getByTestId('instance-ai-artifacts-sidebar-slot')).toBeInTheDocument(); expect(firstRender.getByTestId('instance-ai-artifacts-panel-toggle')).toBeInTheDocument(); + expect(localStorage.getItem(LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN('thread-1'))).toBe( + 'false', + ); store.threads = [ { @@ -1916,10 +1972,10 @@ describe('InstanceAiThreadView', () => { firstRender.unmount(); const refreshedRender = renderView({ props: { threadId: 'thread-1' } }); - const restoredPreview = await refreshedRender.findByTestId('instance-ai-agent-preview-stub'); - expect(restoredPreview).toHaveAttribute('data-agent-id', 'agent-1'); - expect(await refreshedRender.findByTestId('instance-ai-preview-panel')).toBeVisible(); + expect(refreshedRender.queryByTestId('instance-ai-agent-preview-stub')).not.toBeInTheDocument(); + expect(await refreshedRender.findByTestId('instance-ai-preview-panel')).not.toBeVisible(); + expect(refreshedRender.getByTestId('instance-ai-artifacts-sidebar-slot')).toBeInTheDocument(); }); describe('Fix with AI card', () => { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useCanvasPreview.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useCanvasPreview.test.ts index 75ffce95950..d14ecac2ae5 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useCanvasPreview.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/useCanvasPreview.test.ts @@ -132,7 +132,11 @@ function createMockRoute(threadId = 'thread-1') { // Test helper — create composable + flush // --------------------------------------------------------------------------- -function setup(options?: { threadOverrides?: Partial }) { +function setup(options?: { + threadOverrides?: Partial; + initialAgentId?: () => string | undefined; + previewOpenState?: () => boolean | undefined; +}) { const thread = createMockThread(); if (options?.threadOverrides) Object.assign(thread, options.threadOverrides); const route = createMockRoute(); @@ -141,6 +145,8 @@ function setup(options?: { threadOverrides?: Partial }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any thread: thread as any, threadId: () => route.params.threadId, + initialAgentId: options?.initialAgentId, + previewOpenState: options?.previewOpenState, }); return { ...result, thread, route }; @@ -1106,6 +1112,31 @@ describe('useCanvasPreview', () => { expect(ctx.activeTabId.value).toBe('agent-1'); expect(ctx.isPreviewVisible.value).toBe(true); }); + + test('restores the attached agent instead of an earlier helper workflow', async () => { + const ctx = setup({ previewOpenState: () => true }); + ctx.thread.isHydratingThread = true; + registerWorkflow(ctx.thread, 'workflow-1', 'Helper workflow'); + registerAgent(ctx.thread, 'agent-1', 'Support Agent', 'proj-1'); + ctx.thread.messages = [ + makeMessage({ + role: 'user', + attachments: [ + { + type: 'agent', + id: 'agent-1', + name: 'Support Agent', + projectId: 'proj-1', + }, + ], + }), + ]; + ctx.thread.isHydratingThread = false; + await nextTick(); + + expect(ctx.activeTabId.value).toBe('agent-1'); + expect(ctx.activeAgentId.value).toBe('agent-1'); + }); }); describe('isPreviewVisible', () => { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/useCanvasPreview.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/useCanvasPreview.ts index 9a0d6cf16dc..c83ca69830b 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/useCanvasPreview.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/useCanvasPreview.ts @@ -38,12 +38,33 @@ interface UseCanvasPreviewOptions { thread: ThreadRuntime; threadId: () => string; initialAgentId?: () => string | undefined; + previewOpenState?: () => boolean | undefined; + onPreviewOpenChange?: (open: boolean) => void; } -export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOptions) { +export function useCanvasPreview({ + thread, + initialAgentId, + previewOpenState, + onPreviewOpenChange, +}: UseCanvasPreviewOptions) { // --- Tab state --- const activeTabId = ref(); - const isPreviewOpen = ref(false); + const isPreviewOpen = ref(previewOpenState?.() ?? false); + + function setPreviewOpen(open: boolean, persist = true) { + if (isPreviewOpen.value === open) return; + isPreviewOpen.value = open; + if (persist) onPreviewOpenChange?.(open); + } + + watch( + () => previewOpenState?.(), + (open) => { + if (typeof open === 'boolean') setPreviewOpen(open, false); + }, + { immediate: true }, + ); const buildingArtifactIds = useBuildingArtifactIds(thread); @@ -152,7 +173,23 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt (id) => { if (!id || activeTabId.value !== undefined) return; activeTabId.value = id; - isPreviewOpen.value = true; + if (previewOpenState?.() !== false) setPreviewOpen(true, false); + }, + { immediate: true }, + ); + + watch( + [ + () => previewOpenState?.(), + allArtifactTabs, + () => thread.isHydratingThread, + initialArtifactId, + ], + ([open, tabs, isHydrating, initialId]) => { + if (isHydrating) return; + if (open === true && activeTabId.value === undefined && tabs[0]) { + activeTabId.value = tabs.some((tab) => tab.id === initialId) ? initialId : tabs[0].id; + } }, { immediate: true }, ); @@ -161,11 +198,11 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt function selectTab(tabId: string) { activeTabId.value = tabId; - isPreviewOpen.value = true; + setPreviewOpen(true); } function closePreview() { - isPreviewOpen.value = false; + setPreviewOpen(false); } /** @@ -176,7 +213,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt function openWorkflowPreview(workflowId: string): boolean { if (activeTabId.value === workflowId && isPreviewOpen.value) return false; activeTabId.value = workflowId; - isPreviewOpen.value = true; + setPreviewOpen(true); return true; } @@ -188,7 +225,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt function openDataTablePreview(dataTableId: string, _projectId: string): boolean { if (activeTabId.value === dataTableId && isPreviewOpen.value) return false; activeTabId.value = dataTableId; - isPreviewOpen.value = true; + setPreviewOpen(true); return true; } @@ -200,7 +237,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt function openAgentPreview(agentId: string, _projectId: string): boolean { if (activeTabId.value === agentId && isPreviewOpen.value) return false; activeTabId.value = agentId; - isPreviewOpen.value = true; + setPreviewOpen(true); return true; } @@ -247,7 +284,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt if (thread.isHydratingThread) return; activeTabId.value = latestBuildResult.value.workflowId; - isPreviewOpen.value = true; + setPreviewOpen(true); workflowRefreshKey.value++; }, { flush: 'sync' }, @@ -278,7 +315,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt if (thread.isHydratingThread) return; activeTabId.value = latestBuilderTarget.value.workflowId; - isPreviewOpen.value = true; + setPreviewOpen(true); }, { flush: 'sync' }, ); @@ -307,7 +344,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt if (thread.isHydratingThread) return; activeTabId.value = latestAgentBuilderTarget.value.targetAgentId; - isPreviewOpen.value = true; + setPreviewOpen(true); }, { flush: 'sync' }, ); @@ -367,7 +404,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt const targetId = latestUpdateResult.value.workflowId; activeTabId.value = targetId; - isPreviewOpen.value = true; + setPreviewOpen(true); workflowRefreshKey.value++; }, { flush: 'sync' }, @@ -393,7 +430,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt if (thread.isHydratingThread) return; activeTabId.value = latestDataTableResult.value.dataTableId; - isPreviewOpen.value = true; + setPreviewOpen(true); dataTableRefreshKey.value++; }, { flush: 'sync' }, @@ -417,7 +454,7 @@ export function useCanvasPreview({ thread, initialAgentId }: UseCanvasPreviewOpt const remaining = allArtifactTabs.value.filter((t) => t.id !== deletedId); activeTabId.value = remaining.length > 0 ? remaining[0].id : undefined; if (!activeTabId.value) { - isPreviewOpen.value = false; + setPreviewOpen(false); } } });