Skip to content

Commit e351184

Browse files
Kosinkadinkampcode-comJedrzej Kosinskideepme987
authored
feat(launcher): unified-window title bar + panels (closes #499) (#500)
* feat(launcher): add title-bar panel buttons in ComfyUI windows Introduces infrastructure for the unified-window approach: each ComfyUI window's native title bar now has three buttons (ComfyUI / Install Settings / Launcher Settings) that swap which view occupies the body below the title bar. Main process changes (src/main/index.ts): - ComfyWindowEntry now tracks panelView, activePanel, lastTheme, and a bound layoutViews() so multiple windows lay out independently. - The panel WebContentsView is created lazily on first non-comfy switch. ComfyUI's WebContents stays alive but is hidden + zero-bounded so its page state survives across panel switches. - New IPC handler comfy-window:set-panel filters by titleBarView sender so cross-window panel switches can't bleed. - applyComfyTheme now sends via comfy-titlebar:theme-changed IPC instead of executeJavaScript; the same theme is replayed on title-bar ready. Title bar (resources/comfyTitleBar.html + new comfyTitleBarPreload): - Replaces the static label-only HTML with three panel buttons + active state, locked-down CSP, and a typed preload bridge. - macOS keeps its 78px traffic-light padding via a body class instead of inline executeJavaScript hacks. Panel renderer (src/renderer/panel.html + src/renderer/src/panel/): - New renderer entry that reuses the existing SettingsView for the Launcher Settings panel and shows a placeholder for Install Settings. - Subscribes to settings-changed broadcasts so multiple open panels stay in sync (Phase 1 cross-panel sync; see thread for design notes). - Reads installationId + initial panel from URL params; subscribes to panel-switch IPC for in-renderer view switches. Broadcast registry (src/main/lib/ipc/shared.ts): - _broadcastToRenderer now also forwards to a registry of extra WebContents (panel + title-bar views), since BrowserWindow.getAllWindows doesn't surface child WebContentsViews. Auto-cleanup on 'destroyed'. New settings broadcast: - registerSettingsHandlers emits settings-changed on every set-setting, letting any open panel refetch. Build + types: - electron.vite.config.ts now declares the second renderer entry (panel.html) and the new title-bar preload. - ElectronApi gains onSettingsChanged + onPanelSwitch. - en.json gains titleBar.* keys. Tests: - broadcast.test.ts covers the registry behavior (delivery, destroyed cleanup, explicit unregister, multi-target). - comfyTitleBar.test.ts asserts the title-bar HTML's button wiring, CSP, and bridge-channel subscriptions. - csp.test.ts now runs against both renderer entries via describe.each. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): address Phase 1 panel-switch race + focus handoff Follow-up to the title-bar panel-buttons commit, addressing two material issues raised in code review: 1. Mid-load panel-switch race ensurePanelView now registers a one-time did-finish-load handler that re-pushes the *current* entry.activePanel to the panel renderer. This fixes the case where a user clicks Install Settings then Launcher Settings before the first load completes — previously the renderer would mount on the stale URL-param hint, ignoring the second click. The URL params remain as the initial-mount fallback for when the load wins the race. 2. Focus handoff on panel switches Added focusActiveBody() called after every setActivePanel(); moves OS focus to comfyView.webContents or panelView.webContents so keyboard input lands in the right place after a button click. Skipped when the parent window is not focused, and deferred to the did-finish-load handler when the panel is still loading. Also hardened: - Panel load promises are now \�oid load.catch(() => {})\ so a window closed mid-load doesn't surface as an unhandledRejection in the main-process Datadog forwarder. - onTitleBarReady's direct theme send now guards against a destroyed titleBarView.webContents. New tests: - src/renderer/src/panel/PanelApp.test.ts covers the regression: panel responds to onPanelSwitch IPC even when the URL param suggested a different initial panel, plus an unknown-key sanity check. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): allow inline script in title-bar CSP The title bar's CSP was \script-src 'self'\ which blocked the inline <script> tag that wires up the bridge — buttons stayed empty (no text) and the theme-changed IPC handler never registered, so ComfyUI's theme wasn't reflected in the title bar. Relaxed to \script-src 'unsafe-inline'\. The HTML is loaded from disk with no network access, so this is acceptable. Updated the matching assertion in comfyTitleBar.test.ts. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): tab-style title bar with install identity as the comfy tab Moves the panel buttons from the right edge to the left and styles them as tabs (underline on active). The comfy tab no longer shows a generic 'ComfyUI' label — it now displays the install name + source label that used to live as a separate title span. The dedicated title element is removed; the comfy tab IS the install identity. Rest of the title bar (right of the tabs, left of the native window controls) is a drag spacer. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * style(launcher): revert title-bar buttons to pill style Underlined-tab style conflicts visually with the per-instance tabs we use for running ComfyUI windows. Pill style (rounded background + border on active) keeps these buttons distinct as window-level controls. The comfy 'tab' keeps its install-name + source-label content; the layout still has the buttons on the left. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): convert title bar to a Vite renderer entry The title bar HTML lived in resources/ as a hand-rolled vanilla-JS file using system-ui + ad-hoc hex colors. It now lives as a third Vite-built renderer entry (src/renderer/comfyTitleBar.html + comfyTitleBar/main.ts + TitleBarApp.vue) so it shares Desktop 2.0's Inter font, design tokens (--surface, --border, --text-muted), and overall styling with the launcher and panel renderers. Behavior changes: - Title bar uses Inter (was system-ui). - Background, text, and border colors now resolve from the same CSS custom properties as the rest of the app, with the comfy-frontend theme report still able to override them at runtime. - macOS fullscreen handling moved from main-process executeJavaScript to a comfy-titlebar:fullscreen-changed IPC + reactive class on the Vue component (cleaner, no DOM mutation from main). Plumbing: - electron.vite.config.ts gains the third renderer entry. - Main process loads dev URL or built file; promise rejection is swallowed for window-closed-mid-load. - Bridge gains onFullscreenChanged. - Old static resources/comfyTitleBar.html removed. Tests: - TitleBarApp.test.ts replaces the static-HTML test, covering button rendering, ready signal, click forwarding, dynamic title, active highlight, is-mac, and is-fullscreen state. - csp.test.ts splits into two blocks: telemetry renderers (launcher, panel) keep the existing assertions; title-bar renderer asserts a tighter, telemetry-free CSP. Amp-Thread-ID: https://ampcode.com/threads/T-019df24f-90ae-72aa-9597-040545b803ce Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): fill in install-settings panel with inline DetailModal Phase 2 of the unified-window work. The Install Settings title-bar button now opens a real installation-scoped UI inside the ComfyUI window's panel WebContentsView instead of a 'coming soon' placeholder. - DetailModal gains an 'inline' prop that swaps the modal-overlay framing (constrained box, close button) for a full-bleed layout suitable for embedding directly inside the panel renderer.- PanelApp wires the install-settings branch to the existing IPC pipeline: installationStore (auto-refetches on onInstallationsChanged), sessionStore (drives REQUIRES_STOPPED action-guard checks), launcherPrefs (primary/pin), and a ProgressModal overlay for actions that fire show-progress. No per-source or per-action conditionals — all behavior comes from getDetailSections like the launcher window's flow.- Tests cover the URL-driven install lookup, the broadcast-driven refetch, and the missing-install placeholder. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): add gutter padding to launcher-settings panel content The panel renderer mounts SettingsView directly inside .panel-content, which had no padding — so the tab-mode SettingsView's text rendered flush against the left edge of the window with only the .view-scroll's 8px scrollbar margin on the right. Match the launcher window's '.content' (24px x 28px) padding for non-install-settings panels; install-settings keeps padding 0 because its inline DetailModal owns its own gutter. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): refresh ComfyUI window title on install rename The comfy tab in the unified-window title bar (and the OS-level window title) were captured at launch time and never updated, so renaming an installation from the Install Settings panel left both stale until the window was reopened. - installations.update() now emits an 'updated' event on a new installationEvents EventEmitter exported from src/main/installations.ts.- onLaunch in main subscribes to that event for the duration of the comfy window, recomputing both the title-bar tab text and the OS window title (via a new refreshOsWindowTitle helper that combines the tracked install name with the latest page title) when its install record changes. The listener is detached on window close. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): capture Phase 3 design constraints for the unified window Notes-only doc that records constraints surfaced while Phase 1/2 were shipped, so they aren't lost before Phase 3 planning begins: - Every panel must be safe to render with the instance not running (shutdown-for-update, restart, crash, mid-launch). Spell out the lifecycle states the Comfy tab body needs to render explicitly. - Replace the 'primary install' concept with 'recent install' as the driver of startup picker / open-existing flows. Track recency both globally and per source category. - When an install is deleted from inside its own panel, main needs to actively close the parent ComfyUI window — handleNavigateList in PanelApp is a no-op today. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): use scrollbar-gutter for view-scroll instead of asymmetric padding-right The hardcoded 8px right padding made content sit closer to the right edge than the left whenever the view-scroll lived inside a parent with symmetric padding (most notably the new install-settings panel). Switching to scrollbar-gutter: stable lets the browser reserve scrollbar space symmetrically, so content keeps its parent's gutters regardless of whether the scrollbar is currently visible. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * fix(launcher): hide redundant Settings breadcrumb inside the panel The tab-mode SettingsView renders a top-of-page breadcrumb to mark you-are-here inside the launcher window. In the panel, the title bar already labels the active button, so the breadcrumb is visual noise that pushes the actual settings down. Scope the hide to .panel-content with a :deep(.view.active > .toolbar) override so the launcher-window rendering is unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): close the ComfyUI window when its install is deleted from the panel Until now the install-settings panel's handleNavigateList was a no-op, so a successful delete or migrate left the parent ComfyUI window open with a missing-install placeholder until the user closed it manually. Adds a close-comfy-window IPC channel that main uses to look up the BrowserWindow by installation id and close it; the panel calls window.api.closeComfyWindow(installationId) from handleNavigateList. Updates docs/unified-window-phase3-notes.md so Phase 3 reuses this same teardown path instead of inventing new ones. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * revert(launcher): restore view-scroll padding-right that scrollbar-gutter regressed scrollbar-gutter: stable reserves layout space for the scrollbar but does not push content away from it, so the scrollbar ended up flush against the right-most content. The original padding-right: 8px gave that breathing room — restore it. The asymmetry vs. padding-left is intentional: the toolbar above view-scroll sits at the parent's left padding (no extra inset), and content inside view-scroll lining up with that toolbar is more visually important than perfect symmetry with the right edge. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): add Phase 3 consolidation directives (Directories tab, Cache rename, File menu, no Running tab) Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): note Downloads should become a top-bar panel, not a floating overlay Adds section 6 to the Phase 3 notes: the existing DownloadsPanel.vue floating overlay should be promoted to a first-class title-bar panel using the same WebContentsView swap pattern as Install Settings / Launcher Settings, eliminating the z-order awkwardness of stacking it on top of the Comfy WebContentsView. Amp-Thread-ID: https://ampcode.com/threads/T-019df2ae-48b3-711f-8fea-a47bdcf92b48 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): track per-source-category last-launched timestamps Adds the data layer needed by the upcoming startup picker / recency-driven flows so they can ask 'what's the most recent install in source category X?' without scanning every record. - Extend InstallationRecord (and the renderer-side Installation type) with an optional lastLaunchedAtByCategory: Record<string, number> alongside the existing global lastLaunchedAt. - Add installations.markLaunched(installationId, category?) which stamps both fields atomically through the same enqueue path used by update(), and emits the same installationEvents 'updated' event so existing subscribers (title-bar refresh, etc.) keep working. - Route _addSession in src/main/lib/ipc/shared.ts through markLaunched, resolving the install's source category via sourceMap[inst.sourceId]?.category. - Add installations.getRecent() and installations.getRecentByCategory(category, resolveCategory). getRecentByCategory falls back to global lastLaunchedAt for installs that predate the per-category field; resolveCategory is passed in by the caller so installations.ts stays free of any source-plugin dependency. - Cover the new helpers in src/main/installations.test.ts (14 tests). Purely additive; the 'primary install' surface (star button, primaryInstallId pref, set-primary-install action) is intentionally untouched and gets removed separately as part of the dashboard rethink. Amp-Thread-ID: https://ampcode.com/threads/T-019df2f9-d111-70a5-80b1-f067cc194b4b Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): mark per-category recency tracking as DONE in Phase 3 notes The data layer for section 2's 'recent install' direction landed in the previous commit. Update the notes so the next reader (likely the Downloads-as-title-bar-panel work from section 6) knows recency data is already available and primary remains intentionally untouched until the dashboard rethink. Amp-Thread-ID: https://ampcode.com/threads/T-019df2f9-d111-70a5-80b1-f067cc194b4b Co-authored-by: Amp <amp@ampcode.com> * refactor(launcher): markLaunched takes a category resolver, not a string Code-review follow-up. Switching markLaunched's second parameter from \category?: string\ to \ esolveCategory?: (inst) => string | undefined\ lets the helper compute the category itself inside its own enqueue lock, mirroring the dependency-injection pattern getRecentByCategory already uses. - Drops the redundant \installations.get()\ round-trip in _addSession (the install list was being scanned twice — once for get(), again inside markLaunched's enqueue). - Keeps installations.ts free of any source-plugin dependency: the resolver is supplied by the caller (typically \(inst) => sourceMap[inst.sourceId]?.category\). - Tests updated to pass resolveCategory through; added two new cases (resolver returns undefined → only global stamp; resolver receives the freshly-loaded record) for 16 tests total in this file, 728 total. - Phase 3 notes updated to document the new signature. Amp-Thread-ID: https://ampcode.com/threads/T-019df2f9-d111-70a5-80b1-f067cc194b4b Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): refine Phase 3 section 2 with the chooser-view design Records the chooser-view direction agreed in the design discussion: instead of auto-opening the most-recent install at startup (which gets stuck when that install is no longer bootable), the recency signal feeds a chooser view that surfaces Recent and All sections plus a pinned Cloud row above the table. The chooser collapses Phase 3's Dashboard and Installs surfaces into one screen, lives in an install-less host window (the same window shape used by the File-menu creation flows), and is the moment when the primary-install system is removed wholesale (gold-star button, primaryInstallId pref, set-primary-install action, ensureDefaultPrimary). Amp-Thread-ID: https://ampcode.com/threads/T-019df2f9-d111-70a5-80b1-f067cc194b4b Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): keep comfy window alive after stop, add lifecycle body The Comfy tab body now renders an explicit lifecycle view (stopped / launching / stopping / crashed) when no ComfyUI process is running inside the host window. Previously both onStop and onComfyExited destroyed the window outright, so a stopped-but-window-still-alive state did not exist. Decouple stop-process from destroy-window: * onStop and onComfyExited no longer call window.destroy() — they call refreshComfyTabBody(id) instead, which swaps the body to the lifecycle panel while leaving the window, ComfyUI WebContentsView, title bar, install-settings panel, and saved bounds intact. Window destruction remains bound to explicit close paths only: user closes the window, app quits, or close-comfy-window IPC fires (install deleted from inside its own panel). * onLaunch grows a reuse fast-path: when an entry already exists for the install (carried over from a previous launch), the existing comfyView is repointed at the new URL and the body swaps back from lifecycle to the live ComfyUI view. The reload / did-fail-load closures now read the URL through entry.comfyUrl so they don't go stale across stop+restart cycles. Body-mode routing: * New BodyMode union ('comfy' | 'comfy-lifecycle' | 'install-settings' | 'launcher-settings') models what the body area actually renders. ComfyPanelKey stays at the three user-visible title-bar pills — 'comfy-lifecycle' is internal and never appears on the title bar. * computeBodyMode(entry, id) is the single source of truth for layout and event-driven body swaps. The Comfy pill maps to 'comfy' when _runningSessions has the install, otherwise 'comfy-lifecycle'. * layoutViews, setActivePanel, ensurePanelView's did-finish-load rebroadcast, and focusActiveBody all route through computeBodyMode so they cannot disagree about which view should be visible. * The title bar is still notified with the user-visible ComfyPanelKey, so its three-pill UI is unchanged. Renderer: * New ComfyLifecycleView reads sessionStore (isLaunching, isStopping, errorInstances) and renders the appropriate state. The Start / Restart button emits show-progress so PanelApp's existing ProgressModal owns the launch flow — same path as DashboardCard / DetailModal kick off launches today. * PanelApp's PanelKey gains 'comfy-lifecycle' alongside the existing install-settings / launcher-settings keys; onPanelSwitch validates against a shared VALID_PANELS set so unknown keys are still rejected. Tests: * PanelApp.test.ts gets two new cases covering URL-driven and IPC-driven switches into the lifecycle view. * New ComfyLifecycleView.test.ts covers the four lifecycle states and the show-progress emission for Start. Amp-Thread-ID: https://ampcode.com/threads/T-019df354-4ff6-73b6-a618-0b40f72b34f4 Co-authored-by: Amp <amp@ampcode.com> * refactor(launcher): remove primary install system The launcher's 'primary install' surface (gold-star button, primaryInstallId pref, set-primary-install action, ensureDefaultPrimary, autoAssignPrimary, isPromotableLocal helper) goes away ahead of the chooser-view rebuild in Phase 3 step 2 of the unified-window work. Recency + manual selection covers the same user need without the dual-source-of-truth problem. Pin stays untouched per the design notes - it remains useful as a 'show prominently in chooser' affordance. Renderer: - Drop primaryInstallId / setPrimary / isPrimary / syncPrimaryFromMain from useLauncherPrefs (and matching tests). - Drop the gold-star button + confirmSetPrimary flow from DetailModal. - Drop the Star indicator in DashboardCard / InstanceCard. The unused sourceCategory prop on InstanceCard goes with it. - Drop the set-primary entry from useInstallContextMenu. - DashboardView swaps the primary-vs-latest dual card for a single 'lead' card driven by latest-launched-local (with a first-local fallback). This view is destined for deletion in Phase 3 step 2e; the simplified rendering is purely to keep it compiling until then. Main: - Drop isPromotableLocal, ensureDefaultPrimary, autoAssignPrimary, handleSetPrimaryInstall, the set-primary-install dispatch case, the primary-ensure block in get-installations, and the cleanup in get-installations / removal paths. - Drop ensureDefaultPrimary from MigrationTools (standaloneMigration + the migrate session action). - Rename the OEM workflow-import filter from isPromotableLocalInstall to isLocalNonDesktopInstall now that 'promotable to primary' has no meaning. - Drop primaryInstallId from KnownSettings + SETTINGS_SCHEMA. The data is purely advisory so a one-shot drop-on-load handles the migration. i18n: - Drop dashboard.primary, dashboard.changePrimary*, dashboard.setPrimary* strings from en.json and zh.json (and zh's stray dashboard.primaryInstall). 735 -> 734 tests (one removed setPrimary test, one new drop-on-load test in settings.test.ts; useLauncherPrefs.test.ts loses two setPrimary cases net of the slimmed loadPrefs test). Amp-Thread-ID: https://ampcode.com/threads/T-019df364-45f0-779d-af79-328812ce4bc1 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): add chooser view (preview) Phase 3 step 2b of the unified-window work. Builds the renderer-only ChooserView component that will replace the standalone Dashboard + Installs surfaces with a single 'what install do I want to open right now' picker. Layout per docs/unified-window-phase3-notes.md section 2: - Pinned Cloud promo row above the table (single row, not inside it). - Recent section sourced from lastLaunchedAt (descending), top N non-cloud installs (default 5). - All section listing every non-cloud install. - One scrollable view, both visible at once (NOT tabs). - No primary affordance (the primary system was retired in step 2a). - Pin stays as 'always surface' affordance via the existing context menu (pin/unpin) and a row indicator. Renders inside the existing launcher window for visual preview via a new 'Chooser (preview)' sidebar entry. Step 2c hooks it into the install-less host window as the Comfy tab body when no install backs the entry; step 2e drops the preview sidebar entry alongside Dashboard and Installs. Renderer-only — no main-process / IPC changes. Recency is computed from Installation.lastLaunchedAt on the existing installationStore; the Phase 3 prep landing already exposes lastLaunchedAtByCategory on the same record for future per-source-category filtering inside the chooser. Wired so click on a row emits 'pick' (App.vue currently routes that to the existing detail modal as the safe preview behaviour); right- click opens the existing install context menu (pin/unpin/dismiss- error/view-details). Empty state offers a single CTA to the new- install flow. ChooserView.test.ts covers: empty state + CTA wiring, cloud promo row placement, recent ordering by lastLaunchedAt (desc), pick emission, and cloud exclusion from Recent/All. 6 new tests, 740 total. Amp-Thread-ID: https://ampcode.com/threads/T-019df364-45f0-779d-af79-328812ce4bc1 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): wire install-less chooser host window (Phase 3 step 2c+2d) Step 2c — install-less host window infrastructure: - New openChooserHostWindow() function in main creates a comfy-shaped window with installationId: null, keyed by a synthetic chooser:N. The Comfy pill resolves to the chooser body via computeBodyMode(); the Install Settings pill is hidden and the title bar shows a "Choose an install" fallback label. - Title bar (TitleBarApp.vue) detects install-less mode from the empty installationId URL param exposed by the preload bridge, filters out the Install Settings tab, and accepts the fallback label pushed via the existing comfy-titlebar:title-changed channel. - Temporary tray entry "Open Chooser Window (preview)" so the chooser host can be exercised end-to-end before the startup picker / File menu lands. Step 2d — chooser pick triggers a real launch: - New close-host-window IPC closes the BrowserWindow that owns the calling panel WebContents; PanelApp's chooser pick handler uses it to retire the chooser host once the install's own window has opened. - handleChooserPick now reuses useListAction (mirroring DashboardCard's Open button) so it inherits the existing progress modal, port-conflict resolution, and telemetry pipeline. It subscribes to onInstanceStarted before kicking off the launch and only closes the host window when the install's window actually appears (so cancelled launches keep the chooser visible for retry). - Already-running installs short-circuit to focusComfyWindow + close- host-window without going through the launch action. The transitional pickInstallationForHost IPC introduced earlier in this work has been removed — pick is now renderer-driven. * feat(launcher): open chooser host window at startup (Phase 3 startup picker) - app.whenReady now opens an install-less chooser host window alongside the launcher window, so the chooser is the user's primary surface going forward. Both coexist until step 5 retires the launcher window entirely. - macOS 'activate' (dock click) prefers focusing an existing chooser host, falls back to the launcher window, falls back to creating a fresh chooser host. - Tray entry promoted from "Open Chooser Window (preview)" to "Choose an Install" and now uses the focus-or-open helper so it doesn't stack duplicate windows. - New helpers findFirstChooserHostWindow() and openOrFocusChooserHostWindow() centralise the singleton lookup so startup, activate, and tray all behave consistently. The raw openChooserHostWindow() helper stays available for the upcoming File menu's "New Window" entry, which always creates a fresh one. * feat(launcher): rewrite ChooserView as a golden-ratio recents grid Replaces the previous Cloud-row + Recent-section + All-section layout with a single grid of golden-ratio (1.618:1) tiles, per the Phase 3 design discussion: - Top-left tile: New Install (always visible across filters). - Next tile: Cloud — picks an existing cloud install if one is set up, otherwise routes to the new-install flow as a Try-Cloud CTA. Hidden when filtering to local/desktop/remote where it would be noise. - Following tiles: every other install (local / desktop / remote) ordered by lastLaunchedAt desc, never-launched at the end. - Filter chips above the grid (All / Local / Desktop / Cloud / Remote) narrow the install list. New Install stays visible regardless; the Cloud filter shows just the Cloud entry-point. - Each install tile carries a type icon (Cloud / Monitor / Globe / Box for cloud / desktop / remote / local) so the source kind is visible at a glance. - Status overlays mirror DashboardCard (running / stopping / in-progress). - Reordering is intentionally NOT supported — recency is the order. This is the "what install do I want to open right now" surface that replaces Dashboard + Installs + Running together. The dashboard / installs / running views in the launcher window will be retired in a follow-up commit once the title-bar redesign and File menu land. Locale keys for the grid (newInstall, newInstallDesc, filter*) added in both en.json and zh.json. Tests rewritten against the new layout. * refactor(launcher): rename Settings "Downloads" section to "Cache" (Phase 3 §4) The section's contents are really the on-disk cache (model files, wheels, GitHub release tarballs, etc.) — i.e. blobs the launcher pulls down on behalf of an install — not the in-flight downloads list. "Cache" reflects what the user actually controls here. - registerSettingsHandlers.ts: settings.downloads → settings.cache. - locales/en.json + zh.json: settings.downloads key replaced with settings.cache (existing cacheDir / maxCachedFiles fields untouched). * feat(launcher): title bar v2 — File menu (left) + center install pill with caret (Phase 3) Replaces the three-pill layout (Comfy, Install Settings, Launcher Settings) with the design discussed for Phase 3: - LEFT: a "File" menu button. Dropdown carries: * "New Window" — always opens a fresh install-less chooser host window (the focus-existing path remains on the tray entry). * "Desktop 2 Settings" — opens the launcher-settings body inside the current window (no longer a separate top-bar pill). - CENTER: a single install pill with the install identity (or the "Choose an install" fallback for install-less host windows). Click the name to surface the Comfy body. Click the caret to open a dropdown of install-scoped actions: * "Install Settings" — opens the install-settings body inside the current window (no longer a separate top-bar pill). * "Check for Updates" — focuses the launcher window's Settings page for now (per-install update UX is part of step 5). - The install caret is hidden in install-less host windows since there is no install-scoped menu to expose; the File menu is the only menu available in that mode. - Click-outside / Escape collapses any open menu. New preload bridge methods openNewWindow() and checkForUpdates() forward to two new IPC channels handled in main: comfy-window:new-chooser-window -> openChooserHostWindow() comfy-window:check-for-updates -> bringToFront(mainWindow) Test suite for TitleBarApp expanded from 9 to 13 cases — the new File-menu and install-caret menu paths are exercised end-to-end through the mock bridge. * feat(launcher): visual swap-in-place for chooser pick (Phase 3) The chooser pick flow currently closes the install-less host window and launches the install in a fresh one. Without this change, the new install window opens at the install's previously-saved bounds (or the default 1280x900), jumping visibly away from where the user just clicked. New transfer-host-bounds-to-install IPC stamps the calling chooser host window's current bounds onto the install's saved-bounds slot BEFORE the launch fires, so the new window appears exactly where the chooser was — visually a swap-in-place even though it's structurally close+open. PanelApp's handleChooserPick calls the IPC right before executeChooserAction. The IPC is no-op for install-backed callers so install-settings panels can't accidentally clobber another install's bounds. * feat(launcher): add Directories panel reachable from install pill caret (Phase 3) Step toward retiring the launcher window: ModelsView and MediaView now mount inside the host window as a single 'directories' panel, reachable from a new 'Directories' item on the install-pill caret menu in the title bar. Wiring: - 'directories' added to ComfyPanelKey + VALID_PANELS in src/main/index.ts and to PanelKey + VALID_PANELS in src/renderer/src/panel/PanelApp.vue. Mirrored in the BodyMode union and the inlined ComfyPanelKey type in src/preload/comfyTitleBarPreload.ts and TitleBarApp.vue (the title-bar renderer keeps its own copy of the union since tsconfig.web doesn't see the preload .ts directly). - setActivePanel() refuses 'directories' for install-less host windows alongside 'install-settings' — the install caret menu (which exposes both) is hidden in that mode, so a stray IPC payload must not be able to wedge an install-less window into a body mode that has no install to render. Renderer: - New DirectoriesView.vue under src/renderer/src/views — combines the models sections (DirCard list, primary marker) and media sections (shared input/output settings) under one breadcrumb so the user has one place to manage on-disk directories per install. Source layer is unchanged; future work will pull the shared folder-tree pieces into common components. - PanelApp.vue routes activePanel === 'directories' to DirectoriesView. - TitleBarApp.vue grew a third entry on the install caret dropdown ('Install Settings | Directories | Check for Updates'), with the active-state highlight when activePanel === 'directories'. i18n: - Added \directories.title\ to locales/en.json and locales/zh.json. The existing \models.*\ and \media.*\ keys still drive the individual sections inside DirectoriesView; collapsing them into \directories.*\ is part of the §3 directories merge proper. Tests: - TitleBarApp.test.ts: install-caret menu now expects 3 items (was 2); new test asserts Directories item routes to bridge.setPanel('directories'). - PanelApp.test.ts: new tests for the directories panel — both initial-load (panel=directories in URL) and runtime panel-switch IPC. DirectoriesView is mocked out so the test doesn't need to wire getModelsSections / getMediaSections fakes. Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): host new-install/track/load-snapshot/quick-install flows in panels (Phase 3) Step 2e (Stage 2) — wires the launcher window's install-creation / import flows into the host window so they no longer require focusing the launcher window. The chooser empty-state CTA now switches the host's panel body to the new-install flow in-place instead of bouncing through openNewInstallFromHost. Wiring: - Four new panel keys added in lockstep across all routing layers: 'new-install' | 'track' | 'load-snapshot' | 'quick-install'. Mirrored in ComfyPanelKey + VALID_PANELS in src/main/index.ts, the BodyMode union, the inlined ComfyPanelKey types in src/preload/comfyTitleBarPreload.ts and TitleBarApp.vue (the title bar keeps its own copy of the union since tsconfig.web doesn't see the preload TS file directly), and PanelKey + VALID_PANELS in src/renderer/src/panel/PanelApp.vue. Renderer: - The four launcher-window modal components (NewInstallModal, TrackModal, LoadSnapshotModal, QuickInstallModal) are now mounted inside PanelApp as full-panel bodies, each gated by activePanel === '<flow>'. Their existing emits map to the panel host: close → switchPanel('chooser') (back to recents) navigate-list → switchPanel('chooser') (no launcher list anymore) show-progress → existing ProgressModal pipeline - New switchPanel(panel) helper centralises the activePanel assignment and runs the imperative open() reset on flow panels after a nextTick. Mirrors what the launcher window's App.vue did via useNavigation.invokeWhenReady; without it form state would carry over between successive entries to the same flow. - handleChooserShowNewInstall and the no-launch-action fallback in handleChooserPick now route through switchPanel('new-install') instead of window.api.openNewInstallFromHost(). The chooser host becomes a real install-less workspace. - onPanelSwitch IPC handler funnels through switchPanel so flow panels reached via main-driven panel switches also get their open() reset. - The flow panels reuse the modal components' .view-modal-content root, so a small panel-flow CSS shim neutralises the modal sizing (max-width, border-radius, box-shadow, margin) and lets the content fill the panel area. The panel-content gutter is dropped for these branches since the modal body already owns its scrolling padding. Tests: - PanelApp.test.ts: ChooserView and the four flow modals are now mocked (the real components hit window.api.getSources etc which isn't part of the mock harness). New tests cover: - chooser show-new-install routes through switchPanel('new-install') (replaces the openNewInstallFromHost stub assertion shape) - flow-panel close emit returns the host to the chooser - panel=track / load-snapshot / quick-install URL params route to the right flow panel on initial mount - 755/755 tests pass (was 750; +5). What this does NOT yet retire: - handleChooserShowDetail still calls openNewInstallFromHost — that's Stage 3 (it should focus the install's own window on install-settings, not bounce to a creation flow). - createMainWindow() and the launcher window itself are still alive — Stage 4. This commit only makes the chooser CTA self-sufficient inside the host window. Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): wire Check for Updates to runCheck + UpdateBanner in panel (Phase 3) Step 2e (Stage 3) — break the install host title bar's 'Check for Updates' dependency on the launcher window, prep the tray entry to survive launcher window removal, and surface update results inside the host window so the user can see the check outcome without the launcher window in scope. Wiring: - updater.runCheck is now exported. The 'comfy-window:check-for-updates' IPC handler invokes it directly with source='title-bar-check' instead of focusing the launcher window. The result already flows through the existing 'update-available' / 'update-error' broadcast pipeline so any subscribed renderer surface updates without further work. - showMainWindow() (the tray 'Show App' / double-click target) falls back to openOrFocusChooserHostWindow() when mainWindow is gone. This keeps the tray entry working while the launcher window still exists AND once it's removed in step 4 — no transitional dead click. The chooser host is the right surface for 'show me Desktop' in the unified-window world. - UpdateBanner is now mounted at the top of PanelApp's panel-shell (above panel-content) so the host window's 'Check for Updates' entry has a visible result surface. UpdateBanner is auto-hide when there's no update info, so it costs nothing in the steady state. Mirror of the launcher window's App.vue \<UpdateBanner />\ placement — same self-contained component, no props/emits. Tests: - PanelApp.test.ts: UpdateBanner is mocked to a stub div so the test harness's window.api mock doesn't need to grow update-related methods. 755/755 tests still pass. What this does NOT yet retire: - mainWindow / createMainWindow() / launcher window App.vue still exist; this commit just removes the install host's title bar's dependency on them. handleChooserShowDetail in PanelApp.vue still routes to openNewInstallFromHost — that and the actual launcher window deletion are step 4. Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): retire the launcher window, chooser host is the only entry-point (Phase 3) Step 2e (Stage 4a) — drops createMainWindow() and stops creating the launcher BrowserWindow at app startup. The install-less chooser host window (openChooserHostWindow / openOrFocusChooserHostWindow) is now the only entry-point surface; per-install ComfyUI windows continue to host install-scoped panels via the title bar caret menu. Main: - createMainWindow() function removed entirely. Tray creation moved to app.whenReady directly (was bound to the launcher window's ready-to-show). Chinese-mirrors first-run prompt is dropped here and will land in the first-run flow panel (next step). The launcher-window zoom plumbing is gone — per-install ComfyUI windows manage their own zoom independently. - mainWindow is now `const mainWindow: BrowserWindow | null = null`. Historical guard checks (`if (mainWindow && !mainWindow.isDestroyed())`) short-circuit to no-ops without forcing a sweep of every call site; a follow-up will scrub the dead branches. - IPC handlers and call sites that focused mainWindow now route to the chooser host: app.activate (macOS dock click), second-instance handler, tray "Show App" + double-click. The previous separate "Choose an Install" tray entry is folded into the same focus path. showMainWindow() helper is gone. - reset-zoom IPC stubbed to a no-op (launcher-only). - Pruned now-unused imports: nativeTheme, setMainWindow, titleBarOverlayForTheme, updateTitleBarOverlay, setMainWindowId. Renderer: - open-new-install-from-host IPC + openNewInstallFromHost() preload bridge are removed. Last consumer (PanelApp chooser CTA) switched to switchPanel('new-install') in the previous commit. - handleChooserShowDetail now routes through handleChooserPick — the chooser host has no install backing it, so 'View Details' means 'open this install' which surfaces the install's own ComfyUI window where Install Settings is reachable from the title bar caret. A future enhancement can spawn a non-launching install-backed host window on install-settings, but that's a larger main-side change. What this does NOT yet do (intentionally — separate cleanup): - DashboardView, InstallationList, DashboardCard, RunningView, App.vue, the launcher-window i18n keys (dashboard.*, installations.*, running.*), useNavigation's TabKey union, and the index.html entry that was the launcher window's renderer are all still on disk. They're orphaned now (nothing renders them) but kept to keep this commit tractable. Stage 4b will scrub them and the residual main-side dead branches. Verification: 755/755 tests pass (no test-surface change). typecheck + lint + build all green. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 * fix(launcher): restore renderer-side telemetry after retiring launcher window (Phase 3) Stage 4a regression fix — the launcher window's `src/renderer/src/main.ts` held the renderer-side bootstrap (Datadog RUM init, PostHog init, window error hooks, comfy-event telemetry forwarders, `desktop2.session.*` emitters). Once `createMainWindow()` was dropped, that file stopped executing and the renderer side of telemetry/error-reporting silently went dark — no `desktop2.session.started`, no `desktop2.session.system_info`, no `desktop2.session.installation_started`, no Datadog RUM error forwarding from renderer surfaces. Fix: - Extract the side-effect bootstrap into a new shared module `src/renderer/src/lib/rendererBootstrap.ts` exposing a single idempotent `initializeRendererBootstrap()` entry-point. - Call it from `src/renderer/src/panel/main.ts` (the chooser host / per-install host renderer entry — the primary renderer entry-point now that the launcher window is gone). - The launcher window's `src/renderer/src/main.ts` is left untouched for this commit; it's orphaned (never executed) and will be deleted alongside `App.vue` / DashboardView / InstallationList etc. in the Stage 4b cleanup. The new module is self-contained — no Vue / Pinia / i18n imports — so it works regardless of which Vue app the renderer mounts. Verification: 755/755 tests pass; typecheck / lint / build green. Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): note pending unification of new-install + quick-install flows Amp-Thread-ID: https://ampcode.com/threads/T-019df537-56df-713d-a6c3-b6e98d824364 Co-authored-by: Amp <amp@ampcode.com> * refactor(launcher): scrub launcher-window orphans (Phase 3 Stage 4b) The launcher window was retired in commit cecd154; this stage prunes the now-dead surface area it left behind so the only entry-point surfaces are the install-less chooser host and per-install ComfyUI windows. Renderer entry & views deleted: - `src/renderer/index.html` + `src/renderer/src/main.ts` + `App.vue` (the launcher window's own renderer entry point) - `views/DashboardView.vue`, `views/InstallationList.vue`, `views/RunningView.vue` (launcher tabs) - `components/DashboardCard.vue`, `components/InstanceCard.vue` (only used by the deleted views) + InstanceCard test - `components/ViewShell.vue` (only used by App.vue) - `composables/useNavigation.ts` + `useControllerRegistration.ts` (the launcher window's overlay-stack + imperative-controller registry) `electron.vite.config.ts` drops the `index` rollupOptions input. The renderer build now only emits `panel.html` and `comfyTitleBar.html`. CSP test loses its `index.html` row for the same reason. Modal components no longer call `useControllerRegistration` — PanelApp already drives them via `ref`s + `switchPanel()`'s post-mount `open()` reset, so the controller registry was dead weight. Affected: NewInstallModal, QuickInstallModal, TrackModal, LoadSnapshotModal, ProgressModal. `SettingsView.vue` loses its overlay/`mode` branch. The launcher window was the only caller that rendered it as an overlay; PanelApp uses tab mode. The `NavigationMode` type goes away with `useNavigation.ts`. Main process scrubbed: - The dead `mainWindow` const + its placeholder declaration (no callers reference it anymore). - `forwardDatadogError` no longer pretends to send `dd-error` to a non-existent launcher renderer; it now broadcasts via `_broadcastToRenderer` so any open panel WebContentsView (which hosts the renderer telemetry bootstrap) can forward to Datadog RUM. PostHog Node capture stays as the always-on fallback. - The restart-failed `comfy-output` broadcast targeting mainWindow is deleted. The install's own comfyView is mid-load at that point so inline messaging would race; logging + the existing splash error path cover the user-facing UX. - Updated `mainWindow`-related comments to reflect the cleanup. i18n keys pruned from `locales/en.json` + `locales/zh.json`: - whole `sidebar.*` namespace (launcher sidebar is gone) - unused `dashboard.*`: title, welcome, welcomeDesc, installComfyUI, quickLaunch, recent, cloudSection, noPinned, pinnedInQuickLaunch - unused `running.*`: empty, instances, errors, crashed, exitCode, inProgress - unused `list.*`: title, trackExisting, newInstall, empty, emptyHint, emptyFilter, new, running, viewError, dragToReorder, filterAll, filterLocal, filterRemote, filterCloud The remaining keys are still referenced by ChooserView, MigrationBanner, DetailModal, ConsoleModal, ProgressModal, LoadSnapshotModal, useActionGuard, useInstallContextMenu, and SnapshotFilePreviewContent. Verification: 725/725 tests pass; typecheck + lint + build all green. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019df564-d370-77be-b6f1-cfc9a21f824b * feat(launcher): title-bar dropdowns rendered as child BrowserWindow popups (Phase 3 §14) Replace native Menu.popup() title-bar dropdowns with HTML rendered inside a frameless transparent child BrowserWindow, per the Chrome/Discord/VS Code pattern. Each parent (comfy / chooser host) window gets one reusable popup, pre-warmed on title-bar-ready and kept hidden between uses by parking off-screen with opacity 0 — opens are then just setBounds + setOpacity(1) + focus, with main waiting for a render-ack from the renderer before flipping opacity so the user never sees a stale frame of the previous open's content. Also bundles in earlier title-bar / chooser-host polish staged on this branch: title-bar v3 (§7) — install pill collapsed to a single click target, Back/Forward arrows moved next to it, hamburger menu on the left; isHoverActive hover-gating to fix stuck :hover after menu close; per-menu MENU_REOPEN_GUARD_MS (100ms) suppression; chooser-host theme follow (§12, §13) — onThemeChanged callback wires Settings → Theme flips through to the chooser host title bar + OS overlay; bringToFront on chooser-host launch (§11). Amp-Thread-ID: https://ampcode.com/threads/T-019df63e-be8d-70ae-9776-724aa810ca1b Co-authored-by: Amp <amp@ampcode.com> * docs(launcher): queue §15–§19 — waffle reorg, lifecycle gestures, full-screen flows, status pills, naming pass Capture the next batch of open UX work on the unified-window branch: - §15 — move Directories from the install pill into the global waffle menu (it's a cross-install concern). - §16 — fill in missing window-level lifecycle gestures: return to dashboard from inside an install-backed window, Close All Windows, Import Snapshot as New Install. - §17 — turn startup / update modals into full-screen takeovers with an explicit interrupt-vs-keep-running split on the window controls (× interrupts, − keeps it running; open question on dropping − entirely). - §18 — surface restart-required + update-available state via title-bar pills: Desktop-2-scoped to the right of the waffle menu, ComfyUI-scoped to the right of the install pill. - §19 — coordinated naming + flow-titles pass: 'Desktop 2 Settings' → 'App Settings', grand title + subtitle on every hosted flow (new-install / track / load-snapshot / quick-install) so users landing on a per-step heading still see the broader context. Status footer refreshed to reference §15–§19. Amp-Thread-ID: https://ampcode.com/threads/T-019df63e-be8d-70ae-9776-724aa810ca1b Co-authored-by: Amp <amp@ampcode.com> * refactor(launcher): move Directories from install pill to File menu (Phase 3 §15) Directories is a global / cross-install affordance — the launcher's view of disk-level state (models, outputs, inputs) has nothing per-install about it, so it doesn't belong on the install pill caret menu where it sat alongside the install-scoped Install Settings + Check for Updates entries. §15 moves it into the File / waffle menu where the other global affordances live, leaving the install pill strictly install-scoped. Changes: - `buildTitleMenuItems` in `src/main/index.ts`: - File menu now: `[New Window, Directories, Desktop 2 Settings]`. - Install menu now: `[Install Settings, ─, Check for Updates]` (the separator is unchanged from before; the entry that left was the `'directories'` row). - `activateTitleMenuItem` moves the `'directories'` activation branch from the install kind to the file kind, alongside the existing `'launcher-settings'` branch — both route through `setActivePanel(parentEntryId, key)`. - `setActivePanel`'s install-less rejection narrows from `(panel === 'install-settings' || panel === 'directories')` to just `panel === 'install-settings'`. Directories is install-agnostic (`DirectoriesView.vue` reads global `getModelsSections()` / `getMediaSections()` with no installationId), so install-less host windows can host it just like they already host launcher-settings. Install Settings stays gated — the install caret menu is still suppressed when there's no install backing the window, but a stray IPC payload still can't wedge an install-less window into that body mode. The `'directories'` panel key remains in all four sync points (ComfyPanelKey + VALID_PANELS + BodyMode in main, ComfyPanelKey in the title-bar preload, the inlined ComfyPanelKey in TitleBarApp.vue, and PanelKey + VALID_PANELS in PanelApp.vue) — only the menu-routing changes; the panel renderer is unaffected. Doc: marks §15 DONE in `docs/unified-window-phase3-notes.md`. The remaining queued waffle entries from §16 (Return to Dashboard, Close All Windows, Import Snapshot as New Install) stay open under their own section. Verification: pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run test — all green, 725/725. Amp-Thread-ID: https://ampcode.com/threads/T-019df69a-9879-73e2-bf03-d2d9ab247be9 Co-authored-by: Amp <amp@ampcode.com> * refactor(launcher): drop "Check for Updates" install-pill entry + orphan IPC The install-pill caret menu's "Check for Updates" entry didn't surface meaningful feedback at the title-bar level — clicking it triggered `updater.runCheck('title-bar-check')`, but the result either flowed to the per-install settings update section (reachable through Install Settings anyway) or to the global UpdateBanner. The redundant menu entry made the install pill busier without giving the user a real new gesture, so it goes away. Removing the menu entry leaves the original Phase-3-title-bar-v2 `comfy-window:check-for-updates` IPC channel + its preload bridge method orphaned (the bridge method had no caller anywhere — §14 moved popup activation server-side via `comfy-titlemenu:item-activated`, so the IPC route through the renderer-side bridge has been dead code since b0d341b). Scrub them in the same commit per the Stage-4b orphan-cleanup pattern. Changes: - `buildTitleMenuItems('install', entry)` in `src/main/index.ts` now returns just `[Install Settings]`. The separator and the `'check-for-updates'` row are gone; with Directories already moved to the File menu in §15, Install Settings is the only install-scoped affordance the install pill carries. - `activateTitleMenuItem`'s install branch drops the `'check-for-updates'` arm. - The `comfy-window:check-for-updates` IPC handler (and its 11-line doc comment) is removed from `src/main/index.ts`. The popup-routed activation path was the only caller; updates kicked off elsewhere (auto-check on startup, manual-check from launcher settings, download-button retry) all flow through `updater.runCheck` directly from inside `lib/updater.ts` and are unaffected. - `comfyTitleBarPreload.ts`: drops the `checkForUpdates(): void` method from the `ComfyTitleBarBridge` interface and the matching `ipcRenderer.send('comfy-window:check-for-updates')` impl. - `TitleBarApp.vue`: drops `checkForUpdates: () => void` from the inlined `Bridge` interface — the renderer never called it (the pre-§14 caret-button menu was removed when the popup BrowserWindow rewrite landed). - `TitleBarApp.test.ts`: drops the `checkForUpdatesCalls` counter from the mock bridge state and the corresponding mock impl. No test asserted on it (none of the 14 cases exercised that path), so this is purely deduplication. - The `comfy-titlemenu:item-activated` payload contract is unchanged — popup → main now only accepts `'new-window' | 'directories' | 'launcher-settings' | 'install-settings'` ids, with unknown ids silently ignored. - The §14 popup comment block is updated to drop the stale "runCheck" mention from the activation-handler list. - Doc update in `docs/unified-window-phase3-notes.md` documents the install-menu shape under §15. Verification: pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run test — all green, 725/725. Amp-Thread-ID: https://ampcode.com/threads/T-019df69a-9879-73e2-bf03-d2d9ab247be9 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): Close Window + Close All Windows in File menu (Phase 3 §16) The unified-window world has multiple host windows open at once, but the only way to close them today is the OS-level X button per window. Tray quit closes the entire app. §16 of the Phase 3 plan calls for a waffle- menu middle step: close one host window, or close every host window, without taking the app down. This commit lands the two well-defined entries from §16: - **Close Window**: closes just the parent host window. Each host window's existing `close` handler runs the full teardown sequence (`stopRunning` + webContents close + `window.destroy()`), so the menu entry just dispatches `entry.window.close()` on the popup's parent and lets the existing teardown path do the work. - **Close All Windows**: new `closeAllHostWindows()` helper that snapshots `Array.from(comfyWindows.values())` first (the per-window `closed` callback deletes from `comfyWindows`, which would otherwise invalidate the live iteration) and dispatches `close()` on each entry. Both install-backed and chooser host windows participate; the tray + app stay alive. Menu is now: ``` New Window Close Window Close All Windows ───────────── Directories Desktop 2 Settings ``` The two TODO §16 entries — Return to Dashboard and Import Snapshot as New Install — stay open for follow-ups. Return to Dashboard wants an "in-place" swap from install-backed to install-less host (clear installationId, re-key under chooser:N, hide comfyView, repaint panel, reset theme + title bar) which is a substantial rewrite, and the doc TBD on install-pill identity click vs. waffle entry remains open. Import Snapshot as New Install is a brand-new flow variant that we'll land alongside the §4b new-install / quick-install unification so we only thread "starting from a snapshot" into one canonical create-install panel. Both are documented in the doc update. Implementation notes: - `buildTitleMenuItems('file', entry)` adds the two new ids and a trailing separator that splits window-management from page-navigation. The install pill menu is unchanged. - `activateTitleMenuItem` routes `'close-window'` / `'close-all-windows'` inside the `entry.kind === 'file'` branch. Both branches let the trailing `hideTitleMenuPopup` run as usual — `hideTitleMenuPopup` is guarded against an already-destroyed popup, which matters because closing the popup's parent auto-destroys the popup before this line runs. - `closeAllHostWindows()` lives next to `quitApp()` in `src/main/index.ts` (both are window-iteration helpers, neither belongs in `lib/`). Verification: pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run test — all green, 725/725. Amp-Thread-ID: https://ampcode.com/threads/T-019df69a-9879-73e2-bf03-d2d9ab247be9 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): confirm Close All Windows when more than one window is open The new "Close All Windows" File menu entry from the previous commit closed every host window in one click without any prompt. With multi- install setups that's a footgun — a misclick or a stale popup state could blow away three windows mid-install. Gate the action on a native confirmation dialog whenever there's more than one host window to close. The dialog mirrors the legacy launcher's quit-warning modal pattern (commit `d22bdf6` — "improve quit warning modal with detailed active items"). The user sees: - The list of open windows by `window.getTitle()` (so they know exactly which surfaces are about to vanish — install names for install-backed windows, "Choose an install" for chooser hosts). - When `ipc.hasActiveOperations()` is set, the same per-category rollup the launcher used: running ComfyUI sessions, in-progress install / update / migration operations, and active model downloads. Pulled from `ipc.getActiveDetails()` which already exposes this exact contract. Changes: - New `confirmAndCloseAllHostWindows(parentWindow)` helper in `src/main/index.ts`. Snapshots live entries (mirrors what `closeAllHostWindows` does so the iteration is stable across the per-window `closed` callbacks), short-circuits to a direct close when ≤ 1 window is open (single-window close is indistinguishable from `Close Window` — prompting there is needless friction), builds the multi-line `detail` string, and awaits `dialog.showMessageBox` parented to the popup's parent window. Buttons are `[Close All, Cancel]` with `defaultId`/`cancelId` both set to Cancel — an inadvertent Enter or Esc dismisses safely. - `activateTitleMenuItem`'s `'close-all-windows'` branch now resolves the popup's parent window and dispatches through the new helper instead of calling `closeAllHostWindows()` directly. Trailing `hideTitleMenuPopup` is unchanged — it runs synchronously and hides the popup before the dialog opens, so they don't compete for focus. - `dialog` is added to the `electron` import line at the top of `src/main/index.ts`. The codebase already uses `dialog` from several `lib/ipc/*Handlers.ts` files (snapshot save/load, app paths) for native dialogs, so this is consistent with existing patterns. - Doc update in `docs/unified-window-phase3-notes.md` documents the confirmation flow under §16. I considered using the renderer-side `useModal` confirm pattern instead, but a native dialog is the right surface here: the action is initiated from main, the popup is mid-teardown when the confirmation needs to appear (so threading a renderer modal would require holding the popup open or routing through one of the host window's panel WebContentsViews), and a native dialog matches OS conventions for "are you sure?" prompts on destructive actions. Verification: pnpm run typecheck && pnpm run lint && pnpm run build && pnpm run test — all green, 725/725. Amp-Thread-ID: https://ampcode.com/threads/T-019df69a-9879-73e2-bf03-d2d9ab247be9 Co-authored-by: Amp <amp@ampcode.com> * feat(launcher): "Return to Dashboard" File menu entry (Phase 3 §16) The unified-window world had no gesture for "I'm done with this install but want to keep the window open and pick something else" — the only ways to leave an install were close-the-whole-window or use the install pill to swap to a different install. §16 of the Phase 3 plan calls for a Return to Dashboard waffle entry that swaps the install body out for the chooser body without losing the window itself. Implementation — swap-via-close, not single-window swap. The new `returnToDashboard(parentEntryId)` helper: 1. Captures the install-backed window's bounds + maximised state. 2. Opens a fresh chooser host via `openChooserHostWindow()`. 3. Applies the captured bounds (or maximises) on the new window so it appears at the same screen position / size — the user perceives an in-place body swap once the new window paints. 4. Dispatches `close()` on the original window. Its existing close handler runs the full teardown (`stopRunning` + webContents close + `window.destroy()`), so the install gets stopped cleanly with no extra plumbing. A true single-window swap would re-key the `comfyWindows` entry from the installationId to a synthetic `chooser:N`, null out `entry.installationId`, tear down the comfy-specific wiring (download manager, theme observer, panelView's loaded URL with the old installationId baked into the query string), and re-route the closure-bound `installationId` reads inside `layoutViews()` and several install-backed event handlers — the install-backed `layoutViews` in particular is bound at construction with `const entry = comfyWindows.get(installationId)`, so re-keying breaks that lookup. That's a substantial rewrite of construction-time bindings; the swap-via-close approach delivers the user-facing affordance with a brief flicker as the cost. The cost is small in practice — the new chooser host opens at the same bounds, so the visual delta is the inner content swapping mid-flight. Menu changes: - `buildTitleMenuItems('file', entry)` conditionally inserts `'return-to-dashboard'` between `'new-window'` and `'close-window'` when `entry.installationId !== null`. Install-less chooser host windows are already on the dashboard body, so the entry would be a no-op there — better to omit it than to grey out a meaningless item. - `activateTitleMenuItem`'s file branch dispatches `returnToDashboard(entry.parentEntryId)` for the new id. The popup is parented to the original window an…
1 parent 2ec1bdf commit e351184

