Skip to content

Adopt extra model paths install scoped - #1522

Open
morgroth123-commits wants to merge 185 commits into
Comfy-Org:adopt-extra-model-paths-install-scopedfrom
morgroth123-commits:adopt-extra-model-paths-install-scoped
Open

morgroth123-commits wants to merge 185 commits into
Comfy-Org:adopt-extra-model-paths-install-scopedfrom
morgroth123-commits:adopt-extra-model-paths-install-scoped

Conversation

@morgroth123-commits

Copy link
Copy Markdown

No description provided.

Kosinkadink and others added 30 commits June 13, 2026 00:14
…omfy-Org#1097)

* feat(updater): default startup install + installer UI on Windows

Flip installUpdatesOnStartup and showInstallerUI from opt-in (default off)
to default-on on Windows, mirroring the autoInstallUpdates opt-out pattern
(!== false). Windows now applies staged updates at startup (disabling the
crash-prone electron-updater install-on-quit) and shows the NSIS progress
window during the install. Set either setting to false in settings.json to
opt back out. No-op on macOS/Linux.

Amp-Thread-ID: https://ampcode.com/threads/T-019eb80f-cba2-715c-8d1c-b5d98540cbec
Co-authored-by: Amp <amp@ampcode.com>

* docs(updater): fix comments stale after default-on flip

Amp-Thread-ID: https://ampcode.com/threads/T-019eb80f-cba2-715c-8d1c-b5d98540cbec
Co-authored-by: Amp <amp@ampcode.com>

* refactor(updater): share Windows opt-out gate; drop Option B/C wording

Addresses CodeRabbit review on Comfy-Org#1097:
- Extract isWindowsOptOutGate() so the startup-install and installer-UI gates
  share one win32 + setting !== false check (no drift).
- Replace plan-reference 'Option B/C' wording in tests/comments with
  behavior-based descriptions.

Amp-Thread-ID: https://ampcode.com/threads/T-019eb80f-cba2-715c-8d1c-b5d98540cbec
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(titlebar): match ComfyUI theme inside instances

- Drive the title-bar header + OS window-controls overlay from ComfyUI's
  reported bg while inside an instance, instead of locking to brand purple;
  symbol color is luminance-derived so the window controls stay legible on
  any theme
- Un-stub `isLight` so the existing `.is-light` chrome activates, and extend
  it to the resting/hover/open states it never covered (install pill,
  downloads tray, icon buttons) so nothing washes out or lifts to invisible
  yellow on a light bar
- Scoped to attached instances only — the dashboard/chooser keeps its purple

* feat(chooser): show install version alongside the update pill

- Render the current version pill independently so an available update no
  longer hides it (was a v-if/v-else-if chain)
- Right-align the update/migrate action so it reads as the affordance, apart
  from the source + version metadata; version becomes the secondary shrink
  target after source
- Carry the target version in the update label ("Update v0.25.0") via the new
  `chooser.updatePillVersion` key, sourced from `statusTag.version`

* refactor: dedupe luminance + action-dispatch logic (CodeRabbit)

- Extract the renderer's canvas-normalize + lightness test into a single
  `isColorLight` helper (`lib/colorScheme.ts`), reused by `useTitleBarIdentity`
  and `TitlePopupApp` instead of two copies; it reuses the shared
  `perceivedLuminance` math
- Collapse the guarded `emit('trigger-action', …)` repeated across the update +
  migrate pills into one `triggerInstallAction` method in `ChooserInstallTile`
… native prompts with in-app dialogs (Comfy-Org#942)

* fix(adopt): fail clearly when Legacy Desktop adoption can't source ComfyUI

When in-place adoption of a Legacy Desktop install could not obtain the
ComfyUI source (no pre-swap staged copy and the git clone failed), the
source-missing dialog offered a 'Switch to managed env' button. Choosing
it threw 'source-missing-switch-to-managed', which the migrate dispatcher
mapped to { ok: true, navigate: 'new-install' }. But navigate:'new-install'
is unhandled in the renderer, so the operation reported success while
doing nothing — leaving the user back where they started (detail view on
the dashboard-tile path, or the chooser on the first-time-migration path).

Replace the fake-success escape with a clear failure: drop the
'Switch to managed env' option, keep Retry, and on any non-retry choice
throw a source-missing error that the dispatcher surfaces as
{ ok: false, message } with a user-friendly message suggesting a new
install. This fixes both the dashboard-tile and first-use migration
surfaces at once, since both flow through the same backend + ProgressModal
error banner.

Also fixes a latent bug where an unexpected prompt choice would silently
break the source loop, leaving sourceMode null and adoption continuing
without a source.

Closes Comfy-Org#917

Amp-Thread-ID: https://ampcode.com/threads/T-019e96a8-4f9f-709a-920e-b7659a9140a7
Co-authored-by: Amp <amp@ampcode.com>

* test(telemetry): drop stale source-missing-switch-to-managed assertion

The switch-to-managed escape hatch was removed when adoption was changed
to fail clearly on a missing ComfyUI source, so the synthetic
'source-missing-switch-to-managed' error string is no longer produced
anywhere. Drop the dead bucketError assertion and refresh the comment to
describe the real failure (no staged copy and no working clone).

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* fix(adopt): replace native message-box prompts with in-app dialogs

The Legacy Desktop adoption flow asked the user mid-operation questions
(tcc folder access, broken venv, missing ComfyUI source, confirm-adopt)
via Electron's native dialog.showMessageBox — the only native message
box left in the app. It popped up as an ugly OS modal on top of the
in-app ProgressModal takeover.

Route those prompts through the existing in-app dialog system instead.
showAdoptPrompt now does a correlated IPC round-trip to the originating
renderer: main sends an 'adopt-prompt' request (pre-translated labels +
button index mapping kept main-side), the renderer surfaces it with
useDialogs() alert/confirm above ProgressModal, ACKs delivery, and
replies with the chosen button index.

Robustness: prompts are correlated by promptId + webContentsId so a
wrong/duplicate response is ignored; a 5s ACK timeout, window-destroyed,
and operation-abort all reject and fall back to the prompt's cancel
choice so the backend never blocks. The renderer bridge serializes
prompts and falls back to cancel on any dialog error.

Native OS file pickers (showOpenDialog/showSaveDialog) are intentionally
left as-is — those are appropriate OS-native UI, not message boxes.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* fix(adopt): harden in-app prompt bridge after review

Code-review follow-ups to the adopt-prompt IPC bridge:

- ACK delivery from the raw onAdoptPrompt callback instead of inside the
  serialized handler, so a second prompt is acknowledged immediately even
  while the first dialog is open (matches the 'ACKs immediately' design and
  avoids a spurious main-side ACK timeout).
- Drop the severity-based button tone: the only error-type prompt is
  source-missing whose primary action is Retry, which is not destructive —
  a red Retry button was misleading. Prompt buttons are now always primary.
- Wrap the renderer response send so a failed send can't poison the prompt
  chain and stall later prompts.
- Wrap main-side sender.send so a destroyed-sender throw settles/cleans up
  the pending entry, listeners, and timer immediately instead of leaking
  until the ACK timeout.
- Sanitize the returned button index (NaN / out-of-range / non-integer from
  a buggy renderer) to fall back to cancel rather than throwing a TypeError.

Adds tests for immediate ACK under serialization, chain resilience on send
failure, wrong-sender response rejection, and malformed button index.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* i18n(zh): sync adopt source-missing keys after main merge

Add zh translations for adoptPromptDetail and adoptSourceMissingFailed,
drop the stale adoptPromptSwitchToManaged orphan, and update the
source-missing message to match the en wording (no switch-to-managed).

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* fix(adopt): route picker migrate through panel so in-app prompts work

The title-popup picker dispatched migrate-to-standalone as an inline
background op, which runs in main with a stub sender that has no
EventEmitter methods. Adopt prompts (venv-broken, source-missing) are
bridged only by the panel, so the prompt never reached a renderer: no
takeover appeared, the ACK timed out, and cleanup crashed main with
'sender.removeListener is not a function'.

- Route migrate-to-standalone to the panel ProgressModal (same-host) so
  runAction is invoked from the panel renderer (real WebContents + bridge).
- Let the reconstructed picker->panel apiCall self-stop a running install.
- Harden requestAdoptPromptButton: reject incapable/destroyed senders
  before arming the timer, and make listener cleanup never throw.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* fix(progress): restore vertical spacing on finished/error takeover

PR Comfy-Org#1019 (fb6f46e) removed the column gap from .brand-progress__stack
during the log-driven progress rewrite, but the finished-state rows
(success actions, error message, error CTAs) still carry margin-top:-4px
tuned for that gap. Without it the banner, error detail, and Back/Reboot
buttons collapsed flush together on the operation-failed screen.

Restore the stack's column gap; in-flight is unaffected (single flex
child) and the rows' negative margins fine-tune on top of it.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

