Skip to content

feat(host): seamless in-place window-mode transitions + foundational cleanup#551

Merged
Kosinkadink merged 78 commits into
mainfrom
chore/desktop2-cleanup
May 15, 2026
Merged

feat(host): seamless in-place window-mode transitions + foundational cleanup#551
Kosinkadink merged 78 commits into
mainfrom
chore/desktop2-cleanup

Conversation

@Kosinkadink
Copy link
Copy Markdown
Member

@Kosinkadink Kosinkadink commented May 13, 2026

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-host IPC handler (src/main/index.ts) accepts the chooser-host renderer's claim and registers a pendingAttachClaims entry keyed by install id.
  • onLaunch consumes the claim, calls rebuildComfyViewIfNeeded (handles unique-partition installs by swapping entry.comfyView to a persist:${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), then attachInstall against the same host.
  • returnToDashboard (src/main/host/detach.ts) → entry.detachInstall()_detachInstallImpl runs the symmetric undo: _installCleanup, navigate comfyView to about:blank, reset entry identity, loadTitleBarUrl(_, ''), applyChooserHostTheme, destroyPanelView + ensurePanelView('chooser'), layout.
  • New loadTitleBarUrl() and destroyPanelView() helpers shared between attach + detach + close paths.
  • Close-handler teardown wraps every step in safeTeardown(source, fn) so a single throw can't skip BrowserWindow.destroy() (errors forwarded to Datadog with their source tag).

Bugs fixed during validation

  • layoutViews stale-closure (createHostWindow.ts): operated on the captured comfyView instead of entry.comfyView. After rebuildComfyViewIfNeeded swapped the view (Standalone / Portable installs), the new view was added to contentView with default 0×0 invisible bounds — ComfyUI loaded but never painted. Now reads entry?.comfyView ?? comfyView. The strengthened lifecycle.test.ts "split-view architecture" test now asserts getVisible() + non-zero bounds.
  • Spawn-collision overlap: File → New Window opened the new chooser at exactly the same x/y/w/h as the existing one — looked like the previous window had simply re-rendered. New cascadeOffsetForCollisions walks live host origins and bumps (x, y) by 30px per collision (with chain detection).
  • Bounds-key drift: saveWindowBounds was bound to the construction-time opts.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 reads liveBoundsKeyFor(entry) so each identity slot only persists what the host did while it was that identity.
  • Sibling-aware default sizing: when a live host of the same identity already exists, open at the canonical 1280×900 default offset from the sibling's origin. First-spawn (no sibling) restores saved bounds.
  • Chooser hosts always default-size: the dashboard isn't a workspace the user customizes — it's a launcher surface. Skips both getSavedBounds restore and persistBounds save for chooser hosts so cold start, app relaunch, and File → New Window all open at the canonical default.
  • Stranded attach claims: when the existing-entry path takes over a launch, consumeAttachClaim(installationId) drops the stale claim instead of leaving it pinned in the map until the chooser host closes.

E2E coverage

e2e/lifecycle.test.ts adds:

  • launches ComfyUI from chooser tile — snapshots BrowserWindow.ids before/after the click; same id must survive the launch (in-place flip guard).
  • split-view architecture now also asserts comfyVisible === true + comfyBounds.{width,height} > 0 (regression guard for the stale-closure bug).
  • return-to-dashboard flips install host in place (same window id) — drives the File menu's Return to Dashboard via a new returnFirstInstallHostToDashboard E2E 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.ts was ~4,500 lines)

Host module split (src/main/host/)

  • registry.tscomfyWindows map, installationIdToWindowKey index, pendingAttachClaims, isChooserHost / isInstallHost predicates, computeBodyMode.
  • createHostWindow.ts — shared createHostWindow() + the openChooserHostWindow wrapper, loadTitleBarUrl, cascadeOffsetForCollisions, buildComfyView, rebuildComfyViewIfNeeded, expectedPartitionFor, theme helpers.
  • attach.tsattachInstall(entry, opts) with _installCleanup for symmetric undo.
  • detach.tsconsultPanelRendererClose, closeAllHostWindows, confirmAndCloseAllHostWindows, returnToDashboard, _detachInstallImpl.
  • panelView.tsensurePanelView, destroyPanelView, setActivePanel, refreshComfyTabBody, focusActiveBody, sendToPanelDeferred, panel-routing IPC.

Dedicated unit tests for each module (registry.test.ts, createHostWindow.test.ts, attach/detach/panelView test files).

Popup primitive

  • New EmbeddedPopupView lifecycle primitive (src/main/popups/embeddedPopupView.ts).
  • popups/{titlePopup,titleTooltip,systemModal}.ts adopt it.

Renderer composable extractions

  • TitleBarApp: useTitleBarHoverGate, useTitleBarIdentity, useTitleBarMenus, useTitleBarTooltip, useUpdatePills.
  • PanelApp: useChooserHandoff, useFirstUseChain, usePanelOverlays, useDeepLinkRouter, useAppUpdatePrompts, useSendFeedback.
  • ChooserView: ChooserInstallTile.vue + shared chooser-tiles.css.
  • SnapshotTab: SnapshotInspector.vue + lib/snapshots.ts.

Other extractions

  • lib/windowState.ts — bounds cache.
  • lib/contextMenu.tsattachContextMenu.
  • lib/processErrorHandlers.tsforwardDatadogError etc.
  • 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.tsWebContentsPage eval-bridge facade. Required because the chooser host's parent BrowserWindow has no DOM (just hosts WebContentsView children); Playwright's connectOverCDP hangs during enumeration in that topology, so we run executeJavaScript against each child's webContents directly.
  • e2e/support/{chooserHelpers,devHooks,electronHarness,evalRetry,hoverGate}.ts — facades and retry helpers for the new architecture.
  • New tests: 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.ts rewritten against the new harness; capped at 3 minutes per test (was 10).
  • playwright.config.ts drops the testIgnore block.

Comment / doc cleanup

Per ComfyUI-Launcher/AGENTS.md (no plan-step / phase / track / stage references in code comments). Sweeps across version-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). Added loadTitleBarUrl JSDoc that documents the explicit handshake contract — every channel onTitleBarReadyHandler must re-push since the navigation drops cached messages.

Verification

pnpm run typecheck          # passes
pnpm run lint               # passes
pnpm run build              # passes
pnpm run test               # 937 vitest tests pass
pnpm run test:e2e:windows -- --project=lifecycle
  # 52/52 pass in 2.5m (43 fast Windows + 9 lifecycle)

Notes

Kosinkadink and others added 30 commits May 12, 2026 21:29
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>
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>
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>
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>
Kosinkadink and others added 19 commits May 13, 2026 16:16
…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>
@Kosinkadink Kosinkadink changed the title chore: extract window state + context menu, drop plan-step comments, salvage e2e suite feat(host): seamless in-place window-mode transitions + foundational cleanup May 15, 2026
Kosinkadink and others added 7 commits May 15, 2026 13:43
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>
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>
@Kosinkadink Kosinkadink merged commit 7dfd109 into main May 15, 2026
9 checks passed
@Kosinkadink Kosinkadink deleted the chore/desktop2-cleanup branch May 15, 2026 22:23
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.

1 participant