ANTHROPIC_API_KEY is optional: Agent SDK uses Claude Code OAuth
session when running locally (detects CLAUDE_CODE_SSE_PORT).
In CI, ANTHROPIC_API_KEY from secrets is used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Gemini-only agentic loop had ~47% success rate — too low to be
useful as a fallback. Now ANTHROPIC_API_KEY is required for issue
reproduction. Fails clearly if missing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
addInitScript runs before page load — Vue's app mount destroys the
cursor div when it takes over the DOM. Using addScriptTag after login
ensures the cursor persists in the stable DOM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Locator.click/hover bypasses our page.mouse monkey-patch. Now
clickByText, hoverMenuItem, clickSubmenuItem get the element
bounding box and update cursor overlay manually.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The grep \{"verdict":\s*"[^"]+ captures up to but not including the
closing quote. The second grep for "[A-Z_]+"$ then fails because
there's no closing quote. Fixed: match "verdict":\s*"[A-Z_]+ then
extract [A-Z_]+$ (no quotes needed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Video review prompt now requests a ## Verdict JSON block:
{"verdict": "REPRODUCED|NOT_REPRODUCIBLE|INCONCLUSIVE", "risk": "low|medium|high"}
- Deploy script reads JSON verdict first, falls back to grep
- Eliminates all regex-matching false positives permanently
- Theme: light mode is default, dark via prefers-color-scheme:dark
- Cards use solid backgrounds, grain overlay only in dark mode
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add prefers-color-scheme:light media query with light palette.
Replace hardcoded dark oklch values with CSS variables.
Light mode: white surfaces, dark text, subtle borders, no grain overlay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- loadDefaultWorkflow now calls app.resetToDefaultWorkflow() via JS API
instead of navigating File → Load Default menu (menu item name varies)
- pressKey reverted to instant press() — the 400ms hold via down/up
prevented Escape from propagating to parent dialog (#10397 BEFORE video
showed wrong behavior because hold intercepted the event)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of manually calling moveCursorOverlay in each action,
patch page.mouse.move/click/dblclick/down/up globally. Now EVERY
mouse operation shows the cursor — text clicks, menu hovers, etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
"could not be confirmed" contains "confirmed" which matched the
positive reproduc|confirm check. Now caught by the negative check first.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Agent SDK returned "model not found" for claude-sonnet-4-6-20250514.
Correct ID is claude-sonnet-4-6.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Headless Chrome's Playwright CDP doesn't trigger DOM mousemove events
reliably. Now executeAction calls __moveCursor(x,y) directly after
every mouse.move/click/drag. Cursor is an SVG arrow (white + outline).
Click state shown via scale animation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pressKey now uses keyboard.down/up with 400ms hold instead of
instant press(). Shows subtitle "⌨ Escape" and the keyboard HUD
catches the held state for video frame capture.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Agent system prompt now instructs Claude to demonstrate BOTH working
(control) and broken (test) states when bug is triggered by a setting
- Added docs/qa/backlog.md with future improvements: Type B/C comparisons,
TTS, pre-seeding, cost optimization, environment-dependent issues
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Shows "QA @ abc1234" linking to the pipeline code commit
- Shows start time → deploy time in header
- Helps trace which version of QA scripts generated each report
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Architecture:
- Claude Sonnet 4.6 plans and reasons (via Claude Agent SDK)
- Gemini 2.5 Flash watches video buffer and describes what it sees
- 4 tools: observe(), inspect(), perform(), done()
observe(seconds, focus): builds video clip from screenshot buffer,
sends to Gemini with Claude's focused question.
inspect(selector): searches a11y tree for specific element state.
perform(action, params): executes Playwright action.
done(verdict, summary): signals completion.
Falls back to Gemini-only loop if ANTHROPIC_API_KEY not set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Agent reasoning shown as subtitle bar at bottom of video during recording
- After recording, generates TTS audio via OpenAI API (tts-1, nova voice)
- Merges audio clips at correct timestamps into the video with ffmpeg
- Requires OPENAI_API_KEY env var; gracefully skips if not set
- No-sandbox + disable-dev-shm-usage for headless Chrome compatibility
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Download QA guide artifact in report job
- Extract prerequisites, test focus, and steps from guide JSON
- Display below the purpose description: focus → prerequisites → steps
- Separated by a subtle divider with smaller font
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Report site shows "PR #N aims to..." or "Issue #N reports..." block
above the video cards, extracted from pr-context.txt
- Multi-pass video links fall back to pass1 when qa-{os}.mp4 is 404
- More negative verdict patterns: "does not demonstrate", "never tested"
- Risk uses first word of Overall Risk (avoids "high confidence" match)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "does not demonstrate", "steps were not performed", "never tested"
to NOT_REPRO patterns (fixes#9101 false positive)
- Risk detection uses first word of Overall Risk section instead of
grepping entire text (fixes "high confidence" matching HIGH)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tsx compiles arrow functions with __name helpers that don't exist in
browser context. Using addScriptTag with plain JS string avoids this.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set<string>() in page.evaluate causes __name ReferenceError in browser.
Use untyped Set() since browser JS doesn't support TS generics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
"fails to reproduce" contains "reproduce" — must check negatives first
within each report. Across reports, REPRODUCED still wins (multi-pass).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Injects a persistent overlay in bottom-right corner that displays
currently held keys (e.g. "⌨ Space", "⌨ CTRL+C"). Makes keyboard
interactions visible in the recording for both human and AI reviewers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple report files exist, badge shows "2/3 REPRODUCED" instead
of just "REPRODUCED". Single-pass issues still show plain verdict.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When multiple passes exist and one confirms while another is
inconclusive, the badge should show REPRODUCED. Previously
INCONCLUSIVE was checked first, hiding successful reproductions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "Clone" context menu item doesn't exist in Nodes 2.0 mode.
Using Ctrl+C/Ctrl+V works in both legacy and Nodes 2.0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- #10307: preflight clones KSampler node, hint says drag to overlap
- #7414: preflight clicks numeric widget, hint says drag to change value
- #7806: preflight takes baseline screenshot, hint gives exact coords
for holdKeyAndDrag with spacebar
- Hints now reference "Preflight already did X, NOW do Y" pattern
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Agent was wasting turns re-doing loadDefaultWorkflow and setSetting
that preflight already executed. Now the system prompt includes
"Already Done" section listing preflight actions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Auto-execute prerequisite actions (enable Nodes 2.0, load default
workflow) BEFORE the agentic loop starts. Agent model ignores prompt
hints but preflight guarantees nodes are on canvas.
- Add "fails to reproduce" to NOT REPRODUCIBLE badge patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add issues:[labeled] trigger and qa-issue label support
- Resolve github.event.issue.number for issue-triggered runs
- Include issue labels in context (feeds keyword matcher for hints)
- Remove qa-issue label after run completes (same as qa-changes/qa-full)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scan issue context for keywords (clone, copy-paste, spacebar, resize,
sidebar, scroll, middle-click, node shape, Nodes 2.0, etc.) and inject
specific MUST-follow action steps into the agentic system prompt.
Addresses 9 INCONCLUSIVE issues where agent had actions available
but didn't know when to use them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Annotations now use cyan dashed border + monospace "QA:" prefix
instead of red solid labels that look like UI error messages
- Video review prompts explicitly tell reviewer to ignore QA annotations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change preload=metadata to preload=auto for full video download
- Add _headers file with Accept-Ranges for Cloudflare Pages
- Add custom seekbar (range input + buffer indicator) that works
even without server HTTP range request support
- Seekbar shows buffered progress and allows dragging to any point
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previous prompt said "test the specific behavior" which was too vague,
leading to generic UI walkthroughs instead of targeted tests.
New prompt: explicitly instructs to read the diff, trigger the exact
scenario the PR fixes, and avoid generic menu screenshots.
Also added reload action to before/after prompt for state persistence tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix quality badge now reads "## Overall Risk" section only
- Prevents false MAJOR ISSUES from severity labels or negated phrases
- "Low" risk → APPROVED, "High" → MAJOR ISSUES, "Medium" → MINOR ISSUES
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
setup-frontend must run first to install node/pnpm, then rebuild
with PR code. Also re-install sno-skills deps after switching back
so QA scripts' dependencies are available.
Also gitignore .claude/scheduled_tasks.lock.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When triggered via sno-qa-* push, the workflow checks out the PR code
to build its frontend, but this replaces qa-record.ts which only
exists on sno-skills. Fix: build PR frontend, then checkout back to
sno-skills so QA scripts remain available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SC2231: quote glob expansions in for loop
- SC2002: use sed directly instead of cat | sed
- SC2086: quote variable in echo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update DefaultThumbnail test to match size-full class change
- Fix shellcheck warnings in qa-batch.sh (SC2001, SC2207)
- Fix shellcheck warnings in qa-deploy-pages.sh (SC2034, SC2235, SC2231, SC2002)
- Add qa-report-template.html to oxfmt ignore (minified, not formattable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "confirmed" didn't match "confirms"/"reproducible" — use "reproduc|confirm" stem
- "partial" matched unrelated text — require "partially reproduced" specifically
- collectVideoCandidates now finds qa-session-*.mp4 for multi-pass reviews
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Check INCONCLUSIVE before reproduced/confirmed in badge detection
- Exclude markdown headings from reproduced grep match
- Add --pass-label to qa-video-review.ts for unique multi-pass filenames
- Pass pass label from workflow YAML when reviewing numbered sessions
- Collect all pass-specific reports in deploy script HTML
- Add addNode/cloneNode convenience actions to qa-record agent
- Improve strategy hints for visual/rendering bug reproduction
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove push trigger (was for dev testing only)
- Restore concurrency group (was commented out for dev)
- Move misplaced import in qa-analyze-pr.ts to top of file
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Strengthen prompt: MUST use openMenu → hoverMenuItem → clickMenuItem
in that order. Previous runs skipped openMenu causing silent failures.
- Add CI Job link to the QA report site header for quick navigation
to the GitHub Actions run that generated the report.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHub Actions has a 21000 char limit per expression. The combined
badge setup step exceeded this after adding the dual badge generator.
Split into its own step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Badge now reads: QA Bot | REPRODUCED | Fix: APPROVED
Not all issues are bugs — could be feature requests too.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reproducing a bug is a successful outcome for the QA bot.
Blue (#2196f3) = bot succeeded. Red = bot found problems with the fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PRs now show one badge with three segments:
QA Bot | Bug: REPRODUCED | Fix: APPROVED
Instead of two separate badges. Uses gen-badge-dual.sh which
renders label + bug status + fix status in one SVG.
Issues still use single two-segment badge:
QA Bot | FINISHED: REPRODUCED
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Inject fake cursor (red dot with click animation) via addInitScript
since headless Chrome doesn't render the system cursor in video
- Add hover-before-click delay to clickByText and canvas clicks
so viewers can see where the cursor moves before clicking
- Add 'annotate' action: shows a floating label at (x,y) for N ms
so AI can draw viewer attention to important UI state changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: runAgenticLoop never read the QA guide — agent saw
"No issue context provided" for issues. Now reads qaGuideFile,
parses structured fields, and injects into system prompt.
Also: fetch issue body via gh issue view in workflow, increase
budget to 120s/30 turns, add focus reminders, smarter stuck
detection (50px grid normalization + action-type frequency),
reject invalid click targets, add loadDefaultWorkflow and
openSettings convenience actions, strategy hints in prompt.
Fix pre-existing typecheck error in eslint.config.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PRs now get two separate badges:
- Bug: REPRODUCED / NOT REPRODUCIBLE / PARTIAL (before branch)
- Fix: APPROVED / MAJOR ISSUES / MINOR ISSUES (after branch)
Issues keep a single badge: FINISHED: REPRODUCED / etc.
Both badge-bug.svg and badge-fix.svg served from the deploy site.
PR comment shows all three: ![badge] ![bug] ![fix]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FINISHED is not standalone — always shows result:
- FINISHED: REPRODUCED / NOT REPRODUCIBLE / PARTIAL (issues)
- FINISHED: APPROVED / MAJOR ISSUES / MINOR ISSUES (PRs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Badge shows QA pipeline status, deployed at each stage:
- PREPARING (blue) — setting up artifacts
- ANALYZING (orange) — running video review
- Final status with color:
- Issues: REPRODUCED (red) / NOT REPRODUCIBLE (gray) / PARTIAL (yellow)
- PRs: APPROVED (green) / MAJOR ISSUES (red) / MINOR ISSUES (yellow)
Badge served as /badge.svg from the same Cloudflare Pages site.
Included in PR comment as .
Also restore @ts-expect-error for import-x plugin type incompatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace single-shot step generation in reproduce mode with an agentic
loop where Gemini sees the screen after each action and decides what
to do next. For multi-bug issues, decompose into sub-issues and run
separate recording passes.
- Extract executeAction() from executeSteps() for reuse
- Add reload and done action types
- Add captureScreenshotForGemini() (JPEG q50, ~50KB)
- Add runAgenticLoop() with sliding window history (3 screenshots)
- Add decomposeIssue() for multi-pass recording (1-3 sub-issues)
- Update workflow to handle numbered session videos (qa-session-1, etc.)
- Add custom video controls below each video with frame stepping
- Frame back/forward buttons (1 frame at 30fps, 10 frames skip)
- Speed selector: 0.1x, 0.25x, 0.5x (default), 1x, 1.5x, 2x
- Keyboard shortcuts: arrow keys for frame step, space for play/pause
- SMPTE-style timecode display (m:ss.ms)
- Default 0.5x speed since AI operates UI faster than humans
- Videos no longer autoplay (pause on load for inspection)
- Zero external dependencies (pure HTML5 video API)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reproduce video must be max 5 minutes (short, focused demo)
- Phase 4 reuses the environment from Phase 3 (no re-setup)
- Use video-start/video-stop commands (not --save-video flag)
- Start recording right before steps, stop immediately after
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Claude agent-driven issue reproduction skill that:
- Phase 1-2: Research issue and set up environment (custom nodes, workflows, settings)
- Phase 3: Record research video while exploring interactively via playwright-cli
- Phase 4: Record clean reproduce video with only the minimal repro steps
- Phase 5: Generate structured reproduction report
Key difference from the old approach: Claude agent explores and adapts
instead of blindly executing a Gemini-generated static plan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gemini was right-clicking empty canvas instead of nodes because it
didn't know where the default workflow nodes are positioned. Now the
prompt includes approximate coordinates for all 7 default nodes and
clarifies the difference between node context menu vs canvas menu.
Also fixes TS2352 in page.evaluate by using double-cast through unknown.
- Add --target-url CLI option to qa-video-review.ts
- Include target URL in generated markdown reports
- Add clickable issue/PR link in deployed HTML report header
- Workflow passes the target URL automatically
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused @ts-expect-error directives in eslint.config.ts
- Simplify LazyImage prop types from ClassValue to string
- Fix DialogInstance to avoid infinitely deep type instantiation
- Use cn() in DefaultThumbnail for class merging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fillDialog now tries: PrimeVue dialog → node search box → focused input → keyboard fallback
- clickSubmenuItem now tries: PrimeVue tiered menu → litegraph context menu → role menuitem
- Fixes double-click-to-add-node flow and right-click context menu clicks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap each step in try/catch so failed steps don't abort the recording
- Add 5s timeout to clickByText to prevent 30s hangs on disabled elements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gh pr view can't distinguish PRs from issues — it succeeds for both.
Use the REST API endpoint repos/{owner}/{repo}/pulls/{number} which
returns 404 for issues.
- Enforce requestTimeoutMs via Gemini SDK requestOptions
- Add 100MB video size check before base64 encoding
- Sanitize screenshot filenames to prevent path traversal
- Sort video files by mtime for reliable rename
- Validate --mode arg against allowed values
- Add Content-Length pre-check in downloadMedia
- Add GitHub domain allowlist for media downloads (SSRF mitigation)
- Add contents:write permission and git config for report job
- Update Node.js requirement in SKILL.md from 18+ to 22+
Instead of crashing the entire recording session when Gemini generates
an invalid key name (e.g. "mouseWheelDown"), catch the error and
continue with remaining steps.
Add a "Behavior Changes" table (Behavior, Before, After, Verdict)
alongside the existing timeline comparison. This gives reviewers a
quick high-level view of all behavioral differences before diving
into the frame-by-frame timeline.
Instruct Gemini to output the Before vs After section as a markdown
table with Time, Type, Severity, Before, After columns for easier
comparison. Update HTML template table styles with fixed layout and
column widths optimized for the 5-column comparison format.
- Log raw Gemini response for debugging when parsing fails
- Handle possible wrapper keys in response
- Make qa-before/qa-after run even if analyze-pr fails (only gate
on resolve-matrix success)
When running on push events for sno-qa-* branches without an open PR,
extract the PR number from the branch name so analyze-pr can fetch
the full PR thread for analysis.
Add Gemini Pro-powered PR analysis that generates targeted QA guides
from the full PR thread (description, comments, screenshots, diff).
The analyze-pr job runs on lightweight ubuntu before recordings start,
producing qa-guide-before.json and qa-guide-after.json that are
downloaded by recording jobs to produce more focused test steps.
Graceful fallback: if analysis fails, recordings proceed without guides.
download-artifact@v7 merges all files flat regardless of
merge-multiple setting. Use separate path dirs (before/after)
and copy all files into the report directory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
download-artifact@v7 defaults merge-multiple to true, which puts all
files flat in qa-artifacts/ instead of per-artifact subdirectories.
The merge step expects qa-artifacts/qa-before-{os}-{run}/ subdirs,
so the report directory never gets created and video review finds
no files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of running before/after sequentially in a single job with
fragile git stash/checkout gymnastics, split into two independent
parallel jobs on separate runners:
resolve-matrix → qa-before (main) ─┐
→ qa-after (PR) ─┴→ report
- qa-before: uses git worktree for clean main branch build
- qa-after: normal PR build via setup-frontend
- report: downloads both artifact sets, merges, runs Gemini review
Benefits:
- Clean workspace isolation (no git checkout origin/main -- .)
- ~2x faster (parallel execution)
- Each job gets its own ComfyUI server (no shared state)
- Eliminates entire class of workspace contamination bugs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Pre-seed step creates qa-ci via API, so the "New user" form
shows "already exists" error. Fix by selecting the existing user
from the dropdown first, falling back to a unique username.
The localStorage userId bypass doesn't work because the server
validates user IDs and rejects the simple 'qa-ci' string. Instead,
detect the login page by its input fields and create a user via the
"New user" text input, which is how real users would log in.
Firefox headless doesn't support WebGL, causing "getCanvas: canvas is
null" errors. Switch to Chromium which has full headless WebGL support.
Also fix login flow to wait for async router guard to settle and
create user via text input as fallback.
Add coordinate fallback when .comfy-menu-button-wrapper selector isn't
found, and capture a debug screenshot after login to diagnose what the
page looks like when the editor UI fails to render.
The openComfyMenu was clicking at hardcoded coordinates (20, 67) which
missed the menu button. Now uses .comfy-menu-button-wrapper selector
matching the browser tests. Also fixes menu item hover/click selectors
to use .p-menubar-item-label and .p-tieredmenu-item classes, and adds
a wait for the editor UI to fully load before executing test steps.
The QA recordings were stuck on the user selection screen because CI
has no existing users. Fix by pre-seeding localStorage with userId,
userName, and TutorialCompleted before navigation, plus creating a
qa-ci user via API as a fallback.
The main branch build step was running pnpm install with main's lockfile,
which removed @google/generative-ai from node_modules. Move the reinstall
to after restoring PR files so the QA recording script can find its deps.
git checkout - uses @{-1} which requires a previous branch switch.
Since we use 'git checkout origin/main -- .' (file checkout, not branch
switch), there is no @{-1} ref. Use HEAD to restore from current branch.
Also restore proper concurrency group.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass the comprehensive test plan from .claude/skills/comfy-qa/SKILL.md
to Gemini when generating test steps. This gives Gemini knowledge of all
12 QA categories (canvas, menus, sidebar, settings, etc.) so it picks
the most relevant tests for each PR.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nx build runs typecheck as a prerequisite (via @nx/vite/plugin config).
Use vite build directly for the main branch comparison build.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Main branch may have transient TS errors when built with the PR
branch's lockfile. Since we only need the dist for visual comparison,
run nx build directly instead of pnpm build (which includes typecheck).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the unreliable codex exec approach with a Playwright script
(qa-record.ts) that uses Gemini to generate targeted test steps from
the PR diff, then executes them deterministically via Playwright's API.
Key changes:
- New scripts/qa-record.ts: Gemini generates JSON test actions, Playwright
executes them with reliable helper functions (menu nav, dialog fill, etc.)
- Remove codex CLI and playwright-cli dependencies
- Remove 150+ lines of prompt templates from pr-qa.yaml
- Firefox headless with video recording (same approach proven locally)
- Fallback steps if Gemini fails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Tighten BEFORE prompt to 15s snapshot (show old state only)
- Add qa-generate-test.ts: Gemini-powered Playwright test generator
- New workflow step: generate .spec.ts and push to {branch}-add-qa-test
- Tests assert UIUX behavior (tab names, dirty state, visibility)
- Build both main (dist-before/) and PR (dist/) frontends in focused mode
- Run QA twice: BEFORE on main branch frontend, AFTER on PR branch
- Send both videos to Gemini in one request for comparative analysis
- Side-by-side dashboard layout with Before (main) / After (PR) panels
- Comparative prompt evaluates whether before confirms old behavior
and after proves the fix works
- Falls back to single-video mode when no before video available
moov atom was at end of file (8.6MB offset) — browser had to download
the entire video before seeking. Keyframes were only every 10 seconds.
Add -movflags +faststart (moov before mdat) and -g 60 (keyframe every
2.4s at 25fps) to ffmpeg conversion.
- Remove autoplay/loop so video timeline is seekable
- Skip card generation for platforms without recordings
- Add --pr-context flag to qa-video-review.ts so Gemini evaluates
against PR purpose instead of just describing what happened
- Workflow now builds pr-context.txt from PR title/body/diff
The Codex agent was spending 35s browsing the "Getting Started" template
gallery instead of testing the PR's changes. Pre-seeding this setting
via the ComfyUI API ensures the agent lands directly in the graph editor.
The Codex agent was spending time on login flow, template browsing,
and general smoke testing instead of testing the PR's actual changes.
Changes:
- Add 30-second time budget for video recording
- Move video-start AFTER login and editor verification
- Explicitly prohibit template browsing and sidebar exploration
- Reduce test steps to 3-6 targeted actions
- Restructure prompt with clear Instructions/Rules sections
Replace crude sed-based markdown conversion with client-side
rendering via marked.js CDN. Adds proper table, list, and
code styling for the report section.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the OpenAI GPT-based frame extraction approach (ffmpeg + screenshots)
with Gemini 2.5 Flash's native video understanding. This eliminates false
positives from frame-based analysis (e.g. "black screen = critical bug" during
page transitions) and produces dramatically better QA reviews.
Changes:
- Remove ffmpeg frame extraction, ffprobe duration detection, and all related
logic (~365 lines removed)
- Add @google/generative-ai SDK for native video/mp4 upload to Gemini
- Update CLI: remove --max-frames, --min-interval-seconds, --keep-frames flags
- Update env: OPENAI_API_KEY → GEMINI_API_KEY
- Update workflow: swap API key secret and model in pr-qa.yaml
- Update report: replace "Frames analyzed" with "Video size"
- Add note in prompt that brief black frames during transitions are normal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
playwright-cli doesn't support 'evaluate' command. Instead, instruct
Codex to quickly fill the username input and click Next on user-select
page BEFORE starting video recording, so the video only shows actual
QA testing.
storageState config doesn't work with playwright-cli. Instead, use
evaluate to set Comfy.userId/userName after opening the page, then
navigate back. This skips user-select before video-start so the
recording only shows actual QA testing.
Write a Playwright storageState JSON with Comfy.userId/userName pre-set
so the app loads directly to the graph editor. Saves ~40s per QA run
that was wasted on navigating the user-select page.
The convert step was using find which picked up a 0-byte file from
playwright's videos/ directory instead of the valid qa-session.webm.
Now prefers qa-session.webm explicitly and skips empty files.
Codex was using pnpm dlx instead of the global playwright-cli.
Pre-install chromium in setup step and make prompt explicit about
using the global command directly without pnpm/npx.
Replace claude --print with codex exec for cheaper QA runs.
Uses codex-mini-latest model ($1.50/$6 vs Sonnet $3/$15).
Uses existing OPENAI_API_KEY secret (no new secrets needed).
- Replace saveVideo config (didn't produce video) with explicit
playwright-cli video-start/video-stop commands in QA prompt
- Remove apt-get install ffmpeg step (pre-installed on GH runners)
- Switch video review model from gpt-4o to gpt-4.1-mini
- Enable saveVideo in playwright-cli config for real video recording
- Replace screenshot stitching with webm→mp4 conversion
- Move video review step before deploy so reports are included
- Add GPT video review reports inline on the Cloudflare Pages site
- Each video card now has expandable "GPT Video Review" section
- Set .playwright/cli.config.json with outputDir pointing to screenshots/
- This way bare 'playwright-cli screenshot' auto-saves to the right place
- Create screenshot directory before Claude runs (don't rely on Claude)
- Collect step now searches working directory for stray PNGs
- Simplified prompt: no --filename needed, just 'playwright-cli screenshot'
Screenshots were saved to artifact root but stitch looked in frames/.
Now: prompt tells Claude to save to screenshots/ dir with numbered names,
collect step consolidates PNGs there, stitch step globs from screenshots/.
Removed video-start/video-stop (Claude doesn't use them).
- Add playwright-cli config with outputDir and saveVideo
- Use video-start/video-stop instead of relying on screenshot frames
- Add fallback artifact collection from .playwright-cli/ default dir
- Simplify prompts to focus on video recording workflow
The escaped \$QA_ARTIFACTS in the heredoc produced literal text
'$QA_ARTIFACTS' in the prompt. Claude's Bash tool didn't reliably
expand this env var, so no screenshots or reports were saved.
Remove the escapes so the heredoc expands the variable to the actual
path (e.g. /home/runner/work/_temp/qa-artifacts).
Backtick-wrapped playwright-cli examples in the unquoted heredoc were
being interpreted as bash command substitution, producing empty prompts.
Replace backtick syntax with plain "Run:" prefixed commands.
- Remove all Xvfb/ffmpeg screen recording infrastructure from qa job
(captured blank display since playwright-cli runs headless)
- Add screenshot instructions to QA prompts: Claude saves sequential
frames to $QA_ARTIFACTS/frames/ after every interaction
- Stitch screenshots into video via ffmpeg in report job (2fps)
- Merge video-review job into report job (4 jobs → 3 jobs)
- Unified PR comment with video links + video review in <details> collapse
- Clean up stale QA_VIDEO_REVIEW_COMMENT markers from prior runs
Move extra_server_params input to env var to prevent shell injection
from untrusted input. Replace wait-for-it pip dependency with a
cross-platform curl polling loop.
Add Claude Code skills and a label-triggered QA workflow:
- .claude/skills/comfy-qa/SKILL.md: 12-category QA test plan using
playwright-cli for browser automation
- .github/workflows/pr-qa.yaml: CI workflow triggered by qa-changes
(focused, Linux) or qa-full (3-OS matrix) labels. Records screen via
ffmpeg, runs Claude CLI with playwright-cli, deploys video gallery to
Cloudflare Pages, posts PR comment with GIF thumbnails, and runs
OpenAI vision-based video review
- scripts/qa-video-review.ts: frame extraction + GPT-4o analysis
- scripts/qa-video-review.test.ts: unit tests for video review
- knip.config.ts: resolve knip errors for ingest-types package
`formatJsonValue()` uses a loose regex `^\d{4}-\d{2}-\d{2}` to detect
date-like strings, which matches non-date strings like
`"2024-01-01-beta"`.
Changes:
- Require ISO 8601 `T` separator: `/^\d{4}-\d{2}-\d{2}T/`
- Validate parse result with `!Number.isNaN(date.getTime())`
- Use `d()` i18n formatter for consistency with `formatDate()` in the
same file
## Summary
Scaffolds the new apps/website/ Astro 5 + Vue 3 marketing site inside
the monorepo.
## Changes
- apps/website/ with package.json, astro.config.mjs, tsconfig, Nx
targets
- @comfyorg/design-system/css/base.css — brand tokens + fonts (no
PrimeVue)
- pnpm-workspace.yaml catalog entries for Astro deps
- .gitignore and env.d.ts for Astro
## Stack (via Graphite)
- **[1/3] Scaffold** ← this PR
- #10141 [2/3] Layout Shell
- #10142 [3/3] Homepage Sections
Part of the comfy.org website refresh (replacing Framer).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10140-feat-scaffold-Astro-5-website-app-design-system-base-css-1-3-3266d73d365081688dcee0220a03eca4)
by [Unito](https://www.unito.io)
## Summary
Restores `getAdditionalUserInfo` from Firebase Auth so sign-up telemetry
fires when *either* Firebase or the UI context identifies a new user,
fixing a regression from #10388.
## Changes
- **What**: In `loginWithGoogle` and `loginWithGithub`, call
`getAdditionalUserInfo(result)` and OR it with the UI-provided
`options?.isNewUser` flag: `is_new_user: options?.isNewUser ||
additionalUserInfo?.isNewUser || false`. Added 8 parameterized unit
tests covering the OR truth table (Firebase true, UI true, both false,
null result).
## Review Focus
The OR semantics: if either source says new user, we send `sign_up`
telemetry. Previously only the UI flag was checked, which missed cases
where the user lands directly on the OAuth provider without going
through the sign-up view.
## Testing
Unit tests cover all branches of the OR logic. An e2e test is not
feasible here because it would require completing a real OAuth flow with
Google/GitHub (interactive popup, valid credentials, CAPTCHA) and
intercepting the resulting `getAdditionalUserInfo` response from
Firebase — none of which can be reliably automated in a headless
Playwright environment without a live Firebase project seeded with
disposable accounts.
Fixes#10447
## Summary
Fix subgraph node slot connector links appearing misaligned after
workflow load, caused by a transform desync between LiteGraph's internal
canvas transform and the Vue TransformPane's CSS transform.
## Changes
- **What**: Changed `syncNodeSlotLayoutsFromDOM` to use DOM-relative
measurement (slot position relative to its parent `[data-node-id]`
element) instead of absolute canvas-space conversion via
`clientPosToCanvasPos`. This makes the slot offset calculation
independent of the global canvas transform, eliminating the frame-lag
desync that occurred when `fitView()` updated `lgCanvas.ds` before the
Vue CSS transform caught up.
- **Cleanup**: Removed the unreachable fallback path that still used
`clientPosToCanvasPos` when the parent node element wasn't found (every
slot element is necessarily a child of a `[data-node-id]` element — if
`closest()` fails the element is detached and measuring is meaningless).
This also removed the `conv` parameter from `syncNodeSlotLayoutsFromDOM`
and `flushScheduledSlotLayoutSync`, and the
`useSharedCanvasPositionConversion` import.
- **Test**: Added a Playwright browser test that loads a subgraph
workflow with `workflowRendererVersion: "LG"` (triggering the 1.2x scale
in `ensureCorrectLayoutScale`) as a template (triggering `fitView`), and
verifies all slot connector positions are within bounds of their parent
node element.
## Review Focus
- The core change is in `useSlotElementTracking.ts` — the new
measurement approach uses `getBoundingClientRect()` on both the slot and
its parent node element, then divides by `currentScale` to get
canvas-space offsets. This is simpler and more robust than the previous
approach.
- SubgraphNodes were disproportionately affected because they are
relatively static and don't often trigger `ResizeObserver`-based
re-syncs that would eventually correct stale offsets.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9121-fix-resolve-subgraph-node-slot-link-misalignment-during-workflow-load-3106d73d365081eca413c84f2e0571d6)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
The custom context menu provided by the frontend exposes widget specific
options. In order to support renaming, promotion, and favoriting, there
needs to be a way to access this context menu when targeting a textarea.
However, always displaying this custom context menu will cause the user
to lose access to browser specific functionality like spell checking,
translation, and the ability to copy paste text.
This PR updates the behaviour so that the native browser context menu
will display when the text area already has focus. Our custom frontend
context menu will continue to display when it does not.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10454-Use-native-context-menu-for-focused-textareas-32d6d73d365081909673d81d6a6ba054)
by [Unito](https://www.unito.io)
Rebased and adopted from #5774 by @felixturner.
## Changes
- Remove unused font-size properties (`NODE_TEXT_SIZE`,
`NODE_SUBTEXT_SIZE`, `DEFAULT_GROUP_FONT`) from theme palettes and color
palette schema
- Replace `DEFAULT_GROUP_FONT`/`DEFAULT_GROUP_FONT_SIZE` with a single
`GROUP_TEXT_SIZE = 20` constant (reduced from 24px)
- Use `NODE_TITLE_HEIGHT` for group header height instead of `font_size
* 1.4`
- Vertically center group title text using `textBaseline = 'middle'`
- Use `GROUP_TEXT_SIZE` directly in TitleEditor instead of per-group
`font_size`
- Remove `font_size` from group serialization (no longer per-group
configurable)
## Original PR
Closes#5774
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9839-feat-Improve-group-title-layout-3216d73d36508112a0edc2a370af20ba)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Felix Turner <felixturner@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Problem
When upgrading from V1 to V2 draft persistence, users' open workflow
tabs were lost. V1 stored tab state (open paths + active index) in
localStorage via `setStorageValue` fallback, but the V1→V2 migration
only migrated draft payloads — not these tab state pointers.
This meant that after upgrading, all previously open tabs disappeared
and users had to manually reopen their workflows.
## Solution
Add `migrateV1TabState()` to the V1→V2 migration path. After draft
payloads are migrated, the function reads the V1 localStorage keys
(`Comfy.OpenWorkflowsPaths` and `Comfy.ActiveWorkflowIndex`) and writes
them to V2's sessionStorage format via `writeOpenPaths()`.
The `clientId` is threaded from `useWorkflowPersistenceV2` (which has
access to `api.clientId`) through to `migrateV1toV2()`.
## Changes
- **`migrateV1toV2.ts`**: Added `migrateV1TabState()` + V1 key constants
for tab state
- **`useWorkflowPersistenceV2.ts`**: Pass `api.clientId` to migration
call
- **`migrateV1toV2.test.ts`**: Two new tests proving tab state migration
works
## Testing
TDD approach — RED commit shows the test failing, GREEN commit shows it
passing.
All 123 persistence tests pass.
- Fixes#9974
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10007-fix-migrate-V1-tab-state-pointers-during-V1-V2-draft-migration-3256d73d36508103b619e521c1b603f5)
by [Unito](https://www.unito.io)
## Summary
The preload error toast fires whenever any custom node extension fails
to load via dynamic `import()`. In practice, this is almost always
caused by third-party plugin bugs rather than ComfyUI core issues.
Common triggers include:
- Bare module specifiers (e.g., `import from "vue"`) that the browser
cannot resolve without an import map
- Incorrect relative paths to `scripts/app.js` due to nested web
directory structures
- Missing dependencies on other extensions (e.g., `clipspace.js`)
Since many users have multiple custom nodes installed, the toast
frequently appears on startup — sometimes multiple times — with a
generic message that offers no actionable guidance. This creates
unnecessary alarm and support burden.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10445-fix-disable-preload-error-toast-triggered-by-third-party-plugin-failures-32d6d73d365081f281efcd6fe90642a5)
by [Unito](https://www.unito.io)
## Summary
Adds custom status messages that are shown under the previews in order
to provide additional progress feedback to the user
Nodes matching the words:
Save, Preview -> Saving
Load, Loader -> Loading
Encode -> Encoding
Decode -> Decoding
Compile, Conditioning, Merge, -> Processing
Upscale, Resize -> Resizing
ToVideo -> Generating video
Specific nodes:
KSampler, KSamplerAdvanced, SamplerCustom, SamplerCustomAdvanced ->
Generating
Video Slice, GetVideoComponents, CreateVideo -> Processing video
TrainLoraNode -> Training
## Changes
- **What**:
- add specific node lookups for non-easily matchable patterns
- add regex based matching for common patterns
- show on both latent preview & skeleton preview
- allow app mode workflow authors to override status with custom
property `Execution Message` (no UI for doing this)
## Review Focus
This is purely pattern/lookup based, in future we could update the
backend node schema to allow nodes to define their own status key.
## Screenshots (if applicable)
<img width="757" height="461" alt="image"
src="https://github.com/user-attachments/assets/2b32cc54-c4e7-4aeb-912d-b39ac8428be7"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10369-feat-App-mode-add-execution-status-messages-32a6d73d3650814e8ca2da5eb33f3b65)
by [Unito](https://www.unito.io)
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Problem
Since PR #8520 (`feat(persistence): fix QuotaExceededError and
cross-workspace draft leakage`), all workflow tabs are lost when the
browser is closed and reopened.
PR #8520 moved tab pointers (`ActivePath`, `OpenPaths`) from
`localStorage` to `sessionStorage` for per-tab isolation. However,
`sessionStorage` is cleared when the browser closes, so the open tab
list is lost on restart. The draft data itself survives in
`localStorage` — only the pointers to which tabs were open are lost.
Reported in
[Comfy-Org/ComfyUI#12984](https://github.com/Comfy-Org/ComfyUI/issues/12984).
Confirmed via binary search: v1.40.9 (last good) → v1.40.10 (first bad).
## Changes
Dual-write tab pointers to both storage layers:
- **sessionStorage** (scoped by `clientId`) — used for in-session
refresh, preserves per-tab isolation
- **localStorage** (scoped by `workspaceId`) — fallback for browser
restart when sessionStorage is empty
Also adds:
- `storageAvailable` guard on write functions for consistency with
`writeIndex`/`writePayload`
- `isValidPointer` validation on localStorage reads to reject stale or
malformed data
## Benefits
- Workflow tabs survive browser restart (restores V1 behavior)
- Per-tab isolation is preserved for in-session use (sessionStorage is
still preferred when available)
## Trade-offs
- On browser restart, the restored tabs come from whichever browser tab
wrote last to localStorage. If Tab A had workflows 1,2,3 and Tab B had
4,5 — the user gets whichever tab wrote most recently. This is the same
limitation V1 had with `Comfy.OpenWorkflowsPaths` in localStorage.
- Previously (post-#8520), opening a new browser tab would only restore
the single most recent draft. With this fix, a new tab restores the full
set of open tabs from the last session. This may be surprising for
multi-tab users who expect a clean slate in new tabs.
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All 121 persistence tests pass
- [x] Manual: open multiple workflow tabs → close browser → reopen →
tabs restored
- [x] Manual: open two browser tabs with different workflows → refresh
each → correct tabs in each
FixesComfy-Org/ComfyUI#12984
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10336-fix-restore-workflow-tabs-on-browser-restart-3296d73d365081b7a7d3e91427d08d17)
by [Unito](https://www.unito.io)
<!-- QA_REPORT_SECTION -->
---
## 🔍 Automated QA Report
| | |
|---|---|
| **Status** | ✅ Complete |
| **Report** |
[sno-qa-10336.comfy-qa.pages.dev](https://sno-qa-10336.comfy-qa.pages.dev/)
|
| **CI Run** | [View
workflow](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/23373697656)
|
Before/after video recordings with **Behavior Changes** and **Timeline
Comparison** tables.
## Summary
Refactor essentials tab node organization to eliminate duplicated logic
and restrict essentials to core nodes only.
## Changes
- **What**:
- Extract `resolveEssentialsCategory` to centralize category resolution
(was duplicated between filter and pathExtractor).
- Add `isCoreNode` guard so third-party nodes never appear in
essentials.
- Replace `indexOf`-based sorting with precomputed rank maps
(`ESSENTIALS_CATEGORY_RANK`, `ESSENTIALS_NODE_RANK`).
<img width="589" height="769" alt="image"
src="https://github.com/user-attachments/assets/66f41f35-aef5-4e12-97d5-0f33baf0ac45"
/>
## Review Focus
- The `isCoreNode` guard in `resolveEssentialsCategory` — ensures only
core nodes can appear in essentials even if a custom node sets
`essentials_category`.
- Rank map precomputation vs previous `indexOf` — functionally
equivalent but O(1) lookup.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10433-refactor-clean-up-essentials-node-organization-logic-32d6d73d36508193a4d1f7f9c18fcef7)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Refactors the error system to improve separation of concerns, fix DDD
layer violations, and address code quality issues.
- Extract `missingNodesErrorStore` from `executionErrorStore`, removing
the delegation pattern that coupled missing-node logic into the
execution error store
- Extract `useNodeErrorFlagSync` composable for node error flag
reconciliation (previously inlined)
- Extract `useErrorClearingHooks` composable with explicit callback
cleanup on node removal
- Extract `useErrorActions` composable to deduplicate telemetry+command
patterns across error card components
- Move `getCnrIdFromNode`/`getCnrIdFromProperties` to
`platform/nodeReplacement` layer (DDD fix)
- Move `missingNodesErrorStore` to `platform/nodeReplacement` (DDD
alignment)
- Add unmount cancellation guard to `useErrorReport` async `onMounted`
- Return watch stop handle from `useNodeErrorFlagSync`
- Add `asyncResolvedIds` eviction on `missingNodesError` reset
- Add `console.warn` to silent catch blocks and empty array guard
- Hoist `useCommandStore` to setup scope, fix floating promises
- Add `data-testid` to error groups, image/video error spans, copy
button
- Update E2E tests to use scoped locators and testids
- Add unit tests for `onNodeRemoved` restoration and double-install
guard
Fixes#9875, Fixes#10027, Fixes#10033, Fixes#10085
## Test plan
- [x] Existing unit tests pass with updated imports and mocks
- [x] New unit tests for `useErrorClearingHooks` (callback restoration,
double-install guard)
- [x] E2E tests updated to use scoped locators and `data-testid`
- [ ] Manual: verify error tab shows runtime errors and missing nodes
correctly
- [ ] Manual: verify "Find on GitHub", "Copy", and "Get Help" buttons
work in error cards
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10302-refactor-error-system-cleanup-store-separation-DDD-fix-test-improvements-3286d73d365081838279d045b8dd957a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- The "Show Advanced Inputs" footer button was missing `headerColor`
style binding, causing it to not sync with the node header color (unlike
the "Enter Subgraph" button which already had it)
- Extracted the repeated `{ backgroundColor: headerColor }` inline style
(4 occurrences) into a `headerColorStyle` computed
## Screenshots
before
<img width="211" height="286" alt="스크린샷 2026-03-24 154312"
src="https://github.com/user-attachments/assets/edfd9480-04fa-4cd4-813d-a95adffbe2d3"
/>
after
<img width="261" height="333" alt="스크린샷 2026-03-24 154622"
src="https://github.com/user-attachments/assets/eab28717-889e-4a6b-8775-bfc08fa727ff"
/>
## Test plan
- [x] Set a custom color on a node with advanced inputs and verify the
footer button matches the header color
- [x] Verify subgraph enter button still syncs correctly
- [x] Verify dual-tab layouts (error + advanced, error + subgraph) both
show correct colors
### Why no E2E test
Node header color is applied as an inline style via `headerColor` prop,
which is already passed and tested through the existing subgraph enter
button path. This change simply extends the same binding to the advanced
inputs buttons — no new data flow or interaction is introduced, so a
screenshot-based E2E test would add maintenance cost without meaningful
regression coverage.
## Summary
Extract duplicated click-vs-drag detection logic into a shared
`useClickDragGuard` composable and `exceedsClickThreshold` pure utility
function.
## Changes
- **What**: New `useClickDragGuard(threshold)` composable in
`src/composables/useClickDragGuard.ts` that stores pointer start
position and checks squared distance against a threshold. Also exports
`exceedsClickThreshold` for non-Vue contexts.
- Migrated `DropZone.vue`, `useNodePointerInteractions.ts`, and
`Load3d.ts` to use the shared utility
- `CanvasPointer.ts` left as-is (LiteGraph internal)
- All consumers now use squared-distance comparison (no `Math.sqrt` or
per-axis `Math.abs`)
## Review Focus
- The composable uses plain `let` state instead of `ref` since
reactivity is not needed for the start position
- `Load3d.ts` uses the pure `exceedsClickThreshold` function directly
since it is a class, not a Vue component
- Threshold values preserved per-consumer: DropZone=5,
useNodePointerInteractions=3, Load3d=5
Fixes#10356
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10357-refactor-extract-shared-click-vs-drag-guard-utility-32a6d73d3650816e83f5cb89872fb184)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Reduce settings dialog size and autofocus search input for better
usability.
## Changes
- **What**: Reduce dialog size from `md` to `sm` (max-width 1400px →
960px); autofocus search input on open
## Review Focus
User feedback indicated the settings dialog was too wide and search
required an extra click to focus.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10396-fix-improve-settings-dialog-UX-32c6d73d365081e29eceed55afde1967)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Add grid view mode for multi-image batches in ImagePreview (Nodes 2.0),
replicating the Nodes 1.0 grid UX where all output images are visible as
clickable thumbnails.
## Changes
- **What**: Multi-image batches now default to a grid view showing all
thumbnails. Clicking a thumbnail switches to gallery mode for that
image. A persistent back-to-grid button sits next to navigation dots,
and hover action bars provide gallery toggle, download, and remove.
Replaced PrimeVue `Skeleton` with shadcn `Skeleton`. Added `viewGrid`,
`viewGallery`, `imageCount`, `galleryThumbnail` i18n keys.
## Review Focus
- Grid column count strategy: fixed breakpoints (2 cols ≤4, 3 cols ≤9, 4
cols 10+) vs CSS auto-fill
- Default view mode: grid for multi-image, gallery for single — matches
Nodes 1.0 behavior
- `object-contain` on thumbnails to avoid cropping (with `aspect-square`
containers for uniform cells)
Fixes#9162
<!-- Pipeline-Ticket: f8f8effa-adff-4ede-b1d3-3c4f04b9c4a0 -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9241-feat-add-grid-view-mode-for-multi-image-batches-in-ImagePreview-3136d73d36508166895ed6c635150434)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- When the cloud backend returns a 403 (user not whitelisted), the
frontend showed a generic "Prompt Execution Error" dialog with a cryptic
message
- Now catches 403 responses specifically and shows an "Access
Restricted" dialog with the backend's actual error message
- Adds `status` field to `PromptExecutionError` so error handlers can
distinguish HTTP status codes
## Changes
- `api.ts`: Added optional `status` to `PromptExecutionError`, pass
`res.status` from `queuePrompt`
- `app.ts`: New `else if` branch in the prompt error handler for `status
=== 403` — shows "Access Restricted" with the backend message
## Backwards compatible
- **Old backend** (`"not authorized"`): Shows "Access Restricted: not
authorized"
- **New backend**
([cloud#2941](https://github.com/Comfy-Org/cloud/pull/2941), `"your
account is not whitelisted for this feature"`): Shows "Access
Restricted: your account is not whitelisted for this feature"
- No behavior change for non-403 errors
## Related
- Backend fix: Comfy-Org/cloud#2941
- Notion: COM-16179
## Test plan
- [ ] Submit a prompt as a non-whitelisted user → should see "Access
Restricted" dialog with clear message
- [ ] Submit a prompt as a whitelisted user → no change in behavior
- [ ] Submit a prompt that fails for other reasons (missing nodes, etc.)
→ existing error handling unchanged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10402-fix-show-clear-error-dialog-for-403-whitelist-failures-32c6d73d365081eb9528d7feac4e8681)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Matt Miller <mattmiller@Matts-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
- When users open/import a workflow with missing nodes, we already track
this via Mixpanel
- This adds a parallel fire-and-forget POST to
`/api/internal/cloud_analytics` so the data also lands in **ClickHouse**
as `frontend:missing_nodes_detected` events
- Payload includes `missing_class_types[]`, `missing_count`, and
`source` (file_button/file_drop/template/unknown)
## Motivation
The frontend is where the **high-value** missing node signal lives —
most users see "missing nodes" and never submit. The backend only
catches the rare case where someone submits anyway. This change captures
both sides.
Companion cloud PR: https://github.com/Comfy-Org/cloud/pull/2886
## Changes
- `MixpanelTelemetryProvider.ts`: Added
`reportMissingNodesToClickHouse()` private method, called from
`trackWorkflowImported()` and `trackWorkflowOpened()`
- Only fires when `missing_node_count > 0`
- Fire-and-forget (`.catch(() => {})`) — no impact on user experience
- Uses existing `api.fetchApi()` which handles auth automatically
## Test plan
- [ ] Open a workflow with missing nodes → verify
`frontend:missing_nodes_detected` event appears in ClickHouse
- [ ] Open a workflow with no missing nodes → verify no event is sent
(check network tab)
- [ ] Verify Mixpanel tracking still works as before
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10132-feat-send-missing-node-data-to-ClickHouse-3266d73d365081559db5ed3efde33e95)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Promoted primitive subgraph inputs (String, Int) render their link
anchor at the header position instead of the widget row. Renaming
subgraph input labels breaks the match entirely, causing connections to
detach from their widgets visually.
## Changes
- **What**: Fix widget-input slot positioning for promoted subgraph
inputs in both LiteGraph and Vue (Nodes 2.0) rendering modes
- `_arrangeWidgetInputSlots`: Removed Vue mode branch that skipped
setting `input.pos`. Promoted widget inputs aren't rendered as
`<InputSlot>` Vue components (NodeSlots filters them out), so
`input.pos` is the only position fallback
- `drawConnections`: Added pre-pass to arrange nodes with unpositioned
widget-input slots before link rendering. The background canvas renders
before the foreground canvas calls `arrange()`, so positions weren't set
on the first frame
- `SubgraphNode`: Sync `input.widget.name` with the display name on
label rename and initial setup. The `IWidgetLocator` name diverged from
`PromotedWidgetView.name` after rename, breaking all name-based
slot↔widget matching (`_arrangeWidgetInputSlots`, `getWidgetFromSlot`,
`getSlotFromWidget`)
## Review Focus
- The `_arrangeWidgetInputSlots` rewrite iterates `_concreteInputs`
directly instead of building a spread-copy map — simpler and avoids the
stale index issue
- `input.widget.name` is now kept in sync with the display name
(`input.label ?? subgraphInput.name`). This is a semantic shift from
using the raw internal name, but it's required for all name-based
matching to work after renames. The value is overwritten on deserialize
by `_setWidget` anyway
- The `_widget` fallback in `_arrangeWidgetInputSlots` is a safety net
for edge cases where the name still doesn't match (e.g., stale cache)
Fixes#9998
## Screenshots
<img width="847" height="476" alt="Screenshot 2026-03-17 at 3 05 32 PM"
src="https://github.com/user-attachments/assets/38f10563-f0bc-44dd-a1a5-f4a7832575d0"
/>
<img width="804" height="471" alt="Screenshot 2026-03-17 at 3 05 23 PM"
src="https://github.com/user-attachments/assets/3237a7ee-f3e5-4084-b330-371def3415bd"
/>
<img width="974" height="571" alt="Screenshot 2026-03-17 at 3 05 16 PM"
src="https://github.com/user-attachments/assets/cafdca46-8d9b-40e1-8561-02cbb25ee8f2"
/>
<img width="967" height="558" alt="Screenshot 2026-03-17 at 3 05 06 PM"
src="https://github.com/user-attachments/assets/fc03ce43-906c-474d-b3bc-ddf08eb37c75"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10195-fix-subgraph-promoted-widget-input-slot-positions-after-label-rename-3266d73d365081dfa623dd94dd87c718)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: jaeone94 <jaeone.prt@gmail.com>
## Summary
- Extract all 75 `quickRegister()` mapping entries from
`modelToNodeStore.ts` into a new `modelNodeMappings.ts` constants file
- The store now iterates over the `MODEL_NODE_MAPPINGS` array instead of
having inline calls
- **Zero behavioral change** — same mappings, same order, same runtime
behavior
## Motivation
Adding new model-to-node mappings is currently a code change to the
store. By separating the data into its own file:
- New mappings are a **pure data change** (append a tuple to an array)
- The data file can have its own CODEOWNERS entry, so mapping PRs can be
merged without requiring frontend team review
- Easier to audit — all mappings visible in one place without
interleaved store logic
### Before
```ts
// 250+ lines of quickRegister() calls mixed into store logic
quickRegister('checkpoints', 'CheckpointLoaderSimple', 'ckpt_name')
quickRegister('checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name')
// ... 73 more
```
### After
```ts
// modelNodeMappings.ts — pure data
export const MODEL_NODE_MAPPINGS = [
['checkpoints', 'CheckpointLoaderSimple', 'ckpt_name'],
['checkpoints', 'ImageOnlyCheckpointLoader', 'ckpt_name'],
// ...
]
// modelToNodeStore.ts — just iterates
for (const [modelType, nodeClass, key] of MODEL_NODE_MAPPINGS) {
quickRegister(modelType, nodeClass, key)
}
```
## Test plan
- [ ] "Use" button in model browser still works for all model types
- [ ] No regressions in model-to-node resolution (same mappings, same
order)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10237-refactor-extract-model-to-node-mappings-into-separate-data-file-3276d73d365081988656e2ddae772bbc)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Problem
GA4 shows **zero** `sign_up` events from cloud.comfy.org. All new users
(Google/GitHub) are tracked as `login` instead of `sign_up`.
**Root cause:** `getAdditionalUserInfo(result)?.isNewUser` from Firebase
is unreliable for popup auth flows — it compares `creationDate` vs
`lastSignInDate` timestamps, which can differ even for genuinely new
users. When it returns `null` or `false`, the code defaults to pushing a
`login` event instead of `sign_up`.
**Evidence:** GA4 Exploration filtered to `sign_up` + `cloud.comfy.org`
shows 0 events, while `login` shows 8,804 Google users, 519 email, 193
GitHub — all new users are being misclassified as logins.
(Additionally, the ~300 `sign_up` events visible in GA4 are actually
from `blog.comfy.org` — Substack newsletter subscriptions — not from the
app at all.)
## Fix
Use the UI flow context to determine `is_new_user` instead of Firebase's
unreliable API:
- `CloudSignupView.vue` → passes `{ isNewUser: true }` (user is on the
sign-up page)
- `CloudLoginView.vue` → no flag needed, defaults to `false` (user is on
the login page)
- `SignInContent.vue` → passes `{ isNewUser: !isSignIn.value }` (dialog
toggles between sign-in/sign-up)
- Removes the unused `getAdditionalUserInfo` import
## Changes
- `firebaseAuthStore.ts`: `loginWithGoogle`/`loginWithGithub` accept
optional `{ isNewUser }` parameter instead of calling
`getAdditionalUserInfo`
- `useFirebaseAuthActions.ts`: passes the option through
- `CloudSignupView.vue`: passes `{ isNewUser: true }`
- `SignInContent.vue`: passes `{ isNewUser: !isSignIn.value }`
## Testing
- All 32 `firebaseAuthStore.test.ts` tests pass
- All 19 `GtmTelemetryProvider.test.ts` tests pass
- Typecheck passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10388-fix-use-UI-flow-context-for-sign_up-vs-login-telemetry-32b6d73d3650811e96cec281108abbf3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Differentiates the subscription pricing dialog between personal and team
workspaces with distinct visual treatments and a two-stage team
workspace upgrade flow.
### Changes
- **Personal pricing dialog**: Shows "P" avatar badge, "Plans for
Personal Workspace" header, and "Solo use only – Need team workspace?"
banner on each tier card
- **Team pricing dialog**: Shows workspace avatar, "Plans for Team
Workspace" header (emerald), green "Invite up to X members" badge, and
emerald border on Creator card
- **Two-stage upgrade flow**: "Need team workspace?" → closes pricing →
opens CreateWorkspaceDialog → sessionStorage flag → page reload →
WorkspaceAuthGate auto-opens team pricing dialog
- **Spacing**: Reduced vertical gaps/padding/font sizes so the table
fits without scrolling
### Key decisions
- sessionStorage key `comfy:resume-team-pricing` bridges the page reload
during workspace creation
- `onChooseTeam` prop is conditionally passed only to the personal
variant
- `resumePendingPricingFlow()` is called from WorkspaceAuthGate after
workspace initialization
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9901-feat-differentiate-personal-team-pricing-table-with-two-stage-team-workspace-flow-3226d73d365081e7af60dcca86e83673)
by [Unito](https://www.unito.io)
## Summary
FormDropdown Outputs tab only showed the first output for multi-output
jobs because the Jobs API `/jobs` returns a single `preview_output` per
job.
## Changes
- **What**: When history assets include jobs with `outputs_count > 1`,
lazily fetch full outputs via `getJobDetail` (cached in
`jobOutputCache`) and expand them into individual dropdown items.
Single-output jobs are unaffected. Added in-flight guard to prevent
duplicate fetches.
- This is a consumer-side workaround in `WidgetSelectDropdown.vue` that
becomes a no-op once the backend returns all outputs in the list
response (planned Assets API migration).
## Review Focus
- The `resolvedMultiOutputs` shallowRef + watch pattern for async data
feeding into a computed. Each `getJobDetail` call is cached by
`jobOutputCache` LRU, so no redundant network requests.
- This fix is intentionally temporary — it will be superseded when
OSS/cloud both return full outputs from list endpoints.
## No E2E test
E2E coverage is impractical here: reproducing requires a running ComfyUI
backend executing a workflow that produces multiple outputs, then
inspecting the FormDropdown's Outputs tab. The unit test covers the
lazy-loading logic with mocked `getJobDetail` responses.
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Prevent the nightly survey popup from appearing when the feature being
surveyed is currently disabled.
## Root Cause
`useSurveyEligibility` checks the feature usage count from localStorage
but does not verify whether the surveyed feature is currently active.
When a user uses the new node search 3+ times (reaching the survey
threshold), then switches back to legacy search, the usage count
persists in localStorage and the survey popup still appears despite the
feature being off.
## Steps to Reproduce
1. Enable new node search (Settings > Node Search Box Implementation >
"default")
2. Use node search 3+ times (double-click canvas, search for nodes)
3. Switch back to legacy search (Settings > Node Search Box
Implementation > "litegraph (legacy)")
4. Wait 5 seconds on a nightly localhost build
5. **Expected**: No survey popup appears (feature is disabled)
6. **Actual**: Survey popup appears because eligibility only checks
stored usage count, not current feature state
## Changes
1. Added optional `isFeatureActive` callback to `FeatureSurveyConfig`
interface
2. Added `isFeatureActive` guard in `useSurveyEligibility` eligibility
computation
3. Configured `node-search` survey with `isFeatureActive` that checks
the current search box setting
## Red-Green Verification
| Commit | Result | Description |
|---|---|---|
| `99e7d7fe9` `test:` | RED | Asserts `isEligible` is false when
`isFeatureActive` returns false. Fails on current code. |
| `01df9af12` `fix:` | GREEN | Adds `isFeatureActive` check to
eligibility. Test passes. |
Fixes#10333
## Summary
Add Vue Testing Library (VTL) infrastructure and pilot-migrate
ComfyQueueButton.test.ts as Phase 0 of an incremental VTL adoption.
## Changes
- **What**: Install `@testing-library/vue`,
`@testing-library/user-event`, `@testing-library/jest-dom`, and
`eslint-plugin-testing-library`. Configure jest-dom matchers globally
via `vitest.setup.ts` and `tsconfig.json`. Create shared render wrapper
at `src/utils/test-utils.ts` (pre-configures PrimeVue, Pinia, i18n).
Migrate `ComfyQueueButton.test.ts` from `@vue/test-utils` to VTL. Add
warn-level `testing-library/*` ESLint rules for test files.
- **Dependencies**: `@testing-library/vue`,
`@testing-library/user-event`, `@testing-library/jest-dom`,
`eslint-plugin-testing-library`
## Review Focus
- `src/utils/test-utils.ts` — shared render wrapper typing approach
(uses `ComponentMountingOptions` from VTU since VTL's `RenderOptions`
requires a generic parameter)
- ESLint rules are all set to `warn` during migration to avoid breaking
existing VTU tests
- VTL coexists with VTU — no existing tests are broken
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10319-test-add-Vue-Testing-Library-infrastructure-and-pilot-migration-3286d73d3650812793ccd8a839550a04)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Upgrade Nx from 22.5.2 to 22.6.1.
## Changes
- **What**: Bumped nx, @nx/eslint, @nx/playwright, @nx/storybook, and
@nx/vite from 22.5.2 to 22.6.1.
- **Dependencies**: nx, @nx/eslint, @nx/playwright, @nx/storybook,
@nx/vite updated to 22.6.1.
## Review Focus
All Nx migrations ran with no changes needed — this is a straightforward
version bump.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10370-chore-upgrade-nx-22-5-2-22-6-1-32a6d73d36508191988bdc7376dc5e14)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
We've had some reports of issues selecting inputs/nodes when trying to
use the builder in LiteGraph mode and due to the complexity of the
canvas system, we're going to enable Nodes 2.0 when entering the builder
to ensure the best experience.
## Changes
- **What**:
- When entering builder select mode automatically switch to Nodes 2.0
- Extract reusable component from features toast
- Show popup telling user the mode was changed
- Add hidden setting for storing "don't show again" on the switch popup
## Review Focus
- I have not removed the LiteGraph selection code in case someone still
manages to enter the builder in LiteGraph mode, this should be cleaned
up in future
## Screenshots (if applicable)
<img width="423" height="224" alt="image"
src="https://github.com/user-attachments/assets/cc2591bc-e5dc-47ef-a3c6-91ca7b6066ff"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10337-feat-App-mode-Switch-to-Nodes-2-0-when-entering-builder-3296d73d3650818e9f3cdaac59d15609)
by [Unito](https://www.unito.io)
## Summary
<!-- One sentence describing what changed and why. -->
Enable Quiver AI icon for partner nodes
## Changes
- **What**: <!-- Core functionality added/modified -->
- Enable Quiver AI icon for partner nodes
No Quiver AI nodes now, just mock the data to see how it will look
<img width="1062" height="926" alt="image"
src="https://github.com/user-attachments/assets/1b81a1cc-d72c-413d-bd75-a63925c27a4b"
/>
## Review Focus
When Quiver AI nodes provided, the icon should show at both node library
tree, but also the PreviewCard label.
<!-- Critical design decisions or edge cases that need attention -->
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots (if applicable)
<!-- Add screenshots or video recording to help explain your changes -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10366-feat-enable-Quiver-AI-icon-for-partner-nodes-32a6d73d365081d4801ec2619bd2c77c)
by [Unito](https://www.unito.io)
## Summary
Add a waveform-based audio player component (`WaveAudioPlayer`)
replacing the native `<audio>` element, with authenticated API fetch for
cloud audio playback.
## Changes
- **What**:
- Add `useWaveAudioPlayer` composable with waveform visualization from
audio data (Web Audio API `decodeAudioData`), playback controls, and
seek support
- Add `WaveAudioPlayer.vue` component with compact (inline waveform +
time) and expanded (full transport controls) variants
- Replace native `<audio>` in `MediaAudioTop.vue` and `ResultAudio.vue`
with `WaveAudioPlayer`
- Use `api.fetchApi()` instead of bare `fetch()` to include Firebase JWT
auth headers, fixing 401 errors in cloud environments
- Add Storybook stories and unit tests
## Review Focus
- The audio URL is fetched via `api.fetchApi()` with auth headers,
converted to a Blob URL, then passed to the native `<audio>` element.
This avoids 401 Unauthorized in cloud environments where `/api/view`
requires authentication.
- URL-to-route extraction logic (`url.includes(apiBase)`) handles both
full API URLs and relative paths.
[screen-capture.webm](https://github.com/user-attachments/assets/44e61812-0391-4b47-a199-92927e75f8b4)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10158-feat-add-WaveAudioPlayer-with-waveform-visualization-and-authenticated-audio-fetch-3266d73d365081beab3fc6274c39fcd4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
- The DraggableList component takes a v-model.
- When a drag is completed, it reassigns to v-model so an update event
fires
- App Builder sets v-model="appModeStore.selectedInputs"
- Thus a completed drag operation effectively performs
appModeStore.selectedInputs = newList
- In appModeStore, selectedInputs is a reactive. Thus, after a drag/drop
operation occurs, selectedInputs in the store is not the same value as
appModeStore.selectedInputs
When a reliable repro for the issue had not yet been found, an attempted
earlier fix for this was to swap from watchers to directly updating
`selectedInputs`. Since this change makes the code cleaner and still
make the code safer, the change is left in place.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10342-Don-t-use-watcher-for-loading-app-mode-selections-3296d73d365081f7b216e79c4f4f4e8d)
by [Unito](https://www.unito.io)
## Summary
- Hide the "Comfy API Key" login button and help text from the sign-in
modal when running on Cloud
- API key auth is only used on local ComfyUI; Cloud has
Firebase-whitelisted Google/GitHub auth
- The button was appearing on Cloud as a relic of shared code between
Cloud and local ComfyUI
## Context
[Discussion with Robin in
#proj-cloud-frontend](https://comfy-organization.slack.com/archives/C09FY39CC3V/p1773977756997559)
— API key auth only works on local because Firebase requires whitelisted
domains. On Cloud, SSO (Google/GitHub) works natively, so the API key
option is unnecessary and confusing.
<img width="470" height="865" alt="Screenshot 2026-03-20 at 9 53 20 AM"
src="https://github.com/user-attachments/assets/5bbdcbaf-243c-48c6-9bd0-aaae815925ea"
/>
## Test plan
- [ ] Verify login modal on local ComfyUI still shows the "Comfy API
Key" button
- [ ] Verify login modal on cloud.comfy.org no longer shows the "Comfy
API Key" button
## Summary
- Add keybinding preset system: save, load, switch, import, export, and
delete named keybinding sets stored via `/api/userdata/keybindings/`
- Preset selector dropdown with "Save Changes" button for modified
custom presets, and "Import keybinding preset" action
- More-options menu in header row with save as new, reset, delete,
import, and export actions
- Search box and menu teleported to settings dialog header (matching
templates modal layout)
- 11 unit tests for preset service CRUD operations
Fixes#1084Fixes#1085
## Test plan
- [ ] Open Settings > Keybinding, verify search box and "..." menu
appear in header
- [ ] Modify a keybinding, verify "Default *" shows modified indicator
- [ ] Use "Save as new preset" from menu, verify preset appears in
dropdown
- [ ] Switch between presets, verify unsaved changes prompt
- [ ] Export preset, import it back, verify bindings restored
- [ ] Delete a custom preset, verify reset to default
- [ ] Verify "Save Changes" button appears only on modified custom
presets
- [ ] Run `pnpm vitest run
src/platform/keybindings/presetService.test.ts`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9681-feat-import-export-keybinding-presets-31e6d73d3650810f88e4d21b3df3e2dd)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Description
On cloud, asset filenames are content hashes (e.g. `16bcbbe5...png`)
while the UI shows `display_name` (e.g. `ComfyUI_00011_`). Fuse.js was
only searching the `name` field, so typing the visible name in the Media
Assets search returned no results.
## Changes
### What
- Add `display_name` to the Fuse.js search keys in
`useMediaAssetFiltering`, so users can search by the name they actually
see in the panel.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10254-fix-search-media-assets-by-display_name-in-addition-to-name-3276d73d3650813dbaf0ccd9d57ccfa3)
by [Unito](https://www.unito.io/)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Add e2e test covering node copy/paste (Ctrl+C/V), image paste onto a
selected LoadImage node, and image paste on empty canvas creating a new
LoadImage node
- Extract `simulateImagePaste` into reusable
`ClipboardHelper.pasteFile()` with auto-detected filename and MIME type
- Add test workflow asset `load_image_with_ksampler.json` and
`image32x32.webp` image asset
## Test plan
- [ ] `pnpm typecheck:browser` passes
- [ ] `pnpm exec eslint browser_tests/tests/copyPaste.spec.ts` passes
- [ ] New test passes in CI: `Copy paste node, image paste onto
LoadImage, image paste on empty canvas`
- [ ] Existing copy/paste tests unaffected
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10233-test-add-copy-paste-node-and-image-paste-e2e-tests-3276d73d365081e78bb9f3f1bf34389f)
by [Unito](https://www.unito.io)
## Summary
Eliminate the double `goto()` in `ComfyPage.setup()` by using
`addInitScript` to seed localStorage before the first navigation.
## Changes
- **What**: Move route mocking and localStorage seeding before `goto()`,
replacing `page.evaluate` with `page.addInitScript` so values are set
before app JS executes on first load. Removes the second `goto()` call
entirely.
## Review Focus
- Verify all Playwright E2E shards pass — the `clearStorage: false` and
`mockReleases: false` paths should be unaffected.
- Note: `addInitScript` persists for the page lifetime (like
`FeatureFlagHelper.seedFlags` already does), so any subsequent
`page.reload()` or `goto()` in tests will also clear storage. This
should be fine since tests that use `clearStorage: false` skip this
block.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10313-perf-eliminate-double-page-navigation-in-Playwright-test-setup-3286d73d36508158a8edeefb27fcae20)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix workflow loading for nested subgraphs with duplicate node IDs by
configuring subgraph definitions in topological (leaf-first) order.
## Changes
- **What**: Three pre-existing bugs that surface when loading nested
subgraphs with colliding node IDs:
1. Subgraph definitions configured in serialization order — a parent
subgraph's `SubgraphNode.configure` would run before its referenced
child subgraph was populated, causing link/widget resolution failures.
2. `_resolveLegacyEntry` returned `undefined` when `input._widget`
wasn't set yet, instead of falling back to `resolveSubgraphInputTarget`.
3. `_removeDuplicateLinks` removed duplicate links without updating
`SubgraphOutput.linkIds`, leaving stale references that broke prompt
execution.
- **What (housekeeping)**: Moved `subgraphDeduplication.ts` from
`utils/` to `subgraph/` directory where it belongs.
## Review Focus
- Topological sort correctness: Kahn's algorithm with edges from
dependency→dependent ensures leaves configure first. Cycle fallback
returns original order.
- IO slot link repair: `_repairIOSlotLinkIds` runs after
`_configureSubgraph` creates IO slots, patching any `linkIds` that point
to links removed by `_removeDuplicateLinks` during `super.configure()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10314-fix-configure-nested-subgraph-definitions-in-dependency-order-3286d73d36508171b149e238b8de84c2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
- When in app mode, workflows can be loaded by dragging and dropping as
elsewhere.
- Dragging a file which is supported by a selected app input to the
center panel will apply drop effects on the specific input
- This overrides the loading of workflows
- There's not currently an indicator for where the image will go. This
is being considered for a followup PR
- Outputs can be dragged from the assets panel onto nodes
- This fixes behaviour outside of app mode as well
- Has some thorny implementation specifics
- Non-core nodes may not be able to accept these inputs without an
update
- Node DragOver filtering has reduced functionality when dragging from
the assets pane. Nodes may have the blue border without being able to
accept a drag operation.
- When dropped onto the canvas, the workflow will load (a fix), but the
workflow name will be the url of the image preview
- The entire card is used for the drag preview
<img width="329" height="380" alt="image"
src="https://github.com/user-attachments/assets/2945f9a3-3e77-4e14-a812-4a361976390d"
/>
- Adds a new scroll-shadows tailwind util as an indicator that more
content is available by scrolling.
- Since a primary goal was preventing API costs overflowing, I've made
the indicator fairly strong. This can be tuned later if needed

- Initial support for text outputs in App Mode
- Also causes jobs with text outputs to incorrectly display in the
assets panel with a generic 'check' icon instead of a text specific
icon. This will need a dedicated pass, but shouldn't be overly onerous
in the interim.
<img width="1209" height="735" alt="text output"
src="https://github.com/user-attachments/assets/fcd1cf9f-5d5c-434c-acd0-58d248237b99"
/>
NOTE: Displaying text outputs conflicted with the changes in #9622. I'll
leave text output still disabled in this PR and open a new PR for
reconciling text as an output so it can go through dedicated review.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10122-App-Mode-dragAndDrop-text-output-and-scroll-shadows-3256d73d3650810caaf8d75de94388c9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Set topbar menus to non-modal so they dismiss when clicking
inputs/textareas inside nodes with Nodes 2.0 enabled.
## Changes
- **What**: Add `:modal="false"` to `ContextMenuRoot` in WorkflowTab and
`DropdownMenuRoot` in WorkflowActionsDropdown.

## Review Focus
Modal reka-ui menus set `body.pointer-events: none` and prevent
`focusOutside` dismissal. With Nodes 2.0, widget components use
`@pointerdown.capture.stop` to prevent node dragging, which also blocks
reka-ui's document-level outside-click detection. Non-modal menus allow
`focusin`-based dismissal, which is unaffected by pointerdown stopping.
## Testing
An E2E regression test for this fix requires Nodes 2.0 to be explicitly
enabled (feature-flag guarded), opening a specific topbar menu, and then
clicking inside a canvas node's textarea — an interaction sequence that
has no existing Playwright fixture/helper pattern in the codebase; the
fix itself is a one-line :modal="false" attribute change on reka-ui
primitives whose behavior is documented and tested upstream.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10310-fix-set-topbar-menus-to-non-modal-so-they-dismiss-on-canvas-interaction-3286d73d3650815287d1c66c6ffd4814)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fixes a regression introduced in v1.41.21 where
`_removeDuplicateLinks()` (added by #9120 / backport #10045) incorrectly
removes valid links during workflow loading when the target node has
widget-to-input conversions that shift slot indices.
- Fixes https://github.com/Comfy-Org/workflow_templates/issues/715
## Root Cause
The `_removeDuplicateLinks()` method added in #9120 uses
`node.inputs[link.target_slot]` to determine which duplicate link to
keep. However, `target_slot` is the slot index recorded at serialization
time. During `LGraphNode.configure()`, the `onConnectionsChange`
callback triggers widget-to-input conversions (e.g., KSamplerAdvanced
converting `steps`, `cfg`, `start_at_step`, etc.), which inserts new
entries into the `inputs` array. This shifts indices so that
`node.inputs[target_slot]` no longer points to the expected input.
**Concrete example with `video_wan2_2_14B_i2v.json`:**
The Wan2.2 Image-to-Video subgraph contains a Switch node (id=120)
connected to KSamplerAdvanced (id=85) cfg input. The serialized data has
two links with the same connection tuple `(origin_id=120, origin_slot=0,
target_id=85, target_slot=5)`:
| Link ID | Connection | Status |
|---------|-----------|--------|
| 257 | 120:0 → 85:5 (FLOAT) | Orphaned duplicate (not referenced by any
input.link) |
| 276 | 120:0 → 85:5 (FLOAT) | Valid (referenced by node 85
input.link=276) |
When `_removeDuplicateLinks()` runs after all nodes are configured:
1. KSamplerAdvanced is created with 4 default inputs, but after
`configure()` with widget conversions, it has **13 inputs** (shifted
indices)
2. The method checks `node.inputs[5].link` (target_slot=5 from the
LLink), but index 5 is now a different input due to the shift
3. `node.inputs[5].link === null` → the method incorrectly decides link
276 is not referenced
4. **Link 276 (valid) is removed, link 257 (orphan) is kept** →
connection lost
This worked correctly in v1.41.20 because `_removeDuplicateLinks()` did
not exist.
## Changes
Replace the `target_slot`-based positional lookup with a full scan of
the target node's inputs to find which duplicate link ID is actually
referenced by `input.link`. Also repair `input.link` if it still points
to a removed duplicate after cleanup.
## Test Plan
- [x] Added regression test: shifted slot index scenario
(widget-to-input conversion)
- [x] Added regression test: `input.link` repair when pointing to
removed duplicate
- [x] Existing `_removeDuplicateLinks` tests pass (45/45)
- [x] Full unit test suite passes (6885/6885)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes (0 errors)
- [x] Manual verification: loaded `video_wan2_2_14B_i2v.json` in clean
state — Switch→KSamplerAdvanced cfg link is now preserved
- [ ] E2E testing is difficult for this fix since it requires a workflow
with duplicate links in a subgraph containing nodes with widget-to-input
conversions (e.g., KSamplerAdvanced). The specific conditions —
duplicate LLink entries + slot index shift from widget conversion — are
hard to set up in Playwright without a pre-crafted fixture workflow and
backend node type registration. The unit tests cover the core logic
directly.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10289-fix-_removeDuplicateLinks-incorrectly-removes-valid-link-when-slot-indices-shift-3286d73d36508140b053fd538163e383)
by [Unito](https://www.unito.io)
## Summary
Fix broken link rendering (noodles disappearing or going to wrong
positions) when switching between app mode and graph mode tabs.
## Changes
- **What**: When the graph canvas is hidden via `display: none` in app
mode, slot elements lose valid DOM measurements. On switching back,
links rendered at stale coordinates or disappeared. This PR rekeys
`LGraphNode` components by workflow path, adds measurability guards to
skip hidden slots, clears stale layouts, and watches `linearMode` to
trigger a full slot layout resync on mode transitions.
## Review Focus
- The `isSlotElementMeasurable` guard skips elements that are
disconnected or have zero-size rects — verify this doesn't inadvertently
skip slots during normal graph rendering.
- The `linearMode` watcher clears all slot layouts when entering app
mode and requests a full resync when leaving — confirm no flicker or
race with the RAF-based sync scheduler.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10273-fix-resync-slot-layouts-when-switching-between-app-mode-and-graph-mode-3276d73d3650812f9366dae53c7b2d37)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fire a client-side `subscription_success` event to GTM when a
subscription activates, enabling LinkedIn and Meta conversion tracking
tags.
## Changes
- **What**: Wire `trackMonthlySubscriptionSucceeded()` into both
subscription detection paths (legacy dialog watcher + workspace
billingOperationStore). This method existed but had zero call sites
(dead code). The event name remains `subscription_success` (not
`purchase`) to avoid double-counting with the server-side GA4
Measurement Protocol purchase event and the existing Google Ads Purchase
conversion tag in GTM.
## Review Focus
- billingOperationStore only fires telemetry for `subscription`
operations, not `topup`.
- Legacy flow: telemetry fires in the existing `isActiveSubscription`
watcher, before `emit("close", true)`.
- Workspace flow: telemetry fires in `handleSuccess()` after status
update but before billing context refresh.
- No event name change: `subscription_success` was the original name and
avoids collision with the server-side `purchase` event.
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
Fix the bottom-right graph canvas toolbar (zoom controls, fit-to-view,
minimap toggle) not being visible on mobile devices in normal graph
mode.
## Problem
PrimeVue applies `overflow: hidden` to all `.p-splitterpanel` elements
by default. The `GraphCanvasMenu` component is absolutely positioned
(`right-0 bottom-0`) inside the `graph-canvas-panel` SplitterPanel. On
mobile viewports, the panel's bounding box can be smaller than the full
canvas area, causing the toolbar to be clipped by the `overflow:
hidden`.
## Solution
Add `overflow-visible` to the `graph-canvas-panel` SplitterPanel class
to override PrimeVue's default `overflow: hidden`. This allows the
absolutely-positioned toolbar (and minimap) to remain visible regardless
of viewport size.
<img width="873" height="1056" alt="image"
src="https://github.com/user-attachments/assets/7239a5ce-8ce8-4e1d-a8ff-6d6d3c61f5da"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10168-fix-make-graph-canvas-toolbar-visible-on-mobile-3266d73d36508130b675e839cb748fd5)
by [Unito](https://www.unito.io)
## Summary
Fixes a regression introduced in #9680 where groups and nodes could
render in different coordinate spaces when loading legacy
`workflowRendererVersion: "Vue"` workflows with Vue nodes mode enabled.
- Add post-normalization sync to copy normalized LiteGraph node bounds
into `layoutStore` during `loadGraphData`
- Keep sync scoped to Vue nodes mode and only when normalization
actually ran
- Add unit tests for the new layout-store sync helper
- Add Playwright regression coverage for legacy Vue workflow load path
using `groups/nested-groups-1-inner-node`, asserting node/group
centering-gap distances remain within baseline tolerances
## Testing
- pnpm test:unit
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm test:unit
src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.test.ts
- pnpm exec eslint
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm exec oxlint src/scripts/app.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm typecheck
- pnpm typecheck:browser
- pnpm exec eslint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec oxlint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec playwright test
browser_tests/tests/vueNodes/groups/groups.spec.ts --grep "legacy Vue
workflows"
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10256-fix-resync-vue-node-layout-store-after-legacy-normalization-3276d73d365081568eebc6aa0827d943)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
- On graph change, set the `graph.id` as location hash
- On hash change, navigate to the target `graph.id` either in the
current, or any other loaded workflow.
`canvasStore.currentGraph` does not trigger when `app.loadGraphData` is
called. A trigger could be forced here, but I'm concerned about side
effects. Instead `updateHash` is manually called.
Code search shows that there are no current custom nodes using
`onhashchange`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6811-Allow-graph-navigation-by-browser-forward-backward-2b26d73d365081bb8414fdf7c3686124)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Fix `findProvidersWithFallback` to try all parent paths progressively
instead of jumping from exact match to top-level segment only.
Previously `"a/b/c"` would try `"a/b/c"` then `"a"`, now correctly tries
`"a/b/c"` → `"a/b"` → `"a"`
- Use empty key `''` for `DownloadAndLoadCogVideoControlNet` and
`DownloadAndLoadCogVideoModel` registrations so `shouldUseAssetBrowser`
returns false — these nodes use HuggingFace repo names, not file-based
assets, so the asset browser finds nothing and shows an empty dropdown
## Test plan
- [x] All 46 existing `modelToNodeStore` tests pass
- [ ] Verify CogVideo ControlNet and HF model dropdowns show options
(after backend deploy)
- [x] Verify "Use" button on CogVideo/ControlNet/* models creates the
correct node
- [ ] Verify GGUF asset browser still works
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Copy Url button in MissingModelRow now copies a browsable page URL
instead of a direct download URL
- HuggingFace: `/resolve/` → `/blob/` (file page with model info and
download button)
- Civitai: strips `/api/download` or `/api/v1` prefix (model page)
## Changes
- Add `toBrowsableUrl()` to `missingModelDownload.ts` — converts
download URLs to browsable page URLs for HuggingFace and Civitai
- Update `MissingModelRow.vue` to use `toBrowsableUrl()` when copying
- Add 5 unit tests covering HuggingFace, Civitai, and non-matching URL
cases
## Test plan
- [x] Unit tests pass (14/14) — `toBrowsableUrl` covered by 5 dedicated
tests
- [x] Lint, format, typecheck pass
- [x] Manual: load workflow with missing HuggingFace models, click Copy
Url, verify copied URL opens the file page
- [x] Manual: load workflow with missing Civitai models, click Copy Url,
verify copied URL opens the model page
### Why no E2E test
The Copy Url button is only visible when `!isCloud &&
model.representative.url && !isAssetSupported`. Writing an E2E test for
the clipboard content would require:
1. A test fixture with a real HuggingFace/Civitai URL (fragile — depends
on external availability)
2. Granting `clipboard-read` permission in Playwright context
3. Ensuring `isAssetSupported` evaluates to `false` against the local
test server's node definitions
The URL transformation logic is a pure function, fully covered by unit
tests. E2E clipboard content verification has no existing patterns in
the codebase and would be environment-dependent and flaky.
---------
Co-authored-by: Jin Yi <jin12cc@gmail.com>
## Summary
Address code review feedback from #10134 by renaming the component and
improving implementation quality.
## Changes
- Rename `ResultGallery` → `MediaLightbox` across all references
- Replace `useEventListener(window, 'keydown')` with `@keydown` on
dialog element
- Remove change detector tests (`renders close button`, `prevents
default on arrow keys`)
- Remove redundant `toBeVisible()` before Playwright click (implicit
wait)
- Update keyboard tests to dispatch on dialog element instead of
`window`
- Sort button icon sizes (`icon-sm`, `icon`, `icon-lg`)
- Wire zoom event to lightbox in `MediaAssetCard` story via
`context.args`
- Add standalone `MediaLightbox` Storybook story under
`Platform/Assets/`
Fixes#10134
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10235-refactor-Rename-ResultGallery-to-MediaLightbox-and-address-code-review-3276d73d365081299b42f682373a12f1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Add `quickRegister()` entries for LTX Video prompt enhancer models:
- `LLM/Llama-3.2-3B-Instruct` → `LTXVPromptEnhancerLoader` (`llm_name`)
- `LLM/Florence-2-large-PromptGen-v2.0` → `LTXVPromptEnhancerLoader`
(`image_captioner_name`)
These mappings ensure the "Use" button in the model browser correctly
creates
the appropriate loader node when users click on these LLM models.
## Test plan
- [x] Verify "Use" button works for LLM/Llama-3.2-3B-Instruct in model
browser
- [x] Verify "Use" button works for LLM/Florence-2-large-PromptGen-v2.0
in model browser
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10234-feat-add-model-to-node-mappings-for-LTX-Video-prompt-enhancer-3276d73d36508154847cdbae8c6e9bf1)
by [Unito](https://www.unito.io)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Fix 3D asset disappearing when switching between 3D and image outputs in
app mode — missing `onUnmounted` cleanup leaked WebGL contexts.
## Changes
- **What**: Add `onUnmounted` hook to `Preview3d.vue` that calls
`viewer.cleanup()`, releasing the WebGL context when Vue destroys the
component via its v-if chain. Add unit tests covering init, cleanup on
unmount, and remount behavior.
## Review Focus
When switching outputs in app mode, Vue's v-if chain destroys and
recreates `Preview3d`. Without `onUnmounted` cleanup, the old `Load3d`
instance (WebGL context, RAF loop, ResizeObserver) leaks. After ~8-16
toggles, the browser's WebGL context limit is exhausted and new 3D
viewers silently fail to render.
<!-- Pipeline-Ticket: e36489d2-a9fb-47ca-9e27-88eb3170836b -->
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- WebcamCapture's `serializeValue` was ignoring the `/upload/image`
response and returning the original timestamp-based filename
(`webcam/${timestamp}.png [temp]`)
- This breaks content-addressed storage backends (e.g. Comfy Cloud)
where the server returns a different (hash-based) filename, causing
`ImageDownloadError` at inference time
- Now reads `data.name` and `data.subfolder` from the upload response,
matching the pattern already used by `useNodeImageUpload` and
`useMaskEditorSaver`
- No behavioral change for local ComfyUI — the server already returns
the same filename, it was just being ignored
## Test plan
- [x] WebcamCapture node captures and uploads an image successfully on
local ComfyUI
- [x] WebcamCapture node works correctly on Comfy Cloud (no
`ImageDownloadError`)
- [ ] Duplicate filename edge case: upload two captures with same
timestamp — server-renamed file is used correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10220-fix-use-server-response-filename-in-WebcamCapture-serialization-3266d73d3650818aa9bdff8d27586180)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
`queueStore.update()` previously used a request-ID guard that discarded
responses whenever a newer call had been initiated. During concurrent
job execution, rapid WebSocket events (`status`, `execution_success`,
etc.) created a sustained stream of `update()` calls where:
1. **Every response got discarded** — each returning response saw a
higher `updateRequestId`, so data was never applied to the store until
the event burst subsided
2. **All HTTP requests still fired** — the guard didn't suppress
outgoing calls, just discarded stale responses after the fact
This caused the UI to freeze showing stale job states (e.g. completed
jobs still showing as "Running") during bursts, then snap all at once
when the storm ended.
## Changes
Replace the request-ID guard with a single-flight coalescing pattern:
- **At most one fetch in flight** at a time — no request spam
- **Every response is applied** — no UI starvation
- **Dirty flag** triggers one re-fetch if calls arrived during flight —
eventual consistency
## Testing
Added 5 unit tests covering:
- Concurrent calls coalesce into a single re-fetch
- No response starvation (every response applied)
- No duplicate requests when no concurrent calls
- Loading state transitions through coalesced re-fetches
- Normal sequential behavior preserved
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10203-fix-replace-stale-request-guard-with-single-flight-coalescing-in-queueStore-update-3266d73d365081088656e4e55ca4dbd3)
by [Unito](https://www.unito.io)
## Summary
The `computed` in `usePromotedPreviews` only tracked `nodeOutputs` as a
reactive dependency. GLSL live previews (and other preview-only sources)
write to `nodePreviewImages` instead of `nodeOutputs`, so promoted
preview widgets on SubgraphNodes never re-evaluated when live previews
updated.
## Changes
**Production** (`usePromotedPreviews.ts` — 3-line fix):
- Add `nodePreviewImages[locatorId]` as a second reactive dependency
alongside `nodeOutputs[locatorId]`
- Guard now passes when *either* source has data, not just `nodeOutputs`
**Tests** (`usePromotedPreviews.test.ts`):
- Add `nodePreviewImages` to mock store type and factory
- Add `seedPreviewImages()` helper
- Add `getNodeImageUrls.mockReset()` in `beforeEach` for proper test
isolation
- Two new test cases:
- `returns preview when only nodePreviewImages exist (e.g. GLSL live
preview)`
- `recomputes when preview images are populated after first evaluation`
- Clean up existing tests to use hoisted `getNodeImageUrls` mock
directly instead of `vi.mocked(useNodeOutputStore().getNodeImageUrls)`
## What this supersedes
This is a minimal re-implementation of #9461. That PR also modified
`promotionStore.ts` with a `_version`/`_touch()` monotonic counter to
manually force reactivity — that approach is dropped here as it is an
anti-pattern (manually managing reactivity counters instead of using
Vue's built-in reactivity system). The promotionStore changes were not
needed for this fix.
## Related
- Supersedes #9461
- Prerequisite work: #9198 (add GLSLShader to canvas image preview node
types)
- Upstream feature: #9201 (useGLSLPreview composable)
- Adjacent: #9435 (centralize node image rendering state in
NodeImageStore)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10165-fix-track-nodePreviewImages-in-usePromotedPreviews-for-GLSL-live-preview-propagation-3266d73d365081cd87d0d12c4c041907)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Harden subgraph test coverage: remove low-value change-detector tests,
consolidate fixtures, add behavioral coverage, and fix test
infrastructure issues. Includes minor production code corrections
discovered during test hardening.
## Changes
- **What**: Comprehensive subgraph test suite overhaul across 6 phases
- Removed change-detector tests and redundant assertions
- Consolidated fixture helpers into `subgraphHelpers.ts` /
`subgraphFixtures.ts`
- Added Pinia initialization and fixture reset to all test files
- Fixed barrel import violations (circular dependency prevention)
- Added behavioral coverage for slot connections, events, edge cases
- Added E2E helper and smoke test for subgraph promotion
- Exported `SubgraphSlotBase` from litegraph barrel for test access
- **Production code changes** (minor correctness fixes found during
testing):
- `resolveSubgraphInputLink.ts`: iterate forward (first-connected-wins)
to match `_resolveLinkedPromotionBySubgraphInput`
- `promotionSchema.ts`: return `[]` instead of throwing on invalid
`proxyWidgets`; console.warn always (not DEV-only)
- `LGraph.ts`: disconnect-after-veto ordering fix
- `litegraph.ts`: barrel export swap for `SubgraphSlotBase`
- **Stats**: 349 tests passing, 0 skipped across 26 test files
## Review Focus
- Tests that merely asserted default property values were deleted
(change detectors)
- Fixture state is now reset via `resetSubgraphFixtureState()` in
`beforeEach`
- All imports use `@/lib/litegraph/src/litegraph` barrel to avoid
circular deps
- Production changes are small and directly motivated by test findings
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Migrate two TypeScript lint rules from ESLint to oxlint and upgrade
`eslint-plugin-oxlint` to 1.55.0.
## Changes
- **Rule migration**: Move `typescript/no-import-type-side-effects` and
`typescript/no-empty-object-type` (with `allowInterfaces: 'always'`)
from `eslint.config.ts` to `.oxlintrc.json`.
- **Plugin upgrade**: Bump `eslint-plugin-oxlint` from 1.25.0 to 1.55.0,
which auto-disables additional ESLint rules now covered by oxlint. This
surfaced pre-existing `no-unused-expressions` violations (the `void`
prefixes and ternary-to-if/else changes), which are fixed in a separate
commit.
- **Dependencies**: None — `eslint-plugin-oxlint` auto-disables the
corresponding ESLint rules when they appear in `.oxlintrc.json`.
## Review Focus
- Verify the two migrated rules produce identical diagnostics in oxlint
as they did in ESLint
## Migration Status
See `temp/plans/eslint-to-oxlint-migration.md` for what can/can't
migrate and why.
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Fix nested SubgraphNode input slots doubling on each page reload
- Root cause: during configure, `_configureSubgraph` recreates
`SubgraphInput` objects with new references, and the `input-added` event
handler used `===` identity check which failed for these new objects,
causing `addInput()` to duplicate inputs
- Add `id`-based fallback matching in the `input-added` handler and
rebind `_subgraphSlot` with re-registered listeners
## Changes
**`SubgraphNode.ts:614-622`**: Add UUID `id` fallback to the `===`
reference check in the `input-added` event handler. When a stale
reference is matched by id, call `_addSubgraphInputListeners()` to
update `_subgraphSlot` and re-register listeners on the new
`SubgraphInput` object.
**`SubgraphNode.test.ts`**: 2 regression tests for nested subgraph
reconfigure scenarios.
## Test plan
- [x] Existing SubgraphNode tests pass (6 passed, 34 skipped)
- [x] New tests verify inputs don't duplicate after single and repeated
reconfigure cycles
- [x] Manual: create a subgraph containing another subgraph node, save,
reload — input slots should remain unchanged
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10187-fix-prevent-nested-SubgraphNode-input-slots-from-doubling-on-reload-3266d73d3650817286abea52365a626e)
by [Unito](https://www.unito.io)
## Summary
- Update the E2E test expectation for missing nodes overlay behavior
when switching between opened workflows
- `showMissingNodes` was intentionally changed to `true` in
`workflowService.ts` so users always see missing node pack info when
switching tabs, allowing them to install missing packs without a page
reload
- The test previously asserted the overlay should NOT reappear; now it
asserts the overlay SHOULD reappear
## Test plan
- [x] CI E2E tests pass with updated expectation
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10190-test-update-E2E-to-expect-missing-nodes-overlay-on-tab-switch-3266d73d36508106a48efff7309bd4e5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add historical trend visualization (ASCII sparklines + directional
arrows) to the performance PR report, showing how each metric has moved
over recent commits on main.
## Changes
- **What**: New `sparkline()`, `trendDirection()`, `trendArrow()`
functions in `perf-stats.ts`. New collapsible "Trend" section in the
perf report showing per-metric sparklines, direction indicators, and
latest values. CI workflow updated to download historical data from the
`perf-data` orphan branch and switched to `setup-frontend` action with
`pnpm exec tsx`.
## Review Focus
- The trend section only renders when ≥3 historical data points exist
(gracefully absent otherwise)
- `trendDirection()` uses a split-half mean comparison with ±10%
threshold — review whether this sensitivity is appropriate
- The `git archive` step in `pr-perf-report.yaml` is idempotent and
fails silently if no perf-history data exists yet on the perf-data
branch
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9939-feat-add-trend-visualization-with-sparklines-to-perf-report-3246d73d36508125a6fcc39612f850fe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Enable virtual nodes (e.g. Set/Get) to resolve their output source
directly when the source lives in a different subgraph.
## Changes
- **What**: Added optional resolveVirtualOutput method on LGraphNode and
a new resolution path in ExecutableNodeDTO.resolveOutput that checks it
before falling through to the existing getInputLink path. Includes unit
tests for the three code paths (happy path, missing DTO, fallthrough).
## Review Focus
- Fully backwards compatible — no existing node implements
resolveVirtualOutput, so the new path is always skipped for current
virtual nodes (Reroute, PrimitiveNode, etc.).
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots
Simple example in actual use, combined with new changes in KJNodes
allows using Get nodes inside subgraphs:
<img width="2242" height="1434" alt="image"
src="https://github.com/user-attachments/assets/cc940a95-e0bb-4adf-91b6-9adc43a74aa2"
/>
<img width="1436" height="440" alt="image"
src="https://github.com/user-attachments/assets/62044af5-0d6e-4c4e-b34c-d33e85f2b969"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10111-feat-resolveVirtualOutput-for-cross-subgraph-virtual-nodes-eg-Set-Get-3256d73d3650816a9f20e28029561c58)
by [Unito](https://www.unito.io)
## Summary
Replace `app.rootGraph.getNodeById()` with `resolveNode()` so node
lookups search subgraphs, fixing broken image copy/paste and display for
nodes inside subgraphs.
## Changes
- **What**: Updated 7 call sites across 5 files to use `resolveNode()`
from `litegraphUtil.ts` instead of `app.rootGraph.getNodeById()`. The
`resolveNode` function already existed and searches both the root graph
and all subgraphs. Added a unit test verifying subgraph node resolution
in `nodeOutputStore`.
## Review Focus
The fix is mechanical — each call site simply swaps
`app.rootGraph?.getNodeById(id)` for `resolveNode(id)`. The
`resolveNode` utility iterates `graph.subgraphs` if the node is not
found in the root graph.
Fixes#9993
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10009-fix-resolve-nodes-in-subgraphs-for-image-copy-paste-and-display-3256d73d3650814f9467c53999f5d755)
by [Unito](https://www.unito.io)
## Summary
Remove the 225px minimum width constraint from reroute nodes so they
render at their intended ~75×26px size.
## Changes
- **What**: Reroute nodes now bypass the `min-w-[225px]` CSS constraint
and bottom padding applied to regular nodes. `resizable: false` is set
on the RerouteNode constructor to hide the resize handle. An
`isRerouteNode` computed in `LGraphNode.vue` gates these behaviors by
checking `nodeData.type === "Reroute"`.
## Review Focus
- Detection mechanism uses `type === "Reroute"` (explicit) rather than
`titleMode === NO_TITLE` (semantic but too broad). See PR #8574 as prior
art for reroute-specific conditionals.
- PR #7993 (open) modifies `LGraphNode.ts` pos/size setters but does not
touch `LGraphNode.vue` template or resize callback — no conflict
expected.
Fixes#4704
## Screenshots (if applicable)
Reroute nodes now render at ~75px wide instead of being forced to 225px
minimum.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8734-fix-vueNodes-decrease-default-size-of-reroute-nodes-3016d73d3650816dbeead93517a52f25)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
- `waitForAuthInitialization` in `api.ts` was silently passing through
without actually waiting for Firebase auth
- `authStore.isInitialized` is unwrapped by Pinia (plain boolean, not a
ref), so `until()` received a static value
- `until()` without `.toBe()` returns a builder object, not a promise —
`Promise.race` treated it as immediately resolved
- Fixed with `storeToRefs` to preserve the ref and `.toBe(true)` to
return an actual promise
## Test plan
- [ ] Verify cloud mode API calls wait for Firebase initialization
before firing
- [ ] Verify non-cloud mode is unaffected
- [ ] Verify no 401s on initial page load in cloud mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10115-fix-broken-Firebase-auth-gate-in-API-layer-3256d73d365081e39b0df4d644f38c84)
by [Unito](https://www.unito.io)
Upgrades `pnpm/action-setup` from v4.2.0 to v4.4.0 across all 16
workflow files and the shared `setup-frontend` action.
## Why
GitHub Actions will force Node.js 24 as the default starting June 2,
2026. The v4.2.0 pin ran on Node.js 20 and emitted deprecation warnings
on every CI run. v4.4.0 was released specifically to address this,
updating the action runtime to Node.js 24.
- Fixes the warning: *"pnpm/action-setup@41ff72... Actions will be
forced to run with Node.js 24 by default starting June 2nd, 2026"*
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10137-ci-upgrade-pnpm-action-setup-to-v4-4-0-Node-js-24-3266d73d36508176b157fcd1d33f2274)
by [Unito](https://www.unito.io)
## WIP — waiting on Typeform ID from Alex Tov (Monday)
Pre-wires `trackFeatureUsed('node-search')` in
`NodeSearchBoxPopover.vue`. Increments a localStorage counter each time
the user opens node search. Currently a no-op because the
`FEATURE_SURVEYS` registry is empty.
**Monday**: Alex provides Typeform ID → add registry entry → survey goes
live on nightly builds.
### What this PR does
- Imports `useSurveyFeatureTracking` composable
- Calls `trackFeatureUsed()` in `showNewSearchBox()`
### What's still needed (will update this PR)
- [x] Add `node-search` entry to `FEATURE_SURVEYS` in
`surveyRegistry.ts` with Typeform ID
- [x] Set up Typeform → Slack webhook to
`#frontend-nightly-user-feedback`
- [ ] Test end-to-end on nightly build
### How the survey system works
After 3 uses of node search on a nightly build, a Typeform survey
popover slides in (once per user, 4-day global cooldown between
surveys). Eligibility: nightly + localhost only, respects opt-out.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9934-WIP-Track-node-search-usage-for-nightly-survey-3246d73d365081308847dd4c0085f21c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- On cloud deployments, the bootstrap sequence was firing authenticated
API calls (`/api/users`, `/api/settings`, `/api/userdata`, `/api/i18n`)
before verifying the user was authenticated via Firebase, causing
repeated 401 responses
- Moves the Firebase auth gate to the top of `startStoreBootstrap()` and
waits for both `isInitialized` **and** `isAuthenticated` before making
any API calls
- The router guard already redirects unauthenticated users to login;
once they authenticate, `isAuthenticated` becomes `true` and the
bootstrap proceeds normally
## Test plan
- [ ] Verify on cloud deployment: unauthenticated users see no 401 API
errors in the network tab
- [ ] Verify on cloud: after login, settings/i18n/workflows load
correctly
- [ ] Verify non-cloud deployments are unaffected (no `isCloud` guard
hit)
- [ ] Unit tests pass (`pnpm test:unit -- --run
src/stores/bootstrapStore.test.ts`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9909-fix-gate-cloud-API-calls-behind-Firebase-authentication-3236d73d3650819287e4e4c68623463b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Replaces two separate PR comment workflows (bundle size + performance)
with a single unified report that posts one combined comment per PR.
## Changes
- **What**: New `pr-report.yaml` aggregator workflow triggers on both
`CI: Size Data` and `CI: Performance Report` completions. Finds sibling
workflow runs by PR head SHA. Renders combined report via
`unified-report.js` (shells out to existing `size-report.js` and
`perf-report.ts`). Sections show "pending" or "failed" placeholders when
data is unavailable.
- **Breaking**: Removes `pr-size-report.yaml` and `pr-perf-report.yaml`.
Legacy `<!-- COMFYUI_FRONTEND_SIZE -->` and `<!-- COMFYUI_FRONTEND_PERF
-->` comments are auto-cleaned on first run.
- **Dependencies**: None
## Review Focus
- Concurrency key uses `head_sha` so the later-completing workflow
cancels the earlier report run, ensuring the final comment always has
both sections.
- Stale-run guard: verifies workflow_run SHA matches the live PR head
before posting.
- The `workflow_dispatch` re-trigger path from `pr-size-report.yaml` is
not carried forward — the unified workflow handles re-trigger naturally
via its dual-trigger design.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9911-feat-unified-PR-report-combining-bundle-size-and-runtime-perf-3236d73d365081baac1cce6f0d9244ac)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Revives #6845. One-time modal introducing Comfy Cloud to macOS desktop
users on first launch, with copy aligned to the latest comfy.org/cloud
page.
## Changes
- **What**: One-time cloud notification modal for macOS + Electron
users, shown 2s after first launch. Includes updated messaging (400 free
monthly credits, no-setup GPU access), telemetry tracking, and UTM
parameters (`utm_id`, `utm_source_platform`).
- **Dependencies**: None
## Review Focus
- Copy alignment with comfy.org/cloud
- Platform guard: macOS + Electron only
- UTM parameters for funnel attribution
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10116-feat-add-cloud-notification-modal-for-macOS-desktop-users-3256d73d36508105995edb71253ae824)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Keep the restored job details popover visible when the job history
sidebar is docked on the left edge of the workspace.
## Changes
- **What**: Replace the fixed `right`-based hover popover positioning
with viewport-aware `left` positioning so the popover opens on the side
with available space, reuse that logic in both `JobAssetsList` and
`QueueJobItem`, and add coverage for left-edge/right-edge placement plus
the job history sidebar integration.
## Review Focus
Please verify the hover popover opens on-screen for left-docked job
history, and that queue overlay / legacy queue row behavior still
matches the intended hover handoff.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9679-fix-keep-job-details-popover-on-screen-in-sidebar-31e6d73d3650816d9e7ffb0749430218)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
description: 'Reproduce a GitHub issue by researching prerequisites, setting up the environment (custom nodes, workflows, settings), and interactively exploring ComfyUI via playwright-cli until the bug is confirmed. Then records a clean demo video.'
---
# Issue Reproduction Skill
Reproduce a reported GitHub issue against a running ComfyUI instance. This skill uses an interactive, agent-driven approach — not a static script. You will research, explore, retry, and adapt until the bug is reproduced, then record a clean demo.
## Architecture
Two videos are produced:
1.**Research video** — the full exploration session: installing deps, trying things, failing, retrying, figuring out the bug. Valuable for debugging context.
2.**Reproduce video** — a clean, minimal recording of just the reproduction steps. This is the demo you'd attach to the issue.
```
Phase 1: Research → Read issue, understand prerequisites
## Phase 4: Record Clean Demo — Reproduce Video (max 5 minutes)
Once the bug is confirmed, **stop the research video** and **close the research browser**:
```bash
playwright-cli video-stop
playwright-cli close
```
Now start a **fresh browser session** for the clean reproduce video (Video 2).
**IMPORTANT constraints:**
- **Max 5 minutes** — the reproduce video must be short and focused
- **No environment setup** — server, user, custom nodes are already set up from Phase 3. Just log in and go.
- **No exploration** — you already know the exact steps. Execute them quickly and precisely.
- **Start video recording immediately**, execute steps, stop. Don't leave the recording running while thinking.
1. **Open browser and start recording**:
```bash
playwright-cli open "http://127.0.0.1:8188"
playwright-cli video-start
```
2. **Execute only the minimal reproduction steps** — no exploration, no mistakes. Just the clean sequence that demonstrates the bug. You already know exactly what works from Phase 3.
description: 'Comprehensive QA of ComfyUI frontend. Navigates all routes, tests all interactive features using playwright-cli, generates a report, and submits a draft PR. Works in CI and local environments, cross-platform.'
---
# ComfyUI Frontend QA Skill
Perform comprehensive quality assurance of the ComfyUI frontend application by navigating all routes, clicking interactive elements, and testing features. Generate a structured report and submit it as a draft PR.
# Option B: Build frontend and use preview server (no backend features)
pnpm build && pnpm preview &
```
2. Wait for server readiness by polling the URL (retry with 2s intervals, 60s timeout)
### Browser Automation Setup
Use **playwright-cli** for browser interaction via Bash commands:
```bash
playwright-cli open http://127.0.0.1:8188 # open browser and navigate
playwright-cli snapshot # capture snapshot with element refs
playwright-cli click e1 # click by element ref from snapshot
playwright-cli press Tab # keyboard shortcuts
playwright-cli screenshot --filename=f.png # save screenshot
```
playwright-cli is headless by default (CI-friendly). Each command outputs the current page snapshot with element references (`e1`, `e2`, …) that you use for subsequent `click`, `fill`, `hover` commands. Always run `snapshot` before interacting to get fresh refs.
For local dev servers behind proxies, adjust the URL accordingly (e.g., `https://[port].stukivx.xyz` pattern if configured).
## Step 2: QA Test Plan
Navigate to the application URL and systematically test each area below. For each test, record:
- **Status**: pass / fail / skip (with reason)
- **Notes**: any issues, unexpected behavior, or visual glitches
- **Screenshots**: take screenshots of failures or notable states
Automated QA report covering all frontend routes and features."
git push -u origin "$BRANCH_NAME"
# Create draft PR assigned to comfy-pr-bot
gh pr create \
--draft \
--title "QA Report: $(date +%Y-%m-%d)"\
--body "## QA Report
Automated frontend QA run covering all routes and interactive features.
See \`docs/qa/\` for the full report.
/cc @comfy-pr-bot"\
--assignee comfy-pr-bot
```
## Execution Notes
### Cross-Platform Considerations
- **Windows**: Use `pwsh` or `cmd` equivalents for shell commands. `gh` CLI works on all platforms.
- **macOS**: Keyboard shortcuts use Cmd instead of Ctrl in the actual app, but Playwright sends OS-appropriate keys.
- **Linux**: Primary CI platform. Screenshot baselines are Linux-only.
### Agent Compatibility
This skill uses **playwright-cli** (`@playwright/cli`) — a token-efficient CLI designed for coding agents. Install it once with `npm install -g @playwright/cli@latest`, then use `Bash` to run commands.
The key operations and their playwright-cli equivalents:
description: 'Detect violations of the layered architecture import rules (base -> platform -> workbench -> renderer). Runs ESLint with the import-x/no-restricted-paths rule and generates a grouped report.'
---
# Layer Architecture Audit
Finds imports that violate the layered architecture boundary rules enforced by `import-x/no-restricted-paths` in `eslint.config.ts`.
## Layer Hierarchy (bottom to top)
```
renderer (top -- can import from all lower layers)
^
workbench
^
platform
^
base (bottom -- cannot import from any upper layer)
```
Each layer may only import from layers below it.
## How to Run
```bash
# Run ESLint filtering for just the layer boundary rule violations
pnpm lint 2>&1| grep 'import-x/no-restricted-paths' -B1 | head -200
```
To get a full structured report, run:
```bash
# Collect all violations from base/, platform/, workbench/ layers
| base -> platform | base/ importing from platform/ |
| base -> workbench | base/ importing from workbench/ |
| base -> renderer | base/ importing from renderer/ |
| platform -> workbench | platform/ importing from workbench/ |
| platform -> renderer | platform/ importing from renderer/ |
| workbench -> renderer | workbench/ importing from renderer/ |
## When to Use
- Before creating a PR that adds imports between `src/base/`, `src/platform/`, `src/workbench/`, or `src/renderer/`
- When auditing the codebase to find and plan migration of existing violations
- After moving files between layers to verify no new violations were introduced
## Fixing Violations
Common strategies to resolve a layer violation:
1.**Move the import target down** -- if the imported module doesn't depend on upper-layer concepts, move it to a lower layer
2.**Introduce an interface** -- define an interface/type in the lower layer and implement it in the upper layer via dependency injection or a registration pattern
3.**Move the importing file up** -- if the file logically belongs in a higher layer, relocate it
4.**Extract shared logic** -- pull the shared functionality into `base/` or a shared utility
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.