## 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
## 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`
## 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
## 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>
2026-07-10 02:11:00 +00:00
336 changed files with 20269 additions and 2774 deletions
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.',
@@ -3483,8 +3483,8 @@ const translations = {
},
'affiliate-terms.page.title':{
en:'Affiliate Terms — Comfy',
'zh-CN':'Affiliate Terms — Comfy'
en:'Affiliate Terms - Comfy',
'zh-CN':'Affiliate Terms - Comfy'
},
'affiliate-terms.page.description':{
en:'Comfy.org Affiliate Program Terms and Conditions.',
@@ -3896,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.',
@@ -4361,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.',
@@ -4387,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.',
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.