Skip to content

Refactor/926 state driven instance navigation#973

Open
MaanilVerma wants to merge 19 commits into
Comfy-Org:mainfrom
MaanilVerma:refactor/926-state-driven-instance-navigation
Open

Refactor/926 state driven instance navigation#973
MaanilVerma wants to merge 19 commits into
Comfy-Org:mainfrom
MaanilVerma:refactor/926-state-driven-instance-navigation

Conversation

@MaanilVerma

@MaanilVerma MaanilVerma commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Refactors instance/window navigation (#926) from logic scattered across three sites into one pure, table-driven decision function. Given the current view (Dashboard | Instance | Cloud) and a clicked target, decideNavigation returns whether to switch in place, restart, focus, or open a new window — and drives the footer CTA label, a new caret split-button, and an in-drawer confirm from that single source. Ships the CTO matrix's behavior changes (Instance→Instance 3-way, cloud/instance/remote always-new-window) and adds Remote handling the matrix never specified.

Changes

What

  • src/shared/navigation/navDecision.ts (new) — decideNavigation(NavInput): NavDecision, a total ReadonlyMap transition table over ViewKind × TargetKind × TargetRun. Pure, O(1), runs identically in renderer + main.
  • src/shared/viewKind.ts (new) — single ViewKind/Category/NavClass vocabulary + navClass (folds remote ⇒ cloud) + viewKindFor (one classifier shared by computeViewKind and the snapshot builder).
  • src/renderer/src/composables/useInstanceNavState.ts (new) — read-model deriving NavInput facts; reuses useInstallCta so the one-install-one-window invariant stays single-sourced. A remote target routes via the cloud cells (non-local URL backend).
  • src/renderer/src/composables/useInstanceActions.ts (new) — single dispatcher routing a decision's verb onto the bridge, with cloud-capacity + local-kill gates; aborts (no unhandled rejection) if a confirm dialog rejects.
  • src/main/index.ts — extracts deliverPickToEntry (shared by swap + new-window), adds the openInstallInNewWindow primitive (focus-existing else spawn a fresh chooser host), and threads a confirmed flag through pickInstallFromPicker so the renderer's in-drawer confirm replaces main's system modal.
  • src/main/popups/titlePopup.ts — adds the comfy-titlepopup:open-install-new-window IPC + currentView/currentCategory on the picker snapshot; picker IPC wirings wrapped in .catch().
  • src/renderer/src/components/settings/ComfyUISettingsContent.vue — footer CTA + caret split-button driven by decideNavigation; leading icons on the CTA and both menus; per-category CTA wording/icon (Open Cloud vs Open Remote); reuses the existing stop action in the caret next to Restart.
  • src/renderer/src/views/comfyUISettings/MoreMenu.vue — optional heading + per-item icon (the plain "More" menu is unchanged).
  • docs/instance-navigation-926-handoff.md (new) — verbatim CTO matrix + Implemented? column, Remote rows (§1b), deviations (§1a), manual-verification + unit-vs-e2e coverage tables.

Breaking

  • None. IPC channels are additive; the primary-action emit payload changed boolean → NavDecision but is internal to the picker. CTA labels for unchanged cells are byte-identical; existing telemetry (instance.switched, instance.opened_new_window) is preserved.

Review Focus

  • The decision table (navDecision.ts) is the whole behavior surface — each matrix cell is one entry. Review it as data against the CTO matrix in the handoff doc. A reachability test bans any no-op CTA for a reachable (host, target, run) combo.
  • Remote == Cloud (target): a remote connection is a non-local URL backend, so it routes through the cloud cells; the component resolves wording/icon per raw category. This fixes the dead "Start"/wrong-"Switch" cases on remote targets.
  • Deliberate deviations (§1a/§1b): Cloud→Dashboard ships new-window (the chip isn't table-driven yet); Cloud/Remote → self resolves to Restart instead of matrix row 16's "second window" (a true second view of one session isn't supported; the allowDuplicate plumbing is kept dormant/reserved).
  • Scope boundaries: pillar-2 lifecycle-action consolidation (useInstallContextMenu thinning) is a deferred follow-up. The 3-way confirm rendering and focus-existing-for-a-real-window are unit/manual-only (e2e can't spawn a live ComfyUI process) — see the coverage table.

Testing

State-driven navigation splits into a pure decision (unit-testable exhaustively) and side-effecting wiring (e2e via real bridge → IPC → window): unit pins what the decision is for every cell; e2e pins that the wiring fires for the risky paths. Un-e2e-able cells are documented with their reason in the handoff doc.

  • navDecision.test.ts — every matrix cell + the full ViewKind × TargetKind × TargetRun × class × intent cross-product (totality), caret/new-window selection, and the no-dead-no-op-CTA reachability guard.
  • useInstanceActions.test.ts — verb→bridge routing for all verbs; cloud-capacity + switch/restart gates; the 3-way (switch / open-new / cancel) outcomes; dispatch-reject and bridge-undefined guards; dormant allowDuplicate passthrough.
  • useInstanceNavState.test.ts / registry.test.ts — run-state derivation; remote routes like cloud (incl. Instance→Remote ≡ Instance→Cloud); computeViewKind (null/unknown category).
  • e2e/nav-matrix-{dashboard,instance,cloud}.test.ts — 8 tests asserting via recorded IPC + live BrowserWindow counts: same-window launch, focus-existing, new-window spawn (parent undisturbed), allowDuplicate primitive.

pnpm run typecheck (node/web/e2e/integration) + pnpm run lint clean; 1983 unit tests pass; e2e --project=lifecycle green (pnpm run build first — e2e runs the built bundle).

Manual verification matrix

# Current view Click target Target state Proposed action Primary CTA Secondary Implemented?
1 Dashboard Dashboard n/a No-op
2 Dashboard Instance A Not running Launch + same window Start
3 Dashboard Instance A Running Focus existing Switch
4 Dashboard Cloud Not running Open same window Open Cloud Open in new window
5 Dashboard Cloud Already open Focus existing Switch Open in new window
6 Dashboard + New Instance n/a Install wizard New Install
7 Instance A Dashboard n/a New window (A keeps running) Open Dashboard
8 Instance A Instance A Self Restart in place Restart (Stop — deferred)
9 Instance A Instance B Not running Switch (in-drawer 3-way) Switch Open in new window → Switch / Open in new window / Cancel
10 Instance A Instance B Running Focus existing Switch
11 Instance A Cloud Not running New window (A keeps running) Open Cloud
12 Instance A Cloud Already open Focus existing Switch Open in new window
13 Cloud Dashboard n/a Switch same window Open Dashboard ⚠️ ships new-window (§1a)
14 Cloud Instance A Not running New window (cloud keeps running) Open in new window
15 Cloud Instance A Already open Focus existing Switch (Restart — deferred)
16 Cloud Cloud Self Restart in place Restart ⚠️ self → Restart, not second window (§1b)
17 Cloud + New Instance n/a Install wizard in new window New Install

Remote rows (not in the original CTO matrix — Remote ≡ Cloud target)

# Current view Click target Target state Behavior Primary CTA Implemented?
R1 Dashboard Remote Not running Open same window (= row 4) Open Remote
R2 Dashboard Remote Already open Focus existing (= row 5) Switch
R3 Instance A Remote Not running New window, A keeps running (= row 11) Open Remote
R4 Cloud / Remote different Cloud/Remote Not running New window, host keeps running Open Remote / Open Cloud
R5 Any Cloud or Remote Running elsewhere Focus existing Switch
R6 Cloud / Remote self Restart in place (second view unsupported) Restart

Full matrix, deviations, and coverage: docs/instance-navigation-926-handoff.md.

Closes #926 closes #911

Lift the implicit "which window does this click target" decision out of
three scattered sites into one pure, table-driven function. Behavior-
identical (current matrix encoded); deltas land in Phase 3.

- shared/viewKind: ViewKind/Category/NavClass + navClass + viewKindFor,
  the single classifier (remote folds into cloud for navigation)
- shared/navigation/navDecision: total O(1) transition table →
  {window, verb, confirm, primaryLabel, secondary}; runs in main + renderer
- main: computeViewKind + currentView/currentCategory on the picker
  snapshot; new openInstallInNewWindow primitive (focus-existing else
  spawn a fresh chooser host) + IPC + preload bridge; deliverPickToEntry
  extracted and shared with the swap path
- renderer: useInstanceNavState (facts) + useInstanceActions (dispatcher);
  footer CTA + caret split-button driven by the decision; picker routes
  the emitted NavDecision through the dispatcher

typecheck (node+web) + lint clean; 1971 tests pass.
…e 3a)

- pickInstallFromPicker: Switch / Open in new window / Cancel via openSystemModalChoiceAsync; 'secondary' → openInstallInNewWindow (keeps A running)
- decision table: instance|instance|stopped gains the new-window caret

No flag. typecheck + lint clean; 1971 tests pass.
- 3 e2e specs (dashboard/instance/cloud): bridge → IPC → window for the navigation deltas (8 tests, lifecycle project)
- record open-install-new-window IPC for assertions
- handoff doc: test-coverage table (unit vs e2e) + why some cells are unit/manual-only
- openInstallInNewWindow fails closed (try/catch) + telemetry after the spawn guard so a failed spawn can't record a false success
- pick/restart picker IPC wirings now .catch() unhandled rejections
- dispatch aborts (no unhandled rejection) when a confirm dialog rejects
- remove dead 'switch-3way' Confirm variant; strip rollout/history from source comments; relocate the swap-contract JSDoc
- tests: dispatch reject + bridge-undefined, caret new-window coverage, unknown-category, e2e parent-undisturbed + reset symmetry

typecheck + lint clean; 1976 unit tests pass.
…d? column

The handoff doc's matrix had been condensed (dropped the deferred Stop/Restart secondaries, folded row-13 into 'source of truth'). Reproduce the original 17-row matrix verbatim with an Implemented? column; deviations (row 13 new-window) and deferrals (rows 8/15) called out in §1a.
Clicking Switch showed a wrong 'Restart?' dialog then closed the picker before main's 'Switch?' modal. Now the picker shows ONE in-drawer 3-way (Switch / Open in new window / Cancel) and stays open until the user commits, matching Restart.

- useInstanceActions: switch routes through confirmSwitch; pickInstall(confirmed:true) so main skips its modal
- pickInstallFromPicker + pick-install IPC + preload bridge take a confirmed flag
- remove the now-dead Confirm type/field from the decision
…op in caret

- leading icons on the primary CTA, caret items, and the More menu (Switch→Replace, Stop→CircleStop, Forget→EyeOff vs Uninstall→Trash2)
- caret dropdown gains a 'Window options' heading + divider, fits content width; MoreMenu supports an optional heading + per-item icon (More menu unchanged otherwise)
- reuse the existing synthetic Stop action in the caret next to Restart (own running local instance)
…rop 'Start (new window)')

It spawns a window, not a swap — so the open-in-new-window label + icon are clearer and consistent with the caret. Removes the now-unused startNewWindow key.
…f → Restart

- targetKind: remote folds to the cloud target rows (fixes Remote→Remote / Cloud→Remote dead 'Start' and Instance→Remote wrong 3-way)
- add cloud|cloud|stopped (open-new) + running-elsewhere (focus) cells; reachability test bans no-op CTAs
- cloud/remote self → Restart; allowDuplicate kept dormant/reserved
- per-category CTA wording + icon: 'Open Remote' (server) vs 'Open Cloud'
@MaanilVerma MaanilVerma marked this pull request as ready for review June 8, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] Tech Debt Implement: Add button to open new instance in a new window

3 participants