Commit e351184
* 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
- assets
- docs
- lib
- locales
- resources
- scripts
- src
- main
- lib
- ipc
- sessionActions
- sources
- standalone
- types
- preload
- renderer
- src
- assets
- comfyTitleBar
- comfyTitleMenu
- components
- composables
- lib
- panel
- stores
- types
- views
- types
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
30 | 40 | | |
31 | 41 | | |
32 | 42 | | |
| |||
Loading
Loading
Loading
Loading
Loading
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
0 commit comments