chore(editor): Allow narrower Assistant chat panels (#37861)

Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
Tuukka Kantola 2026-09-08 11:26:55 +00:00 committed by GitHub
parent ab38a17bd4
commit 6acdf815ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 247 additions and 56 deletions

View File

@ -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';

View File

@ -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<boolean | null>(
() => 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<HTMLElement>('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"
>
<div :class="$style.builderChatHeader" data-test-id="instance-ai-builder-chat-header">
@ -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() {
>
<N8nResizeWrapper
:width="previewPanelWidth"
:min-width="400"
:min-width="previewMinWidth"
:max-width="previewMaxWidth"
:supported-directions="['left']"
:is-resizing-enabled="!isPreviewExpanded"
:is-resizing-enabled="isPreviewResizeEnabled"
:grid-size="8"
@resize="handlePreviewResize"
@resizestart="isResizingPreview = true"

View File

@ -14,6 +14,10 @@ import { usePushConnectionStore } from '@/app/stores/pushConnection.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { SidebarStateKey } from '../instanceAiLayout';
import { NEW_CONVERSATION_TITLE } from '../constants';
import {
LOCAL_STORAGE_INSTANCE_AI_ARTIFACT_PREVIEW_OPEN,
LOCAL_STORAGE_INSTANCE_AI_CHAT_PANEL_WIDTH_RATIO,
} from '@/app/constants';
import type { WorkflowFailuresReport } from '../components/InstanceAiWorkflowPreview.vue';
import type {
FrontendModuleSettings,
@ -1520,7 +1524,7 @@ describe('InstanceAiThreadView', () => {
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', () => {

View File

@ -132,7 +132,11 @@ function createMockRoute(threadId = 'thread-1') {
// Test helper — create composable + flush
// ---------------------------------------------------------------------------
function setup(options?: { threadOverrides?: Partial<MockThread> }) {
function setup(options?: {
threadOverrides?: Partial<MockThread>;
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<MockThread> }) {
// 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', () => {

View File

@ -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<string>();
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);
}
}
});