From 2049d15dbe7e7e034165d76048dde92a8b28a36f Mon Sep 17 00:00:00 2001 From: "n8n-assistant[bot]" <100856346+n8n-assistant[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:54:45 +0000 Subject: [PATCH] fix(editor): Fix blank workflow previews in AI Assistant (backport to release-candidate/2.40.x) (#38914) Co-authored-by: Romeo Balta <7095569+romeobalta@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 --- .../__tests__/instanceAi.store.test.ts | 23 +++++++++++++++++++ .../ai/instanceAi/instanceAi.store.ts | 23 +++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/instanceAi.store.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/instanceAi.store.test.ts index a9d342229e9..9abfd2047a1 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/instanceAi.store.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/instanceAi.store.test.ts @@ -1,4 +1,5 @@ import { setActivePinia, createPinia } from 'pinia'; +import { effectScope, nextTick } from 'vue'; import { beforeAll, afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { ensureThread } from '../instanceAi.api'; import { @@ -191,6 +192,28 @@ describe('useInstanceAiStore - runtime registry', () => { expect(store.getRuntime('thread-1')).toBeUndefined(); }); + it('keeps the runtime watchers alive after the creating scope stops', async () => { + const store = useInstanceAiStore(); + // The thread view creates the runtime in setup. A Suspense duplicate of the + // view is discarded before it mounts, which stops that component scope. + const creatorScope = effectScope(); + const runtime = creatorScope.run(() => store.getOrCreateRuntime('thread-1')); + creatorScope.stop(); + + runtime?.messages.push({ + id: 'm1', + role: 'user', + createdAt: '2026-01-01T00:00:00.000Z', + content: '', + reasoning: '', + isStreaming: false, + attachments: [{ type: 'workflow', id: 'wf-1', name: 'My workflow' }], + }); + await nextTick(); + + expect(runtime?.producedArtifacts.get('wf-1')?.name).toBe('My workflow'); + }); + it('syncs a thread into the sidebar list', async () => { const store = useInstanceAiStore(); mockEnsureThread.mockResolvedValueOnce({ diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.store.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.store.ts index a204e98e67a..e42da317ad6 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.store.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/instanceAi.store.ts @@ -1,5 +1,14 @@ import { defineStore } from 'pinia'; -import { ref, computed, inject, provide, shallowReactive, type InjectionKey } from 'vue'; +import { + ref, + computed, + effectScope, + inject, + provide, + shallowReactive, + type EffectScope, + type InjectionKey, +} from 'vue'; import { useRootStore } from '@n8n/stores/useRootStore'; import { useToast } from '@n8n/composables/useToast'; import { useTelemetry } from '@n8n/composables/useTelemetry'; @@ -66,6 +75,11 @@ export const useInstanceAiStore = defineStore('instanceAi', () => { // --- Thread runtimes --- const runtimes = shallowReactive(new Map()); + // Detached scopes owning each runtime's watchers. The runtime must outlive the + // component that created it: a Suspense duplicate of the thread view can create + // it in setup and be discarded, and a component scope would take the watchers + // (e.g. the resource registry) down with it. + const runtimeScopes = new Map(); const runtimeHooks = { onTitleUpdated: (threadId, title) => { for (const thread of localThreadEntries(threadId)) thread.title = title; @@ -81,8 +95,11 @@ export const useInstanceAiStore = defineStore('instanceAi', () => { const existingRuntime = runtimes.get(threadId); if (existingRuntime) return existingRuntime; - const runtime = createThreadRuntime(threadId, runtimeHooks, projectId); + const scope = effectScope(true); + const runtime = scope.run(() => createThreadRuntime(threadId, runtimeHooks, projectId)); + if (!runtime) throw new Error('Failed to create thread runtime'); runtimes.set(threadId, runtime); + runtimeScopes.set(threadId, scope); return runtime; } @@ -95,6 +112,8 @@ export const useInstanceAiStore = defineStore('instanceAi', () => { if (!runtime) return; runtime.dispose(); + runtimeScopes.get(threadId)?.stop(); + runtimeScopes.delete(threadId); runtimes.delete(threadId); }