138 files changed

Lines changed: 19155 additions & 5176 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ playwright-report/
99
resources/vc_redist.x64.exe
1010
resources/vc_redist.x64.exe.tmp
1111
bootstrap-python/
12+
13+
# Generated by `pnpm run sandbox:gen` (contains a host-absolute path)
14+
*.wsb

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ Flaky tests are **not acceptable** — they must be fixed immediately when disco
2727

2828
If you encounter a flaky test during a run, investigate and fix it before continuing with other work.
2929

30+
## Comments
31+
32+
- **Be concise.** Don't write multi-paragraph comments to justify a small change. One sentence beats five.
33+
- **Never reference plan steps, phases, tracks, or stage IDs** (e.g. "Phase 3 §17", "Track M-7", "Stage W-4"). Plans change; the comment becomes a lie within hours and meaningless once the feature ships. Describe what the code does, not what plan brought it here.
34+
- **Don't narrate history** ("This used to do X, now it does Y"). The git log carries that.
35+
36+
## Follow instructions
37+
38+
When the user gives explicit direction (e.g., "move away from takeovers", "use the unified primitive"), apply it everywhere — search the whole codebase for remaining offenders. **Never silently defer or skip part of an instruction without asking.** If something looks risky, ask; don't decide unilaterally to leave it for later.
39+
3040
## Post-change review: deduplication
3141

3242
After creating or modifying code, check for duplicated logic before committing:

assets/Comfy_Logo_x1024.png

25.5 KB
Loading

assets/Comfy_Logo_x256.png

965 Bytes
Loading

assets/Comfy_Logo_x32.png

-291 Bytes
Loading

assets/Comfy_Logo_x512.png

3.01 KB
Loading

assets/Comfy_Logo_x64.png

1.35 KB
Loading

docs/post-phase3-ux-polish.md

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

docs/post-unification-code-review.md

Lines changed: 687 additions & 0 deletions
Large diffs are not rendered by default.

docs/unified-window-phase3-notes.md

Lines changed: 1795 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)