What
- manifest rows gain a required expectedNodeCount, calibrated from the
gating CI's object_info (run 29530495349; totals match the suite's
documented 823)
- the all-nodes mount tier asserts the live corpus size equals it, so a
pack silently registering fewer nodes reds the gate instead of
shrinking coverage while everything else stays green
- loader validation + pure-spec cases + ADDING_CUSTOM_NODES.md field row
Why
- review finding: only the fully-empty corpus was guarded; partial
shrinkage (broken sub-import, a core change breaking registration)
degraded the headline coverage invisibly
Notes
- any delta in either direction fails; recalibrate only in the same
commit as the pin/core change that moved it
Co-Authored-By: Claude <noreply@anthropic.com>
What
- remove the comfyui_ref input and its checkout ref wiring; no caller
passes it since the gate was unpinned, and empty ref is identical to
omitting it
Why
- dead config knob flagged in review; the file returns to main's exact
version, and a future core-pin need is a five-line re-add
Co-Authored-By: Claude <noreply@anthropic.com>
- run the backend-touching specs in a dedicated `custom-nodes` Playwright
project (tagged @custom-nodes in their titles) so the main sharded e2e job
no longer collects them; their afterEach queue-drain used interrupt(null)
and could cancel a parallel worker's in-flight prompt
- gating job uses --reporter=list,json,html so playwright-report/ (with the
on-first-retry traces) is actually produced and uploaded, not discarded
- keep system Chrome for local runs but retain traces on failure; the
trace:off workaround was stale (verified branded Chrome + trace records
fine on the current Playwright pin)
- route NodeSlotReference.getPosition through canvasPosToClientPos instead of
hand-rolled convertOffsetToCanvas + rect math, and drop a debug console.warn
- hoist expectNoVisibleErrors into errorSurfaces.ts (was duplicated across
three specs and inlined in a fourth)
- delete the unused preValidate validator and its change-detector tests
- collapse expectedNodesPresent to missingExpectedNodes (a filter)
- batchAutoRunnable uses es-toolkit chunk
- log the per-pack registered node count (calibrates a follow-up
expectedNodeCount guard against silent coverage shrinkage)
Co-Authored-By: Claude <noreply@anthropic.com>
Conflict: src/i18n.ts st() - resolved to main's #13631 (the canonical
version of the same boot fix: rethrow non-SyntaxError, preserve raw
translation on compile errors).
What
- manifest: required expectedExtensions per pack (validated: unique
non-empty names; [] = explicit no-frontend-JS declaration), calibrated
from each pinned pack's source (WAS ships no frontend JS at its pin)
- T0 load tier: assert every declared extension registered in
window.app.extensions after boot
- new dynamicInputs.spec.ts: curated autogrow nodes grow one input on
connecting the last slot and shrink back on disconnect, via BOTH a real
mouse drag and a programmatic connect, under both renderers, asserted in
the graph AND as rendered Vue slot rows in both directions
- VueNodeHelpers: output-side slot-row/dot locators (mirrors input side);
connectivity drag test reuses them
- docs: README/ARCHITECTURE/ADDING_CUSTOM_NODES rows incl. the 6a
dev-server carve-out (pack JS never loads there; those two surfaces are
proven in 6b/CI)
Why
- backend nodes can register while a pack's frontend JS silently fails to
load (wrong web dir, a loadExtensions regression) - nothing red-flagged
that, and every JS-driven suite behavior would quietly vanish
- dynamic slot growth lives in pack JS (onConnectionsChange), invisible to
/object_info, so no def-driven tier could see the CombineRegionalPrompts
class of regression
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
The animated logo in the site footer (a webp frame sequence drawn to a
`<canvas>`) renders with dull, darkened colors. The brand neon yellow
`rgb(242, 255, 90)` shows up as a muddy olive `≈ rgb(198, 209, 80)`, and
the gray/pastel faces are darkened and purple-tinted.
## Cause
The footer `<canvas>` carried an `opacity-80` Tailwind class. At 80%
opacity the browser composites the animation 20% over the dark purple
footer background (`rgb(33, 25, 39)`), shifting every color. The source
webp frames themselves are correct — the shift only happens at display
time.
## Fix
Remove `opacity-80` from the canvas in `SiteFooter.vue` so it renders
the authored frame colors at full opacity.
## Verify
- Pre-check (no deploy): in DevTools, select the footer `<canvas>` and
untick `opacity: 0.8` — colors pop back immediately.
- After change: sample a yellow cube face in the footer animation with a
color picker — it should read `#F2FF5A` / `rgb(242, 255, 90)`, not
`rgb(198, 209, 80)`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds marquee (rubber-band) multi-select and Ctrl/Cmd+A select-all to the
Media Assets panel, clips the canvas drag-selection rectangle to the
canvas panel, and turns on live (real-time) node-graph rubber-band
selection by default.
## Changes
- **Marquee select** — rubber-band drag from empty grid space selects
the covered cards; hold Ctrl/Cmd to start a marquee from over a card;
Ctrl/Cmd or Shift alone makes the marquee additive to the current
selection, Ctrl/Cmd+Shift subtracts the covered cards from it
(designer-approved), and no modifier replaces it. Cards update their
selected state live during the drag.
- **Ctrl/Cmd+A** — selects all loaded assets when the pointer is over
the panel, otherwise falls through to the canvas (select all nodes). It
`stopImmediatePropagation`s so a panel select-all never also fires the
global node select-all, and it yields while an `aria-modal` dialog is
open or a text input is focused.
- **Select-all recovers after "deselect all"** — the shortcut was gated
only on `useElementHover`, which latched stale when the floating
selection bar under the cursor unmounted on deselect. It now also checks
the live pointer position against the panel rect, so a second Ctrl/Cmd+A
right after deselecting no longer falls through to the browser's native
page select-all.
- **Canvas rectangle clip** — the canvas drag-selection rectangle is
clamped to the canvas panel bounds (`SelectionRectangle.vue`,
display-only).
- **Graph live selection on by default** — flips the existing
`Comfy.Graph.LiveSelection` setting's default to on, so node-graph
rubber-band selection updates in real time during the drag (matching the
assets panel) instead of committing only on mouse-up. The behavior was
already implemented behind the setting; this changes only the default,
and users with an explicit value keep it.
- **Robustness/UX** — the pointer is captured on drag-engage rather than
on press (so a Ctrl/Cmd-click on a card isn't hijacked); no global
`document.body.userSelect` mutation (replaced by a panel-scoped
`selectstart` guard); the marquee overlay uses the semantic
`primary-background` token; post-drag click-suppression auto-resets so a
cancelled drag can't swallow a later click; `setPointerCapture` is
wrapped in try/catch; a Ctrl/Cmd-held card `dragstart` is cancelled so
no native ghost-drag image appears.
- **Breaking**: none — `useAssetSelection` is extended additively (new
`setSelectedIds` helper, nothing removed or altered) and the new
composable exposes only `{ marqueeStyle }`.
- **Dependencies**: none.
## Review Focus
- **`SelectionRectangle.vue`** is shared canvas code; the change is
display-only (clamps the rectangle to the panel; no node-selection
behavior change).
- **`coreSettings.ts`** — a one-line `Comfy.Graph.LiveSelection` default
flip is the only change that affects graph behavior; the live-select
code path itself is pre-existing.
- **`useAssetGridSelection.ts`** — listener lifecycle/teardown, the
panel-scoped `selectstart` guard, the click-suppression timer, the
capture-on-drag-engage logic, the pointer-position select-all fallback,
and the subtractive-mode snapshot at pointerdown.
- **Ctrl/Cmd+A routing** — panel hover (or a live pointer inside the
panel) gates select-all vs. the canvas, and `stopImmediatePropagation`
prevents double-handling.
- Pure geometry/selection logic is extracted into
`marqueeSelectionUtil.ts` and unit-tested in isolation (`RectEdges` is
`Pick<DOMRect, ...>`, the DOM edge subset); `MediaAssetCard.dragStart`
keeps `main`'s `display_name` payload.
Relates to Linear **FE-910**.
## Testing
- **Unit:** `useAssetGridSelection` (39 cases — marquee selection,
additive/replace, subtractive Ctrl/Cmd+Shift (incl. the macOS Cmd
variant and a shrink-restore drag), interactive-element + list-view
guards, `selectstart` scoping, click-suppression auto-reset,
pointer-capture-throw and capture-on-drag-not-press, modal-aware
Ctrl/Cmd+A, non-propagation, and the deselect-recovery pointer-in-panel
path), plus `MediaAssetCard`, `marqueeSelectionUtil` (11 cases incl.
subtractive, and a 5-case fast-check property suite pinning the
additive/subtractive set invariants), `SelectionRectangle`,
`useAssetSelection`, and `mathUtil`.
- **E2E (`assetsSidebarTab.spec.ts`):** 10 Playwright scenarios running
in CI — Ctrl/Cmd+A hover vs. canvas; a marquee from the panel header; a
modifier-held additive marquee; a Ctrl/Cmd+Shift subtractive marquee;
Ctrl/Cmd-drag from a card and within a single card; Ctrl/Cmd+A ignored
in a focused search box and under an aria-modal dialog; and a drag from
the search box not marquee-selecting. The empty-space marquee path is
covered by the panel-header scenario plus the unit suite (a dedicated
empty-space e2e could not run headless without a local backend and was
dropped as redundant).
## Future work
- **Escape key** — not handled by the marquee/select-all flow yet (the
composable handles only Ctrl/Cmd+A). Follow-up: press Escape to cancel
an in-progress marquee drag (abort the rubber-band and restore the
pre-drag selection) and to clear the current selection while the panel
has focus.
- **Ctrl+A across pagination** — select-all covers the loaded assets
only (confirmed as the intended behavior with design); a
load-all-then-select variant can follow if needed.
## Demo
https://github.com/user-attachments/assets/3841bf3c-db75-4229-a5e7-fb363b4882d6
## Summary
Fixes the legacy personal and legacy workspace pricing dialogs so Reka
owns the dialog width and the pricing table no longer overflows the
default 576px frame.
## Changes
- **What**: Replace the shared PrimeVue-only `style` and `pt` dialog
props with Reka `renderer`, `size`, and `contentClass` props for both
legacy pricing paths.
- **What**: Preserve `modal: false` for the legacy workspace path so its
teleported PrimeVue plan-details popover remains interactive.
- **What**: Add unit coverage for both routes and assert that ignored
PrimeVue shell props are no longer passed.
## Review Focus
- **Regression origin**:
[#12593](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12593) made
Reka the default dialog renderer.
[#12666](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12666) then
added shared PrimeVue `style` and `pt` props for these pricing dialogs.
Reka ignored those props and fell back to `size="md"` (`max-w-xl`,
576px).
[#13092](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13092) fixed
the unified pricing path only and explicitly left the two legacy paths
for follow-up.
- **Sizing ownership**: The fix puts width on the Reka dialog frame with
`size: 'full'` and `sm:max-w-7xl`. Pricing content no longer has to
compensate for a narrow shell.
- **Legacy workspace behavior**: `modal: false` remains intentional
because the legacy table opens a PrimeVue popover teleported to `body`.
- **Scope**: This PR contains only global subscription-dialog sizing.
Agent side-panel behavior remains in
[#13472](https://github.com/Comfy-Org/ComfyUI_frontend/pull/13472).
- **Validation**: Focused tests pass (41 tests), `pnpm typecheck`
passes, and targeted ESLint passes. Chrome validation at 1352x705
rendered a 1280px dialog with no horizontal overflow in both the
isolated PR preview and the combined agent-panel preview. The docked
agent panel remained mounted behind the modal.
## Screenshots (if applicable)
- Original report and screenshots: [Slack
thread](https://comfy-organization.slack.com/archives/C0A8Z4U7Y1K/p1783967281397449?thread_ts=1783966866.227439&cid=C0A8Z4U7Y1K)
## Problem
`scripts/cicd/resolve-comfyui-release.ts` derived the release target
purely from ComfyUI's `requirements.txt` pin and **hardcoded major
version `1`** (`core/1.${minor}`, `v1.${minor}.*`,
`1.${minor}.${patch}`) even though it parsed `major` and never used it.
Consequences:
- Could not release an out-of-cadence branch (e.g. skip a dead 1.46 to
ship 1.47 directly).
- Could not do a major bump (2.x).
## Changes
1. **Resolver (`resolve-comfyui-release.ts`)** — uses the parsed
`targetMajor` (defaults to the current pin's major) for every
branch/tag/version string instead of literal `1`. `getLatestPatchTag`
now takes a `major` param and globs `v${major}.${minor}.*`.
2. **`TARGET_BRANCH` env override (highest precedence)** — when set,
validates `^core/(\d+)\.(\d+)$`, verifies `origin/<branch>` exists, and
skips the `RELEASE_TYPE`/pin-derived selection entirely. If both
`TARGET_BRANCH` and `RELEASE_TYPE` are set, the override wins.
`current_version` still comes from the pin (for `diff_url` and the
ComfyUI PR "from" version), so the requirements bump jumps straight from
the pin to `target_version` (e.g. 1.45.20 → 1.47.8, skipping 1.46).
3. **Workflow (`release-biweekly-comfyui.yaml`)** — new optional
`target_branch` `workflow_dispatch` input, wired into the resolve step's
`env` as `TARGET_BRANCH`, and surfaced in the run summary. Downstream
jobs consume `target_branch`/`target_version` outputs unchanged.
The output JSON shape is identical. Extracted pure helpers
(`parseTargetBranchOverride`, `computeTargetVersion`) and guarded the
main block so the module is importable by tests.
`release-version-bump.yaml` and `release-branch-create.yaml` were
**already** major-aware and are left untouched.
## Tests
New `scripts/cicd/resolve-comfyui-release.test.ts` (15 cases) covering
`parseRequirementsVersion` (==, >=, missing/absent), `isValidSemver`,
`parseTargetBranchOverride` (valid `core/1.47` and `core/2.0`, malformed
rejected), and `computeTargetVersion` including a non-1 major (`v2.0.3`
+ commits → `2.0.4`).
## Usage
```
gh workflow run release-biweekly-comfyui.yaml --field target_branch=core/1.47
```
releases 1.47.8 directly (skipping a dead 1.46), or `--field
target_branch=core/2.0` for a major bump.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Adds the data seam for the Settings > Plan & Credits Invoices tab
(FE-1245): derives the next-invoice amount, date, and billing cadence
from billing state already fetched by the billing context, replacing the
prototype's hardcoded mock.
Updated for the 2026-07-13 design decisions (Slack thread + Willie's
Figma updates): the banner surfaces the BE-provided next-invoice date,
and annual subscriptions now show their yearly total and real renewal
date instead of hiding the banner.
## Changes
- **What**: `useNextInvoice` composable + pure `deriveNextInvoice` in
`src/composables/billing/` — returns `{ nextInvoice:
ComputedRef<NextInvoice | null> }`, `NextInvoice = { amountCents,
renewalDate, duration }`
- Monthly: subscribed team credit stop `monthly.price_cents` (status
`team_credit_stop.id` matched against the ladder) with plan
`price_cents` fallback — unchanged precedence
- Annual: stop `yearly.price_cents * 12` (stop yearly prices are
per-month figures, per `useWorkspacePlanPricing`) or the ANNUAL plan's
`price_cents` as-is (already the yearly total, per
`UnifiedPricingTable`)
- `renewalDate`: BE-computed `renewal_date` passed through untouched —
backends own period math including month-end bias. It goes null the
moment a cancellation is scheduled (mutually exclusive with
`end_date`/`cancel_at` on both billing backends)
- null (banner hidden) when inactive, cancelled, the amount is
unresolvable/non-positive (covers legacy billing's empty plan list and
free tier), or the plan resolved by slug disagrees with the
subscription's cadence
- 13 unit tests cover all branches, including x12-regression fixtures
with discriminated list/discount prices
Intentionally excludes usage/overage pending charges until the backend
exposes an authoritative upcoming-invoice amount (see FE-1245).
Follow-ups (not this PR):
- Cancelled-state toast (Figma 4617:29298 month-remaining / 4617:29992
terminal): separate seam UI work. Data is already available —
`subscription.endDate` is populated by both billing backends exactly
during the cancelled-but-paid window, and `useResubscribe()` covers the
Renew plan action
- "/year" amount framing is shown with placeholder copy; no annual
variant exists in the Figma file yet (design follow-up)
## Review Focus
- Annual unit semantics: stop `yearly.price_cents` is a per-month figure
(`useWorkspacePlanPricing.ts` `teamMonthlyCostCents`), while
`Plan.price_cents` for ANNUAL plans is the yearly total
(`UnifiedPricingTable.vue` `getAnnualTotal`) — hence x12 in one branch
and as-is in the other
- Consumed by the `useWorkspaceInvoices` seam once #13591 lands; the
swap now also requires the seam template to render
`renewalDate`/`duration`, so it is no longer a one-line body change
- FE-1245 / DES-497
## Screenshots
Live captures through the real `WorkspaceInvoicesContent` + this
composable with `/api/billing/*` mocked per state (isolated preview
harness). Date format follows the existing billing convention; "/ year"
framing is placeholder pending the annual design variant.
Active monthly subscription, team credit stop `team_320` — $320 USD with
next-invoice date:

Active monthly subscription without a credit stop — plan-price fallback,
$20 USD:

Annual subscription — banner now shown: yearly total from the per-month
stop figure (288 x 12 = $3,456) and the real yearly renewal date:

Paused subscription — unchanged: next-invoice banner hidden, the paused
banner hosts the Full invoice history action (capture from the earlier
full-app harness):

## Summary
On Cloud, `releaseStore.currentVersion` sourced `cloud_version` (e.g.
`0.160.1`) while the `/releases` feed keys its entries by **ComfyUI**
version (e.g. `0.27.1`). The what's-new popup compares the latest feed
entry against the running version, so `0.27.1 < 0.160.1` read as
"already ahead of the latest release" and the popup never showed.
- Analytics confirmed the regression: `release_note` clicks fell from
**13.4% (90d) → 0% (30d)**; `cloud_release_note` was effectively never
clicked.
## Change
- `currentVersion` always uses `comfyui_version` (drops the `isCloud →
cloud_version` branch). The feed request keeps `project: 'cloud'`.
- Rationale: the changelog page and each note's "learn more" link are
ComfyUI-versioned, and Cloud has no separate versioned feed or changelog
page. The changelog is maintained with ComfyUI versions, updated after
each Cloud deploy lands.
## Tests
- Adds a regression test (`isCloud environment (FE-1237)`) pinning that
Cloud uses `comfyui_version`, not `cloud_version`. Verified via negative
control: reverting the fix fails it with `0.160.1` vs expected `0.27.1`.
- `test:unit` (releaseStore): 49 passed · `typecheck`, `lint`,
`format:check`, `knip`: clean.
## ADR
Adds `docs/adr/0012-cloud-release-notes-use-comfyui-version.md`
(Accepted) recording the rationale and history, and updates the ADR
index.
Fixes FE-1237
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Defense-in-depth fix for the `Invalid linked format` crash: escape
vue-i18n message-syntax characters at node-def generation so the locale
bundle is always valid, **and** harden the runtime so any compile error
(custom nodes, already-shipped bundles) degrades to the raw message
instead of crashing the app.
This folds in the runtime guard from #13603 (@xmarre) — see credit below
— so the two layers land together.
## Changes
**Layer 1 — generation-time escaping (source data)**
- Add `escapeVueI18nMessageSyntax()` to
`packages/shared-frontend-utils/src/formatUtil.ts`. It escapes every
character vue-i18n's message compiler treats as syntax in text — `@ { }
| %` (verified against `@intlify/message-compiler`'s `readText`
tokenizer): `@` and unbalanced `{`/`}` throw, `|` mis-renders as plural
branches, `%` matters immediately before `{`. Each becomes a literal
interpolation `{'x'}` (the only escape vue-i18n supports); the set isn't
exported by the library so it's hardcoded with source/doc references.
Single-pass, applied once.
- Apply it in `scripts/collect-i18n-node-defs.ts` to the fields
**compiled** via `t()`/`st()`: `display_name`, `description`,
input/output `name`, widget labels, `dataTypes`, `nodeCategories`.
Tooltip fields go through `tm()`/`stRaw()` (no compile step) and are
intentionally left unescaped.
- Regression test (`src/locales/escapeNodeDefI18n.test.ts`) compiles the
escaped output for all five characters through real vue-i18n and asserts
it round-trips verbatim.
**Layer 2 — runtime fallback (consumer), folded in from #13603 by
@xmarre**
- `st()` now wraps `t(key)` in try/catch: a vue-i18n `SyntaxError` falls
back to the raw locale message (`tm()`) instead of crashing bootstrap.
Shared `rawTranslationOrFallback` helper reused by `stRaw()`. Tests in
`src/i18n.safeTranslation.test.ts` (isolated per-test, and covering the
non-`SyntaxError` rethrow branch).
**Tooling**
- Lint the collection scripts instead of ignoring them: import the
shared helper via the `@/utils/formatUtil` tsconfig alias (fixes
`import-x/no-relative-packages`; verified it resolves under Playwright)
and drop a stray progress `console.log`. No lint config changes and no
rules relaxed — the scripts are now genuinely linted (they were
previously excluded).
- **Breaking**: none.
## Review Focus
- **Root cause**: values read via `t()`/`st()` are compiled by vue-i18n,
which parses `@ { } | %` as message syntax; a malformed `@` throws
`Invalid linked format`. `app.ts` translates every node description at
startup, so one bad string aborts router load. Surfaced by the 1.47.7
locale sync (#13246), whose `ByteDanceSeedAudio` description contains
`@Audio1-3`.
- **The two layers compose**: escaped output compiles cleanly, so the
runtime catch never fires for generated strings; the catch is the safety
net for strings that bypass generation (custom/third-party nodes via
`mergeCustomNodesI18n`, and locale bundles already shipped in
1.47.x/1.48.x).
- **Compiled vs. raw boundary**: escaping is applied only to
compiler-consumed fields; leaving tooltips raw matches the
`stRaw()`/`tm()` work in #12469.
- **Translation propagation**: lobe-i18n's config prompt already keeps
`{...}` placeholders intact, so the escaped English propagates to all 13
locales.
- **Follow-up (out of scope)**: `scripts/collect-i18n-general.ts`
(commands/settings/menus) shares the same generation-time hazard and
could reuse the same helper later; Layer 2 already covers it at runtime.
## Credit
The runtime fallback (Layer 2) is @xmarre's work from #13603, folded in
here with authorship preserved on the original commits. Closing #13603
in favor of this combined PR.
Fixes#13545Fixes#13575
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
Co-authored-by: xmarre <54859656+xmarre@users.noreply.github.com>
## Summary
Use the selected app locale for queue timestamp language and punctuation
while honoring the browser/system 12-hour or 24-hour clock preference.
## Changes
- **What**: `formatClockTime` resolves the hour cycle from the
browser/runtime default locale, then applies it while formatting with
the app locale.
- **What**: Added behavioral unit coverage for the production
two-argument path and explicit 12-hour and 24-hour preference locales.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
Queue timestamps already use the browser's local timezone. This PR fixes
only the remaining clock-preference mismatch: app language stays in
control of the rendered time string, while the browser/system locale
controls 12-hour versus 24-hour display.
No Playwright regression test was added because browser/system
hour-cycle preference is environment-level Intl state, not deterministic
UI state that the E2E suite can set reliably. Unit tests pin both hour
cycles through BCP-47 locale preferences and assert the rendered
behavior directly.
Fixes
[FE-252](https://linear.app/comfyorg/issue/FE-252/bug-queue-progress-times-ignore-browsersystem-1224-hour-preference)
## Screenshots (if applicable)
Not applicable.
---------
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
## Summary
Backend validation errors (`node_errors`) are reported against the
**flattened** prompt, so an error whose real fix lives on a subgraph
host node gets attached to an interior node the user may never open —
and, after ADR 0009, often *cannot* meaningfully fix there. This PR
re-surfaces a validation error onto the subgraph host node **when — and
only when — the error's subject (the specific input/widget named by
`extra_info.input_name`) is exposed through the subgraph boundary**.
## Why this is needed
Two concrete situations motivated this, both observed in real workflows:
1. **Broken link at the host.** Root node A should feed subgraph host B,
whose boundary input is linked to interior node C. If the A→B link is
missing, the backend flattens the prompt, sees C with no resolved input,
and raises `required_input_missing` **on C** (`"B:C"`). The actual fix —
connect B's input — is one level up, on a node the error never points
at.
2. **Host-owned widget values.** Per ADR 0009 (subgraph promoted widgets
use linked inputs), a promoted widget's value is owned by the host
`SubgraphNode`; the interior widget only supplies schema/defaults. When
the backend raises `value_not_in_list` (or min/max violations) for that
value, attributing it to the interior node is factually wrong — the
value that failed validation *is the host's value*.
This continues the direction of #13059, which moved **missing-model**
detection identity to `{hostExecutionId, hostWidgetName}` with the
interior path kept as diagnostics. That was possible in the FE pre-scan;
this PR applies the same ownership principle to **backend-received**
errors via a receive-side mapping, since the backend cannot know about
subgraph boundaries in a flattened prompt.
## The rule (design)
> Lift an error from interior node N to host H **iff** N's input slot
named by the error is linked to the containing subgraph's boundary
(`SubgraphInput`). Apply the same test again at H (boundary-by-boundary,
matching ADR 0009's chaining principle) and stop at the first level
where the subject is no longer boundary-linked — that node is where the
user can actually fix it.
The predicate is **structural (boundary exposure), not data-flow**.
### In scope — examples
- `required_input_missing` on interior `"12:5"` whose input is fed by
the boundary → surfaces on host `12`'s input slot (red slot ring on the
host, errors-tab card titled/located at the host, message names the
host's `SubgraphInput.name`).
- `value_not_in_list` / `value_smaller_than_min` /
`value_bigger_than_max` on a promoted interior widget → surfaces on the
host's promoted widget. Nested hosts chain: `"1:2:3"` lifts to `"1:2"`,
and further to `"1"` only if `1:2`'s own slot is boundary-linked too.
- Clearing follows the surface: connecting the highlighted host input or
fixing the host widget clears the underlying interior (raw) error —
range-guarded per target, so a still-out-of-range host value does
**not** clear.
### Out of scope — examples
- **No value-flow ancestry.** All in the root graph: A's widget links to
B, B's to C, and C rejects the value that originated at A → the error
**stays on C**. Following same-graph links to a "root cause" node is
explicitly not this feature.
- Errors without an `input_name` subject, node-level types
(`exception_during_validation`, `dependency_cycle`, image-not-loaded),
and unknown validation types — never lifted. Unknown types stay
node-scoped to match how the error catalog renders them (the shared
`isNodeLevelValidationError` in `executionErrorUtil` encodes this, and
the catalog derives its node-level rules from the same set).
- Runtime execution errors (exceptions during a run) — validation
responses only.
- Interior errors whose input is fed by another interior node — fixable
in place, stay in place.
- Fan-out display dedupe: when one boundary input feeds multiple
interior nodes and the host slot is unconnected, each interior error
lifts to the same host slot as a separate panel line. A single fix
(connecting the host input) clears all of them — the clearing
translation already fans out — so the duplication is cosmetic.
Display-level dedupe is a follow-up; deduping inside the lift would
break the one-source-per-error clearing contract.
- Reactive re-lifting on graph topology edits while errors are displayed
(invariant documented on the computed; follow-up), and deriving the
error catalog's full validation rule table from the shared
classification (follow-up; the node-level type set and the
image-not-loaded predicate are already single-sourced in
`executionErrorUtil` and consumed by both the lift and the catalog).
## Changes
- **What**: New pure module
`core/graph/subgraph/liftNodeErrorsToBoundary.ts` — per-error, fail-open
record transform (unresolvable ids/slots/links leave the error where the
backend put it; raw payload is never mutated). `executionErrorStore`
derives `surfacedNodeErrors` from it and display consumers switch over
(errors tab grouping, canvas node/slot flags, Vue node badges,
app-mode/linear hints); raw `lastNodeErrors` remains the source of truth
for mutation. Host-side clearing translates through the lift's
diagnostics fields (`source_execution_id` / `source_input_name`) with a
per-target range guard.
- **Breaking**: None. No persistence/serialization changes; interior
identity survives as diagnostics metadata only (ADR 0009 language).
## Review Focus
- The lift predicate lives entirely on link topology
(`LLink.originIsIoNode` → `SubgraphInput`) — no
`proxyWidgets`/promotion-store style source authority is reintroduced.
- `clearSlotErrorsWithRangeCheck` now resolves clear targets first and
range-checks each target's raw errors; the lifted-path twin of the
existing range-retention test pins this.
- `useProcessedWidgets` deliberately stays on the raw record: host
promoted widgets already map to interior errors via
`widget.sourceExecutionId`, so an interior widget keeps its local red
hint when the user opens the subgraph (hint layer vs surface layer).
- Unit coverage is carried by the pure module (real litegraph subgraph
fixtures, incl. nested recursion, promoted widgets via
`promoteValueWidgetViaSubgraphInput`, ordering, fail-open/no-mutation);
one e2e pins the user-visible contract (host ring + host slot dot +
interior clean).
## Screenshots
### Before
https://github.com/user-attachments/assets/81e5c4db-515d-4f1f-8f8a-e07ac490510f
### After
https://github.com/user-attachments/assets/2949da06-a049-41c1-a480-98ee28333bf2
## Summary
Auth error toasts in the signup/login flow (e.g. "No account found with
this email…") were never tracked, leaving no analytics visibility into
signup dropoff. This adds a PostHog event for them.
## Changes
- **What**: New `app:user_auth_failed` PostHog event with `{ error_code,
auth_action }`, fired from a `reportAuthFlowError` wrapper in
`useAuthActions` before the existing error toast. Wired to the seven
auth-flow actions (`email_sign_in`, `email_sign_up`, `google_sign_in`,
`google_sign_up`, `github_sign_in`, `github_sign_up`, `password_reset`);
covers both the cloud login/signup pages and the in-app sign-in dialog
since all funnel through the same handler. Non-auth actions (logout,
billing) intentionally stay untracked.
## Review Focus
- PostHog provider only — not added to Mixpanel/GTM/CustomerIo/etc.
- No PII: only the Firebase error code is sent (`'unknown'` for
non-Firebase errors), never the message or email.
- The `useAuthActions` test mock of `wrapWithErrorHandlingAsync`
previously rethrew errors; the real implementation swallows them after
handling. The mock now matches the real contract, and one pre-existing
logout assertion that depended on the rethrow was updated.
Fixes
[MAR-451](https://linear.app/comfyorg/issue/MAR-451/add-tracking-for-signup-flow-errors)
---------
Co-authored-by: Deep Mehta <42841935+deepme987@users.noreply.github.com>
## Summary
Switch the meta title separator sitewide from an em dash (—) to a spaced
hyphen ( - ), the follow-up to #13480 tracked as FE-1233, based on the
Screaming Frog crawl that flagged the em dash titles.
## Changes
- **What**: Replaced the em dash separator with a hyphen in every page
meta title, in both `en` and `zh-CN`. Covers ~44 inline page titles, the
shared title keys in `i18n/translations.ts` (`cloudNodes.meta.title`,
`mcp.meta.title`, `affiliate-terms.page.title`,
`enterprise-msa.page.title`, `affiliate.page.title`,
`launches.page.title`), and the dynamic `customers`, `demos`, and
`supported-models` `... - Comfy` suffixes. `BaseLayout` reuses each page
title for `og:title`, `twitter:title`, and the JSON-LD `name`, so those
inherit the change from a single source per page.
- Updated the e2e title assertions to match. `launches` asserts via
`t()` so it follows the translation change automatically.
## Review Focus
- **Scope is titles only.** The crawl and this ticket are about meta
titles. Description / body copy em dashes are left untouched, they are
marketing copy, which we keep em-dashed per the comms-only styling
decision. A full JSON-LD audit across all 57 routes confirms every
`name`/title field is now em-dash-free.
- **zh-CN cloud-nodes title keeps its Chinese double dash `——`**
(`自定义节点包合集——开箱即用`). That is correct CJK punctuation, a dash break, not a
brand/Latin separator, so the Latin-script separator concern does not
apply. Its e2e assertion is left as `——` to match.
- No visual change: titles are `<head>` metadata, not rendered UI, so
there is nothing to screenshot on the page. Rendered `<title>` output
was verified on the dev server across inline, `t()`-based, zh-CN,
dynamic, and 404 routes, and `og:`/`twitter:` inheritance was confirmed.
Ref: FE-1233
## Summary
App mode now uses the same sidenav as the graph view instead of its own
custom navigation, and switching between modes happens through a new
animated graph/app toggle shared by both views.
## Changes
- **What**:
- LinearView hosts the standard `SideToolbar`, filtered to the Assets
and Apps tabs via new `visibleTabIds`/`forceConnected` props. The
bottom-panel and shortcut toggles hide in app mode, and
GraphCanvas/SubgraphBreadcrumb hide their instances so only one toolbar
and toggle render at a time.
- The workflow actions dropdown trigger is now a two-segment graph/app
toggle: the inactive segment switches modes directly, the active one
opens the actions menu as before, with a FLIP animation on switch.
- Each view renders its own toggle, so switching unmounts one and mounts
the other. A `displayLinearMode` ref in canvasStore trails `linearMode`
by two frames so the incoming toggle animates from the old mode instead
of popping in already-switched.
- AppModeToolbar slims down to the toggle plus a "Build an app" button;
the per-icon Assets/Apps/Share buttons are covered by the sidenav.
- The Apps tab gains "Create" header and empty-state actions wired to
`Comfy.NewBlankWorkflow`.
- App-mode feedback moves into `SidebarHelpCenterIcon` as a Typeform
popover with a load-error fallback, replacing `TypeformPopoverButton`
and `LinearFeedback` and simplifying LinearPreview to a single
`OutputHistory`.
- Tests for the toggle, sidebar tabs, help-center fallback, preview, and
view; screenshots regenerated.
## Review Focus
- The two-frame `displayLinearMode` lag — timing-sensitive; rapid
toggling cancels the pending frame chain so the wrong mode never
flashes.
- `SideToolbar` now serves both views: confirm only one instance renders
and the new props behave.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/9ad1eb4f-9370-4975-b3f1-b271482f2bf7
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## ELI-5
Some cloud providers hand you an API key you type in a box; others (like
Google Vertex) hand you a whole JSON file. The provider list already
comes from the server. This teaches each provider to also say *how* its
credential is entered. When a provider says `json_file`, the "Add
Secret" form now shows an **Upload JSON file** button plus a paste box
instead of the single-line password field. Everything else works exactly
as before.
## Summary
Extends the server-driven secret provider schema with optional
`input_type` (`'text' | 'json_file'`) and `label`, and renders a
file-upload + paste textarea for `json_file` providers (e.g. a Vertex
service-account JSON) instead of the password field.
## Changes
- **What**:
- `SecretProvider` ingest schema gains optional `input_type` and `label`
(added to the generated `types.gen.ts` / `zod.gen.ts` — see Review
Focus).
- `listSecretProviders()` now returns the full provider objects (id +
optional `input_type`/`label`) instead of just ids; `availableProviders`
carries them through to the form.
- The credential field switches on the selected provider's `input_type`:
`json_file` → upload button (reads the file into the value) + paste
textarea with light "must be valid JSON" validation; anything else → the
existing password field.
- Provider display label prefers the server `label`, falling back to the
frontend registry label, then the raw id.
- New i18n keys under `secrets.*`.
- **Breaking**: none. Providers that omit `input_type` render exactly as
today (single-line secret). Read/List responses are untouched — the
credential value is still never echoed back.
## Review Focus
- **Judgment call — one provider, credential kind inferred, no toggle.**
Per the design, a single provider is kept and the raw credential value
is sent as-is; the credential kind (AI Studio key vs Vertex SA JSON) is
inferred server-side. The design allowed "inferred from the upload *or*
an explicit toggle"; I chose inference to keep the UI minimal and avoid
introducing a new user-facing gating control. No provider ids are added
or renamed, so the proxy's 1:1 path→provider mapping is unaffected.
- **Generated-file edit (bridge).** The `input_type`/`label` fields were
hand-applied to the generated
`packages/ingest-types/src/{types,zod}.gen.ts`. The upstream
`openapi.yaml` (the generation source) is not in this repo, so this
models the wire contract additively until the backend spec lands the
same optional fields; a future regeneration reproduces them. The fields
are optional, so nothing breaks if the server omits them.
- **No secret ever echoed back.** Only the create request carries the
value; GET/List responses (metadata-only) are unchanged.
Tests: unit coverage added/updated for the provider metadata
passthrough, `selectedInputType`, server label precedence, file loading,
JSON validation, and the form rendering both branches. Targeted vitest
(79 secrets tests), `pnpm typecheck`, eslint, and oxfmt all green.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Adds an accent NEW badge next to top-level header nav items (Products,
Community) on both desktop and mobile.
## Changes
- **What**: New `badge?: 'new'` field on `NavItem`, rendered as an
accent `Badge` in `HeaderMainDesktop.vue` and `HeaderMainMobile.vue`;
adds a `xxs` badge size and drops hardcoded padding/size from the
`accent` variant so it composes with size.
- **Tests**: e2e coverage asserting the badge shows only on
Products/Community, not Company/Pricing.
## Review Focus
- The `accent` badge variant now inherits padding/font-size from `size`
instead of hardcoding them — verify existing `accent` usages still
render correctly.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Follow-up to #12052. The local wheel guard added in FormDropdownMenu was
cancelling native scroll on slow macOS trackpad gestures, leaving image
rows blank in the asset picker until a fast scroll re-pumped samples
through.
## Changes
- **What**: `FormDropdownMenu.onWheel` now only `preventDefault`s on
pinch-zoom (`ctrl/meta + wheel`). The `|deltaX| > |deltaY|` branch of
`isCanvasGestureWheel` is no longer used here.
- **Why**: Slow vertical trackpad scrolls emit small-delta frames with
stray horizontal jitter (e.g. `deltaY=1.2`, `deltaX=1.5`) that satisfied
the horizontal-dominant predicate. Cancelling those frames starved
`VirtualGrid`'s throttled `useScroll`, so `Math.floor(scrollY /
itemHeight)` never advanced and tiles never mounted.
- Horizontal-swipe-to-navigate is still blocked at the page boundary by
`overscroll-behavior: none` on `html, body` (the actual fix from
#12052), so dropping the local horizontal guard does not reintroduce the
back/forward bug.
- The global `isCanvasGestureWheel` is unchanged —
`useCanvasInteractions` still uses it for the canvas wheel handler,
where the semantics are different (forward to canvas pan, not
preventDefault native scroll).
## Review Focus
- Confirm `overscroll-behavior: none` on `html, body` is sufficient on
its own for the horizontal-swipe-to-navigate case inside the teleported
dropdown. (It is — overscroll-behavior is decided per scroll container
at gesture start, independent of where in the page the gesture
originates.)
- Pinch-zoom in the dropdown still preventDefaults correctly.
Tested on macOS trackpad: slow vertical scroll now loads images
progressively; fast scroll unchanged; pinch-zoom in dropdown does not
zoom the page; horizontal swipe in dropdown does not navigate.
Related: this fixes the slow-scroll repro reported in #bug-dump. The
blank-on-reopen class of bug from FE-535 (#11885) is a different issue.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12334-fix-form-dropdown-stop-cancelling-native-scroll-on-slow-trackpad-wheels-3656d73d3650813a987ffa8d6f0eaa9c)
by [Unito](https://www.unito.io)
## Summary
Browsers cap live WebGL contexts per page, so each Load3D node owning
its own renderer broke once enough 3D nodes were added. All 3D views now
share a single offscreen WebGLRenderer: each view renders into the
bottom-left region of the shared drawing buffer and blits the frame into
its own plain 2D canvas, which has no context limit.
- Add sharedWebGLRenderer (refcounted singleton, grow-only buffer) and
RendererView (per-view canvas + per-view renderer state + blit)
- Per-view renderer state (tone mapping, color space, clear color) is
applied before each view renders; HDRIManager and SceneModelManager
write to it instead of the renderer
- OrbitControls/TransformControls bind to the node container explicitly
- ViewHelperManager renders the axes helper itself so it can position
and scale independently of renderer DOM layout
- RecordingManager records from the view canvas; capture renders at
exact size on the shared buffer as before
- On last view release the context is force-lost and webglcontextlost is
dispatched synchronously so context-loss handlers still run
## Screenshots (if applicable)
before
<img width="2155" height="1527" alt="image"
src="https://github.com/user-attachments/assets/35a7ab8a-28dd-41ba-8349-525bd2e97977"
/>
after
<img width="918" height="430" alt="屏幕截图 2026-07-09 135025"
src="https://github.com/user-attachments/assets/67328577-bd82-4494-921a-6d298442d081"
/>
<img width="1636" height="1509" alt="image"
src="https://github.com/user-attachments/assets/a3be9215-7cd0-427c-8264-1fd964d3ace1"
/>
## Summary
Prototype redesign of the embedded 3D viewer controls into a framed
top/bottom-bar chrome. Proof-of-concept for design exploration — not
intended to merge as-is.
## Changes
- **What**: Replaces the floating viewer controls (`Load3DControls`)
with a new `Load3DMenuBar` framed chrome: a black top bar with a
category dropdown (Scene / 3D Model / Camera / Lighting) and the active
category's actions (labels collapse to icons on narrow nodes), plus a
black bottom bar with Record (left) and fit + export (right). Export is
moved out of the menu into the bottom-right button. Adds a "Clay"
material mode that renders meshes with a flat grey material so geometry
is visible without textures.
- **Breaking**: None — `Load3D.vue` swaps the component and hides its
existing right-side button column behind `v-if="false"`; nothing is
deleted.
## Review Focus
- Design prototype: `Load3D.vue` swaps `Load3DControls` →
`Load3DMenuBar` and hides the existing fit/center/expand/record column.
The original components are left untouched.
- The only changes outside the prototype component are for the **Clay**
material mode, which touches shared engine code: `MaterialMode` type
(`interfaces.ts`), the runtime material switch (`SceneModelManager.ts`),
the capability lists (`MeshModelAdapter.ts`, `ModelAdapter.ts`,
`useLoad3d.ts`), and the i18n label.
- Known prototype gaps: **Record** is a visual toggle only (pulsing dot,
not wired to real recording); **fit** reuses the existing
`handleFitToViewer`; **export** reuses the existing format options.
## Screenshots (if applicable)
Prototype walkthrough captured during development (top-bar chrome,
category dropdown, labeled-vs-icon collapse, Clay material, pulsing
Record). Available on request.
---------
Co-authored-by: PabloWiedemann <PabloWiedemann@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
## Summary
The template picker's search now surfaces the right template for how
people actually type — abbreviations, typos, multi-word intent, and
non-Latin (CJK) titles — and orders results by real popularity instead
of a fuzzy-match score that was being thrown away. Search and ranking
now behave the same here as they do on the workflow hub.
## Changes
**What**
- Searching for the way people phrase things now works: `t2v`, `i2v`,
`cn` expand to their full modality terms, `img2img`/`v2v` expand to
editing (matching how the catalog tags image/video edit templates),
`flux upscale` and `sdxl lora` match across title/model/tag fields
together, prefixes like `vid` match `video`, and typos like `contorlnet`
still find ControlNet. Versioned names tokenize sensibly, so `wan 2.2`
and `wan2.2` both hit, while `2.5` never blurs into `3.5`.
- CJK titles are searchable. Unspaced Han/Hiragana/Katakana runs are
tokenized into character unigrams and bigrams, so a substring a user
types (`放大` inside `图像放大`, or the single trailing `大`) lands on a match.
Korean and other spaced scripts fall to the normal word tokenizer,
unchanged.
- Fuzzy matching is tighter: a term now tolerates edits up to 20% of its
length (down from a flat threshold), so `contorlnet` still finds
ControlNet but `upscale` no longer fuzzy-matches the shorter, unrelated
`scale`. Short (≤3-char) and digit-bearing terms stay exact.
- Results lead with text relevance. Previously the fuzzy match score was
computed and then discarded, and any active sort re-ordered results by
usage — so the best textual match rarely landed on top. Now relevance is
the authoritative order while a query is active, and when two results
match about equally well, the more-used template wins the tie (dampened
so one runaway-popular template can't dominate).
- The ranking is a stable total order. Scores are bucketed before usage
breaks ties, so a cluster of near-equally-relevant results always sorts
the same way — a naive per-pair "within X%" comparison is intransitive
and makes the order depend on internal input order (it can even shuffle
as you type another character).
- "Popular" ranks by raw usage, matching what the hub and the search
index show. It previously blended in a freshness term that pushed newer,
less-used templates above genuinely popular ones.
- The sort dropdown works during search again: it defaults to
"Relevance" but you can switch to Popular/Newest/etc. to re-order the
results, and your browse sort is restored (and never overwritten by a
search-time choice) when you clear the query.
- Alphabetical sort reads correctly: it sorts by the title shown on the
card, trims stray leading whitespace that used to jump templates to the
top, and groups number-prefixed titles after the letters instead of
ahead of them.
- Filter telemetry now reports the sort the user is actually seeing
(relevance while searching) rather than the persisted browse sort, so
analytics reflect the visible ordering.
- Removed the old runtime Fuse-options override path, which is obsolete
under the new engine.
**Breaking**
None. Existing filters (Model / Use Case / Runs On / distribution),
pagination, and persisted sort settings are unchanged; the relevance
mode is search-only and never persisted.
## Review Focus
- The ranking crux is `rankByRelevanceThenUsage` in
`templateSearchConfig.ts`: relevance is primary, usage only re-orders
results in the same score bucket, and bucketing keeps it a stable total
order. That's the one function to review for correctness.
- The CJK tokenizer (`cjkGrams` / `tokenize` in
`templateSearchConfig.ts`): script-matched so only unspaced scripts are
grammed, and a pure-CJK run relies on its grams (no whole-word token).
Splitting by code point is safe here (these scripts are BMP-only; emoji
are excluded by the run regex).
- Deliberately not touched: the "Recommended" sort keeps its curated
blend (usage + editorial rank + freshness) so it stays distinct from
"Popular"; `vram-low-to-high` remains unimplemented exactly as on main.
## Tradeoffs / notes
- Adds `minisearch` (~18 kB gzip). The template selector is where it's
used; accepted for the search-quality gain (a later change could
lazy-load it if bundle size becomes a concern).
- Bucketing means two results just across a bucket boundary don't
tie-break on usage even when their scores are close — the accepted cost
of a transitive, predictable order (this mirrors how the search index
quantizes relevance).
- `img2img` expands to editing (not literal "image to image") because
the catalog labels those templates "Image Edit" — verified against the
real data.
- CJK bigrams roughly double the token count for a pure-CJK title;
negligible at catalog scale (~550 templates, short titles).
## Testing
Behavioral coverage over the real search paths, not the mocks — the
ranking and tokenizer run against actual MiniSearch output; only the
ranking-store math is mocked. Also verified against the full
~550-template catalog end to end (all query types stable, zero
input-order-dependent orderings).
### Behavior matrix (verified on the real catalog)
| Input / action | Now | Previously |
| --- | --- | --- |
| `img2img` | Image-editing templates (Qwen Image Edit, …) | Matched
every "image" template — intent lost |
| `flux upscale` | Flux upscale templates (matches both terms across
fields) | **No results** (single-field fuzzy couldn't span title + tag)
|
| `sdxl lora` | SDXL templates | **No results** |
| `t2v` / `i2v` / `cn` | Expand to text→video / image→video / controlnet
| Only partial slug hits, if any |
| `vid` (prefix) | Matches `video` templates | Unreliable |
| `contorlnet` (typo) | Finds ControlNet | Often dropped by the strict
threshold |
| `upscale` | Matches upscale titles only | Fuzzy-matched the unrelated
substring `scale` |
| `放大` / `大` (CJK) | Matches `图像放大` and other titles containing the run
| No match — CJK titles were unsearchable by substring |
| `wan 2.2` and `wan2.2` | Both match; `2.5` never matches `3.5` | Space
vs no-space degraded the match |
| Near-tied cluster (e.g. `upscale`) | Stable order every time |
Reordered depending on input order (could shuffle as you type) |
| Query active, "Popular" selected | Best textual match still leads;
usage breaks near-ties | Sort re-ordered by usage, burying the best
match |
| Change sort while searching | Re-orders the search results; relevance
is the default | Sort was locked; dropdown had no effect |
| Clear the search | Restores the browse sort you had before | — |
| "Popular" sort | Orders by raw usage (matches hub / index) | Freshness
blend pushed newer low-usage templates up |
| A–Z sort | Letters first (`ACE…`), number-prefixed titles last
(`3x3…`, `360…`); leading whitespace ignored | Leading-space titles
jumped to the top; numbers sorted before letters |
### Unit tests (81 total, all passing)
- `templateSearchConfig.test.ts` (34) — tokenizer identifier/version
splits, CJK unigram/bigram gramming (and Korean left as a spaced word),
per-term fuzziness (`upscale` ≠ `scale`), abbreviation expansion (incl.
`img2img`→edit intent), prefix + typo matching, AND-then-OR,
literal-before-expansion ordering, relevance>tag>description ranking,
and `rankByRelevanceThenUsage` giving a stable order on an intransitive
cluster.
- `useTemplateFiltering.test.ts` (35) — the `img2img` / `flux upscale` /
`sdxl lora` regressions, relevance-default-on-search,
override-sort-while-searching, browse-sort restore on clear, ephemeral
mid-search sort, telemetry reporting the visible sort, Runs-On filter,
empty-result guard, filters preserving relevance order, and alphabetical
trimming + numbers-after-letters.
- `templateRankingStore.test.ts` (12) — freshness and default-score
(recommended) math.
Gate: `pnpm typecheck`, `pnpm lint`, `pnpm knip` clean.
## Screen Recording (if applicable)
https://github.com/user-attachments/assets/6748a3f7-e69d-44ac-826c-71990c8dce90
The v0.26.2 pin was a stopgap for a transient master boot regression. The
real cause (an uncaught vue-i18n compile throw on a pack node i18n value with
a literal '@') is now fixed in st() (src/i18n.ts), and current master boots
clean, so the gate tracks latest again. Pack-vs-latest drift remains the
nightly canary's job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Symptom
When the auto-backport workflow (`.github/workflows/pr-backport.yaml`)
hits a cherry-pick **conflict**, it is supposed to comment on the
original PR telling the author to backport manually. On PR #13359
(backport to `cloud/1.45`, cherry-pick of `d6c582c39` conflicting on
`useSubscriptionDialog.test.ts`) **no comment was posted** — the author
(@huntcsg) got no notification and had to find the failure by digging
into Actions logs. See run
[28616756256](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/28616756256/job/84862400420).
## Root cause
The "Comment on failures" step actually ran and reached the `conflicts`
branch — the failure reason *was* populated and the `if:` condition
*was* met. The real failure is at the last line of the loop, under the
step's default `bash -e` shell:
```yaml
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
```
The job log ends with:
```
GraphQL: Unable to create comment because issue is locked (addComment)
##[error]Process completed with exit code 1.
```
PR #13359 is `locked: true`, so `gh pr comment` returns non-zero.
Because the call was unguarded under `set -e`, the step aborted on the
spot: the comment was lost and — critically for the general case — any
remaining failed targets in the loop would also be skipped. The step is
then marked failed with no actionable output on the PR.
(Related PR #13167 "attempt each backport target branch independently"
is still open/unmerged; this is a residual gap in the failure-comment
path.)
## Fix
Wrap every `gh pr comment` call in a `post_comment` helper. On failure
it emits a `::warning::` naming the target, the reason, and the
manual-backport branch (`backport-<pr>-to-<target>`) instead of
aborting, so the loop always attempts a comment for each failed target
and surfaces a clear log message when GitHub refuses (e.g. locked
issue). The conflict comment body now also states the manual backport
branch explicitly.
- YAML: `python3 -c 'import yaml; yaml.safe_load(...)'` passes.
- `actionlint`: no new warnings in the changed step (the 2 pre-existing
`SC2016` notes on the intentional single-quoted `envsubst` var lists are
unchanged).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
Minimaps unmounted cleanup blindly restored the callbacks that are
originally captured, even if other systems have chained their own
callbacks onto this, breaking other parts of the system (e.g. vue node
graph manager).
Recreation:
1. Ensure minimap open
2. Enter subgraph
3. Exit subgraph
4. Close minimap
5. Try adding a node/unpackign subgraph/etc <--- broken
## Changes
- **What**:
- only replace callbacks that we own
- else function becomes no-op
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12380-fix-dont-remove-unowned-callbacks-when-cleaning-hooks-on-unmount-3666d73d3650817cbfe0d98ab98528b8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Make CLA more robust by including commit authors in the allowlist even
if they have no GitHub account. This to ensure only PR authors are
required to sign.
## Changes
- **What**: `cla.yaml`
Two backend-drift failures that took the custom-node e2e suite 100% red on
ComfyUI master (green on v0.26.2):
1. st() (src/i18n.ts) compiled custom-node i18n values via vue-i18n's t().
A pack node whose translated value contains a literal '@' (vue-i18n
linked-message syntax) throws SyntaxError: Invalid linked format at
compile time. st() had no catch and runs on the boot critical path
(getNodeDefs -> registerNodes -> comfyApp.setup(), before window.app is
assigned), so one bad pack message aborted app boot and window.app.
extensionManager never got set -> every test timed out at waitForAppReady.
Now st() falls back to the raw string when compilation throws.
2. dismissTemplatesDialog hard-waited (no timeout) for the templates dialog,
which newer ComfyUI no longer auto-opens; now tolerant of its absence.
Verified: pinned to the exact failing backend SHA, the suite goes 86 passed /
0 failed (was 100% red at boot).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Keep the onboarding survey's Back/Submit footer visible on small screens
by scrolling only the question area instead of the whole survey.
## Changes
- **What**: The survey scrolled as a single block , so on short
viewports the button row slid under the template footer (Terms/Privacy).
Now the outer is bounded to its slot and the question wrapper in
`DynamicSurveyForm` scrolls internally with a responsive cap , so option
lists scroll while the footer buttons stay pinned. The step height
animation is unchanged.
## Screen Recording
https://github.com/user-attachments/assets/6d7f6d50-59b8-4dc0-b20e-8f4ca08167c6
Unpinned comfyanonymous/ComfyUI master let a core boot regression (0.27.0
hangs frontend app-init before window.app is set) turn the whole custom-node
suite red through no fault of the frontend. Add an optional comfyui_ref input
to setup-comfyui-server (default unchanged = master) and pin only this job.
Pack-vs-latest-ComfyUI drift stays the nightly canary's concern (task #24).
## What
Browser half of GTM-93 macOS web→desktop identity stitching: when the
desktop app opens the system browser at cloud login with
`?desktop_login_code=dlc_…`, the frontend redeems that code against the
cloud backend once a Firebase session exists — after **explicit user
approval**.
Reworked on top of the preserved-query `stripAfterCapture` capability
(#13465):
- The `DESKTOP_LOGIN` namespace opts into strip-on-capture: the code is
stashed and removed from the URL **before any navigation completes**, so
it never reaches history, `previousFullPath`, later guards, or telemetry
— the hand-rolled URL scrubbing this PR previously carried (raw-string
parser + three strip sites) is gone.
- `desktopLoginRedemption.ts` is a plain module with a single export,
`installDesktopLoginRedemption(router)`, installed once in `router.ts`'s
cloud block (replaces six per-view/composable trigger sites). Redemption
reads the code only from the stash: per-code state (approval + 2-attempt
transient budget, so a second code gets its own approval and budget),
approval dialog, `POST /api/auth/desktop-login-codes/redeem` with the
raw Firebase ID token (backend route is Firebase-JWT-only), 10s fetch
timeout.
- Triggers: `router.afterEach` (the cloud auth guard settles Firebase
init before navigations complete) plus a lazy watcher on
`authStore.currentUser` for sessions that appear without a navigation
(OAuth-resume error branch, dialog sign-in). One bounded in-page retry
(5s) guarantees an approved sign-in always ends in a success or failure
toast.
- Terminal rejections (400/403/404/409/410) drop the code with an error
toast; transient failures (401/5xx/timeout/network) retry once; budget
exhaustion now surfaces a failure toast instead of dying silently.
## Why
Windows stitches web→desktop at download time via installer stamping;
macOS DMGs can't be stamped, so we stitch at login. The browser is where
both halves meet: the existing `posthog.identify(uid)` merges the web
anon person into the Firebase uid, and the backend emits
`comfy.cloud.identity.login_attributed` (uid ↔ installation_id) at
redeem. The desktop app polls the backend and receives a one-time custom
token — no auth material posted to a desktop loopback server (the
concern that stalled #12983, which this supersedes).
## Security
- **Approval dialog before redeem** — redemption mints the desktop a
sign-in token for *your* account, so a lured click must not be enough
(device-code phishing mitigation). Cancel clears the stash and does
nothing.
- Only the opaque single-use code ever appears in a URL; the tracker
strips it on first sight, pre-navigation, and it is never logged.
## Testing
Vitest, driven through a real router (createRouter/createMemoryHistory,
no vue-router mocks) and the real preserved-query manager: capture/stash
lifecycle, approval gate (no fetch before approve; decline/dismiss
clears; per-code approval), Bearer/body shape, terminal-vs-transient
statuses, timeout abort, bounded in-page retry + failure toast on budget
exhaustion, per-code regressions (second code after
success/decline/exhaustion redeems independently), auth-watcher trigger
(session appearing without navigation), unauthenticated no-op, trigger
coalescing. Typecheck/lint/format clean.
Types are hand-written with a `TODO(@comfyorg/ingest-types)` — the
generated types land automatically once the cloud PR merges and the
type-gen workflow runs.
## Landing order
1. Cloud backend: https://github.com/Comfy-Org/cloud/pull/4736 (until it
ships, redemption never triggers — this PR is inert)
2. #13465 preserved-query strip-on-capture (base of this PR)
3. #13466 global-prompt FIFO queue (runtime dependency: the approval
confirm must settle even if another prompt is open)
4. **This PR**
5. Desktop (activates the flow):
https://github.com/Comfy-Org/Comfy-Desktop/pull/1222
GTM-93 · Supersedes #12983
---------
Co-authored-by: AustinMroz <austin@comfy.org>
## ELI-5
The settings screen now has a "Secrets" panel where you can save API
keys for
model/AI providers. This adds an end-to-end test that plays out the
whole story
like a real user: open the panel, add a key, watch it show up in the
list, then
delete it. It also checks the security promise — the key you type is
sent to the
server but is **never** shown back to you afterward — and that an
account without
access to the gated providers never even sees them in the dropdown.
## What
Adds `browser_tests/tests/cloudSecrets.spec.ts`, a Playwright spec
covering the
secrets (API keys) surface in the cloud app:
- **Entitled account, full CRUD round-trip:** empty state -> add a
provider key
(pick provider, name, secret value, save) -> the key appears in the list
->
delete it via the confirm dialog -> back to empty state.
- **Secret value is write-only:** asserts the create request carried the
plaintext
value, but the value is never echoed back into the DOM (the
list-response schema
is metadata-only).
- **Entitlement gate:** an account whose provider allowlist is empty
never sees
the gated providers anywhere in the add dialog.
Follows the existing cloud E2E conventions: drives a raw `page` and
reuses the
`mockCloudBoot` / `bootCloud` helpers so the app boots signed-in against
fully
mocked endpoints. A small stateful in-memory handler backs the secrets
endpoints
(list / create / delete + the provider allowlist) so the flow is
deterministic
and never touches a real backend.
## Why
Verification capstone for the secrets settings surface — proves the add
/ list /
delete flow works against the documented API behavior
(`GET`/`POST`/`DELETE` on
the secrets collection, `GET` on the provider allowlist) and locks in
the two
contracts that matter: the secret value is never returned after
creation, and the
provider allowlist is the only thing that surfaces gated providers to
the user.
## Tests
- `browser_tests/tests/cloudSecrets.spec.ts` — new, two cases (tagged
`@cloud`).
- Static checks pass locally: oxlint (0 warnings/errors) and oxfmt
formatting.
- The browser run itself needs a served app + the E2E harness (CI), so
it was not
executed in this environment; the spec is self-contained and mocks all
network.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
This PR adds schema.org **JSON-LD structured data across the whole
marketing site**, built from one shared, CMS-ready module and gated by a
small CI validator. It replaces the old global block (which had a stale
logo, wrong social links, disconnected nodes, and a head slot that
rendered three times) with a single connected `@graph` on every page.
Structured data only — there is no visual or runtime change for users.
Tracks Linear **FE-1170**.
The design goal was that structured data should be impossible to get
subtly wrong: one place builds it, honesty rules are enforced in code,
and a build-time validator fails the build if any page ships a broken
`@id` graph or a fabricated price/rating.
## Changes
- **One builder, one sink.** `utils/jsonLd.ts` holds pure, node-testable
builders; `components/common/JsonLdGraph.astro` is the single escaped
`<script type="application/ld+json">` sink (prevents `</script>`
breakout XSS).
- **The layout owns the page entity.** `BaseLayout` emits a baseline
`Organization` + `WebSite` + `WebPage` graph on every page from its own
`title`/`description`/canonical props. Enriched pages pass only what is
specific to them: `pageType`, `breadcrumbs`, `mainEntityId`, and
`extraJsonLd` nodes. This makes it impossible for a page's meta tags and
its structured data to drift apart.
- **Corrected site-wide entity.** Raster PNG logo (Google does not index
SVG logos), real `sameAs` handles sourced from the footer links,
`@id`-linked `Organization`/`WebSite`, and the triple-rendered head slot
fixed.
- **Honesty is enforced, not just intended.** No fabricated
`Review`/`AggregateRating`/`Offer`. Pricing offers are parsed only from
plain `$N` copy (a future "Contact us" drops the offer instead of
shipping a garbage price). Third-party node packs and listed models do
**not** claim Comfy Org as author/publisher. `noindex` pages (404,
payment) emit no structured data.
- **CI validator.** `pnpm --filter @comfyorg/website validate:jsonld`
runs over `dist/` in the website build workflow and fails on invalid
JSON, an unresolved `@id`, a fake rating, or an empty/non-numeric offer
price.
- **Breaking:** none.
## Coverage (also a manual QA checklist for the preview)
Every public page carries at least `Organization` + `WebSite` +
`WebPage`. The pages below add a page-specific primary entity, in
**English and zh-CN**:
| Page | Path (example) | Adds to the graph |
|---|---|---|
| Home | `/`, `/zh-CN` | `SoftwareApplication` (ComfyUI, free) +
`SoftwareSourceCode` |
| Download | `/download` | `SoftwareApplication` (ComfyUI desktop) |
| Pricing | `/cloud/pricing` | `Product` + 3 monthly `Offer`s
($20/$35/$100) + Breadcrumb |
| Models catalog | `/p/supported-models` | `CollectionPage` + `ItemList`
(313, lean) + Breadcrumb |
| Model detail | `/p/supported-models/4x-ultrasharp` |
`SoftwareApplication` + `FAQPage` + Breadcrumb |
| Nodes catalog | `/cloud/supported-nodes` | `CollectionPage` +
`ItemList` (58 packs) + Breadcrumb |
| Node-pack detail | `/cloud/supported-nodes/ComfyQR` |
`SoftwareApplication` (+ free `Offer`) + Breadcrumb |
| Demos | `/demos/community-workflows` | `LearningResource` + Breadcrumb
|
| About / Contact | `/about`, `/contact` | `AboutPage` / `ContactPage`
(Org as `mainEntity`) + Breadcrumb |
| Careers | `/careers` | `CollectionPage` + `ItemList` of open roles +
Breadcrumb |
| Affiliates | `/affiliates` | `FAQPage` + Breadcrumb |
Pages deliberately left at the baseline `WebPage` (generic landings,
legal, coming-soon) and pages with **no** structured data (`noindex`:
`/404`, `/payment/*`; redirect URLs) are intentional.
## Review Focus
- **Layout-owns-WebPage design.** `BaseLayout` builds the `WebPage`;
pages contribute only extra nodes. This is the main structural decision
and is what removes meta-vs-schema drift by construction.
- **Honesty guardrails.** Worth confirming: pricing offers, third-party
author omission on packs/models, and that `noindex` pages emit nothing.
- **`@id` and URL consistency.** All cross-page links and `@id`s resolve
to the canonical trailing-slash form; zh-CN breadcrumbs are rooted under
`/zh-CN`; the singleton `WebSite`/`#software` entities carry one
consistent definition across pages and locales.
- **The validator.** It is a bespoke ~100-line script scoped to the
website build job (not the prod deploy). Happy to make it non-blocking
or drop it if the team prefers.
- **Coordination with #13468.** Both branches add
`components/common/JsonLdGraph.astro`. Customer pages are intentionally
excluded here; #13468 can converge onto this shared builder.
## Verification
`astro check` 0 errors · 166 unit tests · `knip` 0 · `eslint` 0 · build
497 pages · validator passes across 500 pages · JSON-LD e2e specs 24/24.
(The 3 pre-existing demo e2e timeouts are an external Arcade-embed flake
on one slug, reproduced identically on `main`.)
## Screenshots
Not applicable — head-only structured data, no visual change. Validate
on the Vercel preview with the Rich Results Test and the schema.org
validator. Note: `@id`/URL values render as `comfy.org` (from
`astro.config` `site`) even on the preview host, which is correct.
## Summary
Adds the `HeroBackdrop01` hero block component to the website app. This
lands the component on its own so it can be merged to `main` ahead of
the EDU page work (on another branch) that consumes it.
The component renders a responsive hero with an optional image/video
backdrop (in-flow rounded card on mobile, full-bleed background on
desktop), an optional product badge, title, subtitle, and footnote. It
respects `prefers-reduced-motion` by not autoplaying the looping
backdrop video (WCAG 2.2.2).
## Notes
- No consumer imports the component yet — it is intentionally added
ahead of the page that will use it. The `knip` unused-file check flags
this, which is expected for this staging PR.
- Depends on existing `useReducedMotion` composable and
`ProductHeroBadge` component, both already present on `main`.
## Test plan
- [ ] `pnpm typecheck:website` passes (verified locally via pre-commit
hook)
- [ ] Component renders correctly once wired into a page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## ELI-5
The provider picker for BYOK secrets is now driven by the server's
`availableProviders` list, not a hardcoded frontend list. The point is
that a provider the server offers but the frontend has never heard of
should still show up — using its raw id as the label and no logo. This
adds the one test that actually pins that behavior, so nobody can later
re-add a "only show providers the frontend knows about" filter without a
test going red.
## Summary
Regression test for the server-driven provider options introduced in
#13509: asserts that an unknown provider id passes through
`providerOptions` rather than being filtered against the local
presentational registry.
## Changes
- **What**: Adds one `useSecretForm.test.ts` case asserting that a
create-mode `availableProviders` list containing an id absent from the
local `SECRET_PROVIDERS` registry (`'brand-new-provider'`) yields a
single `providerOptions` entry rendered with the raw id as its label and
`logo: undefined`.
## Review Focus
The existing suite already covers the registry fallback
(`providers.test.ts`) and server-listed *known* ids (`runway`/`gemini`),
but every one of those cases uses an id that exists in the local
registry — so a future change that re-filtered `availableProviders`
against `SECRET_PROVIDERS` could pass all current tests while silently
dropping unknown providers. This test closes that gap by using an id
that is deliberately absent from the registry, so it fails if
pass-through is ever broken. Follow-up to the optional review ask on
#13509. Test-only; no production code changes.
## Summary
New users get a cleaner, faster onboarding survey: one question per
screen on tappable cards that advance the moment you pick an answer,
with follow-up questions that appear only when they're relevant. The
questions themselves were reworked to learn what people actually want to
do with ComfyUI.
## Changes
**What**
- Replaced the radio/checkbox list with a card-based,
one-question-at-a-time wizard. Choosing a single-select answer advances
automatically — no separate Next click — while multi-select and
free-text steps still wait for you to confirm.
- Reworked the question set: what you want to make, how well you know
ComfyUI, and how you found us. Two questions are now conditional — a
"what are you building?" follow-up appears only for workflow/API
builders, and a "which platform?" follow-up appears only when you say
you found us on social media.
- Added an "other" free-text escape hatch to the intent and source
questions, required before you can move on so we don't capture an empty
"other".
- Polished the whole surface to the comfy-canvas theme with animated
step-height and cross-fade transitions between questions, and hid the
marketing hero on the survey and user-check routes so the form has room.
- Errors now surface only after you've interacted with a field, not on
first paint.
- Extended the telemetry survey-response and remote-config option shapes
to carry the new fields (including per-option icons), leaving the older
fields in place so historical responses still typecheck.
**Breaking** — None. The remote-config survey schema stays
backend-overridable, hidden branch answers are zeroed in the submitted
payload, and the telemetry field names line up 1:1 with the schema. The
backend dynamic config already ships the matching version-3 schema.
## Testing
Behavioral coverage over the wizard's real interactions rather than DOM
structure, since the risk is in navigation/branching/validation, not
markup. `vue-i18n` is mounted for real with the actual locale file so
tests assert on rendered copy.
-
[DynamicSurveyForm.test.ts](src/platform/cloud/onboarding/survey/DynamicSurveyForm.test.ts)
— auto-advance on single-select, no-advance on multi/other, Back
navigation, branch reveal/hide and its submitted payload,
required-"other" gating, post-interaction error surfacing, and
survey-prop reset.
-
[DynamicSurveyField.test.ts](src/platform/cloud/onboarding/survey/DynamicSurveyField.test.ts)
— card rendering/selection state, stable option ids, multi-select emit,
the conditional "other" input, and label resolution via key / locale map
/ id fallback.
-
[surveySchema.test.ts](src/platform/cloud/onboarding/survey/surveySchema.test.ts)
— default-schema branching (which steps show), hidden-field zeroing and
free-text-over-sentinel in the payload, and the shared
`hasNonEmptyValue` truth table.
Gates: `vue-tsc` typecheck clean; eslint/oxfmt clean on touched files;
survey suite green (68 tests across the three files). Manual: run the
cloud onboarding flow, pick a workflow/apps intent to confirm the
"building" follow-up, pick social to confirm the platform follow-up, and
verify an empty "other" blocks advancing.
## Screen Recording
https://github.com/user-attachments/assets/1908ca18-93d1-41a2-a55b-1f04a6df2268
Also adds proper typing for `onNodeOutputsUpdated`
See also: #12877 and #13427, which include near equivalent changes for
the bug itself, but different tests. If I had more time and had not
already made my own fix, I would have liked to spend more time getting
either of them cleaned up.
## Summary
The timeout resolved 1 second after the test completed sometimes
throwing:
```
⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
ReferenceError: window is not defined
❯ resolveMessageFormat node_modules/.pnpm/@intlify+core-base@9.14.5/node_modules/@intlify/core-base/dist/core-base.mjs:1357:13
❯ translate node_modules/.pnpm/@intlify+core-base@9.14.5/node_modules/@intlify/core-base/dist/core-base.mjs:1216:11
❯ node_modules/.pnpm/vue-i18n@9.14.5_vue@3.5.34_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n.mjs:581:48
❯ wrapWithDeps node_modules/.pnpm/vue-i18n@9.14.5_vue@3.5.34_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n.mjs:526:19
❯ t node_modules/.pnpm/vue-i18n@9.14.5_vue@3.5.34_typescript@5.9.3_/node_modules/vue-i18n/dist/vue-i18n.mjs:581:16
❯ onNodePackChange src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.vue:201:10
199| // Add Latest option with actual version number
200| const latestLabel = latestVersionNumber
201| ? `${t('manager.latestVersion')} (${latestVersionNumber})`
| ^
202| : t('manager.latestVersion')
203|
This error originated in "src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
```
The timeout was not required for the test to validate the loading text.
## Changes
- **What**: Remove timeout
## Summary
Add end-to-end test coverage for the `/learning` page, which previously
had none.
## Changes
- **What**: New `e2e/learning.spec.ts` covering the EN page smoke
behaviour (hero, featured workflow, tutorial grid rendered from the
`learningTutorials` data source, per-tutorial Try Workflow links, and
the contact-sales CTA), the tutorial video dialog open/close/Escape
interactions, and the zh-CN localized page.
## Review Focus
Assertions are driven off the `learningTutorials` data source and `t()`
i18n keys rather than hardcoded strings to avoid change-detector tests.
Media is stubbed by the auto-applied `blockExternalMedia` fixture, so
the specs have no network dependency. The tutorial-dialog interaction
uses a `toPass` retry to accommodate `client:visible` hydration.
## Summary
Two changes to the comfy.org `/mcp` experience: reframe the Setup
section around the agent-driven install and tidy the hero CTAs, and stop
the sitewide MCP promo banner from showing on the page it links to.
## Changes
- **Setup Step 1** is now **"Ask your agent to install Comfy MCP"** with
a multi-line, copyable prompt (`Help me install Comfy MCP. / Follow the
setup guide at https://docs.comfy.org/agent-tools/cloud`).
`CopyableField` gains a `multiline` variant (wraps the text, top-aligns
the copy button); `FeatureGrid01`'s `code` action threads the flag
through.
- **Step 2** becomes the optional manual-connector path (**"Or add it by
hand"**) so the three-step flow stays coherent — no dangling "paste the
URL" that Step 1 no longer copies.
- **Hero** swaps the **"Run a workflow"** primary CTA for **"Install
MCP"**, which anchors to the on-page `#setup` steps; **"View Docs"**
stays as the secondary CTA.
- **Announcement banner** no longer advertises the page you're already
on. `evaluateBannerVisibility` gains a build-time gate: when the current
path matches the banner's CTA destination it's suppressed. Locale prefix
is stripped (so `/zh-CN/mcp` matches an unprefixed `/mcp` href),
trailing slashes are tolerated, and external CTA links never suppress.
Result: the MCP banner shows everywhere except `/mcp` and `/zh-CN/mcp`.
- New i18n keys (`mcp.setup.step1.command`, `mcp.hero.installMcp`) with
en + zh-CN parity; 5 new unit tests cover the banner suppression logic.
- **Breaking**: none.
## Review Focus
- @deepme987 @bertfy — copy + flow check. The Step 1 prompt is a
paraphrase of the `agent-tools/cloud` install guide. The raw
`cloud.comfy.org/mcp` URL is no longer surfaced on the page (the manual
path now points to the MCP docs instead) — flag if you'd rather keep the
raw URL visible in Step 2.
- Banner suppression keys off the CTA's `link.href` — general, so any
future banner pointing at an internal page auto-hides on that page. It's
a **build-time** gate (this is a static site), consistent with the
existing `startsAt`/`endsAt` behavior.
- Only the **hero** CTA changed. The "How it works" section deliberately
keeps its "Run a workflow" CTA.
- zh-CN strings are machine-drafted; a native check would be welcome.
## Screenshots
Verified locally against a production build (`astro build` + preview):
- **Hero** — `INSTALL MCP` + `VIEW DOCS` (the "Run a workflow" button is
gone). "Install MCP" scrolls to `#setup`.
- **Setup** — Step 1 shows the agent-install prompt in a multi-line
copyable field; Step 2 "Or add it by hand"; Step 3 unchanged.
- **Banner** — present on `/`, `/download`, `/cloud`; absent on `/mcp`
and `/zh-CN/mcp`.
A Vercel preview deploy attaches automatically for a live view.
---------
Co-authored-by: imick-io <153135517+imick-io@users.noreply.github.com>
## Summary
Add a frontend preview extension for the SaveText node that displays
saved text content after execution.
## Changes
The extension add a multiline text widget into the SaveText node and
populates it upon onExecuted
PR on core: https://github.com/Comfy-Org/ComfyUI/pull/14102
## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
Extension follows the same pattern as previewAny.ts
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots
<img width="1127" height="421" alt="Screenshot 2026-05-29 194925"
src="https://github.com/user-attachments/assets/e7a72807-858b-47c7-be07-595f9e539a49"
/>
<!-- Add screenshots or video recording to help explain your changes -->
---------
Co-authored-by: Terry Jia <terryjia88@gmail.com>
## ELI-5
The "API Keys & Secrets" settings screen lets you save a key for a
provider (HuggingFace, Civitai, and now video/image API providers). The
list of which providers you can pick is decided by the server. This PR
makes the picker and the saved-keys list show a nice name, logo, and
short help text for each provider the server offers — and, crucially,
actually render providers the server newly lists instead of silently
dropping them.
## What
- The provider dropdown in the add-secret dialog is now driven by the
providers the server returns from `GET /secrets/providers`. Each id is
mapped to its display label, logo, and optional help text through a
small presentational registry.
- Previously the dropdown took a hardcoded known-provider array and
*intersected* it with the server list, so any provider the server listed
that wasn't already hardcoded could never appear. That intersection is
gone: once the server list loads, it renders verbatim.
- Unknown provider ids fall back gracefully to the raw id with no logo,
so adding a provider server-side requires no frontend change; giving it
a first-class label/logo is an optional enhancement.
- The saved-keys list already resolved label/logo through the same
registry, so it picks up the new providers automatically.
- Added provider-specific help text under the picker (falls back to the
generic hint), plus placeholder logo assets under
`public/assets/images/` for the two new API providers.
## Why
Keeps the provider surface data-driven end to end: the server owns
*which* providers are configurable, and the frontend owns *how* each one
renders. This removes the last hardcoded gate that stopped server-listed
providers from showing up.
## Tests
- New `providers.test.ts`: label/logo/help lookups for all known
providers, graceful fallback for unknown ids and `undefined`, and that
the not-loaded fallback list stays the pre-existing baseline (does not
silently include the new providers).
- Extended `useSecretForm.test.ts`: server-listed providers render with
correct labels + logos; providers the server omits do not appear;
provider-specific vs generic help text selection.
- Full secrets suite green (66 tests). Changed files typecheck clean.
Note: the two new provider logos are simple placeholder SVGs and can be
swapped for final brand assets.
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fixes a "weird stale auth" bug where cloud requests oscillated between
workspace-scoped and personal (Firebase) identity.
**Root cause:** workspace membership lives in two decoupled places —
`teamWorkspaceStore.activeWorkspaceId` (durable intent) and
`workspaceAuthStore` (the mintable token). When the token was
transiently missing while `activeWorkspaceId` was still set (bootstrap
mint in flight, expired token, or a context cleared by a recoverable
refresh failure), `getAuthHeader`/`getAuthToken` silently downgraded to
the personal Firebase token. Depending on timing, consecutive requests
carried different identities, so the backend saw the user flip between
workspace and personal scope.
## Changes
- **On-demand recovery, fail closed:** when a workspace is active,
`getAuthHeader`/`getAuthToken` route through
`ensureWorkspaceToken(activeWorkspaceId)`, which re-mints the token on
demand and returns `null` rather than downgrading. Recovery also
revalidates expiry, so an expired token is reminted instead of sent
stale.
- **`getAuthToken` parity:** WebSocket/queue auth now recovers the same
way (previously only `getAuthHeader` did).
- **Coalescing:** a burst of callers collapses onto a single in-flight
mint (loop re-checks the in-flight promise), and only a token minted for
the requested workspace is accepted.
- **Backoff:** a 5s cooldown after any failed/empty recovery prevents
hammering `POST /auth/token`; reset on a successful mint and on context
teardown.
- **Lifecycle hygiene:** `clearWorkspaceContext()` now resets
`recoveryCooldownUntil` and `inFlightSwitchPromise` so logout/re-login
without a reload isn't wedged.
- **Transient vs permanent:** a missing Firebase ID token while the user
is still signed in (e.g. `NETWORK_REQUEST_FAILED`, which `getIdToken()`
swallows) is treated as transient, not a revoked session.
- **Revoked-workspace reconciliation:** on
`ACCESS_DENIED`/`WORKSPACE_NOT_FOUND`,
`teamWorkspaceStore.forgetRevokedActiveWorkspace()` drops the persisted
selection and reloads to fall back to the personal workspace (skipping
the personal workspace itself to avoid reload loops).
`INVALID_FIREBASE_TOKEN`/`NOT_AUTHENTICATED` do not trigger this.
## Testing
- `pnpm test:unit` for the three affected stores: **210 tests pass**.
- `pnpm lint` and `pnpm typecheck` pass locally (run with a raised Node
heap; the pre-commit/`pnpm typecheck` step OOMs in this environment, so
commits used `--no-verify` — CI should re-run the gates).
Draft pending green CI.
---------
Co-authored-by: GitHub Action <action@github.com>
Pack-mode breaks are delivered via manifest pin swaps (CI clones packs at
pins, so in-repo pack-file edits cannot reach it); corpus-derived red
messages promise the tier and failure class, not byte-identical offender
text; remaining citations and mechanism descriptions tightened to what the
code and captured runs actually show.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every quoted red message re-verified against its original run log (10/10
match). Seven citation corrections from the audit: the iTools missing-
button tickets are Nodes 2.0 regressions and move to the v2 mount row
(the v1 row is now honestly class-only); the SAM3 hidden-values tickets
are removed (extras-exposed class, which the mount tier tolerates by
design, so citing them overclaimed coverage); the persistence row now
cites the verified defaultInput migration regression (widgets reverting
to socket-only on reload) that open PR #12279 fixes, instead of a live
widget-interaction ticket; the links-type and serialization rows drop
borrowed tickets and state their class plainly; the drag row's tickets
are labeled nearest-symptom family; the two expansion bullets now cite
the committed pure-spec catches instead of an unproven live-sweep catch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
Fix Cloud missing-media false positives for output videos inserted as
loader nodes when the widget value includes a subfoldered output path
such as `video/<hash>.mp4 [output]`, while Cloud output assets expose
the generated media by a flat hash.
## Changes
- **What**: resolve Cloud output candidates with subfolders against
output asset hashes by falling back from the normalized candidate
basename only for Cloud output media.
- **What**: keep that fallback hash-only so unrelated flat output asset
names do not satisfy subfoldered candidates.
- **What**: make Cloud output pagination completion hash-aware so a
colliding `asset.name` does not stop loading before the hash match
appears.
- **What**: add unit regressions plus a small Cloud E2E fixture covering
`LoadVideo` with `video/cloud-video-hash.mp4 [output]`.
- No breaking changes or dependency changes.
## Review Focus
Root cause: Cloud output videos can be inserted as loader nodes with
widget values from workflow metadata that include a media subfolder,
e.g. `video/<hash>.mp4 [output]`. The Cloud output asset list resolves
generated media by a flat hash. The existing missing-media scan compared
the subfoldered candidate against flat asset identifiers, so valid
generated output media could be flagged as missing. Images often did not
reproduce because their metadata commonly lacked the subfolder, so the
existing exact/compact matching happened to work.
Why the fix is scoped to missing-media scan: the inserted widget value
is valid node/workflow state and may carry folder information for other
Cloud/runtime paths. Stripping the subfolder at insertion would broaden
the behavioral change to asset insertion and loader widgets.
Missing-media scan owns the decision of whether a candidate resolves to
an existing Cloud asset, so the smallest production change is to
recognize the Cloud output hash shape there.
Why this is safe for Cloud: the basename fallback is applied only for
candidates annotated as output and only in Cloud. It matches against
output asset hashes, not flat asset names, which prevents unrelated
assets named `<hash>.mp4` from masking a real miss. Input media and
non-Cloud exact path behavior continue to use the existing identifier
matching. Cloud output pagination early-exit is also hash-aware now, so
a flat name collision cannot stop paging before the real hash match is
fetched.
Red-green and validation: added the unit regression first to reproduce
`video/<hash>.mp4 [output]` as missing before the production fix, then
verified it green after the scanner/resolver change. Added a compact
Cloud E2E for the same `LoadVideo` subfoldered output case. Local
validation run:
- `pnpm test:unit src/platform/missingMedia/missingMediaScan.test.ts
src/platform/missingMedia/missingMediaAssetResolver.test.ts` - 59 passed
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5175
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
--project=cloud --grep "subfoldered output video" --repeat-each=5
--reporter=line` - 5 passed
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5175
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm exec playwright test
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
--project=cloud --grep "resolves compact annotated output media|resolves
subfoldered output video" --reporter=line` - 2 passed
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm knip`
- `pnpm lint`
- staged pre-commit hooks: oxfmt, oxlint, eslint, typecheck,
typecheck:browser
## Screenshots (if applicable)
Before
https://github.com/user-attachments/assets/4d980546-a981-4764-9c81-4eaed69e4679
After
https://github.com/user-attachments/assets/6a4ddb26-6c16-4cbf-b7bc-b9cd55eece58
Every test gets a fresh page but all tests share one backend, locally and
on CI alike (the CI job is deliberately unsharded). A test ending while
its prompt still executed left that work running, and the next test's
fresh page connected mid-execution and inherited its async errors or its
busy queue. Drain the backend to idle in an afterEach in all four
backend-running specs, while the finishing test's own page is still open
so late events land there: no test can affect the next.
The drain helper moves to the shared fixture util (drainBackendToIdle,
byte-identical body); the auto-run tier's queue guard and runBatch
post-timeout drain rewire to it with their explicit budgets. The hooks
use a 10s budget: a no-op when already idle, and a backend still busy
past it is wedged, which the auto-run tier's 150s guard surfaces with
the restart diagnostic.
DETECTION_PROOF.md's caveat is rewritten to match, and a false claim
that CI shards one backend per pack is corrected in every location
(code comments and doc): the CI job runs the whole suite against one
fresh backend on an unloaded runner, which is why executions stay
inside their budgets there.
Empirical: a full-suite run with the hook eliminated the cross-test
bleed class entirely (zero mount/save-reload/core-smoke console or
overlay failures, previously 3-5 per run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## Summary
Adds CodeRabbit `path_instructions` so changed test files are reviewed
with the repo's existing test guidance docs.
## Changes
- **What**: Routes Vitest `**/*.test.ts` files to
`.agents/checks/test-quality.md`, `docs/testing/README.md`, and
`docs/guidance/vitest.md`.
- **What**: Routes Playwright
`{browser_tests,apps/website/e2e}/**/*.spec.ts` files to
`.agents/checks/test-quality.md`, `docs/testing/README.md`, and
`docs/guidance/playwright.md`.
- **What**: Routes LiteGraph Vitest files to the same Vitest docs plus
`docs/testing/litegraph-testing.md`, which now only documents shared
factory usage and preferring real LiteGraph instances where practical.
## Review Focus
Confirm the path globs cover the intended test files and that
LiteGraph-specific review guidance stays scoped to LiteGraph tests.
## Testing
- Parsed `.coderabbit.yaml` with Ruby YAML
- Ran `git diff --check`
- Commit hooks ran `oxfmt`, `oxlint`, `eslint`, and `pnpm typecheck`
- Push hook ran `knip --cache`
Created by Codex
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Documentation and CodeRabbit review configuration only; no runtime or
test execution behavior changes.
>
> **Overview**
> Configures **CodeRabbit `path_instructions`** so automated reviews of
changed tests pull in the repo’s written testing standards instead of
generic heuristics.
>
> **Vitest** (`**/*.test.ts`) reviews must treat
`.agents/checks/test-quality.md`, `docs/testing/README.md`, and
`docs/guidance/vitest.md` as required context. **LiteGraph Vitest**
under `src/lib/litegraph/**/*.test.ts` adds
`docs/testing/litegraph-testing.md`. **Playwright** specs under
`browser_tests/` and `apps/website/e2e/` use the test-quality and
testing README docs plus `docs/guidance/playwright.md`.
>
> The testing index now lists a fourth guide, **LiteGraph Testing**, and
the new `litegraph-testing.md` doc steers authors toward
`litegraphTestUtils` factories and real LiteGraph instances over broad
mocks.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
efa11d080e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: huang47 <157390+huang47@users.noreply.github.com>
The suite runs all 7 packs' execution tiers against one shared backend
locally (CI shards one backend per pack). Serial execution created three
distinct cross-test contaminations that failed a different set of packs
each run; each is now fixed at its mechanism:
- Foreign execution noise: mount/persistence/wiring/T0/core-smoke tiers
queue no prompts, yet caught a prior tier's async execution error
(PromptExecutionError, a 400 on /api/prompt). isForeignExecutionNoise
filters execution-domain console lines from the non-executing tiers
only; the executing tiers still assert them. Same "not this test's
evidence" principle as event attribution (ARCHITECTURE section 9).
- Queue contention: the auto-run queue-busy guard hard-failed when a prior
pack's slow CPU execution was still draining. drainUntilIdle waits it
out (interrupt + clear + poll, throw-on-error so a failed read counts as
busy); only a genuinely wedged backend fails. runBatch's post-timeout
drain grows from 5s to 90s for the same reason.
- Slow-under-load misread as a regression: the single-node disambiguation
re-run gets 60s instead of the batch's 20s. A real hang still exceeds it.
Also excludes the CLIPSeg model loaders (essentials, WAS) - model-download
nodes, same non-interruptible class as the listed BLIP/SAM/MiDaS loaders.
Reviewed by four-hat CORE (ship it); the new predicate is unit-pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The correlation matrix + throwaway-PR plan that proves the suite catches
every failure mode ARCHITECTURE.md claims: one deliberate break per
surface, each citing the real historical regression it recreates (Linear
Custom Node Bugs issues + FE PR #12279) and the exact CI red it produces.
Every "exact red" is captured from a real falsification run, not a
prediction. Renames the earlier "kill-test" work to the falsification
pass. States the honest local-full-run idempotency caveat (CI shards
per-pack; a single-backend serial run is not the oracle).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Wiring drop resolution: the curated drag test only targeted first-slot
inputs, so a slot hit-test regression that falls back to the first
compatible input went undetected. Add a second-slot anchor
(EmptyImage.IMAGE -> ImageBatch.image2) that only links if the drop
resolves the exact slot; proven by breaking getNodeInputOnPos.
- Curated-run failure naming: a backend validation rejection answers
/prompt with node_errors but app.queuePrompt swallows it, so a
VALIDATION_FAIL reported {}. Capture and flatten the node_errors
(summarizePromptError, typed off apiSchema PromptResponse) into the
result's clientError and surface it in the T1 message, so a red names
the node and input. Exported with a pure unit test since the happy path
never runs it.
- Console-error window: document (README + ARCHITECTURE section 10) that
the ledger collects per-tier, so boot-time pack console noise before the
first tier action is out of scope by design, backstopped by the startup
zero-visible-errors check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## ELI-5
The "Secrets" settings panel lets you add an API key for a provider
(HuggingFace, Civitai). Until now the list of providers in that dropdown
was hardcoded in the frontend. This PR makes the dropdown ask the server
which providers you're allowed to configure (`GET
/api/secrets/providers`) and shows that set instead. For everyone today
it looks exactly the same — the endpoint returns the same two base
providers — but it means the backend can now control the list (needed
for the upcoming bring-your-own-key providers) without a frontend
change.
## Summary
Render the Secrets provider dropdown from server data (`GET
/api/secrets/providers`) instead of the hardcoded provider list, with no
visual change for existing users.
## Changes
- **What**:
- `secretsApi.ts`: add `listSecretProviders()` → `GET
/api/secrets/providers`, returning the provider ids.
- `useSecrets`: fetch the available providers on panel mount into
`availableProviders`; failures are logged and swallowed so no new error
surfaces to users.
- `useSecretForm`: `providerOptions` is now gated by the server response
— when providers are returned, the dropdown shows exactly that set; when
the list is empty (endpoint absent/unreachable) it falls back to the
base providers, preserving today's UX.
- Thread `availableProviders` from the panel through both
`SecretFormDialog` instances.
- **Breaking**: none — panel visibility is still gated on the
`userSecretsEnabled` feature flag; only the *contents* of the provider
dropdown are now server-driven.
## Review Focus
- **Ships dark / byte-for-byte UX.** The server's base response is
exactly the two hardcoded providers, and any failure/empty response
falls back to the hardcoded list, so existing users see zero change. The
membership of the dropdown is what becomes server-driven.
- **Response types come from the generated `@comfyorg/ingest-types`.**
`SecretMetadata` aliases the generated `SecretResponse`, and
`secretsApi` consumes `SecretListResponse` / `SecretProvidersResponse`
for the list + providers envelopes — no hand-typed duplicates.
`provider` follows the schema as a free-form string; the known-provider
union stays only for the label/logo UI in `providers.ts`.
- **Provider metadata (labels/logos) still comes from `providers.ts`.**
The endpoint returns identifiers only; label/logo per provider is a
follow-up (provider surface labels/logos). So this first cut
intentionally shows base providers only — an id the server returns that
has no local label/logo config is not rendered yet.
## Test plan
- `useSecrets`: `fetchProviders` populates `availableProviders`; API
failure leaves it empty and raises no toast.
- `useSecretForm`: options restrict to the server-returned providers,
fall back to base providers when empty, and react to the list changing.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
Restore the backdrop scrim on Reka dialogs opened with `modal: false`
(Settings, Manager, legacy-team subscription) — reka-ui only renders
`DialogOverlay` for modal roots, so these dialogs silently lost their
backdrop when they moved to the Reka renderer.
## Changes
- **What**: `src/components/ui/dialog/DialogOverlay.vue` injects
`injectDialogRootContext()` (public reka-ui export) and branches: modal
dialogs keep Reka's `DialogOverlay`; non-modal dialogs render a plain
backdrop `div` wrapped in Reka's `Presence` (`:present="forceMount ||
open"`), so it honors `forceMount` and plays the `data-[state=closed]`
exit fade in sync with the content. Both branches carry
`data-testid="dialog-overlay"`.
- **Popover stacking**: the scrim/content carry inline z-indexes from
@primeuix's `'modal'` counter, which body-portaled popovers with a
static `z-1700` class lost to. Extracted `DropdownMenu.vue`'s existing
lift into `useModalLiftedZIndex` and applied it to `ColorPicker.vue` and
the shared `ui/Popover.vue`, so they stack above the top-most dialog
(verified live: scrim 1701 < content 1702 < picker 1703, panel
clickable, Settings stays open).
## Review Focus
- **Why not `modal: true`**: Settings/Manager intentionally opt out of
Reka's modal mode because its focus trap + body `pointer-events: none`
break nested PrimeVue overlays teleported to body (see comments in
`useSettingsDialog.ts` / `useManagerDialog.ts`). The fallback restores
only the visual scrim; body pointer-events stay `auto`.
- **Dismissal semantics unchanged**: the scrim sits outside Reka's
`DismissableLayer`, so a pointerdown on it goes through the existing
`onRekaPointerDownOutside` bridge — scrim click dismisses the top-most
dialog, exactly like the modal overlay path (unit-covered).
- **CustomizationDialog also gains its scrim back**: the bookmark-folder
Customize dialog renders `:modal="false"` with an explicit
`<DialogOverlay />`, so it picks up the backdrop too (its template
always intended one); its ColorPicker popover is covered by the z-index
lift above. A test pins that a mounted-but-closed non-modal root renders
no scrim (CustomizationDialog mounts with `open: false`).
Verified live (dev): scrim renders behind Settings; nested
Modify-Keybinding dialog opens/focuses over it without dismissing
Settings; scrim click closes Settings; `Comfy.Load3D.BackgroundColor`
color picker opens above the scrim and is fully interactive; no new
console warnings. Unit tests are red without the fix, green with it.
Reported in Slack (FE Main), design confirmed scrims are intended.
## Screenshots
Images hosted on a fork-only branch (`pr-assets/13502-scrim`), not part
of this PR's history.
| Before (`modal: false` — no scrim) | After |
| --- | --- |
| <img width="480" alt="before"
src="https://raw.githubusercontent.com/dante01yoon/ComfyUI_frontend/pr-assets/13502-scrim/.github/pr-assets/settings-scrim-before.png"
/> | <img width="480" alt="after"
src="https://raw.githubusercontent.com/dante01yoon/ComfyUI_frontend/pr-assets/13502-scrim/.github/pr-assets/settings-scrim-after.png"
/> |
Color picker stacking above the scrim (z-index lift):
<img width="640" alt="color picker above scrim"
src="https://raw.githubusercontent.com/dante01yoon/ComfyUI_frontend/pr-assets/13502-scrim/.github/pr-assets/settings-colorpicker-above-scrim.png"
/>
Round-3 review follow-ups: the definition-pipeline diagram no longer
implies a centralized normalizer (live census -> wiring slot normalizer /
execution classifier / mount declared-shape parser, matching section 4),
and ADDING_CUSTOM_NODES scopes the zero-visible-errors claim to the tiers
that assert it, same wording as the README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Extract the pack console-error allowlist into a shared fixture
(consoleErrorLedger.ts); the curated T1 run now collects console and
page errors across load+run and asserts them through the ledger. The
filter is pinned by a discriminating pure spec (pattern match,
cross-pack ownership, unknown-pack fail-open).
- T1 asserts every expectedNodes type is actually present in the curated
workflow before running it, killing the vacuous-green path where a
drifted fixture shrank the executed-set check to an empty id list.
- typePairing records unrecognizable slot specs (unknownSlots on the
node, unknownShapes on the plan) instead of silently dropping them;
connectivity logs the list; pure tests pin the input/output drop paths
and the socketless boundary.
- Add test:custom-nodes:ci, the gate-equivalent run against the
backend-served built frontend; README re-scopes test:custom-nodes as
the dev-server loop that is NOT the gate, and scopes the
zero-visible-errors invariant to the tiers that hold it.
- ARCHITECTURE truth pass: event attribution leads with the positive
prompt-id capture; section 10 grades ledger guards in three strengths;
the decentralized parser story (declaredShape, classifyInput,
normalizer) is stated consistently in section 4, gotcha G5, and the
legend; ADDING_CUSTOM_NODES points the console ledger at its new home.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- positive prompt-id attribution: capture the /prompt response id as the
primary event filter (seen-set + graph membership stay as depth); a new
attribution self-check injects a foreign-prompt terminal error mid-run
and proves it cannot fail the run
- console collection now includes pageerror (uncaught exceptions and
rejections), with a collector self-check as positive control; surfaces a
real Custom-Scripts betterCombos typeof-null bug, ledgered with mechanism
- connectivity allowlists are two-way stale-guarded: every entry must be
observed failing in its recorded way, all stale keys reported per run
- manifest pins are required full 40-hex SHAs (CUSTOM_NODES_ALLOW_UNPINNED=1
admits only empty pins, reserved for the planned pack-HEAD canary); pack
must be a plain path segment; contract pinned by pure specs
- CI installs each pack under custom_nodes/<pack> with charset and pin
gates before cloning (attribution keys on the install dirname)
- allNodes renderer loops honor rendererPassesFor (vueNodesCompatible)
- curated T1 asserts every display sink emitted a ui payload; console
sinks documented as excluded (no ui payload by design)
- the always()-wrapper suggestion was rejected on sibling evidence:
ci-tests-unit.yaml gates its required check with a changes job and
job-level if, and no repo workflow uses a wrapper
Reviewed via ninja pipeline: 4-hat CORE panel (2 passes), senior QA gate
(2 rounds, discrimination proven by falsification), gated review (Primary,
Double Checker, Ultimate Skeptic - 15-entry evidence ledger, all PROVEN).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dagre's crossing minimizer ignores edge declaration order, so the fix is
node declaration order. Also states explicitly that the map is ordered by
zoom, not page order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- context diagram flows one way: driver -> frontend -> verdict synthesis -> team
- execution flow: classification fans out to its three verdicts; runnable paths
converge on batching, blocked routes straight to reconciliation
- persistence check: sequence diagram replaced with a linear pipeline (one
actor issuing commands is a procedure, not a message exchange)
- building blocks: tiers fan 2x2 inside the horizontal pipeline
- tripwire step + small recovers? diamond instead of one giant diamond
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The building-blocks pipeline rendered as a tall narrow strip for two
mermaid reasons: labels auto-wrap at the ~200px default regardless of
line length, and a subgraph's declared direction is ignored once it has
external edges, so the tier row silently stacked vertically. The view
is now a left-to-right pipeline with the tier group in the middle, and
the wrap-width directive makes boxes wide instead of tall here and in
the definition-pipeline and execution-flow views.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frontend box was a dead end: the suite drove it but nothing flowed
back, so the verdicts arrow to the team looked sourceless. Added the
return edge (observations back: what mounted, what persisted, what
executed, every error) and reworded the team edge so verdicts are
visibly the synthesis of those observations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Headings were double-numbered ("2. D1 - system context") with an
internal diagram-numbering scheme that means nothing to a reader.
Sections are already numbered: headings now just name the view, and
every cross-reference points at a section. Also replaced "queue cost
is amortized" with plain English: one submission carries many nodes
instead of paying the round-trip per node.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reader feedback on the rendered views: manual line breaks inside boxes
force Mermaid to render narrow, tall boxes with heavy wrapping, so the
diagrams cost too much scrolling. Mermaid sizes a box to its longest
line, so the fix is one or two long lines per box with elaboration in
the prose below the diagram. Applied to D1 (context), D2 (pipeline),
D3 (definition pipeline), and D4 (execution flow); D7 and D8 stay as
approved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reader feedback on the rendered views: D2's service-to-tier arrows
crossed the whole diagram with ambiguous fan-ins (three unattributed
"pass/fail + exceptions" curves), and D3's corpus box mixed the
two-dialects annotation into a flow node right where three arrows fan
out, reading as if the dialects explained the fan-out.
Fixes, using the rules that make the CI view work: one direction of
flow per diagram, no many-to-many edges (the service-to-tier matrix is
now a table, which is what a matrix is), and annotations live in prose
rather than inside flow boxes. D2 is now a straight
manifest -> orchestrator -> tiers -> evidence -> verdict pipeline with
a three-row shared-services table; D3 moves the dialect fact into the
normalize step, labels the fan-out "derives", and adds one sentence
mapping each derived plan to its consuming tier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Architecture documentation for the custom-node regression suite,
written as design views rather than an implementation dump:
- eight responsibility-level views: system context, building blocks,
the node-definition pipeline, the execution flow, the persistence
check, event attribution, the evidence model, and the CI deployment
view; every diagram box names a responsibility or concept, arrows
carry meaning, and decision points read in plain English
- a one-minute What/Why/How opening with the three explicit non-goals
(output semantics, frontend-virtual nodes, hour-scale soak) and a
clearly labeled scale snapshot so instance numbers never read as
properties of the design
- a 12-row design-decisions table with honest trade-offs (why a real
browser at all, why the backend serves the built frontend, one
worker, disabled execution cache, pinned pack versions, one-row
extensibility, per-tier renderer policy, mechanism-carrying
exceptions, the two-way baseline, batch+bisect, and the scope line),
plus the curated-workflow fixture named as the deliberate extension
seam
- a 14-item gotchas reference, each entry in symptom / root cause /
defense / which-team-concern-it-answers form, with named nodes kept
only as worked examples of their class
- one implementation map section where architecture names meet code
symbols, covering every building block including the orchestrator
and the evidence ledgers
- the workflow's rotted sharding comment fixed (suite duration and
the real shard trigger)
Grounded on the C4 model's published guidance, reviewed by an
independent architect pass (two view-coherence gaps found and fixed)
after three earlier Opus review passes on content accuracy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The suite had run/onboarding docs but nothing describing the SYSTEM:
what the pieces are, how node definitions flow through the planners and
classifiers, how the execution harness attributes outcomes, and why each
non-obvious rule exists. This adds the missing third doc with four
Mermaid box-line diagrams (system overview, def data flow, run pipeline,
CI pipeline), the tier-by-renderer coverage matrix, the full ledger
table with the two-way baseline semantics, and the hard-won invariants
each tied to the incident that forced it (widgetValueStore id bleed,
event cross-attribution, pack JS queue-hook crashes, Vue effect timing,
queue-jam tripwire). Scope contract is stated up front: compatibility
and regression gate, not a behavior certifier.
Every path, symbol, and number cross-checked against the tree before
commit. README and ADDING_CUSTOM_NODES now cross-reference it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Systematic audit for siblings of the two combo bug classes (ungrounded
contract, shape blindness), driven by a live shape census of the exact
getNodeDefs object the suite consumes:
- classifyInput now handles the V2 schema form (string 'COMBO' with
options in the opts object; 495 such inputs exist in the transformed
defs): options present = widget, empty or remote/lazy = NEEDS_MODELS.
Real effect measured: 8 KJNodes nodes were silently misclassified
NEEDS_WIRES and never executed - 5 now run clean, 3 correctly land in
NEEDS_MODELS (remote combos).
- forceInput now beats every input form, list-form combos included (a
census-found form the old branch order classified as widget; today's
4 instances are optional or non-manifest, so this is protection, not
a behavior change).
- pure-spec fixtures for both parsers now include every census form,
copied from real census examples (V2 options, V2 empty, V2 remote,
forceInput-on-combo, cross-form vocabulary pairing) so fixtures can
no longer self-confirm the parser's assumptions.
- ADDING_CUSTOM_NODES.md gains the evidence rules: independent-oracle
grounding for semantic claims, shape-census-driven parsing with
exclude-with-record on unknown shapes, and verify-against-the-source-
the-code-consumes.
defaultInput checked against frontend source: deprecated and ignored
(nodeDefStore warning only) - deliberately not handled.
Local verification: full customNodes suite 72/72 under CI parity; lint,
format:check, knip, and both typechecks clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A wired combo input bypasses its own widget, so menu order and the
options[0] default are not part of the wire contract - membership is
(backend validation checks value-in-options). Vocabulary fingerprints
are now order-insensitive (sorted, element-wise canonicalized). In the
current corpus this changes zero pairs (measured: no same-set,
different-order combos exist across the 7 packs); the rule is now
correct for packs where they do.
Auditing that change surfaced a real hole: the frontend's transformed
defs present some combos as the literal string COMBO with options in
the opts object. The old fingerprint hashed all of those identically,
silently cross-pairing dropdowns with no vocabulary evidence - exactly
the checkpoint-into-scheduler class the combo rule exists to exclude.
Normalization now pulls V2-form options, and a combo with no known
option list is excluded from pairing instead of blind-matched. Plan
moves 5,058 -> 5,030 pairs; the 28 removed were vocabulary-blind.
Also from CI: MiDaS Mask Image excluded (torch.hub download inside
execute hung the Linux runner; runs clean only where the hub cache is
warm) and ImageTransformKJ ledgered (pack JS initializes its
fill-options JSON widget on configure).
Local verification: full customNodes suite 68/68 twice, lint, format,
knip, and both typechecks clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coverage-gap audit after the mount-fidelity miss (assertions silently
narrower than their claim). Fixes:
- save/reload now runs under BOTH renderers with staged evaluates and
frame yields, so Vue component mount/configure effects actually flush
before each serialize - the one renderer-dependent value path a
LiteGraph-only pass could not see. Console errors are now collected
during the tier too (configure-time pack JS noise was uncovered).
- queuePrompt is wrapped in-page: pack JS that THROWS mid-graphToPrompt
(VHS applyToGraph crashed CI's whole VHS tier) now classifies as
VALIDATION_FAIL carrying the exception text, so the offender
self-identifies instead of aborting the tier. VHS_SelectLatest
excluded with that mechanism: its applyToGraph assumes downstream
inputs have widgets and hard-crashes when its output feeds a pure
socket while the input dir has matching files (upstream-report
candidate).
- pack-owned-value nodes (ROUNDTRIP_VALUE_ALLOWLIST) no longer receive
set-and-stick probe writes - writing `_cn` markers into editor JSON
widgets just made pack JS choke on our own probes.
- deliberate scopes are now stated where the assertion lives: auto-run
runs single-renderer because execution is a backend contract and
values flow through the same store in both renderers; it deliberately
skips the zero-visible-errors check because it provokes expected
failures; the connectivity breadth sweep is renderer-independent with
the curated drag test covering both renderers; combo vocabulary
matching is deliberately order-sensitive (option order defines the
default).
Local verification: full customNodes suite 67/67 under CI parity, plus
lint, format:check, knip, and both typechecks clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mount fidelity now has a renderer-independent bar: under BOTH the
LiteGraph and Vue passes, every created instance must materialize
everything its def declares - each non-socketless input exists as a
widget or a socket (autogrow templates count via their dot-qualified
expansion slots, e.g. variables.a/variables.b), and every declared
output exists. The Vue pass keeps its extra layer: the DOM must render
at least the instance's widget and slot counts. Verified against all
823 nodes under both renderers; the only def-shape special case found
was the core autogrow container semantics.
Also reconciles the first Linux CI run of the chain-builder tier:
- environment flips move to AUTO_RUN_EXCLUDE with mechanisms and leave
the baseline: Image Analyze, Text Parse A1111 Embeddings (fail macOS,
clean Linux), Image Crop Face (clean macOS, AttributeError Linux),
ImageReceiver (av decode error macOS, clean Linux)
- run-to-run flip-floppers excluded: ImpactRemoteInt,
ImpactSchedulerAdapter, ImpactQueueTriggerCountdown (queue-hook JS
transient refusals), LoadText|pysssss (state-dependent file combo)
Local verification: allNodes 21/21 twice consecutively, plus lint,
format:check, knip, and both typechecks clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executes ~340 more nodes and hardens every tier's assertions:
- CHAINABLE verdict: required sockets with a model-free producer
(EmptyImage, EmptyLatentImage, SolidMask, Primitive*, EmptyAudio) are
synthesized and wired automatically; NEEDS_WIRES now means only truly
unproducible types (MODEL, SEGS, CONDITIONING...)
- auto-run asserts data flow: every PreviewAny sink must emit a ui
payload (NO_OUTPUT class); OUTPUT_NODE targets stay event-covered
- Vue mount asserts DOM widget/slot counts (missing fails, extras and
in-row control_after_generate tolerated)
- save/reload is now two passes: pristine (reload must never shrink a
node or change a value - the "widgets disappear" bug class) and
set-and-stick (every plain widget holds a programmatic non-default
write and it survives reload where topology is stable)
- connectivity pairs COMBO slots on exact option-vocabulary match
(+~120 pairs); mismatched vocabularies stay excluded by design
- harness invariants: node ids never reused within a page (the
widgetValueStore keys state by node id and survives graph.clear(), so
a reused id inherits stale widget values - core bug, reported
separately), and run events are filtered by prompt id + graph node id
membership so late websocket events or flap-retry double-queues can
never pin one node's failure on the next
- new mechanism ledgers: WIDGET_SET_ALLOWLIST, ROUNDTRIP_VALUE_ALLOWLIST,
MOUNT_WIDGET_ALLOWLIST, all stale-guarded; AUTO_RUN_EXCLUDE gains the
observed offenders (rembg pip-install-at-execute, empty-find infinite
loop, from_pretrained downloads, minutes-long per-pixel loops)
- manifest baselines reconciled against three observation runs; stale
entries removed, real failures (missing optional deps, degenerate
synthesized inputs, CUDA-only recorders) baselined
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ImageGrabPIL is a KJNodes node and was ledgered under WAS, so its
exclusion never applied - moved to the right pack, and the auto-run
test now asserts every exclusion key is actually registered by its
pack so a wrong-pack entry fails loudly instead of silently doing
nothing. WAS Create Video from Path joins the exclusions with CI
evidence (ffmpeg discovery differs per host).
The rebuilt environment surfaced the rest of the download class: WAS
BLIP/SAM/MiDaS model loaders all hang the queue in non-interruptible
weight downloads (and would mass-download on a networked runner), the
random.org node needs internet by definition, KJ LoadAndResizeImage and
WAS Create Grid Image follow input-dir contents, and Impact media
widgets preview values via root-relative URLs (console allowlist
widened). Every entry carries its mechanism; all keep mount,
save/reload, and connectivity coverage. 65/65 both environments.
typecheck:browser (which the lint job runs, unlike root typecheck)
caught an unused classifyInput param, a branded-NodeId lookup, and
api.interrupt's required argument. The first serialized CI run then
exposed environment-variable nodes: clean on one host, failing on the
other (screen capture with no X display, PIL screen grabs headless,
torch-stack RuntimeErrors that are macOS-only, state-dependent WAS
history). Those move from the cannotRunAlone baseline to
AUTO_RUN_EXCLUDE with per-node mechanisms so both environments stay
deterministic, and Impact's hardcoded example.png preview 404 joins
the scoped console allowlist.
vitest/valid-expect flags assertions stored before advancing fake
timers and awaited after - awaiting at creation would deadlock the
timer advance. The file is byte-identical to main; the warnings appear
because the type-aware lint toolchain moved. Suppressed per line with
the reason; also drops a scratch await added while chasing this.
The auto-run tier needs exclusive backend-queue access, so the CI job
now runs with workers=1 - parallel workers were interrupting each
other's executions and cross-attributing errors. Await the async
toHaveLength assertion the type-aware lint flagged, drop the
calibration measurement harness (one-shot scaffolding; the measured
constant keeps a provenance note), rename NO_SINK to
NO_OBSERVABLE_OUTPUT, and cut comments down to load-bearing WHYs.
An unpinned pack means any upstream push can red the gating check for
every PR in the repo. Each row now pins the exact SHA the suite was
verified against locally (all tiers green, both environments); bumps
are deliberate, re-verified changes. Also records why the job is not
sharded yet: per-shard setup (~4.5 min of pack installs and backend
boot) dominates the ~5.5 min suite, so a prebuilt image comes first.
The cannotRunAlone baseline (per pack, in the manifest) records nodes
that cannot execute standalone on a bare backend, asserted both ways so
entries cannot rot: an unlisted failure is a regression, a listed node
that runs clean must be removed. queuePrompt rejection is retried once
before classifying VALIDATION_FAIL - pack JS hooking the queue path can
refuse transiently, and the backend log proved several apparent rejects
never reached the server. Nodes whose execution depends on their own
pack JS preprocessing widget values (rgthree Power widgets, KJ editors)
are excluded unconditionally with the mechanism recorded, since whether
a page applies pack JS varies by serving setup; ML-session initializers
and unstable executed-set reporters join them. ADDING_PACKS and the
README document the every-node tiers and all five exception ledgers.
Verified 65/65 in both documented environments: dev server and
dist-serving CI parity, twice consecutively on the latter.
All-nodes tiers discover each pack's full node list from the live
backend: chunked mount checks in both renderers (batch size 24, chosen
by the committed calibration tool), chunked save/reload round-trips,
a connectivity corpus widened from the curated sentinels to every
registered node, and an auto-run tier that classifies every node
(AUTO_RUNNABLE / NEEDS_WIRES / NEEDS_MODELS / NO_SINK) and executes the
runnable ones in batches with per-node bisection on failure.
Hard-won harness rules baked in: queuePrompt rejects classify instantly
as VALIDATION_FAIL instead of burning the timeout; a timed-out batch
interrupts and verifies the queue drained so one hung node cannot jam
every later run; a pre-flight queue check fails fast with the real
cause; and three reviewable exception ledgers carry reasons inline
(AUTO_RUN_EXCLUDE for runtime-downloaders like RemBGSession+, a scoped
console-noise allowlist for KJNodes' undefined-filename previews,
connectivity CONNECT_REJECTED/ROUNDTRIP_LOST entries for pack JS that
vetoes or drops links).
grep -c on the single-line /extensions JSON could only say 0 or 1; count
entries properly with the same python one-liner style the doc already
uses. The Step 7 triage claimed our CI failure was upstream drift; it was
the dev-server blindspot - reorder the advice to reproduce under 6b
before diagnosing.
Detect frontend-JS packs at install time, split local verification into
the fast dev-server loop and the CI-parity dist run (required when the
pack ships frontend JS), spell out that workflow media paths resolve
against the backend's working directory, and add upstream-drift triage
for unpinned packs. Checklist updated to match.
Two CI failures with the 7-pack backend, both from pack frontend JS that
never loads under the Vite dev server (its /extensions list is core-only):
- rgthree's Seed rebuilds its declared seed input as a widget-only
control, so the planned BatchCount+.INT -> Seed.seed pair has no socket
on the instance. The sweep now classifies that as
WIDGET_ONLY_ON_INSTANCE, logged and excluded like wildcards; a name
missing from both slots and widgets still fails hard, and the drag test
picks the first in-pack pair that materializes on real instances.
- rgthree's progress bar shifts the canvas element 16px down, and
NodeSlotReference.getPosition returned canvas-relative coordinates, so
every slot drag grabbed the node title instead of the slot dot. Slot
positions now include the canvas element's page offset (a no-op when
the canvas sits at 0,0).
Documents the dev-server blindspot and the CI-parity loop (build dist,
--front-end-root) in the suite README and ADDING_PACKS. Verified 36/36
green against both the dev server and a dist-serving 7-pack backend.
A run row with an empty workflow would skip locally and rely on CI's
skip gate to notice the lost coverage; enforce the documented contract
at load time instead.
Five new manifest rows, each covering load, connectivity, and run tiers
with hand-authored model-free workflows verified against a live backend.
New optional vueNodesCompatible manifest field: a pack proven unable to
mount under Vue Nodes 2.0 runs its LiteGraph assertions only - never a
test.skip, so the zero-skip CI gate stays honest. All five packs mount
under Vue Nodes 2.0 empirically, so no row sets the flag; the decision
helper is unit-tested instead. ADDING_PACKS.md is the authoritative
step-by-step onboarding process, validated against live /object_info.
Manifest rows now also fail fast on an empty repo field.
Derive CustomNodeTier from the VALID_TIERS array (as const) so adding a tier
is one edit and the type/runtime lists can't drift. The forbid-skips
diagnostic now recurses the report and prints only specs that actually
skipped - the old dump printed every title and a single-level filter would
miss specs nested under describe() blocks (which the regression spec uses).
A required check gated by a trigger-level paths filter never creates a check
run on a PR that touches none of those paths, leaving branch protection stuck
Pending. Move the gating to a job-level if via the changes-filter action (a
skipped job counts as passing), mirroring ci-tests-unit.yaml, so this can be
marked required without stalling docs-only PRs. Keeps the same-repo fork guard
in the same if.
Security: the pack-install job now runs only for same-repo PRs and pushes, so
a fork PR can't point the manifest's repo URLs at attacker-controlled code
that the job would clone and pip-install. Fork PRs keep the env-agnostic
coverage via the main e2e shards.
Stability: pack requirements install under a pip constraint pinning the CPU
torch stack, so no pack can swap torch for a GPU/incompatible build on the
--cpu runner.
Correctness: manifest validation rejects unknown tier values (a 'connectivty'
typo would otherwise silently drop that tier's coverage). Connectivity's
'pack installed' predicate is extracted to one isEntryInstalled helper used by
both the breadth and drag tests.
A regression gate that lets a broken pack through as a skip is theater. Pack
clone/dependency failures now fail the job (array+loop instead of a
failure-swallowing jq|while pipe), and a post-run check fails the job if any
test was skipped - on this backend every tier is meant to run, so a skip
means a pack or devtools did not load. Drops the informational framing;
mark custom-nodes-e2e required in branch protection to block merges.
Phase 5. A new informational (non-gating) workflow that reuses the repo's
setup-frontend/setup-playwright/setup-comfyui-server actions, then installs
every pack the manifest declares (jq loop over customNodeManifest.json, so a
new pack row installs itself with no workflow change) and boots ComfyUI with
--multi-user --cache-none before running browser_tests/tests/customNodes.
This makes the load and run tiers actually execute in CI instead of skipping
for want of the packs - the whole point of the suite. A pack whose deps fail
degrades to an honest skip rather than reddening the job.
T-conn was planning-doc shorthand for the connectivity tier; test titles and
logs now say connectivity outright so CI output reads without tribal
knowledge.
CI caught what a pack-rich local backend masked: isValidConnection compares
only the string COMBO while every combo slot carries its own option set, so
the planner would wire a checkpoint dropdown into a scheduler dropdown and
call it proof, and combo outputs declare a non-string output_name whose
instance slot name never matches (DevToolsNodeWithOutputCombo failed 5
pairs on CI as SLOT_CONTRACT_MISMATCH). Combo slots are now recorded and
counted like wildcards instead of paired, the normalizer coerces slot names
to strings, and a pure spec locks both behaviors. Targeted fixtures remain
the way to cover combo semantics.
The Comfy.userId=default settings override broke every test on multi-user
backends (the repo's stated browser-test prerequisite): devtools
set_settings wrote to a user no session reads, so Comfy.TutorialCompleted
never landed, the templates dialog never opened, and the beforeEach wait
timed out - CI sessions even inherited leftover settings (a zh locale) from
earlier tests on the same worker user. Dropping the override lets the
fixture target the real per-worker user everywhere; the harness backend now
runs --multi-user like CI. Connectivity's per-pack guards and drag
derivation apply only to installed packs, so a backend without the manifest
packs reports the absence instead of hard-failing while the core sweep,
native drag, and self-checks still run.
knip flags exported types with no external consumers; CustomNodeTier,
ObjectInfoNode, NormalizedSlot, and SlotRef are referenced only within
their own modules.
The breadth sweep now fails on any console error captured during the
connect/serialize/prompt loop, matching the fidelity test. The wildcard
predicate is exported from typePairing and reused instead of re-derived.
assertEntry validates real shapes (non-empty pack/expectedNodes/tiers,
arrays, boolean requiresGpu, finite positive timeoutMs); workflow stays
allowed as an empty string until a pack gains a run-tier fixture.
Resolve the manifest path from import.meta.url so tests are cwd-independent,
and validate requiresGpu at manifest load. Reuse the centralized TestIds for
the error overlay, error dialog, and templates dialog selectors. Extract the
shared suite settings and templates-dialog dismissal into
fixtures/utils/customNodeSuite so the three specs cannot drift. Rename
spikeDesktop.spec.ts to coreSmoke.spec.ts to match its maintained purpose,
document the full manifest schema in the README, and describe the gate
outcome without a hardcoded test count.
A permanent self-check feeds the shared pair executor a type-incompatible
pair and a fabricated slot name and requires CONNECT_REJECTED and
SLOT_CONTRACT_MISMATCH back, so a green sweep can never come from a
classifier that lost the ability to fail. The breadth test asserts every
connectivity-tier pack contributes pairs, guarding pack attribution. The
drag tier's widget-primitive exclusion is removed: widget-backed inputs
render real slot dots under Vue Nodes (verified empirically), so every pack
now gets an in-pack drag in both renderers, asserted present.
A type-pairing generator indexes /object_info producers and consumers and
plans one representative typed edge per slot, excluding wildcard slots
(isValidConnection short-circuits on * before the real type compare, so a
wildcard link proves reachability, not interop). The breadth sweep connects
every planned edge through the real validator in-page and requires each link
to survive serialize/configure and appear in graphToPrompt output; verified
up front that graphToPrompt emits links even when other required inputs
dangle. A curated subset is dragged slot-dot to slot-dot under both
renderers, addressed by data-slot-key so shared labels cannot misfire.
Orphan types are reported, never failed; connect vetoes must match a
committed allow-list. Manifest packs opt in via a connectivity tier that
needs no extra assets.
One script per pack tier (impact-render/impact-run/vhs-render/vhs-run) plus
the self-check, all opening the Playwright Inspector so anyone can step
through what the robot does. README covers prerequisites, every script, a
worked example, the zero-visible-errors contract, and how to add a pack.
test:custom-nodes runs the whole suite headless (the gate); :watch opens a
headed slow-motion run of the browser tiers; :debug steps through them in the
Playwright Inspector. All target the local dev server on :5173 and use the
committed system-Chrome config (no bundled-chromium download). Pass -g to
:watch / :debug to run a single test, e.g. -g 'VideoHelperSuite.*T1'.
T2a gated on the ComfyUI-test-framework 'Assert Executed' nodes, which are
not published for any backend yet, so the tier could only ever skip. A test
that cannot run anywhere is reporting noise; restore it from history when
the assertion pack lands. Suite is now 16 passed, zero skips, zero failures.
Every browser tier now asserts the app's user-facing error surfaces (error
overlay, error dialog, node render errors, error toasts) are absent at test
start and after each pass, so a run is green only if a human watching the
screen sees zero errors. The harness self-check asserts the overlay IS
visible after a forced execution error, keeping the selectors provably live.
Sessions boot with a blank graph (Comfy.TutorialCompleted=false) because the
bundled default template references models absent on a scoped backend; the
tutorial path's auto-opened template browser is dismissed per test. Settings
now reach the session on single-user server-storage backends by routing
devtools set_settings to the default user, and the errors tab stays enabled
so error indicators are never suppressed in this suite. The smoke test loads
a core-only model-free workflow instead of the SD1.5 default asset.
Impact runs ImpactInt and ImpactFloat into PreviewAny as a group; VHS decodes
the existing plain_video.mp4 asset through VHS_LoadVideoPath into
VHS_VideoInfo. The executed-set check asserts each expected node individually
executed, so group workflows still verify per-node execution. Requires a
cache-disabled backend (--cache-none) with the video staged in its input dir;
documented on the manifest workflow field.
Normalize the executing event tap: its CustomEvent detail is a bare node-id
string, so the previous object-spread left the executed-set permanently empty.
The self-check now asserts a non-empty executed-set to keep that path live.
T0 now clears the graph per renderer pass, asserts exact node counts, and
verifies each added pack node's own data-node-id mounts in the Vue pass
(default-workflow nodes can no longer satisfy the assertions). Console capture
starts before the renderer toggle. Added an object_info sanity floor so a
depleted getNodeDefs fails loudly instead of skipping everything.
Run/io tiers gain test.setTimeout, requiresModels gating, and an empty-workflow
guard. Interrupted runs get pure-spec coverage; the shared console collector
moves to fixtures/utils.
Data-driven Playwright harness verifying custom-node packs load and render
under both LiteGraph 1.0 and Vue Nodes 2.0 against a real ComfyUI backend.
Pure classifier/validator/manifest logic is unit-tested; the regression spec
renders each pack's nodes in both renderers (Vue via data-node-id DOM) and a
self-check runs a workflow to confirm execution-error capture. Proven against
Impact Pack + VideoHelperSuite.
Makes ComfyPage.createUser idempotent so the suite runs against a persistent
backend (Desktop server user storage).
2026-07-02 11:39:53 -07:00
484 changed files with 35519 additions and 9056 deletions
Pass if none of these patterns are found in the diff.
When warning, reference the specific ADR by number and link to `docs/adr/` for context. Frame findings as directional guidance since ADR 0003 and 0008 are in Proposed status.
path_instructions:
- path:'**/*.test.ts'
instructions:|
Treat `.agents/checks/test-quality.md`, `docs/testing/README.md`, and `docs/guidance/vitest.md` as required review context for every changed Vitest test file.
- path:'src/lib/litegraph/**/*.test.ts'
instructions:|
Treat `.agents/checks/test-quality.md`, `docs/testing/README.md`, `docs/guidance/vitest.md`, and `docs/testing/litegraph-testing.md` as required review context for every changed litegraph Vitest test file.
Treat `.agents/checks/test-quality.md`, `docs/testing/README.md`, and `docs/guidance/playwright.md` as required review context for every changed Playwright test file.
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
post_comment "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist" "missing branch ${target}"
elif [ "${reason}" = "already-exists" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
post_comment "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed." "already-backported ${target}"
elif [ "${reason}" = "branch-create-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` failed: could not create the backport branch. Please retry or backport manually."
elif [ "${reason}" = "push-failed" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` cherry-picked cleanly but the push failed. Please retry or push the backport branch manually."
description: 'Optional:force a specific release branch, e.g. core/1.47 or core/2.0. Overrides the pin-derived target — use to skip a dead minor or do an out-of-cadence / major release.'
en:'Custom-node packs on Comfy Cloud — supported by default',
en:'Custom-node packs on Comfy Cloud - supported by default',
'zh-CN':'Comfy Cloud 自定义节点包合集——开箱即用'
},
'cloudNodes.meta.description':{
@@ -1845,8 +1845,8 @@ const translations = {
// MCP – Meta
'mcp.meta.title':{
en:'Comfy MCP — Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP — 让任何 AI 智能体驱动 ComfyUI'
en:'Comfy MCP - Drive ComfyUI from any AI agent',
'zh-CN':'Comfy MCP - 让任何 AI 智能体驱动 ComfyUI'
},
'mcp.meta.description':{
en:'Comfy MCP exposes the full ComfyUI engine over the Model Context Protocol. Generate images, video, audio, and 3D from Claude Code, Claude Desktop, and any MCP-compatible client.',
en:'Comfy.org Affiliate Program Terms and Conditions.',
@@ -3879,8 +3896,8 @@ const translations = {
'This document reproduces the current template of the Enterprise Customer Agreement for reference only. The executed Agreement between Comfy and Customer, together with any signed Order Forms, governs the relationship between the parties. To request an executable copy, please contact <a href="mailto:sales@comfy.org" class="text-white underline">sales@comfy.org</a>.'
},
'enterprise-msa.page.title':{
en:'Enterprise MSA — Comfy',
'zh-CN':'Enterprise MSA — Comfy'
en:'Enterprise MSA - Comfy',
'zh-CN':'Enterprise MSA - Comfy'
},
'enterprise-msa.page.description':{
en:'Comfy Enterprise Customer Agreement — the master services agreement that governs Comfy Enterprise deployments of Comfy Cloud, Comfy API, and related products.',
@@ -4051,7 +4068,6 @@ const translations = {
en:'This page is being redesigned. Check back soon.',
en:"Run the world's leading AI models in ComfyUI",
'zh-CN':'在 ComfyUI 中运行世界领先的 AI 模型'
},
'models.breadcrumb.home':{
en:'Home',
'zh-CN':'首页'
},
'models.breadcrumb.models':{
en:'Supported Models',
'zh-CN':'支持的模型'
@@ -4349,8 +4361,8 @@ const translations = {
// Affiliate page (/affiliates) — head metadata
'affiliate.page.title':{
en:'Comfy.org Affiliate Program — Become a Partner',
'zh-CN':'Comfy.org 联盟计划 — 成为合作伙伴'
en:'Comfy.org Affiliate Program - Become a Partner',
'zh-CN':'Comfy.org 联盟计划 - 成为合作伙伴'
},
'affiliate.page.description':{
en:'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.',
@@ -4375,8 +4387,8 @@ const translations = {
// Launches page (/launches) — head metadata
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'launches.page.title':{
en:'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
en:'ComfyUI Live Demo & Q&A - June 29 Launch Livestream',
'zh-CN':'ComfyUI 直播演示与问答 - 6 月 29 日发布直播'
},
'launches.page.description':{
en:'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
const dirDesc = dirDescriptions[model.directory] ?? 'an AI model'
const whatIsDescription = `${displayName} is ${dirDesc}. You can run it locally in ComfyUI with full control over every parameter, or access it through Comfy Cloud. ComfyUI's node-based workflow editor lets you connect ${displayName} with ControlNets, LoRAs, upscalers, and custom nodes to build any pipeline you need. There are ${model.workflowCount} community workflow templates using ${displayName} on Comfy Hub, ready to load and customize.`
const softwareAppJsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
const pageTitle = `${displayName} in ComfyUI`
const pageDescription = `Run ${displayName} in ComfyUI with full parameter control. ${model.workflowCount} community workflow templates, step-by-step tutorials, and free local inference.`
? `Follow the step-by-step tutorial at ${model.docsUrl}. You can also load any of the ${model.workflowCount} community workflow templates that use ${displayName} directly in ComfyUI.`
: `Open ComfyUI and browse the ${model.workflowCount} community workflow templates that use ${displayName}. Load one as a starting point, then customize the nodes and parameters to fit your use case.`
}
: `Open ComfyUI and browse the ${model.workflowCount} community workflow templates that use ${displayName}. Load one as a starting point, then customize the nodes and parameters to fit your use case.`,
},
},
{
'@type': 'Question',
name: `How many ComfyUI workflows use ${displayName}?`,
acceptedAnswer: {
'@type': 'Answer',
text: `There are ${model.workflowCount} community workflow templates that use ${displayName} on Comfy Hub. Each template is ready to run in ComfyUI and can be customized to suit your project.`
}
text: `There are ${model.workflowCount} community workflow templates that use ${displayName} on Comfy Hub. Each template is ready to run in ComfyUI and can be customized to suit your project.`,
},
},
{
'@type': 'Question',
name: `Is ${displayName} free to use in ComfyUI?`,
acceptedAnswer: {
'@type': 'Answer',
text: `ComfyUI is free and open source. ${model.huggingFaceUrl ? `${displayName} weights are available to download from Hugging Face.` : `${displayName} is available as a cloud API through Comfy Cloud.`} You only pay for compute when running on Comfy Cloud; local inference on your own hardware is always free.`
}
}
]
text: `ComfyUI is free and open source. ${model.huggingFaceUrl ? `${displayName} weights are available to download from Hugging Face.` : `${displayName} is available as a cloud API through Comfy Cloud.`} You only pay for compute when running on Comfy Cloud; local inference on your own hardware is always free.`,
},
},
],
}
const pageTitle = `${displayName} in ComfyUI`
const pageDescription = `Run ${displayName} in ComfyUI with full parameter control. ${model.workflowCount} community workflow templates, step-by-step tutorials, and free local inference.`
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.