From 4c00d39ade18c5efcca64da026f1a75d221cdf56 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 11 Mar 2026 23:54:27 -0700 Subject: [PATCH] fix: app mode widgets disappear after hard refresh (#9621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix all app mode widgets (including seed) disappearing after hard refresh due to a race condition in `pruneLinearData` and a missing reactivity dependency in `mappedSelections`. ## Changes - **What**: Guard `pruneLinearData` with `!ChangeTracker.isLoadingGraph` so inputs are preserved while `rootGraph.configure()` hasn't populated nodes yet. Add `graphNodes` dependency to `mappedSelections` computed in `LinearControls.vue` so it re-evaluates when the graph finishes configuring. ## Review Focus The core fix is a one-line guard change: `app.rootGraph && !ChangeTracker.isLoadingGraph` instead of just `app.rootGraph`. The previous guard failed because `rootGraph` exists as an empty graph during loading — `resolveNode()` returns `undefined` for all nodes and everything gets filtered out. Fixes COM-16193 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9621-fix-app-mode-widgets-disappear-after-hard-refresh-31d6d73d3650811193f5e1bc8f3c15c8) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- .../extensions/linearMode/LinearControls.vue | 1 + src/stores/appModeStore.test.ts | 25 ++++++++++++++++++- src/stores/appModeStore.ts | 7 ++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue index 8b550a0ef9..d79d40cd91 100644 --- a/src/renderer/extensions/linearMode/LinearControls.vue +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -64,6 +64,7 @@ useEventListener( ) const mappedSelections = computed(() => { + void graphNodes.value let unprocessedInputs = appModeStore.selectedInputs.flatMap( ([nodeId, widgetName]) => { const [node, widget] = resolveNodeWidget(nodeId, widgetName) diff --git a/src/stores/appModeStore.test.ts b/src/stores/appModeStore.test.ts index 50db79ee46..c8d4d3e661 100644 --- a/src/stores/appModeStore.test.ts +++ b/src/stores/appModeStore.test.ts @@ -25,7 +25,7 @@ const mockEmptyWorkflowDialog = vi.hoisted(() => { vi.mock('@/scripts/app', () => ({ app: { - rootGraph: { extra: {}, nodes: [{ id: 1 }] } + rootGraph: { extra: {}, nodes: [{ id: 1 }], events: new EventTarget() } } })) @@ -242,6 +242,29 @@ describe('appModeStore', () => { expect(store.selectedOutputs).toEqual([1]) }) + it('reloads selections on configured event', async () => { + const node1 = mockNode(1) + + // Initially nodes are not resolvable — pruning removes them + mockResolveNode.mockReturnValue(undefined) + workflowStore.activeWorkflow = workflowWithLinearData([[1, 'seed']], [1]) + await nextTick() + expect(store.selectedInputs).toEqual([]) + expect(store.selectedOutputs).toEqual([]) + + // After graph configures, nodes become resolvable + mockResolveNode.mockImplementation((id) => + id == 1 ? (node1 as unknown as LGraphNode) : undefined + ) + ;(app.rootGraph.events as EventTarget).dispatchEvent( + new Event('configured') + ) + await nextTick() + + expect(store.selectedInputs).toEqual([[1, 'seed']]) + expect(store.selectedOutputs).toEqual([1]) + }) + it('hasOutputs is false when all output nodes are deleted', async () => { mockResolveNode.mockReturnValue(undefined) diff --git a/src/stores/appModeStore.ts b/src/stores/appModeStore.ts index a2df72e43d..2b820e9fe9 100644 --- a/src/stores/appModeStore.ts +++ b/src/stores/appModeStore.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' import { reactive, computed, watch } from 'vue' +import { useEventListener } from '@vueuse/core' import { useEmptyWorkflowDialog } from '@/components/builder/useEmptyWorkflowDialog' import { useAppMode } from '@/composables/useAppMode' @@ -76,6 +77,12 @@ export const useAppModeStore = defineStore('appMode', () => { { immediate: true } ) + useEventListener( + () => app.rootGraph?.events, + 'configured', + resetSelectedToWorkflow + ) + watch( () => isBuilderMode.value