* fix(adopt): say 'source code' in user-facing copy; address CodeRabbit nits

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9a5e-743c-acad-5e419001522f
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
… login stitch) (Comfy-Org#1114)

* fix(telemetry): never identify the anonymous installation_id (restore login stitch)

PostHog marks any id passed to identify() as an identified person and
refuses to merge one identified id into another. Calling identify() with
the anonymous installation_id at boot (and on logout, and on every
anonymous person-property write) burned it, so the login-time
alias(installation_id -> user_id) was a silent no-op: 0 of 13,528 device
ids ever stitched onto an account.

Invariant: client.identify() is only ever called with the authenticated
Firebase user_id, in bindUserId. All anonymous person-property writes
($set / $set_once) now route through a capture-$set on a dedicated
comfy.desktop.person.set event, which updates the person without emitting
$identify. Fixes the boot bind, unbindUserId (logout), and both
registerPersonProperties paths.

* docs(telemetry): tighten identity-model comments; fix stale fn reference

Trim the verbose docstrings added with the stitch fix to the load-bearing
points, and correct a header reference to a non-existent
setAnonymousDistinctId() (the function is identify()).
…tored IP) (Comfy-Org#1115)

posthog-node runs on the user's machine, so the request IP is the real
user IP and PostHog can derive location. Opt in to country-level cohorts:
disableGeoip:false and stop forcing $ip:''. Precision is bounded to country
by a PostHog ingestion transformation that drops the raw $ip and all
sub-country geo (city/region/coords/postal/timezone), keeping only
$geoip_country_code/name. Privacy policy (legalDocs) updated to disclose
country-level location with IP discarded on receipt.
…#1111)

* feat(chooser): redesign install card for density + recency

- Restructure tile into tiers (name → source·version meta line → recency + action); only the action stays a pill so metadata stops fighting for the row
- Surface last-booted recency on every card, incl. "Not launched yet"
- Drop inline version from the Update pill (bare "Update"); version reads brighter at rest in the meta line
- Add tooltips: full name (only when clipped), meta line (git URL+commit when clipped), exact target version on Update, "Migrate to Standalone" on Migrate
- Extract useTruncation composable for the clipped-text tooltip gate

* refactor(chooser): consolidate update/migrate pill into one renderer

- Replace the duplicated update/migrate Tooltip blocks with a single `actionPill` computed (icon/label/tooltip/class/action) and one template block, mirroring the existing `statusPill` pattern
- Disabled/keyboard/aria wiring now lives in one place

* chore(chooser): drop dead updatePillVersion key and revert test formatting noise

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdba-1ac2-72a3-92da-02b59f5edf01
Co-authored-by: Amp <amp@ampcode.com>

* fix(picker): re-show auto-action confirm when reopening the same install

The dashboard pill (Update/Migrate) opens the instance picker with an autoAction that fires a confirm modal. Reopening the SAME install's pill never re-showed the modal; a different install worked.

Root cause: main sends set-config (new snapshot) then will-show. The picker's onWillShow ran dismissPickerModals(). For the same install, sections are already fresh so the autoAction confirm opened synchronously during set-config, then the later will-show dismissed it. A different install reloads sections async, so its confirm opened after will-show and survived.

Fix: dismiss stale picker modals in onConfig (before the new snapshot can auto-fire), and skip the instance-picker in onWillShow so the fresh confirm is not killed. Non-picker kinds keep will-show cleanup (they can hit the fast path that skips set-config).
Amp-Thread-ID: https://ampcode.com/threads/T-019ecdba-1ac2-72a3-92da-02b59f5edf01
Co-authored-by: Amp <amp@ampcode.com>

* docs(picker): condense modal-cleanup comments to intent-only

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdba-1ac2-72a3-92da-02b59f5edf01
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
…fy-Org#1123)

* refactor(title-popup): hoist popup kind tags into a shared constant

Add `POPUP_KIND` and `PopupTheme` to types/ipc.ts so the popup config
discriminant has one source across main, preload, and the renderer
instead of literal strings re-typed in each.

* feat(title-popup): add the centred downloads-full popup kind

- Open "View All Downloads" as a large centred backdrop popup on the
  reused title-bar view, fed by the existing tray download broadcast
- Generalise global-settings sizing into `computeCenteredCardBounds`
  + `kindIsCentered`, shared by the open path and the resize refit

* fix(title-popup): stop the preload validators dropping new popup kinds

The config and will-show validators gated on a hardcoded kind allowlist,
silently rejecting any kind they didn't enumerate. Key them off
`POPUP_KIND` so the renderer actually receives `downloads-full`.

* feat(downloads): render the full view inside the title-bar popup

- Move the DownloadsModal design into `DownloadsFullView`, rewired from
  the panel store / window.api to the popup's prop + `__comfyTitlePopup`
  bridge, and replace BaseModal chrome with the popup card
- Drop the per-row entrance animation: its lingering transform layer
  blocked pointer hit-testing in the transparent popup view
- Wire the `downloads-full` branch + card chrome into TitlePopupApp

* refactor(panel): drop the downloads-v2 overlay panel path

The full Downloads view no longer mounts in the panel renderer, so remove
the `downloads-v2` body mode and its DownloadsModal mount across the
panel, registry, and host-window layout. `feedback` stays the lone
overlay mode.

* test(downloads): cover the full-popup hand-off; drop downloads-v2 fixtures

- Add e2e for "View All Downloads" opening the full popup with seeded
  rows and for its empty state
- Point the panel-key test fixtures at `feedback` and remove the
  dead DownloadsModal mock now that the component is gone

* fix(downloads): restore hover/cursor in the full popup

Chromium skips mousemove delivery to the transparent popup WebContentsView
when nothing subscribes to pointer-move, freezing :hover and the cursor
until a click forces a hit-test. Add a no-op @mousemove subscriber, and
move the close button in-flow into the header (dropping the panel's
position:relative stacking context) to match the working global-settings
view.

* test(downloads): de-flake the full-popup e2e hand-off

Wait for the tray footer link and stable bounds before clicking "View
All", so the kind-switch isn't raced on the empty (fast-rendering) path.

* refactor(downloads): address review nits in the full popup

- Import the download entry / state / action types from the preload
  bridge instead of re-declaring them locally
- Memoise the status badge into a lookup so it isn't recomputed three
  times per row
- Extract the tray -> "View All" e2e flow into a shared helper

* refactor(title-popup): key remaining kind checks off POPUP_KIND, dedup download types

Amp-Thread-ID: https://ampcode.com/threads/T-019ececa-4fd3-76fe-ac8b-1f37facb91a6
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
…rd (Comfy-Org#1068) (Comfy-Org#1116)

* fix(standalone): show nightly version for Latest on GitHub install card

The install-wizard variant card advertised the ComfyUI version baked into
the standalone bundle for the 'Latest on GitHub' channel. That bundle lags
master by many releases (e.g. v0.20.1 while master is ~v0.24.1), so the card
read '4 minor versions behind' even though picking 'latest' fast-forwards the
install to master HEAD post-install.

Thread the latest stable tag into the 'latest' channel's release data and
advertise it as a nightly (e.g. 'ComfyUI 0.24.1 (nightly)'), falling back to
the bundled version only when the tag can't be resolved (offline). Display
only; the Manage/Update view and actual install behavior were already correct.

Fixes Comfy-Org#1068

Amp-Thread-ID: https://ampcode.com/threads/T-019ece60-fd60-72f9-997f-22e7562d07a3
Co-authored-by: Amp <amp@ampcode.com>

* refactor(standalone): dedupe channel data payload, fix stale comment

Amp-Thread-ID: https://ampcode.com/threads/T-019ece60-fd60-72f9-997f-22e7562d07a3
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
…-Org#1099) (Comfy-Org#1107)

* fix(terminal): activate the right env for git/portable Console (Comfy-Org#1099)

The interactive Console resolved its venv/uv via getActiveVenvDir/
getActiveUvPath, which only branch on `adopted` and otherwise assume the
standalone layout: `<installPath>/ComfyUI/.venv` + `<installPath>/
standalone-env/uv.exe`. For a git-manual install (venv lives at
`venvPath`, no standalone-env) or a portable build (embedded
`python_embeded`, no venv) this activated the wrong/nonexistent venv and
aliased `pip` to a nonexistent `standalone-env\uv.exe`.

Make terminal env resolution source-aware:
- Add `TerminalEnv` + optional `SourcePlugin.getTerminalEnv()`.
- git: activate `venvPath` (its own pip); no standalone-env reference.
- portable: prepend embedded `python_embeded`(+Scripts) to PATH and route
  pip through `python_embeded\python.exe -s -m pip`.
- terminal.ts builds init commands from a `TerminalEnv` (venv-activate vs
  PATH-prepend, optional pip routing) and uses an injected resolver wired
  in the IPC layer, keeping the source-plugin graph (and Electron `app`)
  out of the eagerly imported terminal module.
- standalone/adopted keep the existing default behavior.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca72-a4d7-7658-814a-a2c95bab28fd
Co-authored-by: Amp <amp@ampcode.com>

* fix(terminal): also fix legacy desktop env + harden venv/root detection

Code-review follow-ups:
- desktop (v1) source has the same bug class: it has hasConsole=true, keeps
  its venv at `<installPath>/.venv`, and has no standalone-env. Add
  `getTerminalEnv()` so the Console activates that venv instead of the
  standalone layout.
- git: only activate `venvPath` when `resolveVenvPython()` confirms the
  interpreter still exists; otherwise open a plain shell.
- portable: guard `findPortableRoot` against a missing/unreadable installPath
  so terminal spawn can't throw on a deleted drive.

* feat(terminal): open the Console in the ComfyUI code folder (Comfy-Org#1070)

Start the per-install Console on the ComfyUI repo folder (where main.py
lives) instead of the encompassing install folder, consistently across
install types:
- standalone/adopted: `<installPath>/ComfyUI`
- portable: `<root>/ComfyUI`
- git: the dir containing the resolved `main.py`

Adds an optional `cwd` to `TerminalEnv`, set per source; terminal.ts uses
it and falls back to `installPath` when unset or missing.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca72-a4d7-7658-814a-a2c95bab28fd
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
…y-Org#1120)

Opening a portable (or any local) install's detail section from the
central pill could land on the Terminal/Console tab instead of the
default Update tab.

Cause: the visible-tabs list gates every tab on backend `sections` except
Console, which shows for any local install regardless of sections. While
sections are empty/stale (initial load or retarget), `tabs` collapses to
just `['console']`, and the tab-fallback watcher latched `activeTab` onto
Console; once the real sections arrived, Console was still valid so it
never reverted.

Gate the fallback on `sectionsFresh` so it only reconciles against a
payload that belongs to the current install, never a transient list.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca72-a4d7-7658-814a-a2c95bab28fd

Co-authored-by: Amp <amp@ampcode.com>
…Comfy-Org#1130)

* fix(titlebar): sync menu zoom reset state

* test(titlebar): align dashboard zoom menu e2e

Dashboard zoom is not a supported user path; the old dropdown e2e forced the chooser host's dummy comfyView to zoom and expected Reset Zoom to appear. Keep the forced internal state coverage, but assert the dashboard menu continues to hide Reset Zoom.

* refactor(titlebar): share zoom percent conversion

* test(titlebar): always reset dropdown zoom state

* refactor(titlebar): tighten reset-zoom gate + drop redundant e2e case

* chore: trim comments

---------

Co-authored-by: Nynxz <contact@nynxz.com>
…1134)

- Add `displayLaunchUrl` to cloudUrl.ts — returns the URL host, parsed not hardcoded
- Use it for the `connecting`/`connectingTime`/`cannotConnect` strings so UTM + desktop_device_id no longer leak into the loader
- Keep the full tagged URL for `waitForUrl` and the session record
- Cover host strip, port retention, and non-URL passthrough
Comfy-Org#1131)

* feat(templates): pick a starter template at install, auto-open on first launch

- Add bundledTemplate card field to the standalone source (wizard Advanced)
- Resolve a template's required models dynamically from its workflow JSON
  (site-packages first, GitHub-raw fallback; scans top/node/subgraph models[])
- Persist bundledTemplateId + one-shot pendingTemplateOpen on the install record
- Append ?template=<id>&source=default to the comfy URL on first launch
  (attach.ts), consumed once via clearPendingTemplateOpen; zero frontend changes
- Consent checkbox + downloadTemplateModels flag; starter-template i18n (en/zh)

* feat(templates): download template models in the background during install

- New templateDownloadTask: fire-and-forget at install-begin so bytes overlap
  env setup; own AbortController; per-file failures non-fatal
- New templateDownloadCore (pure, unit-tested): runPool (bounded concurrency 3),
  summarizeTemplateState (cumulative math), formatTemplateSubStatus
- Hot-path chunk callback is O(1) counters only; reader formats off-band
- Export getModelsBaseDir so install-time + in-window downloads agree on the dir
- Kick off from registerInstallationHandlers; abort on install cancel/window close
- Remove the old blocking postInstall download path

* feat(templates): show model download in the launch progress, with rich status + logs

- Splice a synthetic template-models phase into the launch stepper after the
  security scan; a 500 ms reader drives its substatus from shared state
- Rich substatus: speed, ETA, current file (N of M), cumulative X/Y GB
- Stream per-file logs into View logs; seed the launch op's terminal from the
  durable log ring buffer (new logs-snapshot IPC) so install-leg lines survive
- Fix the progress-bar leap: phase weight 0.05 + report indeterminate when the
  download was already complete, real percent (capped 99) while it runs

* docs(templates): starter-templates engineering handoff + Phase 2 plan

- What's done (deeplink + background download), decision matrix, files touched
- Phase 2 decided scope + edge-case dispositions + living build checklist
- Notes the live constants are the Phase-1 test set, not the Phase-2 picks

* feat(templates): retry, MAX_PATH guard, and surfaced download errors

- Per-file 2× auto-retry around download() via a pure withRetry helper;
  a user cancel is fatal (no retry), and .dl-meta resume means a retry
  continues the partial rather than restarting it.
- Windows MAX_PATH guard (truncateForMaxPath) before each write; a name
  too long to fit becomes a per-file skip, not a task failure.
- Surface download failures in the substatus: red + bold + X icon, wired
  end-to-end (ProgressData.error → phaseErrors → ProgressStepVM.isError →
  BrandProgressView .is-error).
- In-task disk pre-check now a surfaced hard error with a disk-specific
  message (templateModelsNoSpace) instead of a silent skip.
- Unit tests for withRetry, truncateForMaxPath, and the disk-error branch.

* feat(templates): cross-OS VRAM detection + warn-decision helper

- detectGPU() now returns vramBytes on any OS: nvidia-smi memory.total
  (authoritative) → os.totalmem() for Apple Silicon → systeminformation
  si.graphics() fallback for AMD/Intel/discrete. Reuses the graphics probe
  the app already runs for telemetry rather than hand-rolling native probes.
  Undefined only when no real number is readable, so the picker never
  false-warns. Exposed via the existing detect-gpu IPC + GPUInfo.vramBytes.
- shouldWarnVram(detected, recommended) pure helper in bundledTemplates:
  warns only when a real detected figure is below the template's
  recommendation; silent on undefined or no recommendation.
- BundledTemplate gains optional recommendedVramBytes (populated in group A).
- Unit tests for shouldWarnVram.

* feat(templates): hard-block install when disk can't fit template models

- checkTemplateDiskOrBlock (installHelpers): free disk vs the selected
  template's model size × 1.1 headroom, with NO continue-anyway — a block
  alert instead, so the user frees space or picks a lighter template rather
  than ending up with a half-downloaded model set + a confusing error row.
- Wired into InstallWizardModal.handleSave, after the existing install-bundle
  disk warn; only fires when a template-with-models is chosen and consented.
- Pure templateDiskRequiredBytes extracted for the threshold math + tested.
- en/zh copy: diskSpace.templateBlock{Title,Message}.

* feat(templates): skip model download → hand off to the downloads tray

When the trailing template-models phase is still running, surface a centered
"Skip model download" button. Clicking hands the resume-capable background
task off to the title-bar downloads tray so the user can enter ComfyUI
immediately — no restart.

- comfyDownloadManager: a separate mirror registry (setTemplateTrayMirror /
  clearTemplateTrayMirror) merged into getDownloadsTrayState's active/recent.
  Kept out of pendingDownloads so the real-download DownloadItem lifecycle
  (cancel/retry/temp-rename) is untouched; the renderer needs no changes.
- templateDownloadTask: mirrorTemplateDownloadToTray polls the shared state
  every 500ms and reflects it via the pure templateStateToTrayEntries mapper
  until terminal; stopTemplateTrayMirror tears it down on window close.
- skip-template-download IPC + preload + ProgressModal button (gated on
  active=template-models, not-errored, <100%).
- Fix: guard op.phaseErrors access with optional chaining (partial op mocks).
- Tests for templateStateToTrayEntries.

* feat(templates): real per-modality showcase picks + media-package fix

- bundledTemplates.ts: one verified showcase per modality, metadata copied
  verbatim from the live workflow_templates index (title/description/size/vram):
  Image flux_schnell, Video text_to_video_wan, Audio audio_stable_audio_example,
  3D 3d_hunyuan3d_image_to_model. Each confirmed to embed a downloadable
  models[]. Dropped image_z_image_turbo (no embedded models → nothing
  pre-downloads) and the doc's stable-audio-3/triposplat ids (absent from the
  current index). Added modality + thumbnailUrl (/templates/<id>-1.webp) +
  recommendedVramBytes, passed through getFieldOptions data.
- Fix: the templates package was split into per-modality media sub-packages
  (…_media_image/_video/_other/…). loadTemplateJson only checked the legacy
  single package, so local resolution always missed and fell back to the slow,
  hang-prone remote path. Now probes all media packages first.

* feat(templates): dedicated starter-template picker step

A full-screen template picker after Configure, before install — a modality
grid of showcase cards (thumbnail + size + modality chip) plus a "blank
canvas" option, with the image template pre-selected.

- TemplatePickerStep.vue renders the bundledTemplate FieldOptions the wizard
  already loads, so selecting drives selections.bundledTemplate. Warns (never
  blocks) when detected VRAM < the template's recommended VRAM; silent when
  VRAM is unknown. Hosts the model-download consent + "Don't show again".
- InstallWizardModal: step machine (configure → template); CTAs
  "Skip & Install" / "Install"; Back returns to Configure. The bundledTemplate
  Advanced card is hidden while the picker step is active (shown as the
  fallback when the picker is gated off).
- Gating: skipTemplatePickerStep setting; "Don't show again" only when
  getInstallationsSummary().localCount > 0; opted-out returning users
  auto-skip the step. Express install installs with no template by
  construction (picks the recommended "None" option).
- Dashboard "Add New Instance" inherits the step (same modal).
- en/zh copy + nested standalone.modality.* labels.

* feat(templates): warn on app quit while template models are downloading

before-quit now checks hasActiveTemplateDownloads() and, when a template-model
download is still in flight, shows a synchronous confirm ("Quit Anyway" /
"Keep Downloading", default = keep) before tearing down. Quitting drops the
download (no resume), so the user gets one chance to back out. Only gates a
real user quit, not an in-progress relaunch/update quit.

* docs(templates): mark Phase 2 groups A–F done in the handoff checklist

All 26 checklist items across groups A–F landed; full suite (2138 tests),
all four typechecks, and eslint are green. Only live pnpm dev runtime
verification (G) remains — interactive, left for the engineer.

* feat(templates): redesign starter-template picker + bundle thumbnails

- Redesign the picker as a compact selectable row list (reuses
  brand-variant-list): thumbnail + title + meta (modality · size · VRAM),
  description expands inside the selected row, full keyboard nav. Drop the
  card grid, the "None" tile, and the consent toggle ("Skip & Install" is the
  blank-canvas / no-download path).
- Bundle the 4 modality thumbnails in-repo (public/images/templates/*.webp,
  downscaled) and point bundledTemplates at them; remote raw.githubusercontent
  previews were blocked by the renderer CSP (img-src 'self' data:).
- Live disk-block in the picker: selecting a too-large template shows a red
  error and disables Install. Extract the shared pure decision
  isTemplateDiskBlocked used by the picker, the wizard, and the save-time gate.
- Drop dead consent-sync in handleSave (buildInstallation derives
  downloadTemplateModels from the template id) and the unused defineExpose.

* feat(templates): show model download as the last launch step + gate reveal

- Move the template-models phase to the end of the launch stepper. Its 500ms
  reader stays silent until the server is up (serverUp flag) so the main
  tracker drives the real phases honestly; only then does the download become
  the active last row (no faked-done / 99% jump).
- Gate the ComfyUI reveal: at port-ready, if the download is still running,
  hold and show "Skip & open ComfyUI" instead of flashing past. Resolve on
  done / skip / cancel; on error (after 2x retry) show a failure line + 3-2-1
  countdown, then open. awaitTemplateDownloadSettled is the single settle
  primitive; requestSkipTemplateDownload releases the gate + mirrors to tray.
- Surface a clearer "requires login or license" line for a gated repo (401/403)
  via describeDownloadFailure; counts toward retry, non-fatal.

* docs(templates): sync handoff to shipped picker + launch gate

- Picker UI (compact rows, bundled thumbnails, no None/consent, live disk-block).
- Stepper: download-last + serverUp-gated reader + waitForTemplateDownloadGate
  (hold reveal until settle/skip; error countdown). Drop the reverted
  reachedLastRealPhase notes.
- 13-row human-reviewer test matrix.

* test(templates): cover launch gate, picker states, and skip path

- gate: awaitTemplateDownloadSettled resolves done/error/cancelled/
  skipped/aborted/absent + clears the stale skip flag on settle
- picker: TemplatePickerStep disk-block, VRAM warn, recommended tag,
  select/keyboard nav, thumbnail-fail glyph
- build: "Skip & Install" (none) builds no model download; a real
  pick sets bundledTemplateId + pendingTemplateOpen + downloadTemplateModels
- docs: rewrite handoff as a durable reference (mental model, decisions,
  edge cases, coverage map, 13-row review matrix); verified file links

* refactor(templates): address PR review — error visibility, cleanup, tests

- surface the silent disk-probe failure in the task log instead of
  swallowing it
- launch gate: emit the failure immediately when the download already
  errored before server-up (closes the ≤500ms blind window)
- extract pure buildTemplateDeeplink; attach.ts uses it + logs a failed
  clearPendingTemplateOpen instead of dropping the error
- prune stale template URLs from the tray-mirror createdAt map (no leak)
- reset the wizard to the Configure step when the source changes
- strip history-narrating comments; convert kept intent to JSDoc
- tests: deeplink round-trip, clearPendingTemplateOpen (incl. legacy
  no-template-fields record), picker Enter/Space native activation

* docs(templates): update handoff

* feat(templates): gate starter-template picker on PostHog experiment + add funnel telemetry

Wraps the new install-wizard template step in the
`desktop-starter-templates-picker` A/B flag (control: legacy single-screen
Configure, treatment: picker step), reuses the cache-first
`telemetryGetExperimentFlag` plumbing used by FirstUseTakeover, and pins the
variant as a person property so any downstream activation event can be sliced
by arm.

Adds `comfy.desktop.template.picker_shown / .selected / .install_confirmed /
.skipped` so the picker funnel is measurable end-to-end against the
existing `comfy.desktop.template.download.skipped` event.

Defaults to control on flag miss / network failure so the legacy flow ships
when the experiment can't be resolved.

* refactor(templates): address CodeRabbit review

Security:
- reject non-HTTPS model URLs and sanitize template-provided model
  name/directory against path traversal (new templateModels.test.ts)

Correctness:
- scope the tray mirror per install so concurrent installs don't clobber
  each other's rows
- abort the template-models launch reader on success/failure so its 500ms
  timer can't leak after a skip
- validate the chosen template id against the known set before persisting
- generation-guard the wizard's open() async setters against reopen races
- ProgressModal: gate skip on op.finished, reset per op, and mark consumed
  only after the skip IPC succeeds

Docs/tests:
- correct the stale GPUInfo.vramBytes JSDoc (AMD/Intel now probed)
- use NO_TEMPLATE_VALUE, fix the none-equivalent picker test, drop a
  double cast, add isError-render and logsSnapshot-seeding coverage

* fix(i18n): correct migrateBannerAction zh translation

Was a corrupted string ('迁移 否w'); now reads '立即迁移'.

* fix(templates): clear finished template rows from the downloads tray

Mirrored template-model rows live in templateTrayMirrorByInstall, not
recentDownloads, so dismissRecentDownload / clearFinishedDownloads left
finished ones lingering in the tray forever.

- dismissRecentDownload also drops a matching mirror row (pruning empty buckets)
- clearFinishedDownloads purges terminal mirror rows, keeping in-flight ones
- test: both cleanup paths drop finished mirror rows; a downloading row survives

* feat(templates): surface running template downloads in All-Downloads modal + auto-open tray

- getAllDownloads() now includes template-mirror rows so the All-Downloads
  modal lists them, matching the tray popup
- setTemplateTrayMirror broadcasts each row via model-download-progress so the
  modal tracks live progress like any real download
- auto-open the downloads tray ~2.5s after ComfyUI reveal when a template
  download is still running; wired into all three onLaunch reveal paths
- tests: getAllDownloads includes mirror rows + per-row broadcast fires

* feat(templates): skip picker when disk too small; wiggle blocked Install

- skip the template step entirely when free disk can't fit even the
  cheapest model-bearing template (new minTemplateModelBytes feeds
  diskTooSmallForAnyTemplate → shouldShowPickerStep); fails open while
  disk space is still loading
- Install button stays clickable but reads disabled when blocked; clicking
  it shakes the disk-error alert instead of installing, mirroring the
  first-use consent nudge (nudgeDiskError exposed by the picker)
- tests: minTemplateModelBytes + the skip-gate threshold; nudgeDiskError
  shakes when blocked / no-ops when not
- docs: both behaviors in gating, decisions, edge cases, coverage, matrix

* docs(templates): disambiguate 'Picker UI' → 'Template Picker UI'

- fix the cross-ref label + stale #picker-ui anchor to #template-picker-ui
- rename the test-coverage row and matrix scenario labels to match

* feat(templates): refresh bundled starter set with animated previews

- Swap to Z-Image-Turbo, Wan 2.1, Stable Audio 3, TripoSplat
- Add previewKind flag (animated vs static) + paired -still.webp frames
- Update bundled thumbnails; drop retired Flux/Hunyuan/old-audio assets

* fix(experiments): assign absent flags on fresh boot

- Back-fill in-memory cache from a settled fetch for keys absent at boot,
  so a fresh-boot session sees its real variant instead of control
- Never flip a key the process already committed to this boot

* feat(templates): bigger row media, alerts above the card

- Enlarge row preview to a 96x60 landscape frame; play animated webp
  with a still fallback under prefers-reduced-motion
- Move disk/VRAM alerts above the card as filled chips so they're always
  visible (never clipped by the list scroll); host owns the blocked shake
- Taller card (min/max-height) with the list scrolling inside; sleeker footer buttons
- Tighten disk/VRAM alert copy to short, crisp essentials

* fix(experiments): await the boot fetch on flag read (match cloudCapacity)

- add getFlagAsync that awaits the in-flight init fetch before reading, so a
  renderer query landing before it settles gets the real variant, not control
- telemetry:getExperimentFlag handler now awaits getFlagAsync
- cache the init promise (initPromise) like cloudCapacity; drop initStarted
- tests: mid-fetch query resolves to the fetched variant; cached flag resolves
  synchronously

* docs(templates): remove stale starter-templates handoff

* feat(install): ship starter-template picker unconditionally (drop A/B)

Builds on Comfy-Org#1084 (@MaanilVerma) — removes the PostHog experiment gating so
the starter-template picker is shown to everyone on the standalone install
path, instead of only the treatment arm.

The picker is a clear UX win (pre-downloaded models + ready-to-run canvas
vs a blank canvas with missing-model errors), so the team chose a full
rollout over measuring the lift. Removing the flag also fixes a first-run
gap where the variant defaulted to control because the experiment flag is
fetched only after telemetry consent (granted on the first-use screen,
moments before the install wizard opens) — so first-run users, the picker's
target population, mostly never saw it.

Changes (InstallWizardModal.vue only):
- Drop the experiment key/variant, the on-open flag read, exposure event,
  and the experiment_* person-property pin.
- shouldShowPickerStep now gates on: standalone source + templateOptions
  present + disk fits + the existing skipTemplatePickerStep user opt-out
  (pickerEnabled). No variant condition.
- Remove the now-unneeded flag-ready await race-guard in
  handleConfigureContinue.
- Keep the picker funnel telemetry (picker_shown / selected /
  install_confirmed / skipped) for adoption monitoring; drop the dead
  'variant' property from each.

Shared experiments.ts infra is untouched (still used by the live
desktop-first-use-fork-default experiment). PostHog side: experiment
ended + archived, flag disabled.

typecheck + lint clean; 92 template/experiment unit tests pass.

* feat(install): always show starter-template descriptions

Drop the expand-on-select animation in the template picker — every row
shows its description statically. The 0fr→1fr grid reveal only surfaced
the description for the selected row, which hid useful info for the other
options the user is choosing between.

* fix(build): raise ToDesktop uploadSizeLimit to 30MB for bundled template thumbnails

The starter-template picker bundles ~1.5MB of preview thumbnails in-repo
(src/renderer/public/images/templates/*.webp) so a fresh first-run user has
offline, CSP-safe previews. That tips the packaged app source from just under
ToDesktop's 20MB default upload cap to ~22MB, failing 'todesktop build' at the
upload step. Raise the cap to 30MB.

* fix(build): drop invalid JSON comment key from todesktop.json

todesktop.json is schema-validated with additionalProperties:false, so the
_comment_uploadSizeLimit key added alongside uploadSizeLimit failed CLI
validation ('not expected to be here'). Keep only the valid uploadSizeLimit:30
(raised from the 20MB default because the bundled ~1.5MB of starter-template
thumbnails push the packaged app source to ~22MB).

* fix(install): address CodeRabbit review — popup auto-open guard, path + matchMedia safety

Three correctness fixes from the CodeRabbit review of Comfy-Org#1131 (all in the
inherited Comfy-Org#1084 picker code):

- titlePopup: the ~2.5s post-launch downloads-tray auto-open only suppressed
  when a *downloads* popup was already open, so it could forcibly replace an
  instance-picker / settings popup the user had just opened. Suppress for ANY
  open/opening popup — an unsolicited auto-open must never interrupt the user.
- templateModels.sanitizeModelPath: reject an empty / '.' / './' directory
  (all normalize to a current-dir ref) so a malformed entry is skipped rather
  than dropping the model file into the models root. Added a regression test.
- TemplatePickerStep: harden the reduced-motion read to
  matchMedia?.(...)?.matches ?? false so it can't throw if matchMedia is
  absent.

Deliberately NOT changed (CodeRabbit flagged but not warranted):
- tray-mirror install-scoping (:173) — already install-scoped in current code.
- clearTemplateTrayMirror renderer removal (:190) — the background download is
  still live after window close, so emitting removal could hide a running
  download; left as-is.
- recommended-badge-from-flag — the 'recommended' flag is on the 'None' option
  (for field-chain auto-select) and filtered out of the cards; the first-card
  badge is intentional (Image is deliberately first + pre-selected).

typecheck + lint clean; templateModels + TemplatePickerStep suites green.

* fix(install): remove template VRAM requirements from the picker

The picker showed a "recommends ~N VRAM" spec line and a "may run slowly"
warning, but the recommended-VRAM figure was set equal to the template's
download size (e.g. 20GB for both) — the index's `vram` was never wired up,
so the numbers were wildly overestimated. That scared users away from
workflows their hardware can run fine.

Remove the VRAM display and warning entirely rather than ship a wrong
signal:
- drop recommendedVramBytes from the bundled-template data + the per-row
  meta line and the GPU-vs-template warning alert
- remove the now-dead GPU VRAM detection (gpu.detectVramBytes via
  nvidia-smi/systeminformation) and the picker's detectGPU pre-roll; the
  Configure-screen GPU label hint still uses detectGPU().label
- drop the templatePickerSpecVram / templateVramWarning strings (en + zh)
  and the shouldWarnVram helper + tests

Disk-space hard-block (a real, correct constraint) is unchanged.

---------

Co-authored-by: Maanil Verma <vermaMaanil97@gmail.com>
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
…fy-Org#1136)

The title-menu item opens a fresh install-less chooser (the dashboard),
not a duplicate of the current window — "New Window" mislabeled what it
does. Relabel to "Open Dashboard" (en + zh + the hardcoded fallback).

Label-only change: the `fileMenu.newWindow` key, the `new-window` item id,
and the `comfy-window:new-chooser-window` IPC are unchanged.
…lts (Comfy-Org#1016)

* Clean up unique browser partitions on delete + drive-aware data defaults

Deleting an install never removed its browser partition. Unique-partition
installs each own a persist:<id> bucket under userData/Partitions/<id> that
nothing else reuses, so every delete leaked one forever. handleDelete now
removes the install record first, then best-effort clears the session
(timeout-bounded) and deletes the partition dir (force + retries) so it can
never hang or lock up the delete. Shared partitions are left untouched.

Also make the large data dirs follow the drive the user picks in the Windows
installer: when the app is installed to a non-system drive, installDir, the
shared models/input/output root, and the download cache default to that drive
instead of always landing on the system drive. Non-Windows and same-drive
installs keep the existing home/userData defaults.

Amp-Thread-ID: https://ampcode.com/threads/T-019eac5b-d0b9-7090-a4dd-491a74ab1fbe
Co-authored-by: Amp <amp@ampcode.com>

* Group drive-redirected data dirs under a single Comfy-Desktop folder

When the app is installed to a non-system drive, installs, shared
models/input/output, and the download cache now live under one
<drive>\\Comfy-Desktop parent instead of three separate folders at the
drive root. Home-dir and non-Windows defaults are unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-019eac5b-d0b9-7090-a4dd-491a74ab1fbe
Co-authored-by: Amp <amp@ampcode.com>

* feat(storage): default new Windows installs to %LOCALAPPDATA% on the system drive

On a system-drive Windows install, the large data dirs (installs, shared
models/input/output, download cache) previously defaulted to the user home
root and the cache to roaming AppData. New installs now group under
%LOCALAPPDATA%\Comfy-Desktop instead, while existing installs keep their
home-root layout. The choice is classified from a home-root footprint and
pinned with a one-time marker so it never flips between launches.

Amp-Thread-ID: https://ampcode.com/threads/T-019eac3f-cda6-74ea-ba76-49da53cadea2
Co-authored-by: Amp <amp@ampcode.com>

* fix(delete): clean up per-install partition even after toggling to shared

browserPartition is user-editable, so an install created as 'unique' (which already created Partitions/<id>) can later read as 'shared'. Gating cleanup on the current setting stranded that dir forever. Remove Partitions/<id> whenever it exists, guarding persist:shared explicitly.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9e54-7730-ba49-adfd9a824b07
Co-authored-by: Amp <amp@ampcode.com>

* refactor: address CodeRabbit review (system-drive anchor, DRY)

- paths.ts: anchor selectedInstallDrive on the OS SystemDrive, not the user profile, so a redirected profile (home on another drive) no longer misclassifies a system-drive install as a redirected one; add regression test.
- settings.ts: use builtinDefaultInstallDir() for the installDir default instead of reimplementing it.
- delete.integration.test.ts: extract a shared invokeDelete() helper.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9e54-7730-ba49-adfd9a824b07
Co-authored-by: Amp <amp@ampcode.com>

* fix(settings): fall back installDir/cacheDir when their volume is gone

If a configured installDir/cacheDir lives on a drive that no longer exists (reinstall on a different drive, a removed disk), reset it to the current default so the app never strands installs/cache on a dead path. Checks the path root, not the leaf, so a created-on-demand custom location that doesn't exist yet is preserved (matches the deliberate not-yet-created behavior). No-op on POSIX (root is always /).

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9e54-7730-ba49-adfd9a824b07
Co-authored-by: Amp <amp@ampcode.com>

* fix(delete): reclaim leaked browser partitions via startup sweep

Manual Windows testing showed the inline deleteBrowserPartition cleanup does not actually remove Partitions/<id>: session.fromPartition().clearStorageData() materializes a live session that locks and re-writes the dir, so the bounded fs.rm loses the race and the folder survives (the mocked integration test couldn't catch this). Add sweepOrphanPartitions(), run at startup before any install session exists, to reliably reclaim any Partitions/inst-* whose install is gone (covers the Windows lock case and crash leftovers). Never touches 'shared' or non-install dirs.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca71-9e54-7730-ba49-adfd9a824b07
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
…) (Comfy-Org#1110)

* fix(operations): surface full substep output on failure (Comfy-Org#1106)

When a substep of an update/migrate/restore fails (e.g. uv pip install),
the user previously saw only a bare exit code (e.g. "requirements install
exited with code 2"), and the streamed output sat behind a collapsed
"View logs" toggle. This made failures black-box (see Comfy-Org#1083).

- Add bounded output capture to the shared pip helpers
  (runUvPipDetailed / installFilteredRequirementsDetailed) and fold the
  output tail into update/migrate failure messages via withOutputTail.
- Surface which packages failed in the snapshot-restore error message.
- Auto-expand the ProgressModal logs panel when an operation finishes with
  an error so the full output is visible immediately, for all operations.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca72-8475-70ec-89a2-ab0c2460da47
Co-authored-by: Amp <amp@ampcode.com>

* fix(operations): address review feedback on substep error surfacing

- updateOrchestrator: the dry-run conflict-check path (the primary user
  "Update" path) now captures pip output via runUvPipDetailed instead of
  the unbounded spawnCommand, and uses the combined output tail instead of
  stderr-or-stdout (which dropped stdout-only failures).
- actions.ts: cap the restore failure package list at 20 with an
  "…and N more" suffix so a large restore can't produce a wall-of-text.
- ProgressModal: make the error auto-expand robust — fire immediately (so
  reopening a finished failed op expands) and drop the terminalOutput gate
  (the accordion is already gated on it), covering the error-before-output
  race.
- pip.ts: clarify the capture cap is characters, not bytes.

Amp-Thread-ID: https://ampcode.com/threads/T-019eca72-8475-70ec-89a2-ab0c2460da47
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…fy-Org#1132)

* feat(telemetry): instrument the install -> boot -> canvas funnel

Adds the missing desktop-side funnel steps so we can see where users drop
between download and first render, instead of only knowing a launch started.

- comfy.desktop.install.phase — per-phase install boundaries (start / end /
  error) for the standalone installer. PostHog carries the funnel timing;
  the error rows ride the Datadog mirror so a monitor can page when a phase
  hard-fails for a population after a release.
- comfy.desktop.comfyui.boot_phase — launch-progress phase timings, buffered
  in memory per installation and flushed ONLY when the boot fails or times
  out (healthy-boot timing is already covered by instance_started, and
  boot_started alone is ~258k/14d — emitting per-phase on every boot would
  multiply that for no gain). The phases explain WHERE a failed boot stalled.
- comfy.desktop.comfyui.boot_failed — port-wait timeout / early process exit /
  renderer load failure / render-process-gone. Datadog-mirrored; paired with
  the flushed boot_phase breakdown.
- comfy.desktop.comfyui.canvas_rendered — first dom-ready of a LOCAL install's
  main frame (the bottom of the install->canvas funnel), with
  server_ready_to_canvas_ms. Deduped per launch; the failed-load leg is
  recorded separately.
- comfy.desktop.first_use.abandoned — the first-use takeover unmounting
  without any completion path firing (the chooser-drop signal; pairs with
  first_use.completed to give onboarding its denominator).
- comfy.desktop.cloud.entry_blocked — the cloud capacity gate on every gated
  entry (picker / first_use), with raw flag + tier + decision, so we can see
  how many cloud entries the kill-switch shed vs. warned vs. let through.

boot_phase / install.phase buffers are bounded and terminally cleared.

* test(telemetry): cover boot-phase buffer, canvas-render, and cloud capacity gate

- bootPhaseBuffer: failure-only flush, first-write-wins per phase, bounded
  lifecycle (start resets, success clears, flush emits one boot_phase per
  buffered phase + returns last phase + clears).
- canvasEntry: first-render dedup per installation, failed-load leg bypasses
  the dedup, server_ready_to_canvas_ms from the session anchor.
- useCloudCapacity.confirmEntry: cloud.entry_blocked decisions
  (no_op / declined / proceeded), normal flag emits nothing, paid-user
  relaxation past a disabled kill-switch.

* docs(telemetry): trim verbose comments in boot-phase + canvas-entry taps
…Org#1145)

Follow-ups to the merged install -> boot -> canvas instrumentation (Comfy-Org#1132);
all side-channel-hardening, no behavior change to the funnel events:

- canvasEntry: clamp server_ready_to_canvas_ms to >= 0 so a backward clock
  step can't emit a negative, funnel-polluting duration.
- installer.withInstallPhase: isolate every onPhase tap in try/catch so a
  throwing telemetry callback can never abort or mask an install.
- install.emitInstallPhase: guard classification/emission so side-channel
  metrics fail quietly (this is also the installer's onPhase tap).
- useCloudCapacity.test: fix the loadComposable return type — it resolved to
  the composable function, not its call result.
…unify the desktop funnel) (Comfy-Org#1147)

* feat(telemetry): stamp installation_id on every event default

Set defaultEventProperties.installation_id in identify() once the device
id is bound at boot, so renderer events (routed in over IPC) and main
events share a single join key. Removes the person_id/installation_id
split that breaks the install->boot->run funnel. Does not identify the
anon id (only bindUserId calls client.identify); pure event property.

* feat(telemetry): emit install.completed at every local-install finish

captureInstallCompleted existed but was never called, so the
comfy.desktop.install.completed funnel event never fired. Wire it into the
three local-install completion sites — fresh express/manual (install-instance
handler), Desktop-1 adopt, and snapshot migrate — each firing once when the
install is ready to boot (distinct from comfyui.boot_started, which fires every
launch). Carries {installation_id, method, express}. Excludes the cloud fork
(no local install) and the version-update path. Best-effort via capture().

This isolates the install -> boot transition the funnel was missing, and rides
the installation_id super-property so it stitches to the rest of the desktop
journey.

* chore(ci): untrack node_modules symlink (broke pnpm install --frozen-lockfile)
* Add Comfy Desktop bridge types package

* Simplify bridge type generation

* Move remote bridge flag into Desktop bridge

* Fix bridge type entrypoint for NodeNext

* fix: consume published desktop bridge types

* fix: keep legacy remote marker for hosted frontend
* Add Comfy Desktop bridge types package

* Simplify bridge type generation

* Move remote bridge flag into Desktop bridge

* Fix bridge type entrypoint for NodeNext

* Expose ComfyUI telemetry bridge

* Bound telemetry IPC payloads
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ess rate (Comfy-Org#1156)

boot_started had no positive terminal, so server-boot success could only
be inferred per-machine -- and boot_started fires ~5.8x/machine (retries,
relaunches), so a per-launch rate was not computable at all.

Add a per-launch boot_id (generated once, reused across port/reboot
retries since tryLaunch recurses) stamped on boot_started, boot_failed,
and a new comfy.desktop.comfyui.boot_completed emitted in main right
after the session registers (server confirmed up). boot_completed carries
installation_id, boot_id, boot_time_ms, and the retry counters.

The boot-success rate becomes a clean per-attempt funnel:
count(boot_completed.boot_id) / count(distinct boot_started.boot_id).
boot_completed is a success/funnel event (PostHog only), deliberately not
on the Datadog mirror list so it never pages on a healthy boot.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
benceruleanlu and others added 27 commits August 26, 2026 19:00
…fy-Org#1416)

* feat(bridge-types): own and publish the frontend bridge contract

Fold the frontend copy's additions (openModelAccessPage, doc comments) into
src/types/comfyDesktopBridge.ts, the file the preload implements, and
regenerate the package from it at 0.2.0.

Both preloads now import that source directly instead of the stale npm
build of themselves, so comfyPreload's hand-written intersection is gone
and `satisfies ComfyDesktop2BridgeImplementation` binds the preload to the
contract in the same commit that changes either.

A CI check regenerates the package and fails on any diff, and merging a
version bump publishes.

* fix(bridge-types): gate contract changes on a version bump

The publish workflow only fires on changes to the package's package.json,
so a PR that edited the contract and regenerated it without bumping would
pass bridge-types:check and merge, leaving npm on the old contract. CI now
fails that PR.

Also fixes the README example, which awaited openModelAccessPage outside a
try and so skipped the fallback the doc comment promises on rejection.

* fix(bridge-types): close the gaps between the gate and the publish

The gate compared against pull_request.base.sha, which is the base tip as
of the PR's last sync rather than the fork point, so main's later commits
were attributed to the PR. Use the merge base, and widen the watched set
to index.d.ts/index.js — both ship in the package's `files`, so either can
change the published entrypoint without the old gate noticing.

semver excludes prereleases from a `>x.y.z` range, so any prerelease bump
failed the gate and the publish workflow's `next` dist-tag path was
unreachable. Pass -p. Dropping to npx also removes a full dev-dependency
install (with native rebuilds) from every PR.

Publishing now fails instead of skipping when the version is already on
npm and the run came from a merge, so a green publish job means it
published; manual dispatch keeps the idempotent skip. Trigger on push to
main rather than pull_request: closed, which passes no secrets for
fork-authored PRs and needs a merge_commit_sha guard. Drop github.workflow
from the concurrency group — inside a called workflow it resolves to the
caller, so dispatch and on-merge runs of one version never serialised.

README: isRemote() is required, not optional; add noopener to the fallback
example; say that the version bump is what publishes.

* fix(bridge-types): widen the sync gate to every published declaration

bridge-types:gen prettier-formats every *.d.ts in the package and both
are published, but the gate diffed only comfyDesktopBridge.d.ts, so a
committed misformat of index.d.ts passed. format:check does not cover
packages/, so nothing else caught it either.

* fix(bridge-types): make isRemote optional and close the publish gaps

isRemote landed 2026-06-15, three months after downloadModel, and replaced
the window.__comfyDesktop2Remote global rather than adding new information.
Builds from that window therefore expose downloadModel without isRemote --
but the contract declared isRemote required, so the frontend calls it
unguarded behind a downloadModel check those very builds satisfy. It throws
before every fallback branch, so a missing-model download silently dead-ends.
Declaring it optional forces the guard and turns the throw into a fallback.

Guardrails, so the same class of drift cannot recur:

- The on-merge publish compares the published tarball's declarations instead
  of treating any version collision as a mismatch, which lets the trigger
  widen to the whole package without a description typo reddening main.
- The version gate diffs the branch tip rather than the merge ref, globs the
  declarations rather than listing them, and covers the merge queue. Its
  version comparison is inline, so a registry outage cannot fail a PR.
- bridge-types:check stages emitted declarations intent-to-add first, so a
  declaration that was never committed fails the check instead of passing it
  and shipping as a dangling reference.
- The preload test derives its shape from the contract instead of restating
  it, and the files allowlist globs *.d.ts since tsc emits one per module.

* fix(bridge-types): export the versions the gate's comparator reads

BASE_VERSION and HEAD_VERSION were assigned but never exported, so the
comparator saw process.env.* as undefined, threw, and the gate reported
"version not bumped" while its own error text printed the correct versions
it had just read. It could never pass, on this PR or any later contract
change. The comparison logic itself is fine -- 11 precedence cases, including
alpha.2 < alpha.10 and release-beats-prerelease, all agree.

* fix(bridge-types): publish from trusted main workflow
…-Org#1460)

* fix(mcp): await the flag fetch before the sidebar gate decides

The sidebar injection read the flag synchronously at dom-ready, before the
async PostHog fetch had populated the cache — so on a cold start (fresh or
wiped install) getFlag returned undefined and the icon never injected, no
matter how many launches. This is why it showed in dev but not the ToDesktop
build, which loses the boot-fetch race every time.

- attach.ts: gate on  (the pattern every other flag
  consumer uses) so injection waits for the fetch to resolve, with an
  isDestroyed guard across the await.
- experiments.ts: the boot fetch is fire-and-forget with getFlagAsync as its
  only awaiter, so the 1500ms cap wasn't protecting boot — it just discarded
  slow-but-successful fetches and left the cache unseeded. Raise it to a 10s
  hang-guard so a slow network still resolves and seeds the cache.

* fix(review): retire the MCP sidebar inject when the attach is torn down

`getFlagAsync` can resolve up to ~10s after the flag fetch, and detach
leaves `comfyContents` alive, so the `isDestroyed()` check alone let a
late resolution inject the sidebar into a detached or hot-swapped view.

- Track attach lifetime with an `attachActive` flag, cleared in
  `_installCleanup`, and require it before injecting.
- Extract the gate as a pure `shouldInjectMcpSidebar` so the truth
  table (incl. resolve-after-cleanup) is unit-tested deterministically.

* docs(attach): tighten the MCP inject-gate comments

Condense the shouldInjectMcpSidebar JSDoc and the attachActive note to
their load-bearing lines; drop the redundant restatement in the test.
…#1461)

* fix(install): keep progress loader clear of the footer, re-aim beams

The centred loader stack (scene + bar + stepper that hangs below it) was
sized against the full viewport, so on a fresh install its stepper text
landed under the footer's Return-to-Dashboard button.

- Reserve the footer band as padding-bottom on the hero so the stack
  centres in the space above it.
- The beams anchor to the scene box, so lifting it moved them; thread a
  beamLift var (ProgressModal -> BrandTakeoverLayout -> BrandBackground)
  to nudge the beam tops back onto the scene. Defaults to 0px, so every
  other surface is untouched.

* refactor(install): aim beams at a fixed screen-centre point

The beams anchored to the loader's scene box, so lifting the loader to
clear the footer dragged the spotlights off with it. Anchor them instead
to a zero-size element pinned at the hero's centre — the old behaviour
where they pointed at the centred wordmark — so they stay put no matter
where the loader stack sits.

Drops the beamLift prop threaded through BrandTakeoverLayout and
BrandBackground; both are back to their original state.

* refactor(install): drop the beam-anchor comments

The class name and aria-hidden already say what the anchor is; the CSS
is self-documenting. Removes the three comments flagged in review.
…erlay flicker (Comfy-Org#1462)

* fix(install): fade each scene clip in once its frame is decoded

Clips pinned off frame 0 seek away from the preloaded frame, so revealing
synchronously showed the mask ground as a black flash. Reveal a clip only
once a decoded frame exists (requestVideoFrameCallback, with a
readyState-gated fallback) and fade it up from a neutral ground over the
unavoidable decode gap.

* feat(mcp): warm the setup film into cache before it's opened

The MCP setup modal streams a ~5 MB remote video; opened cold it showed
black while it loaded. Extract useAssetPrefetch (the idle/busy/network
queue shared with thumbnail warming) and add useMediaPrefetch, which warms
via a detached <video> — a cross-origin fetch() is CSP-blocked here, but a
media-element load fills the same cache the modal reuses.

* fix(mcp): open the overlay panel without a flicker or cold-load delay

The panel view was destroyed on attach and rebuilt cold on the first
MCP/feedback click, and setActivePanel showed it before the renderer had
painted the modal — an opaque frame flashed over the canvas as open/close/
open. Rebuild the panel warm right after attach, and hold it hidden until
the renderer acks it has painted (overlay-ready), with a timeout fallback
so it can never strand hidden.

* fix(review): guard scene re-watch and stale overlay timer; trim comments

- useBrandScene: skip clips already being watched so a loop re-activation
  can't stack a second set of fallback listeners on the same <video>.
- panelView: track the overlay reveal-fallback timer on the entry and clear
  it on the next switch / ack, so a stale one can't reveal a later open early.
- Cut comments to one or two lines across the changed files; drop the CSS
  comment; assert the warm element is muted + preload=auto.

* fix(review): address CodeRabbit findings on the loader/MCP fixes

- useAssetPrefetch: init `cancel` to a no-op so a loader that settles
  synchronously can't hit it in its TDZ and wedge the warm queue.
- useBrandScene: re-arm the paint gate per activation via a per-clip
  token, so a loop re-seek waits for its own decoded frame and a late
  callback from the prior activation can't paint the current seek.
  Hoisted markPaintedWhenReady to a pure, testable module function.
- index: reset a picker-progress chooser host to 'comfy' before the
  panel prewarm, so computeBodyMode keeps the rebuilt panel hidden
  instead of stranding a progress surface over the attached canvas.
- Tests: synchronous-settle drain, paint-gate re-arm + stale-callback,
  and the progress→comfy prewarm reset.

* refactor(install): extract prewarmAttachedPanel, guard its ordering

CodeRabbit: the prewarm test asserted a bare setActivePanel transition,
so it stayed green if index.ts dropped or reordered the reset.

- Extract the reset→ensure→layout sequence into prewarmAttachedPanel,
  so the "reset before ensurePanelView" ordering lives in one named
  place with the rationale on the definition, not the call site.
- Point the test at the helper; it now fails if the reset is removed.
- Trim the comment block down to the load-bearing lines.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…PR (Comfy-Org#1464)

After a tag build, resolve the version-bump PR from the tagged commit
(commits/{sha}/pulls, with a (#N) subject fallback) and upsert a comment
with the ToDesktop build page and per-platform download links. Idempotent
via a hidden marker; runs on build success or failure.
…laceholder visual) (Comfy-Org#1467)

* feat(announcement): MiniMax license announcement modal (copy + CTAs, placeholder visual)

Reuses the WhyTryCloud modal layout for a one-off MiniMax license
announcement. Left media panel is a placeholder; copy is draft. Two CTAs
(Request License -> comfy.org/minimax/license, Learn More -> blog) and
show/cta/dismiss telemetry. Trigger + gating wiring left to a follow-up.

* feat(announcement): open MiniMax announcement from a title-bar news bell

Replace the auto-on-launch modal with a subtle title-bar bell that carries an unread dot until opened, then opens the announcement over the live app via a new 'announcement' overlay panel mode (mirrors the Send Feedback path). Swap the placeholder visual for the MiniMax license hero video with a mute/unmute toggle. Unread state persists via minimaxAnnouncementSeen and clears through the settings-changed broadcast.

* fix(announcement): refer to the model as MiniMax H3

* chore(announcement): remove em dashes from comments

* feat(announcement): finalize MiniMax H3 copy with inline local-license link

Apply Tiger's launch copy (lead + two bullets), replace the CTA buttons with a commercial-license note whose inline 'here' link opens the local-license reach-out (comfy.org/contact, interim) tagged with a Desktop-origin UTM.

* feat(announcement): restore Learn More + Request License CTAs, add free-for-personal note

Keep the two-button footer (Learn More, Request License) with Desktop-origin UTMs, and add a 'Free for personal use' note alongside them.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…odal (Comfy-Org#1469)

Follow-up to the merged announcement modal. Add a third highlight ("Want to use it commercially? Reach out to get a license") and drop the separate personal-use footer note, leaving the Learn More + Request License CTAs.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…1471)

* feat(updater): correlate update telemetry across launches

* fix(updater): make telemetry correlation durable

Amp-Thread-ID: https://ampcode.com/threads/T-01a04ac7-64c3-778d-b10f-af2ddc4a1840
Co-authored-by: Amp <amp@ampcode.com>

* fix(updater): persist installed outcome before cleanup

Amp-Thread-ID: https://ampcode.com/threads/T-01a04ac7-64c3-778d-b10f-af2ddc4a1840
Co-authored-by: Amp <amp@ampcode.com>

* fix(updater): trust sidecar attempt identity

Amp-Thread-ID: https://ampcode.com/threads/T-01a04ac7-64c3-778d-b10f-af2ddc4a1840
Co-authored-by: Amp <amp@ampcode.com>

* fix(updater): unify fallback telemetry field

Amp-Thread-ID: https://ampcode.com/threads/T-01a04ac7-64c3-778d-b10f-af2ddc4a1840
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
… improvements (Comfy-Org#1441)

* feat: add workspace dashboard controls

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: simplify workspace build card

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat(comfybuilder): the client calls the build API and the copy says Build

The comfy-builder API renamed its public vocabulary from distribution to
build: /v1/distributions -> /v1/builds, /v1/distributions/{id}/versions ->
/v1/builds/{id}/versions, /v1/distribution-versions/{id}(/manifest) ->
/v1/build-versions/{id}(/manifest); /v1/build-artifacts/{id}/download is
unchanged. The list envelope field distributions is now builds.

Moves the hand-written client to the new paths and envelope field, points
the URL-asserting unit tests at the new URLs (they fail against the old
client), and renames the user-facing copy in en/zh locales plus the three
hardcoded strings. i18n keys, IPC channel names, type names, testids and
the persisted installations.json fields are deliberately untouched.

* fix: use Build API contract

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: create promoted Builds through Builder

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: open promoted Build drafts directly

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: use deployed Builder routes

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: reorder desktop card actions

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: shorten workspace creation card text

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: use deployed Builder web route

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: show workspace promotion progress

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: load all workspace builds

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: punctuate workspace build card

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: rename snapshot export action

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: remove Forget from instance context menu

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: refine workspace dashboard controls

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: filter workspace builds by compatibility

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: compact workspace filter text

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: shorten workspace build action

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: enable managed instance duplication

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: configure managed Build terminals

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: widen workspace compatibility filter

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: release managed Build model locks

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: keep managed Builds visible while updating

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: simplify new instance heading

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: shorten instance search prompt

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: preserve managed Build data during updates

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: make workspace selector opaque

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: keep workspace selector trigger translucent

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: scope dashboard installs by workspace

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: keep progress footer clear of install steps

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: identify invalid Build model hashes

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: center search above workspace controls

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: avoid repeated workspace authentication

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: support team workspace Build routes

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: use Builder release endpoints

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: space new instance heading

* fix: use Build draft browser route

* fix: use sentence case for No workspace

* fix: use neutral No workspace avatar

* fix: restore new instance subtitle

* fix: tailor new instance subtitles

* fix: show workspace type in selector

* fix: left align dashboard instances

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: hide Personal workspace subtitle

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: clarify local install subtitle

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: reuse New Instance subtitle

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: improve workspace Build picker

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: label Build catalog refreshes

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: preserve workspace identity during startup

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: add public install option to workspaces

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: allow uninstalling failed managed builds

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* feat: select managed Build release targets

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: clarify Build browser handoff

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: move GPU details below install options

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: show managed Build targets directly

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6

* fix: simplify managed Build selection

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6

* fix: allow repeated managed Build installs

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: reset managed Build size display

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: sign out without confirmation

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: allow managed models without hashes

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: improve OIDC completion page

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: stabilize local Git version queries

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: use Comfy Desktop callback branding

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* fix: clarify completed authorization message

Amp-Thread-ID: https://ampcode.com/threads/T-01a021bb-eeb2-7359-b259-6bb1b757bdd6
Co-authored-by: Amp <amp@ampcode.com>

* Fix template picker leaking into managed installs and flaky GPU detection at install time

The wizard's template picker step was gated only on the standalone source,
so visiting the Public tab (which auto-selects standalone and loads template
options) and returning to Managed left stale state that surfaced the picker
before a managed install. Gate the picker on local-install mode and reset all
source-scoped state when the Managed/Public mode toggles.

Install-time artifact resolution re-ran GPU detection from scratch, so a
transient WMI failure mapped the host to the CPU fallback and produced
'No installable build for this machine' on NVIDIA-only builds. Share one
memoized detection result (detectGPUCached) across field options, system
info, detect-gpu, and resolveHost; a null result is not cached so a failed
probe retries instead of poisoning the session.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Share install record allocation and add managed disk preflight

Extract allocateInstallIdentity (unique name, directory allocation,
duplicate-path check) into installIdentity.ts and use it from both the
generic add-installation handler and the managed installBuild flow, which
previously restated the same logic. The managed flow keeps its
workspace-changed re-check between allocation and record creation.

Hoist the ComfyBuilder launch defaults (launchArgs/launchMode/
browserPartition) into COMFYBUILDER_INSTALL_DEFAULTS in
comfybuilder/constants.ts so the install handler and the source plugin
share one definition instead of restating the values.

Managed installs now get the same soft disk-space warning as local
installs: the wizard estimates the install size from the build catalog's
sizeBytes (same 2.25x download-to-installed factor) and shows it in
PathDiskInfo.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Launch managed installs without waiting for model downloads

The environment install (archive download + extract) is now the only thing
that gates a build install; the build's declared models download in the
background through the managed Downloads tray, the same way standalone
installs fetch starter-template models.

- New modelStagingTask owns the background download: model-root lock,
  parked-job release, abort, and recording completion as modelsStaged on
  the install record.
- installEnvironment resolves the model manifest inside the transaction
  (a build whose model list cannot be fetched still fails and rolls back)
  but returns the models for the caller to stage in the background.
- A launch of a build whose staging never finished (crash, cancel, failure,
  or a record from before the flag) silently re-stages; already-downloaded
  models are skipped by hash.
- Version updates stop a running staging before the filesystem swap and
  re-stage the new version's models after it.
- Install cancel/failure and host-window teardown abort the staging task
  alongside the existing template-download teardown.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Keep the main process responsive during boot

Startup work froze every window for seconds on machines with many
installs or large model roots:

- The staged-download scan walked overlapping roots repeatedly and
  processed large Dirent batches without yielding. It now prunes roots
  covered by another root, finds both sidecar suffixes in one walk, and
  yields to the event loop every 500 entries.
- The background version pass spawned an unbounded burst of pygit2
  subprocesses; on Windows each spawn blocks the main thread in
  CreateProcess. The pass is now capped to one spawn at a time.
- get-system-info re-ran nvidia-smi and several WMI/PowerShell probes
  on every call; the hardware probe is now memoized single-flight while
  per-call fields (installs, settings, versions) stay fresh.
- getAppVersion ran a synchronous git describe on every call in
  unpackaged builds; the result is now memoized.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Improve workspace build and instance flows

Amp-Thread-ID: https://ampcode.com/threads/T-01a04619-d056-765e-8e82-80ad2ba8a336
Co-authored-by: Amp <amp@ampcode.com>

* Show the Updating state on dashboard tiles for standalone updates

A managed update flips its record to status 'installing', so the dashboard
tile shows the Updating pill; a standalone update only streamed progress to
the window that started it and the tile showed nothing. Main now broadcasts
'operation-changed' (with a 'get-active-operations' hydration snapshot)
around every dispatched action, sessionStore mirrors it per install, and the
tile keys its Updating state on both signals. The registry is in-memory only,
so a crash mid-operation can never leave a stale busy state. 'check-update'
is now classified as a generic op so a silent version check cannot paint
update UI.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Split status 'updating' out of 'installing' for in-place updates

Fresh installs and in-place managed updates shared status 'installing',
disambiguated by the rollback payload: fresh installs are hidden from the
renderer while updates stay visible and render as Updating. The update flow
now writes status 'updating'; a load-time migration rewrites legacy
mid-update records, so visibility and the dashboard tile read status alone
and showing tiles during a first install can no longer mislabel them.

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Remove dismiss error instance action

Amp-Thread-ID: https://ampcode.com/threads/T-01a049b4-add2-77ff-a064-4b4a0db9f86b
Co-authored-by: Amp <amp@ampcode.com>

* Raise timeout on updater capability tests that re-import the module graph

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Update lifecycle specs for always-open Advanced body and drawer-based Untrack

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Update lifecycle wizard selector for renamed version label

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* Retry cleanup deletes in lifecycle specs to absorb transient Windows file locks

Amp-Thread-ID: https://ampcode.com/threads/T-01a045a7-fa78-70bb-9e24-1216ae381c53
Co-authored-by: Amp <amp@ampcode.com>

* fix: guard sign-out during instance installation

Amp-Thread-ID: https://ampcode.com/threads/T-01a049b4-add2-77ff-a064-4b4a0db9f86b
Co-authored-by: Amp <amp@ampcode.com>

* fix: never let a wedged shell.openExternal strand the sign-in flow

* Address review: guard repeated build cursors, explicit HTTP method, shared popover dismissal

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: James Kwon <hongilkwon316@gmail.com>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
* docs: require PR change breakdowns

Amp-Thread-ID: https://ampcode.com/threads/T-019ffdd6-1ee5-7066-adc9-19c03ede90dd
Co-authored-by: Amp <amp@ampcode.com>

* docs: clarify PR change accounting

Amp-Thread-ID: https://ampcode.com/threads/T-019ffdd6-1ee5-7066-adc9-19c03ede90dd
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
… downloading (Comfy-Org#1473)

* Fix startup update loop when the staged installer is corrupt or still downloading

On Windows, a staged update whose cached installer failed validation put
boots into an infinite loop: the readiness check raced the re-download
electron-updater kicks off and always timed out; the not_ready skip never
armed the loop breaker; and the skip path destroyed the update splash
before any other window existed, which fired window-all-closed and quit
the app, killing the in-flight re-download at a partial size and re-arming
the invalid cache for the next boot.

- Resolve the bounded startup wait as soon as the outcome is knowable:
  a download-progress tick means the cached installer was rejected and a
  re-download started (bail into the app; the download continues in the
  background), and a failed/empty check means ready is unreachable this
  launch.
- Bound repeated not_ready skips of the same staged version with a
  3-strike counter that abandons the stale marker, without clobbering a
  marker restaged during the wait.
- Open the normal UI before destroying the update splash so the app never
  self-quits through window-all-closed while a background re-download is
  in flight.
- Show the splash with checking copy first and only swap to the install
  countdown once the install is committed, holding from that point so the
  countdown plays out fully and never shows for a skipped install.

Refs Comfy-Org#1367
Refs Comfy-Org#1472

Amp-Thread-ID: https://ampcode.com/threads/T-01a05158-0efc-72ab-87a6-5f78ef0be040
Co-authored-by: Amp <amp@ampcode.com>

* Keep the update splash up if the startup surface fails to open

Destroying the splash while it is the only window fires window-all-closed
and quits the app, which would also kill any in-flight background
re-download. If opening the normal UI fails before another window exists,
keep the splash instead of exiting silently.

Amp-Thread-ID: https://ampcode.com/threads/T-01a05158-0efc-72ab-87a6-5f78ef0be040
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
…#1474)

* Fix startup update loop when the staged installer is corrupt or still downloading

On Windows, a staged update whose cached installer failed validation put
boots into an infinite loop: the readiness check raced the re-download
electron-updater kicks off and always timed out; the not_ready skip never
armed the loop breaker; and the skip path destroyed the update splash
before any other window existed, which fired window-all-closed and quit
the app, killing the in-flight re-download at a partial size and re-arming
the invalid cache for the next boot.

- Resolve the bounded startup wait as soon as the outcome is knowable:
  a download-progress tick means the cached installer was rejected and a
  re-download started (bail into the app; the download continues in the
  background), and a failed/empty check means ready is unreachable this
  launch.
- Bound repeated not_ready skips of the same staged version with a
  3-strike counter that abandons the stale marker, without clobbering a
  marker restaged during the wait.
- Open the normal UI before destroying the update splash so the app never
  self-quits through window-all-closed while a background re-download is
  in flight.
- Show the splash with checking copy first and only swap to the install
  countdown once the install is committed, holding from that point so the
  countdown plays out fully and never shows for a skipped install.

Refs Comfy-Org#1367
Refs Comfy-Org#1472

Amp-Thread-ID: https://ampcode.com/threads/T-01a05158-0efc-72ab-87a6-5f78ef0be040
Co-authored-by: Amp <amp@ampcode.com>

* Keep the update splash up if the startup surface fails to open

Destroying the splash while it is the only window fires window-all-closed
and quits the app, which would also kill any in-flight background
re-download. If opening the normal UI fails before another window exists,
keep the splash instead of exiting silently.

Amp-Thread-ID: https://ampcode.com/threads/T-01a05158-0efc-72ab-87a6-5f78ef0be040
Co-authored-by: Amp <amp@ampcode.com>

* Show download progress for auto-installed update downloads

Background downloads started by the auto-install setting kept the cached
app-update state null until the download finished, so the title-bar pill
stayed hidden and Desktop Settings claimed the app was up to date while
hundreds of megabytes were in flight. Pressing check for updates gave no
feedback beyond a spinner. The display pipeline already existed end to
end (progress IPC, Settings progress bar with percent/bytes/speed, pill
downloading label) but was only wired up for user-initiated downloads.

The first download-progress tick of any download now flips the state to
downloading, using the version recorded by the auto-download trigger when
the available state was skipped. A failed auto-on download returns to the
silent idle state so background network blips stay quiet; a failed
user-initiated download still rolls back to available for a manual retry.

Amp-Thread-ID: https://ampcode.com/threads/T-01a05158-0efc-72ab-87a6-5f78ef0be040
Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…fy-Org#1481)

* Add native Windows ARM64 (RTX Spark) support to the install path

Phase 2 of the RTX Spark plan (BE-11059): teach Desktop about the machine
architecture so a native ARM64 build installs the win-nvidia-arm64 bundle
and ships native ARM64 runtime pieces.

- variantMatchesHostArch gates both wizard filters: a native ARM64 app sees
  only -arm64 bundles, an x64 app (also under Prism emulation) never does.
- Native ARM64 bundles get no index-served torch stacks (no trusted index
  publishes win_arm64 wheels); variantAccel makes the accel base explicit.
- afterPack downloads vc_redist.arm64.exe for arm64 packs and installer.nsh
  selects the redist and registry key via IsNativeARM64, embedding only the
  redist for architectures the installer carries.
- Native win-arm64 bootstrap Python: build/fetch scripts, ToDesktop
  beforeBuild hook, targetOverrides in todesktop.json, ${arch} in
  electron-builder.yml, bootstrap-v3 default tag, windows-11-arm CI job.
- ToDesktop CLI 1.22.0 -> 1.28.1, the first schema with targetOverrides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Accept beta- vendor ids so pre-release bundles reach only this build

The environments repo publishes the Windows ARM64 bundle as
beta-win-nvidia-arm64 (ComfyUI-Standalone-Environments#22): shipped desktops
list only win-/mac-/linux- ids, so the beta- prefix keeps the ARM64 card away
from x64 users who lack the architecture filter. This build reads through it.

- stripPlatform, and the renderer's copies in variants.ts and telemetry.ts,
  drop an optional beta- prefix ahead of the platform prefix.
- variantMatchesHost combines the platform check (beta- allowed only directly
  in front of the host prefix) with variantMatchesHostArch; both wizard
  filters use it.
- getVariantLabel appends Beta, so the card reads NVIDIA (ARM64) Beta.
- The snapshot-import handler uses the shared PLATFORM_PREFIX and
  stripPlatform instead of its own copy, so a snapshot taken on a beta bundle
  still maps to its accelerator.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
)

* fix(release): raise ToDesktop source upload limit

* chore: bump version to 1.0.47-rc.3
…omfy-Org#1479)

* feat(templates): let content own the starter-template list

Desktop reads the picker's list from R2 at boot, so it can change without an
app release. Nobody outside engineering could change it, because the file lived
nowhere and there was no safe way to edit it.

Adds the file, a script, a skill and a CI gate:

- `assets/starter-templates.json` — the list, generated from the built-in one,
  so publishing it is a no-op today.
- `scripts/starter-templates.mjs` — list, set, regenerate, validate. Editors
  supply template ids only; titles, descriptions, sizes and thumbnails come from
  the live index, so a description cannot drift from the template it describes.
- `.claude/skills/starter-templates/` — the skill, plus setup for someone
  starting cold: Node 22 and `gh auth login`, no build.
- CI validates the file and re-runs generation to prove nothing was hand-edited.

The rules the script refuses to break are the ones that fail silently in the
app: four cards per tab, one free recommended pick per tab, at most one paid
template per tab. A short tab quietly backfills and a paid auto-pick spends the
user's credits on first run, so neither surfaces as an error.

Publishing to R2 on merge still needs a bucket credential and is not wired here.

* feat(templates): add a whole-list replace for seasonal refreshes

A one-slot `set` is the common case, but swapping most of the list meant four
separate calls. `replace` takes four ids per tab, with `*` for the auto-pick
and `$` for the paid card, and derives everything else. Same validation, so a
wrong count or an unknown id still fails before writing.

* fix(templates): require exactly one paid card per tab, and size every free one

QA of the authoring script found two states it would have accepted, both of
which are silent in the app rather than visible errors.

A tab with zero paid templates passed, because the rule was written as "at most
one". Every tab is meant to showcase exactly one API-node template, so the check
now requires exactly one.

A free template reporting zero bytes also passed, which makes the install-time
disk-space check under-count and can leave a user out of space mid-download.
Free cards must now report a real size, and paid cards must report zero since
they download nothing.

Adds `starter-templates.spec.mjs`, 36 cases covering every way an edit can break
a tab: wrong counts, missing or duplicated recommended and paid picks, a paid
auto-pick, cross-tab duplicate ids, ids that are not upstream or that escape a
path, and that a refused command leaves the file untouched. CI runs it.

* feat(templates): publish the list to R2 and the mirror on merge

Merging to main now uploads `starter-templates.json` to R2 and to the GCS
mirror, then reads both back and fails if either is missing or serving
something different. Desktop reads the file at boot, so a merged change reaches
users on their next launch with no app release.

Both hosts, not just R2: regions where R2's edge is throttled read the mirror,
and a list that exists on only one of them is invisible to half the users it was
written for. This also closes the manual mirror copy that has been outstanding.

Gated on the `starter-templates` environment and on validation passing, so a
broken list cannot reach users. Needs three secrets before it can run:
CLOUDFLARE_ACCOUNT_ID, R2_STARTER_TEMPLATES_TOKEN, GCS_MIRROR_SA_KEY.

* fix(templates): address review on the authoring workflow

CI was failing: the guardrail suite ran under vitest, which needs a dependency
install the job never did. It now runs on `node --test`, so it needs nothing
installed and starts in a second.

The no-drift check failed the job whenever upstream edited a description, which
would redden every open PR and send the author hunting a hand-edit that did not
exist. It warns now, and says upstream is the likely cause.

Three script fixes, each a state the tool would have written:
- an id from another tab was accepted, so an image template could land in the
  audio tab carrying its own title, and the picker groups by the stored modality
- `a,b,,d` passed as four ids, writing a different list than the one asked for
- an unmarked `api_` id in `replace` was written as free, which then failed the
  zero-size rule for the wrong reason

Also drops the "can never drift" claim from the skill: fields refresh when the
commands run, so upstream can move ahead until `regenerate` is next run.

Guardrail suite is 39 cases now. Mutation-checked the modality cross-check and
the api_ default; the empty-slot guard is a clearer error rather than a new
rejection, since `entryFor` already refuses an empty id, and the test says so.

* ci(templates): add a dry run so credentials can be proven before merge

Publishing only ran on merge to main, so a wrong secret would surface on the
merge that ships to users. `workflow_dispatch` with `dry_run` (default true)
authenticates to both hosts and does a real write-then-delete under a throwaway
key, proving the R2 token has write access without touching the file the app
reads. Unchecking dry_run publishes for real.

* fix(ci): drop --remote, which wrangler 3 does not accept

Wrangler 3 writes to remote R2 by default; --remote only exists in 4. The
credential dry run failed on 'Unknown argument: remote' before it ever
authenticated.

* ci(templates): diagnose both hosts in the dry run

The previous check stopped at the first failure, so an R2 403 hid whether the
GCS key could write at all. Each host is now checked independently and reports
what it found: whether the token is valid, whether it can see the bucket, and
whether read and write are separately permitted. That turns 'it failed' into
the specific permission to ask for.

* ci(templates): print credential findings to the log

They only went to the step summary, which the logs API does not return, so the
result was invisible to anyone reading the run output.

* ci(templates): make the credential dry run actually report

The previous attempt was broken three ways: findings went only to the step
summary, which the logs API does not return; a mangled helper recursed into
itself and segfaulted the step; and continue-on-error marked both steps green
regardless, so the run looked like a pass with no output.

Logic moves to two scripts that print to stdout and the summary, run without
error masking, and use the storage JSON APIs directly rather than the gcloud
CLI. Each reports whether the credential is valid, whether it can see the
bucket, and whether read and write are separately permitted, so a refusal names
the permission to request.

* fix(ci): verify account-owned R2 tokens on the right endpoint

The credential check called `/user/tokens/verify`, which only accepts
user-owned tokens. An account-owned token is rejected there with code 1000
"Invalid API Token" even when it is valid and correctly scoped, so the check
failed before it ever reached the bucket and reported a working token as bad.

It now tries `/accounts/{id}/tokens/verify` first and falls back to the user
endpoint, and says which kind it found. Verified against the real secrets: the
token reads and writes `desktop-assets`, and the mirror service account reads
and writes `comfy-desktop-public`. Both are ready to publish.

Also refreshes a template description upstream had edited, which was failing a
guardrail test, and drops the leftover vitest config from before the suite
moved to `node --test`.

* fix(templates): close the gaps QA found in the authoring workflow

Security, in the workflow:
- a manual dispatch with dry_run unticked published whatever branch it ran from
  straight to the file users read at boot; publishing is now pinned to main
- R2 was written before authenticating to GCS, so a bad mirror key left the two
  hosts serving different lists with no rollback; auth happens first now, since
  it writes nothing and is the likeliest failure
- the credential scripts aborted on an unset secret with a bare "unbound
  variable" and reported nothing, which is the opposite of their job
- fixed-name files in /tmp replaced with mktemp, and token validity is parsed
  rather than substring-matched

The validator accepted five states the app silently drops, each of which would
pass CI, publish green, and then shorten a tab: a missing or unusable
mediaSubtype, the reserved ids `.` and `..`, an id past 128 chars, text past
4096, and a size past the 2 TiB cap. It now mirrors every cap the app enforces.

Also: `--id --recommended` bound the flag as the value; passing --recommended
with a paid id reported the resulting document rather than the contradiction
typed; the index fetch had no timeout; and the post-publish check now polls,
since both hosts are behind a CDN that can serve the previous object.

* docs(templates): say exactly one paid card, and re-check on main

The skill said "at most 1 paid per tab" while the validator requires exactly
one, so the doc misled the person it was written for. Also names the
mediaSubtype rule the validator now enforces.

The push filter omitted the script, so a change to the validator re-checked the
committed list on the PR but never again on main. It now matches the PR filter.

* docs(templates): write the skill for the agent, not the reader

It read as a human tutorial, but the agent is the primary consumer. Rewritten as
directives: what to do, when to stop and confirm, and what to report back.

Follows Anthropic's authoring guidance: state what to do rather than narrating
why, since the body stays in context for the whole session and every line is a
recurring cost. Adds the decision points an agent needs and a troubleshooting
section keyed on the script's real error strings, so a failure maps to an action
instead of a retry.

* fix(templates): address the Cursor and CodeRabbit review

`flags.paid ?? id.startsWith('api_')` was a dead fallback, since every caller
passes an explicit boolean and `false ?? x` is `false`. So `--paid` on an
ordinary downloadable template wrote `sizeBytes: 0`, which defeats the
pre-install disk-space check. Now `||`, so an `api_` id is paid either way.

Publishing had no concurrency group, so two merges in quick succession raced to
overwrite the same objects and the older list could win. Queued, not cancelled:
every merge must publish.

The drift check is documented as a warning but runs under `bash -e`, so a
`die()` from an unreachable index failed the step outright, reddening unrelated
PRs. It now warns and exits clean.

`npx wrangler@3` resolved a mutable dependency tree with the write-capable R2
token in scope; pinned to 3.114.1.

Every outbound call now carries connect and max timeouts, `urlopen` a timeout,
and both jobs a `timeout-minutes`, so a stalled endpoint cannot hold a runner to
the six-hour limit.

The GCS probe only proved it could create an object. Publishing overwrites, and
a create-only credential would pass the probe then fail the publish, so it now
overwrites and reports whether delete works.

* fix(templates): type the flags and catch hand-edited fields

`check()` accepted a non-boolean `recommended`. The app reads it with `=== true`,
so a string "true" published clean and then left the tab with no auto-pick.
Both flags are now type-checked.

`validate` only confirmed an id still existed upstream, so a hand-edited title,
description, size or mediaSubtype passed CI and shipped, which is exactly what
the "never hand-edit the JSON" rule forbids. A shrunken sizeBytes also makes the
install-time disk-space check under-count. It now regenerates each entry from
the live index and reports any field that differs.

Three tests discarded the command's exit status and then asserted a count the
committed list already satisfied, so they passed whether or not the feature
worked. They assert success first now, and the two new rules have their own
cases: reverting either fails a test.

* docs(templates): add the H1 markdownlint expects

MD041 wants a top-level heading before any prose.

* fix(templates): stop the validator crashing on malformed input

A malformed entry threw a TypeError instead of being reported. `null` in the
array crashed on `.id`, a numeric title crashed on `.trim()`, and a non-array
`templates` crashed because validate iterated it after check() had already
returned. All fifteen hostile shapes now give a clean refusal.

The hand-edit check reported upstream drift as an error, which would have
reddened every open PR the moment upstream retitled a template. That is the
exact case the workflow deliberately downgrades to a warning, so it warns and
exits clean now.

The GCS upload was writing to standalone-environments/assets/, because the
action preserves the parent directory unless told otherwise. The verify step
would have failed on the mirror after R2 was already overwritten.

Concurrency moved from workflow level to the publish job. Shared with the PR
runs, a queued publish could be displaced by a later PR, which is the opposite
of what the group was added for.

Also: two entries missing an id both hashed to undefined and read as
duplicates; a prototype member like `toString` resolved as a command and exited
0 having validated nothing; the mediaSubtype allowlist rejected valid upstream
subtypes an editor cannot change; the verify curl had no timeouts and aborted
the retry loop under `bash -e`; and the cryptography import is now checked, so a
missing package cannot masquerade as a bad credential.

The test harness kept stderr, so a refusal can be asserted on its reason rather
than on any non-zero exit.
* content(templates): swap image slots 3 and 4

Put PixelDiT ahead of SDXL Turbo in the image picker. Same four
templates, same featured and paid cards; only the order of the two
free cards changes.

* test(templates): assert the slot rule, not the exact order

The round-trip snapshot pinned all four ids per tab, so any content
swap reddened validate until the fixture was hand-updated in lockstep.
Only slots 1 and 2 are load-bearing (recommended auto-pick, then the
paid card); the free cards may reorder freely. Assert that rule against
the shipped list instead, so a slot 3/4 swap needs no spec change.

* docs(templates): teach the skill about the spec fixture and slot rule

The skill said to commit only the JSON, but a content change also
touches the spec when slot 1/2 need re-checking. Document that only
slots 1 and 2 are pinned (recommended, then paid) and that a free-card
reorder needs no fixture edit.
…-Org#1480)

* feat(announcement): swap the launch modal to Comfy Cloud nodes

Reuses the announcement modal the MiniMax license launch shipped, which
was written to be reused by swapping the id and copy. Changes the
telemetry id, the CTA destinations and UTM campaign, the hero media and
the i18n namespace, and adds announcement.cloudNodes.* in both locales.

Uses a NEW cloudNodesAnnouncementSeen setting rather than resetting
minimaxAnnouncementSeen, so everyone who dismissed the previous
announcement still gets the bell for this one. The old key stays defined
because it is already persisted on users' machines.

Renames the CTA key to primaryCta: it now reads "Get Started" and a key
called requestLicenseCta holding that string is a trap for the next
person.

Copy is draft and the hero video and poster do not exist yet; both paths
404 until someone captures a Comfy Cloud node running on canvas.

* fix(announcement): correct cloud-nodes highlight bullet

The third highlight claimed "Nine nodes at launch across image, video
and audio." The nodes shipping in ComfyUI#15935 are sixteen, covering
image and video only (no standalone audio-generation node). Drop the
fragile count rather than swap in a new one likely to go stale again
before this merges.

* feat(announcement): point the cloud-nodes modal at the launch film

Swap the placeholder hero paths for the published launch video and poster on
media.comfy.org, and correct the third highlight now that nine nodes ship at
launch (image, video and audio, not just image and video).

* fix(announcement): correct the cloud-nodes count to eight

get_node_list() in comfy_api_nodes/nodes_comfy_cloud.py registers eight of the
nine node classes the file defines; the reference-to-video node is commented
out pending a server-side fix, so it never reaches the node library.

* fix(announcement): say the cloud nodes are beta before the CTA

Every node ships a [BETA] suffix in its display name, but the modal never said
so, which meant a user could only find out after spending credits. State it in
the lead, with what beta actually means here (the node set is still changing)
rather than a bare label that reads as unreliable.

* fix(announcement): match the website CTAs and fix the cta telemetry

The primary CTA pointed at cloud.comfy.org, the same bug the landing page had:
it sent people to the hosted product this copy tells them they do not need, and
on Desktop they already have a local ComfyUI. The pair now mirrors the website.

  Get Started   -> comfy.org/cloud-nodes
  Read the Docs -> docs.comfy.org/cloud-nodes/overview

Both keep the Desktop UTM so the clicks stay attributable.

Separately, the primary CTA still reported itself as 'request_license' in
telemetry, left over from the MiniMax announcement this component was adapted
from. A Cloud Nodes launch has no licence to request, so every primary click
would have landed in analytics under the wrong name. It reports 'get_started',
and learn_more becomes docs to match the label.

* fix(announcement): 16:9 media, lighter copy, beta as fine print

The hero was cropped to a square. The wrap was height:100% inside an auto-fit
grid with object-fit:cover, so the cell decided the shape and the 16:9 film lost
its sides. It carries aspect-ratio 16/9 now and centres in its column.

Copy, from reading it in the running app:
- open models -> open weight models, which is what these are
- the node count came out; it was detail the modal did not need
- Get Started -> Read More, since the button opens the landing page
- beta moves out of the lead and sits under the actions as fine print, so the
  disclosure is still there without being the second thing you read

Test ids were still announcement-learn-more and announcement-request-license
from the MiniMax modal this was adapted from. Nothing referenced them.

* fix(announcement): move the beta note above the actions

It read as a footnote under the buttons, after the decision point. Above them
it is the last thing you read before choosing, which is where a disclosure
belongs.

* fix(announcement): stop the stacked layout clipping the CTAs

CodeRabbit on Comfy-Org#1480: below 720px of card width .announce-grid drops to one
column, and the 16:9 media I added is then taller than the whole 916/445
card on its own, so .announce-content's overflow:hidden ate the copy and
both CTAs with no way to scroll to them.

Measured in Chromium against the component's own CSS. At 760x700 the
primary CTA sat at y=815 inside a card ending at y=513; at 640x560, which
is an 800x700 panel at 1.25 zoom, y=711 against 417.

Two changes. In one column the card ratio goes away and the media takes
whatever the copy and CTAs leave, cropping via the object-fit: cover it
already has, so nothing scrolls. And .announce-grid scrolls unconditionally,
which covers the case the width query cannot see: a wide but short panel is
also one column, because the card width is height-derived there.

After: both CTAs visible without scrolling at 1280x800, 800x600, 760x700 and
640x560, and reachable by scrolling at 880x440. Two-column geometry is
byte-identical.

---------

Co-authored-by: Deep Mehta <deep@comfy.org>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* test: unmount panel components before DOM teardown

* test: keep panel teardown fix minimal
* fix(linux): reject x64 runtimes on arm64

* fix(linux): align ARM64 ToDesktop resource paths
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: caae066e-7da6-4994-89d5-a1b92be7065e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​semver@​7.7.11001007483100

View full report

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.