feat(host): seamless in-place window-mode transitions + foundational cleanup#551
Merged
Conversation
Move window-bounds cache (WindowBounds, flushWindowState, saveWindowBounds, getSavedBounds, getWindowOptions) into src/main/lib/windowState.ts and the context menu attach helper into src/main/lib/contextMenu.ts. Trim historical narration around the Tray docking import, the removed TRAY_ICON, and the chooser host pill text. Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516 Co-authored-by: Amp <amp@ampcode.com>
Per AGENTS.md, comments must not reference plan steps, phases, tracks, or stage IDs. Sweep removes Phase N / Track M-N / Stage W-N / post-unification-code-review.md F-NN / vN.M.K cross-refs from version-resolve, standalone/actions, settings, index, and a handful of renderer composables and tests. Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516 Co-authored-by: Amp <amp@ampcode.com>
The legacy sidebar/tabs UI assumed by navigation.test.ts and fullscreen.test.ts no longer exists. Delete those plus the orphaned support modules (comfyWindow.ts, navigationHelpers.ts) and re-enable the suite via playwright.config.ts. Add an eval-bridge harness (e2e/support/cdpPages.ts) that runs executeJavaScript against each WebContentsView's webContents, working around Playwright connectOverCDP hanging when the parent BrowserWindow has no DOM. Add chooser/title-bar helpers (chooserHelpers.ts), a new chooser smoke suite (chooser.test.ts), and rewrite launchApp.ts, lifecycle.test.ts, and window-visible.spec.ts against the new harness. Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516 Co-authored-by: Amp <amp@ampcode.com>
Roadmap (P0-P4) for the remaining cleanup ahead of the seamless dashboard <-> instance window transitions feature. Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516 Co-authored-by: Amp <amp@ampcode.com>
clickInstallTile and the post-install waitFor used .chooser-tile + a textContent substring, which silently matched the New Install tile because its description ('Set up a fresh ComfyUI environment.') contains the install-name substring 'ComfyUI'. Switch both call sites to .chooser-tile:not(.chooser-tile-new):not(.chooser-tile-cloud) .chooser-tile-name so we only consider installed tiles' name labels.
Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516
Co-authored-by: Amp <amp@ampcode.com>
…eardown After launch the chooser host transforms in place to host the install, so the chooser panel's .chooser-tile-cta-close is no longer reachable. Close the install's BrowserWindow directly via main-process evaluate; the close handler tears the comfy process down via the same path the chooser-tile button would use. Closing the last window also quits the app, so the post-close poll now treats an evaluate failure as the terminal stopped state. Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516 Co-authored-by: Amp <amp@ampcode.com>
… split
Add regression coverage for the three EmbeddedPopupView-shaped subsystems hung off the chooser host so the upcoming src/main/index.ts extractions can be validated end-to-end:
* title popup + system modal are pre-warmed when title-bar-ready fires.
* title popup opens with menu items on waffle click and closes via __comfyTitlePopup.close().
* title-bar tooltip popup is created on demand via __comfyTitleBar.showTooltip and hides cleanly via hideTooltip.
The first-use takeover would race the renderer mount and lock the title bar (consent-lockdown hides the waffle button), so extend the e2e harness with a SeedOptions.settings hook and pre-seed firstUseCompleted: true for the chooser suite. Settings now seed BEFORE Electron launches so the renderer's first read sees them; installations still seed post-launch via app.getPath('userData').
Amp-Thread-ID: https://ampcode.com/threads/T-019e1f94-0238-767d-8770-7db59757c516
Co-authored-by: Amp <amp@ampcode.com>
…andlers Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
…DownloadHandlers Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e1fcf-7754-703e-9441-11cc2816c591 Co-authored-by: Amp <amp@ampcode.com>
Pulls ComfyWindowEntry, ComfyPanelKey, BodyMode, VALID_PANELS, comfyWindows, the installationIdToWindowKey secondary index, pendingAttachClaims, lastFocusedInstallationId, and the find/openOrFocus helpers (plus bringToFront) out of index.ts and into src/main/host/registry.ts.
Late-bound chooser-host construction via setHostFactories({ createChooser }) so the registry never imports host-construction code (no cycle). index.ts wires the factory at the top of whenReady.
index.ts 3720 -> 3125 lines (-595).
Amp-Thread-ID: https://ampcode.com/threads/T-019e2090-2750-71d1-a4c9-c5aab842cc37
Co-authored-by: Amp <amp@ampcode.com>
Covers nextWindowKey sequencing, computeBodyMode (5 modes incl. install-less + lifecycle gating via _runningSessions), pendingAttachClaims round-trip + overwrite, register/unregister + getEntryByInstallationId (incl. re-index-after-takeover safety), and findPreferredHostByVisibility (visible-beats-minimised, insertion order within bucket, predicate isolation, destroyed-skip). Amp-Thread-ID: https://ampcode.com/threads/T-019e2090-2750-71d1-a4c9-c5aab842cc37 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e2090-2750-71d1-a4c9-c5aab842cc37 Co-authored-by: Amp <amp@ampcode.com>
10 minutes was overkill — the full lifecycle run including download finishes in under 2 minutes locally. Tighten the per-test cap to 180s so a hung install or a stuck process surfaces fast instead of burning a full 10 minute window. Amp-Thread-ID: https://ampcode.com/threads/T-019e2090-2750-71d1-a4c9-c5aab842cc37 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20b6-b1c6-76cf-9baf-54176cc188d1 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e20f2-8d79-75b6-b024-14279bd95fa7 Co-authored-by: Amp <amp@ampcode.com>
Owns the WebContentsView construction + dev/prod URL load + parent-closed teardown + popup-destroyed cleanup + show-fallback timer that all three title-bar / shell popups (titleTooltip, systemModal, titlePopup) currently duplicate inline. Each popup module will keep its own typed entry record (spec, item list, last-synced JSON, ...) and delegate the lifecycle to this class on the next pass. Amp-Thread-ID: https://ampcode.com/threads/T-019e20f2-8d79-75b6-b024-14279bd95fa7 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e2394-e4cc-73b9-a95c-23a5a5143a41 Co-authored-by: Amp <amp@ampcode.com>
…andoff Amp-Thread-ID: https://ampcode.com/threads/T-019e2394-e4cc-73b9-a95c-23a5a5143a41 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e2394-e4cc-73b9-a95c-23a5a5143a41 Co-authored-by: Amp <amp@ampcode.com>
…composables Amp-Thread-ID: https://ampcode.com/threads/T-019e2394-e4cc-73b9-a95c-23a5a5143a41 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e23b2-13b0-726c-9d4f-df64a04c5be7 Co-authored-by: Amp <amp@ampcode.com>
…HoverGate Amp-Thread-ID: https://ampcode.com/threads/T-019e23b2-13b0-726c-9d4f-df64a04c5be7 Co-authored-by: Amp <amp@ampcode.com>
… window Amp-Thread-ID: https://ampcode.com/threads/T-019e23b2-13b0-726c-9d4f-df64a04c5be7 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e23b2-13b0-726c-9d4f-df64a04c5be7 Co-authored-by: Amp <amp@ampcode.com>
…rors Wraps the eval-bridge entry points (cdpPages findWebContentsId / WebContentsPage.evaluate, devHooks seed/setInstall/setApp/getTitlePopupBounds) in a small retry helper. CI runners hit 'Execution context was destroyed' when the host window's child WebContentsViews tear up/down in quick succession; the next tick lands on a fresh evaluation context, so a 50ms backoff retry recovers without masking real failures. Amp-Thread-ID: https://ampcode.com/threads/T-019e2467-d182-720c-afdd-60f91a534f29 Co-authored-by: Amp <amp@ampcode.com>
…llHost predicates P2 cleanup for the seamless-transition feature. Registry now owns the attach-claim lifecycle behind three named helpers (claimAttachHost / consumeAttachClaim / dropAttachClaimsForWindow); pendingAttachClaims is module-private so producer / consumer / cleanup sites can't drift apart. consumeAttachClaim folds get+delete into one take-once call so onLaunch can't double-attach. isChooserHost(entry) / isInstallHost(entry) type-guard predicates live next to computeBodyMode in registry.ts. Routed every semantic chooser- vs-install branch through them: title-popup file-menu builder + activate guard, will-prevent-unload, applyChooserHostTheme(ToAll), attach already-attached guard, returnToDashboard + _detachInstallImpl guards, refreshComfyTabBody loop, chooser-pick claim guard, transfer-host- bounds-to-install. Registry-internal sites that ARE the implementation the predicate is built from (the installationIdToWindowKey index in register/unregisterHostEntry, find-preferred predicates inside the registry itself) keep direct installationId comparisons. Tests updated to use the helpers + cover the take-once contract and per- window cleanup. Amp-Thread-ID: https://ampcode.com/threads/T-019e2467-d182-720c-afdd-60f91a534f29 Co-authored-by: Amp <amp@ampcode.com>
…elpers
Two related P2 cleanups for the seamless-transition feature.
createHostWindow now composes three module-private helpers:
- buildHostSkeleton(opts): BrowserWindow + titleBarView + comfyView +
bound layoutViews + resize/move/fullscreen/focus listeners
- wireTitleBarHandshake(args): comfy-window:title-bar-ready IPC handler
that pushes initial state and pre-warms file-menu / system-modal
popups; returns the bound handler so the lifecycle module can
ipcMain.off it on closed
- installLifecycleListeners(args): close consult-and-destroy flow +
closed-time registry/claim cleanup
createHostWindow itself is now a 35-line composition. A future
swapInstallInPlace can re-run wireTitleBarHandshake +
installLifecycleListeners against an existing skeleton without
rebuilding the BrowserWindow.
attachInstall extracted three independent wire* helpers, each returning
a teardown closure that's collected into _installCleanup:
- wireKeyboardShortcuts: F5/Ctrl+R reload + Ctrl/Cmd zoom shortcuts
- wireFailRetry: backoff retry on did-fail-load; returns { cancel }
which doubles as the relaunch-flow interrupt and the teardown
- wireRenderProcessGone: Datadog report + automatic reload
attachInstall itself shrank from ~330 to ~220 lines. Remaining inline
blocks (theme observer, OS title sync, installation-rename listener,
dom-ready content script) share closure state and are deferred to the
seamless-transition feature, which will need to redesign that shared
state to support an in-place install swap.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2467-d182-720c-afdd-60f91a534f29
Co-authored-by: Amp <amp@ampcode.com>
Five plan/checklist docs whose work has fully landed, with no remaining open items and no references from any other doc or source file. Git history preserves the trail of decisions for anyone who needs to revisit; keeping the live tree clean of completed plans prevents readers from mistaking past work for active scope. - docs/CLEANUP_PLAN.md — pre-seamless-transition cleanup, P0-P4 all shipped on this branch. - docs/E2E_HARDENING_PLAN.md — G0-G4 shipped; G5 was always optional and is not pursued. - docs/357-code-quality-plan.md — issue 357, all phases shipped. - docs/431-view-flattening-plan.md — issue 431, all phases shipped; also referenced files that have since been deleted. - docs/view-flattening-plan.md — superseded by 431-view-flattening-plan and now also retired. Reference / postmortem / ongoing-design docs remain untouched (window-mode-unification-revert.md is referenced from production code; postmortem-reactive-proxy-ipc.md, telemetry.md, theme-colors.md, migration-parity.md, electron-window-parity.md are evergreen references; unified-window-phase3-notes.md, post-phase3-ux-polish.md, post-unification-code-review.md still track open follow-on work). Amp-Thread-ID: https://ampcode.com/threads/T-019e2467-d182-720c-afdd-60f91a534f29 Co-authored-by: Amp <amp@ampcode.com>
… trim claim-attach-host doc Code-review followup. Reverts the speculative wire/skeleton extractions in createHostWindow and attachInstall back to the original linear structure. The extracted helpers existed only for a future swapInstallInPlace caller and added parameter-passing without removing complexity. The wireFailRetry split also created a real bug: its returned cancel function tore down the did-fail-load listener, but that same function was stored in comfyFailRetryTimerCancels and called mid-session by onModelFolderRelaunch — silently killing retry for the rest of the install's lifetime. Inlining restores the original behavior where cancelFailRetry only clears the timer (safe to call from the relaunch flow) and the listeners are torn down by _installCleanup at end-of-life only. Also trims a multi-paragraph history-narrating doc-block on the claim-attach-host IPC handler down to one sentence + the doc link (per AGENTS.md "don't narrate history"). Kept from the prior P2 work: the registry attach-claim helpers (claimAttachHost / consumeAttachClaim / dropAttachClaimsForWindow) and the isChooserHost / isInstallHost type-guard predicates and their call-site routing. Amp-Thread-ID: https://ampcode.com/threads/T-019e2467-d182-720c-afdd-60f91a534f29 Co-authored-by: Amp <amp@ampcode.com>
Flip a single host window between chooser (dashboard) and install modes in-place instead of closing and reopening the window. - index.ts: claim-attach-host IPC + onLaunch handle identity swaps and reload titlebar with new install id; wire destroyPanelView via HostWindowFactories - createHostWindow.ts: extract loadTitleBarUrl; fix stale comfyView closure in close handler (use registry); replace empty catches with forwardDatadogError; guard webContents before isDestroyed() - panelView.ts: add destroyPanelView helper for transition cleanup - detach.ts: returnToDashboard now flips host in place - e2eHooks/devHooks: programmatic returnToDashboard test hook - lifecycle.test.ts: cover same-window-id flip on return-to-dashboard Drop the now-obsolete window-mode-unification-revert plan doc. Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
…n in-place attach paints rebuildComfyViewIfNeeded swaps entry.comfyView when a chooser host is attached in place to a unique-partition install (Standalone / Portable). The captured comfyView in the layoutViews closure pointed at the now-destroyed view, so the freshly-built view kept its default 0x0 invisible bounds and ComfyUI loaded but never painted. Mirrors the same fix already applied to the close handler. Strengthens the lifecycle E2E to assert comfyView is visible with non-zero bounds — the previous coverage only checked the URL, which loaded fine. Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
…an existing one File menu -> New Window (and any other createHostWindow caller) used getWindowOptions(boundsKey) which returns the saved bounds verbatim, so a freshly-spawned chooser host opened at the EXACT same x/y/w/h as the most recent host of the same boundsKey. The new window appeared to just re-render the previous one. Walk live host origins and bump (x, y) by 30px per collision (with chain detection) so the new window cascades visibly past every overlap. Skipped when getWindowOptions returned no x/y (no saved bounds, Electron centers). Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
…awned hosts Two related fixes for the bounds inheritance weirdness exposed by the in-place chooser->install flip: 1. Save bounds under the LIVE entry identity (chooser slot or installation id) instead of the construction-time key. A chooser host that flips to install-backed in place was saving its install-mode user resizes under the chooser slot — and vice versa via Return to Dashboard. Now each identity slot only persists what the user did while the host was that identity. 2. When opening a new host while another host of the same identity is already alive (most common: File -> New Window with a chooser open), use the canonical 1280x900 default size instead of the saved bounds. Saved-bounds inheritance there made the new window size + place identically to the live one, so it looked like the existing window had simply re-rendered. First-spawn (no live sibling) keeps the saved-bounds restore so app relaunches still land at the user's preferred size. Drive-by cleanups from the seamless-transition investigation: - Drop stranded attach claims when the existing-entry path takes over a launch instead of the in-place flip (otherwise the claim sat in the map until the chooser host closed). - Update the now-stale claim-rejection comment in useChooserHandoff: rebuildComfyViewIfNeeded handles partition mismatches in place; the fallback only fires for genuine sender races. - Document the loadTitleBarUrl handshake contract: any new title-bar renderer state must be re-pushed by onTitleBarReadyHandler since the navigation drops cached messages. Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
…heir bounds The dashboard isn't a workspace the user customizes — it's a launcher surface. Restoring last-session bounds across cold starts (and especially across in-place flips that drifted the slot) made the dashboard feel like it had inherited an unrelated window's shape. Force chooser hosts to use the canonical 1280x900 default at construction (skip getSavedBounds + maximized restore), and skip the persistBounds save when the live entry is install-less. Install-backed hosts continue to restore + persist their per-install bounds normally. Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
Replaces 8 repeated try/catch + reportTeardownError blocks with single-line safeTeardown(source, fn) calls. Same functional safety + same explicit telemetry source mapping; the sequence of teardown steps is now readable at a glance. Amp-Thread-ID: https://ampcode.com/threads/T-019e2ce5-2727-71dc-a6a5-a7540e0521a5 Co-authored-by: Amp <amp@ampcode.com>
On macOS, Electron's app.getPath('userData') resolves to ~/Library/Application Support/<appName>/, but the harness was writing the seeded settings.json to ~/.config/<appName>/ for everything except Windows. The seed never reached the renderer, so firstUseCompleted defaulted to false, the FirstUseTakeover auto-mounted, and pushed firstUseMode='consent-lockdown' which v-ifs the title-bar waffle button out of the DOM.
Fix: write to ~/Library/Application Support/<appName>/ on darwin so configDir() in src/main/settings.ts finds the seed. The chooser-test suite happened to dismiss the takeover via clickNewInstallTile + Escape before its first openTitleMenu, masking the bug; the new dropdowns / settings suites call openTitleMenu first and surfaced it.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2d4b-3e1c-76da-877e-ddf6110976db
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e2d4b-3e1c-76da-877e-ddf6110976db Co-authored-by: Amp <amp@ampcode.com>
Non-packaged Electron may resolve app.name to either the package.json value (comfyui-desktop-2) or its binary name (Electron). The first attempt only wrote to ~/Library/Application Support/comfyui-desktop-2/settings.json; CI's Page Snapshot artifact confirmed firstUseCompleted still came back undefined, so the FirstUseTakeover auto-mounted at the consent step and the consent-lockdown push hid the title-bar waffle button. Seed both paths on darwin so the renderer's first read finds the seed regardless of which app name Electron resolves. Amp-Thread-ID: https://ampcode.com/threads/T-019e2d4b-3e1c-76da-877e-ddf6110976db Co-authored-by: Amp <amp@ampcode.com>
The harness used to write settings.json into the platform-specific userData dir before launch. On macOS that dir is rooted at the real pw_dir (Application Support), not HOME, so the seed landed outside the test sandbox and main never read it — the FirstUseTakeover opened, set isConsentLockdown, and hid the title-bar waffle button, breaking dropdowns.test.ts and settings.test.ts. Pass the seed JSON via E2E_SETTINGS_SEED instead. Main writes it to dataPath on first settings.load(), bypassing all userData path guesswork. Amp-Thread-ID: https://ampcode.com/threads/T-019e2d74-aba0-70e8-85d2-454c11a251e5 Co-authored-by: Amp <amp@ampcode.com>
settings.ts:
- Add app.isPackaged guard to maybeSeedFromEnv so the E2E settings seed cannot fire in production builds.
- Delete process.env['E2E_SETTINGS_SEED'] immediately after reading so the JSON payload doesn't bleed into Python/ComfyUI child processes.
host/createHostWindow.ts + host/detach.ts:
- Drop the HostWindowFactories.{ensurePanelView,destroyPanelView} indirection and the entire DetachFactories plumbing — both modules now import panelView.ts directly.
- Clean up the corresponding setHostWindowFactories / setDetachFactories wiring in main/index.ts.
e2e:
- Hoist isPopupVisible, closeTitlePopupIfOpen, and TITLE_REOPEN_SUPPRESSION_MS into support/cdpPages.ts and use them from chooser, downloads-shelf, dropdowns, and settings tests.
- Bump the title-bar reopen-suppression debounce from 150ms to 300ms (TITLE_REOPEN_SUPPRESSION_MS) to take the timer further off the 100ms guard for CI flake margin.
- Move entryCounter = 0 into the downloads-shelf beforeEach for deterministic createdAt ordering across tests.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2da9-1d27-73f4-9b13-ad02e60933ac
Co-authored-by: Amp <amp@ampcode.com>
Bug fixes: - PanelApp.vue: wrap onMounted bootstrap in try/finally so resolveBootstrap always fires; if loadLocale or the store init Promise.all rejected, the bootstrapReady promise stayed pending forever and useDeepLinkRouter would silently wedge any panel-trigger-overlay deep-link the user fired afterwards. - PanelApp.vue: drop the duplicate onFirstUseSkip subscription. useFirstUseChain already owns the same listener, so a single Skip Onboarding click was running completeFirstUseAndDismiss twice. - useChooserHandoff.ts + useFirstUseChain.ts: performChooserLaunch now returns 'focused-running' | 'launched' | 'missing-action'. handleFirstUseComplete dismisses the takeover when the cloud install is already running so the first-use overlay doesn't sit stranded over the focused window. - useChooserHandoff.ts: clear pendingPickUnsub at the START of every prepareChooserHostHandoff call. A previous claimed-by-main launch (or a launch that errored) used to leave its onInstanceStarted listener live; a later unrelated instance-started broadcast could then close this chooser host. - useTitleBarMenus.ts: handleDownloadsTray now calls hideTip() like handleFileMenu does, so the macOS hover tooltip can't outlive the downloads popup opening over it. Cleanup: - usePanelOverlays.ts: drop the unused FirstUseChainHooks.reset field — no implementation, no caller, just misleading docs. Amp-Thread-ID: https://ampcode.com/threads/T-019e2da9-1d27-73f4-9b13-ad02e60933ac Co-authored-by: Amp <amp@ampcode.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Originally branched as a pre-seamless-transition cleanup pass; now also lands the seamless transitions feature itself, the bug fixes shaken out by it, and the supporting refactors / e2e hardening that grew out of the same investigation.
Headline feature: seamless in-place window-mode transitions
The chooser host now flips into the install's host in place instead of close+open swap — same
BrowserWindow.id, no flicker, no window-count blip. Symmetric undo via Return to Dashboard flips back without spawning a new chooser window.Main code paths:
claim-attach-hostIPC handler (src/main/index.ts) accepts the chooser-host renderer's claim and registers apendingAttachClaimsentry keyed by install id.onLaunchconsumes the claim, callsrebuildComfyViewIfNeeded(handles unique-partition installs by swappingentry.comfyViewto apersist:${id}partition view),loadTitleBarUrl(re-navigates the title bar so its URL carries the new install id),destroyPanelView(drops the chooser PanelApp + any in-flight launch overlay), thenattachInstallagainst the same host.returnToDashboard(src/main/host/detach.ts) →entry.detachInstall()→_detachInstallImplruns the symmetric undo:_installCleanup, navigate comfyView toabout:blank, reset entry identity,loadTitleBarUrl(_, ''),applyChooserHostTheme,destroyPanelView+ensurePanelView('chooser'), layout.loadTitleBarUrl()anddestroyPanelView()helpers shared between attach + detach + close paths.safeTeardown(source, fn)so a single throw can't skipBrowserWindow.destroy()(errors forwarded to Datadog with their source tag).Bugs fixed during validation
layoutViewsstale-closure (createHostWindow.ts): operated on the capturedcomfyViewinstead ofentry.comfyView. AfterrebuildComfyViewIfNeededswapped the view (Standalone / Portable installs), the new view was added tocontentViewwith default 0×0 invisible bounds — ComfyUI loaded but never painted. Now readsentry?.comfyView ?? comfyView. The strengthenedlifecycle.test.ts"split-view architecture" test now assertsgetVisible()+ non-zero bounds.cascadeOffsetForCollisionswalks live host origins and bumps(x, y)by 30px per collision (with chain detection).saveWindowBoundswas bound to the construction-timeopts.boundsKey. A chooser host that flipped to install-backed kept saving install-mode resizes under the chooser slot (and vice versa via Return to Dashboard). Now readsliveBoundsKeyFor(entry)so each identity slot only persists what the host did while it was that identity.getSavedBoundsrestore andpersistBoundssave for chooser hosts so cold start, app relaunch, and File → New Window all open at the canonical default.consumeAttachClaim(installationId)drops the stale claim instead of leaving it pinned in the map until the chooser host closes.E2E coverage
e2e/lifecycle.test.tsadds:BrowserWindow.ids before/after the click; same id must survive the launch (in-place flip guard).comfyVisible === true+comfyBounds.{width,height} > 0(regression guard for the stale-closure bug).returnFirstInstallHostToDashboardE2E hook, asserts the host id survives the flip, the comfy frontend unloads, the chooser body re-paints, and a re-launch from the same chooser flips it back to the same id.All 9 lifecycle tests + 43 fast Windows e2e tests pass.
Foundational refactors (originally
index.tswas ~4,500 lines)Host module split (
src/main/host/)registry.ts—comfyWindowsmap,installationIdToWindowKeyindex,pendingAttachClaims,isChooserHost/isInstallHostpredicates,computeBodyMode.createHostWindow.ts— sharedcreateHostWindow()+ theopenChooserHostWindowwrapper,loadTitleBarUrl,cascadeOffsetForCollisions,buildComfyView,rebuildComfyViewIfNeeded,expectedPartitionFor, theme helpers.attach.ts—attachInstall(entry, opts)with_installCleanupfor symmetric undo.detach.ts—consultPanelRendererClose,closeAllHostWindows,confirmAndCloseAllHostWindows,returnToDashboard,_detachInstallImpl.panelView.ts—ensurePanelView,destroyPanelView,setActivePanel,refreshComfyTabBody,focusActiveBody,sendToPanelDeferred, panel-routing IPC.Dedicated unit tests for each module (
registry.test.ts,createHostWindow.test.ts,attach/detach/panelViewtest files).Popup primitive
EmbeddedPopupViewlifecycle primitive (src/main/popups/embeddedPopupView.ts).popups/{titlePopup,titleTooltip,systemModal}.tsadopt it.Renderer composable extractions
useTitleBarHoverGate,useTitleBarIdentity,useTitleBarMenus,useTitleBarTooltip,useUpdatePills.useChooserHandoff,useFirstUseChain,usePanelOverlays,useDeepLinkRouter,useAppUpdatePrompts,useSendFeedback.ChooserInstallTile.vue+ sharedchooser-tiles.css.SnapshotInspector.vue+lib/snapshots.ts.Other extractions
lib/windowState.ts— bounds cache.lib/contextMenu.ts—attachContextMenu.lib/processErrorHandlers.ts—forwardDatadogErroretc.lib/ipc/registerAssetDownloadHandlers.ts,lib/ipc/registerDownloadHandlers.ts.E2E hardening
The pre-existing suite was disabled (
testIgnore) because every test assumed the legacy sidebar / fullscreen UI. Replaced with a new harness:e2e/support/cdpPages.ts—WebContentsPageeval-bridge facade. Required because the chooser host's parentBrowserWindowhas no DOM (just hostsWebContentsViewchildren); Playwright'sconnectOverCDPhangs during enumeration in that topology, so we runexecuteJavaScriptagainst each child'swebContentsdirectly.e2e/support/{chooserHelpers,devHooks,electronHarness,evalRetry,hoverGate}.ts— facades and retry helpers for the new architecture.chooser.test.ts,devhooks-smoke.test.ts,downloads-shelf.test.ts,dropdowns.test.ts,settings.test.ts,title-bar-hover-gate.test.ts,title-bar-hover-gate-comfy-window.test.ts,update-pills.test.ts.lifecycle.test.tsrewritten against the new harness; capped at 3 minutes per test (was 10).playwright.config.tsdrops thetestIgnoreblock.Comment / doc cleanup
Per
ComfyUI-Launcher/AGENTS.md(no plan-step / phase / track / stage references in code comments). Sweeps acrossversion-resolve.ts,standalone/actions.ts,index.ts,settings.ts, and several renderer composables / tests. Removed completed planning docs (357-code-quality-plan.md,431-view-flattening-plan.md,view-flattening-plan.md,window-mode-unification-revert.md). AddedloadTitleBarUrlJSDoc that documents the explicit handshake contract — every channelonTitleBarReadyHandlermust re-push since the navigation drops cached messages.Verification
Notes
origin/main, independent of docs(agents): forbid ending sessions with uncommitted work #550.src/main/index.tsitself shrinks from ~4,500 lines to ~480 (the rest is moved into the host / popup / lib modules listed above).