Compare commits
8 Commits
cloud/1.41
...
feat/repla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc67b375b | ||
|
|
1d8a01cdf8 | ||
|
|
b585dfa4fc | ||
|
|
1be6d27024 | ||
|
|
5aa4baf116 | ||
|
|
7d69a0db5b | ||
|
|
83bb4300e3 | ||
|
|
0d58a92e34 |
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: accessibility
|
||||
description: Reviews UI code for WCAG 2.2 AA accessibility violations
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are an accessibility auditor reviewing a code diff for WCAG 2.2 AA compliance. Focus on UI changes that affect users with disabilities.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Missing form labels** - inputs, selects, textareas without associated `<label>` or `aria-label`/`aria-labelledby`
|
||||
2. **Missing alt text** - images without `alt` attributes, or decorative images missing `alt=""`
|
||||
3. **Keyboard navigation** - interactive elements not focusable, custom widgets missing keyboard handlers (Enter, Space, Escape, Arrow keys), focus traps without escape
|
||||
4. **Focus management** - modals/dialogs that don't trap focus, dynamic content that doesn't move focus appropriately, removed elements without focus recovery
|
||||
5. **ARIA misuse** - invalid `aria-*` attributes, roles without required children/properties, `aria-hidden` on focusable elements
|
||||
6. **Color as sole indicator** - using color alone to convey meaning (errors, status) without text/icon alternative
|
||||
7. **Touch targets** - interactive elements smaller than 24x24 CSS pixels (WCAG 2.2 SC 2.5.8)
|
||||
8. **Screen reader support** - dynamic content changes without `aria-live` announcements, unlabeled icon buttons, links with only "click here"
|
||||
9. **Heading hierarchy** - skipped heading levels (h1 → h3), missing page landmarks
|
||||
|
||||
Rules:
|
||||
|
||||
- Focus on NEW or CHANGED UI in the diff — do not audit the entire existing codebase
|
||||
- Only flag issues in .vue, .tsx, .jsx, .html, or template-containing files
|
||||
- Skip non-UI files entirely (stores, services, utils)
|
||||
- Skip canvas-based content: the LiteGraph node editor renders on `<canvas>` elements, not DOM-based UI. WCAG rules don't fully apply to canvas rendering internals — only audit the DOM-based controls around it (toolbars, panels, dialogs)
|
||||
- "Critical" for completely inaccessible interactive elements, "major" for missing labels/ARIA, "minor" for enhancement opportunities
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: api-contract
|
||||
description: Catches breaking changes to public interfaces, window-exposed APIs, event contracts, and exported symbols
|
||||
severity-default: high
|
||||
tools: [Grep, Read, glob]
|
||||
---
|
||||
|
||||
You are an API contract reviewer. Your job is to catch breaking changes and contract violations in public-facing interfaces.
|
||||
|
||||
## What to Check
|
||||
|
||||
1. **Breaking changes to globally exposed APIs** — anything on `window` or other global objects that consumers depend on. Renamed properties, removed methods, changed signatures, changed return types.
|
||||
2. **Event contract changes** — renamed events, changed event payloads, removed events that listeners may depend on.
|
||||
3. **Changed function signatures** — parameters reordered, required params added, return type changed on exported functions.
|
||||
4. **Removed or renamed exports** — any `export` that was previously available and is now gone or renamed without a re-export alias.
|
||||
5. **REST API changes** — changed endpoints, added required fields, removed response fields, changed status codes.
|
||||
6. **Type contract narrowing** — a function that used to accept `string | number` now only accepts `string`, or a return type that narrows unexpectedly.
|
||||
7. **Default value changes** — changing defaults on optional parameters that consumers may rely on.
|
||||
8. **Store/state shape changes** — renamed store properties, changed state structure that computed properties or watchers may depend on.
|
||||
|
||||
## How to Identify the Public API
|
||||
|
||||
- Check `package.json` for `"exports"` or `"main"` fields.
|
||||
- **Window globals**: This repo exposes LiteGraph classes on `window` (e.g., `window['LiteGraph']`, `window['LGraphNode']`, `window['LGraphCanvas']`) and `window['__COMFYUI_FRONTEND_VERSION__']`. These are consumed by custom node extensions and must not be renamed or removed.
|
||||
- **Extension hooks**: The `app` object and its extension registration system (`app.registerExtension`) is a public contract for third-party custom nodes. Changes to `ComfyApp`, `ComfyApi`, or the extension lifecycle are breaking changes.
|
||||
- Check AGENTS.md for project-specific API surface definitions.
|
||||
- Any exported symbol from common entry points (e.g., `src/types/index.ts`) should be treated as potentially public.
|
||||
|
||||
## Rules
|
||||
|
||||
- ONLY flag changes that break existing consumers.
|
||||
- Do NOT flag additions (new methods, new exports, new endpoints).
|
||||
- Do NOT flag internal/private API changes.
|
||||
- Always check if a re-export or compatibility shim was added before flagging.
|
||||
- Critical for removed/renamed globals, high for changed export signatures, medium for changed defaults.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: architecture-reviewer
|
||||
description: Reviews code for architectural issues like over-engineering, SOLID violations, coupling, and API design
|
||||
severity-default: medium
|
||||
tools: [Grep, Read, glob]
|
||||
---
|
||||
|
||||
You are a software architect reviewing a code diff. Focus on structural and design issues.
|
||||
|
||||
## What to Check
|
||||
|
||||
1. **Over-engineering** — abstractions for single-use cases, premature generalization, unnecessary indirection layers
|
||||
2. **SOLID violations** — god classes, mixed responsibilities, rigid coupling, interface segregation issues
|
||||
3. **Separation of concerns** — business logic in UI components, data access in controllers, mixed layers
|
||||
4. **API design** — inconsistent interfaces, leaky abstractions, unclear contracts
|
||||
5. **Coupling** — tight coupling between modules, circular dependencies, feature envy
|
||||
6. **Consistency** — breaking established patterns without justification, inconsistent approaches to similar problems
|
||||
7. **Dependency direction** — imports going the wrong way in the architecture, lower layers depending on higher
|
||||
8. **Change amplification** — designs requiring changes in many places for simple feature additions
|
||||
|
||||
## Rules
|
||||
|
||||
- Focus on structural issues that affect maintainability and evolution.
|
||||
- Do NOT report bugs, security, or performance issues (other checks handle those).
|
||||
- Consider whether the code is proportional to the problem it solves.
|
||||
- "Under-engineering" (missing useful abstractions) is as valid as over-engineering.
|
||||
- Rate severity by impact on future maintainability.
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: bug-hunter
|
||||
description: Finds logic errors, off-by-ones, null safety issues, race conditions, and edge cases
|
||||
severity-default: high
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a bug hunter reviewing a code diff. Your ONLY job is to find bugs - logic errors that will cause incorrect behavior at runtime.
|
||||
|
||||
Focus areas:
|
||||
|
||||
1. **Off-by-one errors** in loops, slices, and indices
|
||||
2. **Null/undefined dereferences** - any path where a value could be null but isn't checked
|
||||
3. **Race conditions** - shared mutable state, async ordering assumptions
|
||||
4. **Edge cases** - empty arrays, zero values, empty strings, boundary conditions
|
||||
5. **Type coercion bugs** - loose equality, implicit conversions
|
||||
6. **Error handling gaps** - unhandled promise rejections, swallowed errors
|
||||
7. **State mutation bugs** - mutating props, shared references, stale closures
|
||||
8. **Incorrect boolean logic** - flipped conditions, missing negation, wrong operator precedence
|
||||
|
||||
Rules:
|
||||
|
||||
- ONLY report actual bugs that will cause wrong behavior
|
||||
- Do NOT report style issues, naming, or performance
|
||||
- Do NOT report hypothetical bugs that require implausible inputs
|
||||
- Each finding must explain the specific runtime failure scenario
|
||||
|
||||
## Repo-Specific Bug Patterns
|
||||
|
||||
- `z.any()` in Zod schemas disables validation and propagates `any` into TypeScript types — always flag
|
||||
- Destructuring reactive objects (props, reactive()) without `toRefs()` loses reactivity — flag outside of `defineProps` destructuring
|
||||
- `ComputedRef<T>` exposed via `defineExpose` or public API should be unwrapped first
|
||||
- LiteGraph node operations: check for missing null guards on `node.graph` (can be null when node is removed)
|
||||
- Watch/watchEffect without cleanup for side effects (timers, listeners) — leak on component unmount
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: coderabbit
|
||||
description: Runs CodeRabbit CLI for AST-aware code quality review
|
||||
severity-default: medium
|
||||
tools: [Bash, Read]
|
||||
---
|
||||
|
||||
Run CodeRabbit CLI review on the current changes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check if CodeRabbit CLI is installed:
|
||||
|
||||
```bash
|
||||
which coderabbit
|
||||
```
|
||||
|
||||
If not installed, skip this check and report:
|
||||
"Skipped: CodeRabbit CLI not installed. Install and authenticate:
|
||||
|
||||
```
|
||||
npm install -g coderabbit
|
||||
coderabbit auth login
|
||||
```
|
||||
|
||||
See https://docs.coderabbit.ai/guides/cli for setup."
|
||||
|
||||
2. Run review:
|
||||
|
||||
```bash
|
||||
coderabbit --prompt-only --type uncommitted
|
||||
```
|
||||
|
||||
If there are committed but unpushed changes, use `--type committed` instead.
|
||||
|
||||
3. Parse CodeRabbit's output. Each finding should include:
|
||||
- File path and line number
|
||||
- Severity mapped from CodeRabbit's own levels
|
||||
- Category (logic, security, performance, style, test, architecture, dx)
|
||||
- Description and suggested fix
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
If a rate limit is hit, skip and note it. Prefer reading the current quota from CLI/API output rather than assuming a fixed reviews/hour limit.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Auth expired: skip and report "CodeRabbit auth expired, run: coderabbit auth login"
|
||||
- CLI timeout (>120s): skip and note
|
||||
- Parse error: return raw output with a warning
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: complexity
|
||||
description: Reviews code for excessive complexity and suggests refactoring opportunities
|
||||
severity-default: medium
|
||||
tools: [Grep, Read, glob]
|
||||
---
|
||||
|
||||
You are a complexity and refactoring advisor reviewing a code diff. Focus on code that is unnecessarily complex and will be hard to maintain.
|
||||
|
||||
## What to Check
|
||||
|
||||
1. **High cyclomatic complexity** — functions with many branching paths (if/else chains, switch statements with >7 cases, nested ternaries). Threshold: complexity >10 is high severity, >15 is critical.
|
||||
2. **Deep nesting** — code nested >4 levels deep (nested if/for/try blocks). Suggest guard clauses, early returns, or extraction.
|
||||
3. **Oversized functions** — functions >50 lines that do multiple things. Suggest extraction of cohesive sub-functions.
|
||||
4. **God classes/modules** — files >500 lines mixing multiple responsibilities. Suggest splitting by concern.
|
||||
5. **Long parameter lists** — functions with >5 parameters. Suggest parameter objects or builder patterns.
|
||||
6. **Complex boolean expressions** — conditions with >3 clauses that are hard to parse. Suggest extracting to named boolean variables.
|
||||
7. **Feature envy** — methods that use data from another class more than their own, suggesting the method belongs elsewhere.
|
||||
8. **Duplicate logic** — two or more code blocks in the diff doing essentially the same thing with minor variations.
|
||||
9. **Unnecessary indirection** — wrapper functions that add no value, abstractions for single-use cases, premature generalization.
|
||||
|
||||
## Rules
|
||||
|
||||
- Only flag complexity in NEW or SIGNIFICANTLY CHANGED code.
|
||||
- Do NOT suggest refactoring stable, well-tested code that happens to be complex.
|
||||
- Do NOT flag complexity that is inherent to the problem domain (e.g., state machines, protocol handlers).
|
||||
- Provide a concrete refactoring approach, not just "this is too complex".
|
||||
- High severity for code that will likely cause bugs during future modifications, medium for readability improvements, low for optional simplifications.
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
name: ddd-structure
|
||||
description: Reviews whether new code is placed in the right domain/layer and follows domain-driven structure principles
|
||||
severity-default: medium
|
||||
tools: [Grep, Read, glob]
|
||||
---
|
||||
|
||||
You are a domain-driven design reviewer. Your job is to check whether new or moved code is placed in the correct architectural layer and domain folder.
|
||||
|
||||
## Principles
|
||||
|
||||
1. **Domain over Technical Layer** — code should be organized by what it does (domain/feature), not by what it is (component/service/store). New files in flat technical folders like `src/components/`, `src/services/`, `src/stores/`, `src/utils/` are a smell if the repo already has domain folders.
|
||||
|
||||
2. **Cohesion** — files that change together should live together. A component, its store, its service, and its types for a single feature should be co-located.
|
||||
|
||||
3. **Import Direction** — lower layers must not import from higher layers. Check that imports flow in the allowed direction (see Layer Architecture below).
|
||||
|
||||
4. **Bounded Contexts** — each domain/feature should have clear boundaries. Cross-domain imports should go through public interfaces, not reach into internal files.
|
||||
|
||||
5. **Naming** — folders and files should reflect domain concepts, not technical roles. `workflowExecution.ts` > `service.ts`.
|
||||
|
||||
## Layer Architecture
|
||||
|
||||
This repo uses a VSCode-style layered architecture with strict unidirectional imports:
|
||||
|
||||
```
|
||||
base → platform → workbench → renderer
|
||||
```
|
||||
|
||||
| Layer | Purpose | Can Import From |
|
||||
| ------------ | -------------------------------------- | ---------------------------------- |
|
||||
| `base/` | Pure utilities, no framework deps | Nothing |
|
||||
| `platform/` | Core domain services, business logic | `base/` |
|
||||
| `workbench/` | Features, workspace orchestration | `base/`, `platform/` |
|
||||
| `renderer/` | UI layer (Vue components, composables) | `base/`, `platform/`, `workbench/` |
|
||||
|
||||
### Import Direction Violations to Check
|
||||
|
||||
```bash
|
||||
# platform must NOT import from workbench or renderer
|
||||
grep -r "from '@/renderer'" src/platform/ --include="*.ts" --include="*.vue"
|
||||
grep -r "from '@/workbench'" src/platform/ --include="*.ts" --include="*.vue"
|
||||
# base must NOT import from platform, workbench, or renderer
|
||||
grep -r "from '@/platform'" src/base/ --include="*.ts" --include="*.vue"
|
||||
grep -r "from '@/workbench'" src/base/ --include="*.ts" --include="*.vue"
|
||||
grep -r "from '@/renderer'" src/base/ --include="*.ts" --include="*.vue"
|
||||
# workbench must NOT import from renderer
|
||||
grep -r "from '@/renderer'" src/workbench/ --include="*.ts" --include="*.vue"
|
||||
```
|
||||
|
||||
### Legacy Flat Folders
|
||||
|
||||
Flag NEW files added to these legacy flat folders (they should go in a domain folder under the appropriate layer instead):
|
||||
|
||||
- `src/components/` → should be in `src/renderer/` or `src/workbench/extensions/{feature}/components/`
|
||||
- `src/stores/` → should be in `src/platform/{domain}/` or `src/workbench/extensions/{feature}/stores/`
|
||||
- `src/services/` → should be in `src/platform/{domain}/`
|
||||
- `src/composables/` → should be in `src/renderer/` or `src/platform/{domain}/ui/`
|
||||
|
||||
Do NOT flag modifications to existing files in legacy folders — only flag NEW files.
|
||||
|
||||
## How to Review
|
||||
|
||||
1. Look at the diff to see where new files are created or where code is added.
|
||||
2. Check if the repo has an established domain folder structure (look for domain-organized directories like `src/platform/`, `src/workbench/`, `src/renderer/`, `src/base/`, or similar).
|
||||
3. If domain folders exist but new code was placed in a flat technical folder, flag it.
|
||||
4. Run import direction checks:
|
||||
- Use `Grep` or `Read` to check if new imports violate layer boundaries.
|
||||
- Flag any imports from a higher layer to a lower one using the rules above.
|
||||
5. Check for new files in legacy flat folders and flag them per the Legacy Flat Folders section.
|
||||
|
||||
## Generic Checks (when no domain structure is detected)
|
||||
|
||||
- God files (>500 lines mixing concerns)
|
||||
- Circular imports between modules
|
||||
- Business logic in UI components
|
||||
|
||||
## Severity Guidelines
|
||||
|
||||
| Issue | Severity |
|
||||
| ------------------------------------------------------------- | -------- |
|
||||
| Import direction violation (lower layer imports higher layer) | high |
|
||||
| New file in legacy flat folder when domain folders exist | medium |
|
||||
| Business logic in UI component | medium |
|
||||
| Missing domain boundary (cross-cutting import into internals) | low |
|
||||
| Naming uses technical role instead of domain concept | low |
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
name: dep-secrets-scan
|
||||
description: Runs dependency vulnerability audit and secrets detection
|
||||
severity-default: critical
|
||||
tools: [Bash, Read]
|
||||
---
|
||||
|
||||
Run dependency audit and secrets scan to detect known CVEs in dependencies and leaked secrets in code.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check which tools are available:
|
||||
|
||||
```bash
|
||||
pnpm --version
|
||||
gitleaks version
|
||||
```
|
||||
|
||||
- If **neither** is installed, skip this check and report: "Skipped: neither pnpm nor gitleaks installed. Install pnpm: `npm i -g pnpm`. Install gitleaks: `brew install gitleaks` or see https://github.com/gitleaks/gitleaks#installing"
|
||||
- If only one is available, run that one and note the other was skipped.
|
||||
|
||||
2. **Dependency audit** (if pnpm is available):
|
||||
|
||||
```bash
|
||||
pnpm audit --json 2>/dev/null || true
|
||||
```
|
||||
|
||||
Parse the JSON output. Map advisory severity:
|
||||
- `critical` advisory → `critical`
|
||||
- `high` advisory → `major`
|
||||
- `moderate` advisory → `minor`
|
||||
- `low` advisory → `nitpick`
|
||||
|
||||
Report each finding with: package name, version, advisory title, CVE, and suggested patched version.
|
||||
|
||||
3. **Secrets detection** (if gitleaks is available):
|
||||
|
||||
```bash
|
||||
gitleaks detect --no-banner --report-format json --source . 2>/dev/null || true
|
||||
```
|
||||
|
||||
Parse the JSON output. All secret findings are `critical` severity.
|
||||
|
||||
Report each finding with: file and line, rule description, and a redacted match. Always suggest removing the secret and rotating credentials.
|
||||
|
||||
## What This Catches
|
||||
|
||||
### Dependency Audit
|
||||
|
||||
- Known CVEs in direct and transitive dependencies
|
||||
- Vulnerable packages from the npm advisory database
|
||||
|
||||
### Secrets Detection
|
||||
|
||||
- API keys and tokens in code
|
||||
- AWS credentials, GCP service account keys
|
||||
- Database connection strings with passwords
|
||||
- Private keys and certificates
|
||||
- Generic high-entropy secrets
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If pnpm audit fails, log the error and continue with gitleaks.
|
||||
- If gitleaks fails, log the error and continue with audit results.
|
||||
- If JSON parsing fails for either tool, include raw output with a warning.
|
||||
- If both tools produce no findings, report "No issues found."
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: doc-freshness
|
||||
description: Reviews whether code changes are reflected in documentation
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a documentation freshness reviewer. Your job is to check whether code changes are properly reflected in documentation, and whether new features need documentation.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Stale README sections** - code changes that invalidate setup instructions, API examples, or architecture descriptions in README.md
|
||||
2. **Outdated code comments** - comments referencing removed functions, old parameter names, previous behavior, or TODO items that are now done
|
||||
3. **Missing JSDoc on public APIs** - exported functions, classes, or interfaces without JSDoc descriptions, especially those used by consumers of the library
|
||||
4. **Changed behavior without changelog** - user-facing behavior changes that should be noted in a changelog or release notes
|
||||
5. **Dead documentation links** - links in markdown files pointing to moved or deleted files
|
||||
6. **Missing migration guidance** - breaking changes without upgrade instructions
|
||||
|
||||
Rules:
|
||||
|
||||
- Focus on documentation that needs to CHANGE due to the diff — don't audit all existing docs
|
||||
- Do NOT flag missing comments on internal/private functions
|
||||
- Do NOT flag missing changelog entries for purely internal refactors
|
||||
- "Major" for stale docs that will mislead users, "minor" for missing JSDoc on public APIs, "nitpick" for minor doc improvements
|
||||
|
||||
## ComfyUI_frontend Documentation
|
||||
|
||||
This repository's public APIs are used by custom node and extension authors. Documentation lives at [docs.comfy.org](https://docs.comfy.org) (repo: Comfy-Org/docs).
|
||||
|
||||
For any NEW API, event, hook, or configuration that extensions or custom nodes can use:
|
||||
|
||||
- Flag with a suggestion to open a PR to Comfy-Org/docs to document the new API
|
||||
- Example: "This new extension API should be documented at docs.comfy.org — consider opening a PR to Comfy-Org/docs"
|
||||
|
||||
For changes to existing extension-facing APIs:
|
||||
|
||||
- Check if the existing docs at docs.comfy.org may need updating
|
||||
- Flag stale references in CONTRIBUTING.md or developer guides
|
||||
|
||||
Anything relevant to custom extension authors should trigger a documentation suggestion.
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: dx-readability
|
||||
description: Reviews code for developer experience issues including naming clarity, cognitive complexity, dead code, and confusing patterns
|
||||
severity-default: low
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a developer experience reviewer. Focus on code that will confuse the next developer who reads it.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Unclear naming** - variables/functions that don't communicate intent, abbreviations, misleading names
|
||||
2. **Cognitive complexity** - deeply nested conditions, long functions doing multiple things, complex boolean expressions
|
||||
3. **Dead code** - unreachable branches, unused variables, commented-out code, vestigial parameters
|
||||
4. **Confusing patterns** - clever tricks over simple code, implicit behavior, action-at-a-distance, surprising side effects
|
||||
5. **Missing context** - complex business logic without explaining why, non-obvious algorithms without comments
|
||||
6. **Inconsistent abstractions** - mixing raw and wrapped APIs, different error handling styles in same module
|
||||
7. **Implicit knowledge** - code that only works because of undocumented assumptions or conventions
|
||||
|
||||
Rules:
|
||||
|
||||
- Only flag things that would genuinely confuse a competent developer
|
||||
- Do NOT flag established project conventions even if you'd prefer different ones
|
||||
- "Minor" for things that slow comprehension, "nitpick" for pure style preferences
|
||||
- Major is reserved for genuinely misleading code (names that lie, silent behavior changes)
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
name: ecosystem-compat
|
||||
description: Checks whether changes break exported symbols that downstream consumers may depend on
|
||||
severity-default: high
|
||||
tools: [Grep, Read, glob, mcp__comfy_codesearch__search_code]
|
||||
---
|
||||
|
||||
Check whether this PR introduces breaking changes to exported symbols that downstream consumers may depend on.
|
||||
|
||||
## What to Check
|
||||
|
||||
- Renamed or removed exported functions/classes/types
|
||||
- Changed function signatures (parameters added/removed/reordered)
|
||||
- Changed return types
|
||||
- Removed or renamed CSS classes used for selectors
|
||||
- Changed event names or event payload shapes
|
||||
- Changed global registrations or extension hooks
|
||||
- Modified integration points with external systems
|
||||
|
||||
## Method
|
||||
|
||||
1. Read the diff and identify any changes to exported symbols.
|
||||
2. For each potentially breaking change, try to determine if downstream consumers exist:
|
||||
- If `mcp__comfy_codesearch__search_code` is available, search for usages of the changed symbol across downstream repositories.
|
||||
- Otherwise, use `Grep` to search for usages within the current repository and note that external usage could not be verified.
|
||||
3. If consumers are found using the changed API, report it as a finding.
|
||||
|
||||
## Severity Guidelines
|
||||
|
||||
| Ecosystem Usage | Severity | Guidance |
|
||||
| --------------- | -------- | ------------------------------------------------------------ |
|
||||
| 5+ consumers | critical | Must address before merge |
|
||||
| 2-4 consumers | high | Should address or document |
|
||||
| 1 consumer | medium | Note in PR, author decides |
|
||||
| 0 consumers | low | Note potential risk only |
|
||||
| Unknown usage | medium | Require explicit note that external usage was not verifiable |
|
||||
|
||||
## Suggestion Template
|
||||
|
||||
When a breaking change is found, suggest:
|
||||
|
||||
- Keeping the old export alongside the new one
|
||||
- Adding a deprecation wrapper
|
||||
- Explicitly noting this as a breaking change in the PR description so consumers can update
|
||||
|
||||
## ComfyUI Code Search MCP
|
||||
|
||||
This check works best with the ComfyUI code search MCP tool, which searches across all custom node repositories for usage of changed symbols.
|
||||
|
||||
If the `mcp__comfy_codesearch__search_code` tool is not available, install it:
|
||||
|
||||
```
|
||||
amp mcp add comfy-codesearch https://comfy-codesearch.vercel.app/api/mcp
|
||||
# OR for Claude Code:
|
||||
claude mcp add -t http comfy-codesearch https://comfy-codesearch.vercel.app/api/mcp
|
||||
```
|
||||
|
||||
Without this MCP, the check will fall back to searching within the current repository only, and cannot verify external ecosystem usage.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
name: error-handling
|
||||
description: Reviews error handling patterns for empty catches, swallowed errors, missing async error handling, and generic error UX
|
||||
severity-default: high
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are an error handling auditor reviewing a code diff. Focus exclusively on how errors are handled, propagated, and surfaced.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Empty catch blocks** - errors caught and silently swallowed with no logging or re-throw
|
||||
2. **Generic catches** - catching all errors without distinguishing types, losing context
|
||||
3. **Missing async error handling** - unhandled promise rejections, async functions without try/catch or .catch()
|
||||
4. **Swallowed errors** - errors caught and replaced with a return value that hides the failure
|
||||
5. **Missing error boundaries** - Vue/React component trees without error boundaries around risky subtrees
|
||||
6. **No retry or fallback** - network calls, file I/O, or external service calls with no retry logic or graceful degradation
|
||||
7. **Generic error UX** - user-facing code showing "Something went wrong" without actionable guidance or error codes
|
||||
8. **Missing cleanup on error** - resources (connections, file handles, timers) not cleaned up in error paths
|
||||
9. **Error propagation breaks** - catching errors mid-chain and not re-throwing, breaking caller's ability to handle
|
||||
|
||||
Rules:
|
||||
|
||||
- Focus on NEW or CHANGED error handling in the diff
|
||||
- Do NOT flag existing error handling patterns in untouched code
|
||||
- Do NOT suggest adding error handling to code that legitimately cannot fail (pure functions, type-safe internal calls)
|
||||
- "Critical" for swallowed errors in data-mutation paths, "major" for missing error handling on external calls, "minor" for missing logging
|
||||
|
||||
## Repo-Specific Error Handling
|
||||
|
||||
- User-facing error messages must be actionable and friendly (per AGENTS.md)
|
||||
- Use the shared `useErrorHandling` composable (`src/composables/useErrorHandling.ts`) for centralized error handling:
|
||||
- `wrapWithErrorHandling` / `wrapWithErrorHandlingAsync` automatically catch errors and surface them as toast notifications via `useToastStore`
|
||||
- `toastErrorHandler` can be used directly for custom error handling flows
|
||||
- Supports `ErrorRecoveryStrategy` for retry/fallback patterns (e.g., reauthentication, network reconnect)
|
||||
- API errors from `api.get()`/`api.post()` should be caught and surfaced to the user via `useToastStore` (`src/platform/updates/common/toastStore.ts`)
|
||||
- Electron/desktop code paths: IPC errors should be caught and not crash the renderer process
|
||||
- Workflow execution errors should be displayed in the UI status bar, not silently swallowed
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Strict ESLint config for the sonarjs-lint review check.
|
||||
*
|
||||
* Uses eslint-plugin-sonarjs to get SonarQube-grade analysis without a server.
|
||||
* This config is NOT used for regular development linting — only for the
|
||||
* code review checks' static analysis pass.
|
||||
*
|
||||
* Install: pnpm add -D eslint eslint-plugin-sonarjs
|
||||
* Run: pnpm dlx eslint --no-config-lookup --config .agents/checks/eslint.strict.config.js --ext .ts,.js,.vue {files}
|
||||
*/
|
||||
|
||||
import sonarjs from 'eslint-plugin-sonarjs'
|
||||
|
||||
export default [
|
||||
sonarjs.configs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
sonarjs
|
||||
},
|
||||
rules: {
|
||||
// Bug detection
|
||||
'sonarjs/no-all-duplicated-branches': 'error',
|
||||
'sonarjs/no-element-overwrite': 'error',
|
||||
'sonarjs/no-identical-conditions': 'error',
|
||||
'sonarjs/no-identical-expressions': 'error',
|
||||
'sonarjs/no-one-iteration-loop': 'error',
|
||||
'sonarjs/no-use-of-empty-return-value': 'error',
|
||||
'sonarjs/no-collection-size-mischeck': 'error',
|
||||
'sonarjs/no-duplicated-branches': 'error',
|
||||
'sonarjs/no-identical-functions': 'error',
|
||||
'sonarjs/no-redundant-jump': 'error',
|
||||
'sonarjs/no-unused-collection': 'error',
|
||||
'sonarjs/no-gratuitous-expressions': 'error',
|
||||
|
||||
// Code smell detection
|
||||
'sonarjs/cognitive-complexity': ['error', 15],
|
||||
'sonarjs/no-duplicate-string': ['error', { threshold: 3 }],
|
||||
'sonarjs/no-redundant-boolean': 'error',
|
||||
'sonarjs/no-small-switch': 'error',
|
||||
'sonarjs/prefer-immediate-return': 'error',
|
||||
'sonarjs/prefer-single-boolean-return': 'error',
|
||||
'sonarjs/no-inverted-boolean-check': 'error',
|
||||
'sonarjs/no-nested-template-literals': 'error'
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/*.config.*',
|
||||
'**/*.test.*',
|
||||
'**/*.spec.*'
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
name: import-graph
|
||||
description: Validates import rules, detects circular dependencies, and enforces layer boundaries using dependency-cruiser
|
||||
severity-default: high
|
||||
tools: [Bash, Read]
|
||||
---
|
||||
|
||||
Run dependency-cruiser import graph analysis on changed files to detect circular dependencies, orphan modules, and import rule violations.
|
||||
|
||||
> **Note:** The circular dependency scan in step 4 targets `src/` specifically, since this is a frontend app with source code under `src/`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check if dependency-cruiser is available:
|
||||
```bash
|
||||
pnpm dlx dependency-cruiser --version
|
||||
```
|
||||
If not available, skip this check and report: "Skipped: dependency-cruiser not available. Install with: `pnpm add -D dependency-cruiser`"
|
||||
|
||||
> **Install:** `pnpm add -D dependency-cruiser`
|
||||
|
||||
2. Identify changed directories from the diff.
|
||||
|
||||
3. Determine config to use:
|
||||
- If `.dependency-cruiser.js` or `.dependency-cruiser.cjs` exists in the repo root, use it (dependency-cruiser auto-detects it). This config may enforce layer architecture rules (e.g., base → platform → workbench → renderer import direction):
|
||||
```bash
|
||||
pnpm dlx dependency-cruiser --output-type json <changed_directories> 2>/dev/null
|
||||
```
|
||||
- If no config exists, run with built-in defaults:
|
||||
```bash
|
||||
pnpm dlx dependency-cruiser --no-config --output-type json <changed_directories> 2>/dev/null
|
||||
```
|
||||
|
||||
4. Also check for circular dependencies specifically across `src/`:
|
||||
|
||||
```bash
|
||||
pnpm dlx dependency-cruiser --no-config --output-type json --do-not-follow "node_modules" --include-only "^src" src 2>/dev/null
|
||||
```
|
||||
|
||||
Look for modules where `.circular == true` in the output.
|
||||
|
||||
5. Parse the JSON output. Each violation has:
|
||||
- `rule.name`: the violated rule
|
||||
- `rule.severity`: error, warn, info
|
||||
- `from`: importing module
|
||||
- `to`: imported module
|
||||
|
||||
6. Map violation severity:
|
||||
- `error` → `major`
|
||||
- `warn` → `minor`
|
||||
- `info` → `nitpick`
|
||||
- Circular dependencies → `major` (category: architecture)
|
||||
- Orphan modules → `nitpick` (category: dx)
|
||||
|
||||
7. Report each violation with: the rule name, source and target modules, file path, and a suggestion (usually move the import or extract an interface).
|
||||
|
||||
## What It Catches
|
||||
|
||||
| Rule | What It Detects |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| `no-circular` | Circular dependency chains (A → B → C → A) |
|
||||
| `no-orphans` | Modules with no incoming or outgoing dependencies |
|
||||
| `not-to-dev-dep` | Production code importing devDependencies |
|
||||
| `no-duplicate-dep-types` | Same dependency in multiple sections of package.json |
|
||||
| Custom layer rules | Import direction violations (e.g., base → platform) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If pnpm dlx is not available, skip and report the error.
|
||||
- If the config file fails to parse, fall back to `--no-config`.
|
||||
- If there are more than 50 violations, report the first 20 and note the total count.
|
||||
- If no violations are found, report "No issues found."
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: memory-leak
|
||||
description: Scans for memory leak patterns including event listeners without cleanup, timers not cleared, and unbounded caches
|
||||
severity-default: high
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a memory leak specialist reviewing a code diff. Focus exclusively on patterns that cause memory to grow unboundedly over time.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Event listeners without cleanup** - addEventListener without corresponding removeEventListener, especially in Vue onMounted without onBeforeUnmount cleanup
|
||||
2. **Timers not cleared** - setInterval/setTimeout started in component lifecycle without clearInterval/clearTimeout on unmount
|
||||
3. **Observer patterns without disconnect** - MutationObserver, IntersectionObserver, ResizeObserver created without .disconnect() on cleanup
|
||||
4. **WebSocket/Worker connections** - opened connections never closed on component unmount or route change
|
||||
5. **Unbounded caches** - Maps, Sets, or arrays that grow with usage but never evict entries, especially keyed by user input or dynamic IDs
|
||||
6. **Stale closures holding references** - closures in event handlers or callbacks that capture large objects or DOM nodes and prevent garbage collection
|
||||
7. **RequestAnimationFrame without cancel** - rAF loops started without cancelAnimationFrame on cleanup
|
||||
8. **Vue-specific leaks** - watch/watchEffect without stop(), computed that captures reactive dependencies it shouldn't, provide/inject holding stale references
|
||||
9. **Global state accumulation** - pushing to global arrays/maps without ever removing entries, console.log keeping object references in dev
|
||||
|
||||
Rules:
|
||||
|
||||
- Focus on NEW leak patterns introduced in the diff
|
||||
- Do NOT flag existing cleanup patterns that are correct
|
||||
- Every finding must explain the specific lifecycle scenario where the leak occurs (e.g., "when user navigates away from this view, the interval keeps running")
|
||||
- "Critical" for leaks in hot paths or long-lived pages, "major" for component-level leaks, "minor" for dev-only or cold-path leaks
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: pattern-compliance
|
||||
description: Checks code against repository conventions from AGENTS.md and established patterns
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
Check code against repository conventions and framework patterns.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Read AGENTS.md (and any directory-specific guidance files) for project-specific conventions
|
||||
2. Read each changed file
|
||||
3. Check against the conventions found in AGENTS.md and these standard patterns:
|
||||
|
||||
### TypeScript
|
||||
|
||||
- No `any` types or `as any` assertions
|
||||
- No `@ts-ignore` without explanatory comment
|
||||
- Separate type imports (`import type { ... }`)
|
||||
- Use `import type { ... }` for type-only imports
|
||||
- Explicit return types on exported functions
|
||||
- Use `es-toolkit` for utility functions, NOT lodash. Flag any new `import ... from 'lodash'` or `import ... from 'lodash/*'`
|
||||
- Never use `z.any()` in Zod schemas — use `z.unknown()` and narrow
|
||||
|
||||
### Vue (if applicable)
|
||||
|
||||
- Composition API with `<script setup lang="ts">`
|
||||
- Reactive props destructuring (not `withDefaults` pattern)
|
||||
- New components must use `<script setup lang="ts">` with reactive props destructuring (Vue 3.5 style): `const { color = 'blue' } = defineProps<Props>()`
|
||||
- Separate type imports from value imports
|
||||
- All user-facing strings must use `vue-i18n` (`$t()` in templates, `t()` in script). Flag hardcoded English strings in templates that should be translated. The locale file is `src/locales/en/main.json`
|
||||
|
||||
### Tailwind (if applicable)
|
||||
|
||||
- No `dark:` variants (use semantic theme tokens)
|
||||
- Use `cn()` utility for conditional classes
|
||||
- No `!important` in utility classes
|
||||
- Tailwind 4: CSS variable references use parentheses syntax: `h-(--my-var)` NOT `h-[--my-var]`
|
||||
- Use design tokens: `bg-secondary-background`, `text-muted-foreground`, `border-border-default`
|
||||
- No `<style>` blocks in Vue SFCs — use inline Tailwind only
|
||||
|
||||
### Testing
|
||||
|
||||
- Behavioral tests, not change detectors
|
||||
- No mock-heavy tests that don't test real behavior
|
||||
- Test names describe behavior, not implementation
|
||||
|
||||
### General
|
||||
|
||||
- No commented-out code
|
||||
- No `console.log` in production code (unless intentional logging)
|
||||
- No hardcoded URLs, credentials, or environment-specific values
|
||||
- Package manager is `pnpm`. Never use `npm`, `npx`, or `yarn`. Use `pnpm dlx` for one-off package execution
|
||||
- Sanitize HTML with `DOMPurify.sanitize()`, never raw `innerHTML` or `v-html` without it
|
||||
|
||||
Rules:
|
||||
|
||||
- Only flag ACTUAL violations, not hypothetical ones
|
||||
- AGENTS.md conventions take priority over default patterns if they conflict
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: performance-profiler
|
||||
description: Reviews code for performance issues including algorithmic complexity, unnecessary work, and bundle size impact
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a performance engineer reviewing a code diff. Focus exclusively on performance issues.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Algorithmic complexity** - O(n²) or worse in loops, nested iterations over large collections
|
||||
2. **Unnecessary re-computation** - repeated work in render cycles, missing memoization for expensive ops
|
||||
3. **Memory leaks** - event listeners not cleaned up, growing caches without eviction, closures holding references
|
||||
4. **N+1 queries** - database/API calls inside loops
|
||||
5. **Bundle size** - large imports that could be tree-shaken, dynamic imports for heavy modules
|
||||
6. **Rendering performance** - unnecessary re-renders, layout thrashing, expensive computed properties
|
||||
7. **Data structures** - using arrays for lookups instead of maps/sets, unnecessary copying of large objects
|
||||
8. **Async patterns** - sequential awaits that could be parallel, missing abort controllers
|
||||
|
||||
Rules:
|
||||
|
||||
- ONLY report actual performance issues, not premature optimization suggestions
|
||||
- Distinguish between hot paths (major) and cold paths (minor)
|
||||
- Include Big-O analysis when relevant
|
||||
- Do NOT suggest micro-optimizations that a JIT compiler handles
|
||||
- Quantify the impact when possible: "This is O(n²) where n = number of users"
|
||||
|
||||
## Repo-Specific Performance Concerns
|
||||
|
||||
- **LiteGraph canvas rendering** is the primary hot path. Operations inside `LGraphNode.onDrawForeground`, `onDrawBackground`, `processMouseMove` run every frame at 60fps. Any O(n) or worse operation here on the node/link collections is critical.
|
||||
- **Node definition lookups** happen frequently — these should use Maps, not array.find()
|
||||
- **Workflow serialization/deserialization** can involve large JSON objects (1000+ nodes). Watch for deep copies or unnecessary re-parsing.
|
||||
- **Vue reactivity in canvas code** — reactive getters triggered during canvas render cause performance issues. Canvas-facing code should read raw values, not reactive refs.
|
||||
- **Bundle size** — check for large imports that could be dynamically imported. The build uses Vite with `build:analyze` for bundle visualization.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
name: regression-risk
|
||||
description: Detects potential regressions by analyzing git blame history of modified lines
|
||||
severity-default: high
|
||||
tools: [Bash, Read, Grep]
|
||||
---
|
||||
|
||||
Perform regression risk analysis on the current changes using git blame.
|
||||
|
||||
## Method
|
||||
|
||||
1. Determine the base branch by examining git context (e.g., `git merge-base origin/main HEAD`, or check the PR's target branch). Never use `HEAD~1` as the base — it compares against the PR's own prior commit and causes false positives.
|
||||
2. Get the PR's own commits: `git log --format=%H <base>..HEAD`
|
||||
3. For each changed file, run: `git diff <base>...HEAD -- <file>`
|
||||
4. Extract the modified line ranges from the diff (lines removed or changed in the base version).
|
||||
5. For each modified line range, check git blame in the base version:
|
||||
`git blame <base> -L <start>,<end> -- <file>`
|
||||
6. Look for blame commits whose messages match bugfix patterns:
|
||||
- Contains: fix, bug, patch, hotfix, revert, regression, CVE
|
||||
- Ignore: "fix lint", "fix typo", "fix format", "fix style"
|
||||
7. **Filter out false positives.** If the blamed commit SHA is in the PR's own commits, skip it.
|
||||
8. For each verified bugfix line being modified, report as a finding.
|
||||
|
||||
## What to Report
|
||||
|
||||
For each finding, include:
|
||||
|
||||
- The file and line number
|
||||
- The original bugfix commit (short SHA and subject)
|
||||
- The date of the original fix
|
||||
- A suggestion to verify the original bug scenario still works and to add a regression test if one doesn't exist
|
||||
|
||||
## Shallow Clone Limitations
|
||||
|
||||
When working with shallow clones, `git blame` may not have full history. If blame fails with "no such path in revision" or shows truncated history, report only findings where blame succeeds and note the limitation.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Situation | Action |
|
||||
| ------------------------ | -------------------------------- |
|
||||
| Shallow clone (no blame) | Report what succeeds, note limit |
|
||||
| Blame shows PR's own SHA | Skip finding (false positive) |
|
||||
| File renamed | Try blame with `--follow` |
|
||||
| Binary file | Skip file |
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: security-auditor
|
||||
description: Reviews code for security vulnerabilities aligned with OWASP Top 10
|
||||
severity-default: critical
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a security auditor reviewing a code diff. Focus exclusively on security vulnerabilities.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Injection** - SQL injection, command injection, template injection, XSS (stored/reflected/DOM)
|
||||
2. **Authentication/Authorization** - auth bypass, privilege escalation, missing access checks
|
||||
3. **Data exposure** - secrets in code, PII in logs, sensitive data in error messages, overly broad API responses
|
||||
4. **Cryptography** - weak algorithms, hardcoded keys, predictable tokens, missing encryption
|
||||
5. **Input validation** - missing sanitization, path traversal, SSRF, open redirects
|
||||
6. **Dependency risks** - known vulnerable patterns, unsafe deserialization
|
||||
7. **Configuration** - CORS misconfiguration, missing security headers, debug mode in production
|
||||
8. **Race conditions with security impact** - TOCTOU, double-spend, auth state races
|
||||
|
||||
Rules:
|
||||
|
||||
- ONLY report security issues, not general bugs or style
|
||||
- All findings must be severity "critical" or "major"
|
||||
- Explain the attack vector: who can exploit this and how
|
||||
- Do NOT report theoretical issues without a plausible attack scenario
|
||||
- Reference OWASP category when applicable
|
||||
|
||||
## Repo-Specific Patterns
|
||||
|
||||
- HTML sanitization must use `DOMPurify.sanitize()` — flag any `v-html` or `innerHTML` without DOMPurify
|
||||
- API calls should use `api.get(api.apiURL(...))` helpers, not raw `fetch('/api/...')` — direct URL construction can bypass auth
|
||||
- Firebase/Sentry credentials are configured via environment — flag any hardcoded Firebase config objects
|
||||
- Electron IPC: check for unsafe `ipcRenderer.send` patterns in desktop code paths
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: semgrep-sast
|
||||
description: Runs Semgrep SAST with auto-configured rules for JS/TS/Vue
|
||||
severity-default: high
|
||||
tools: [Bash, Read]
|
||||
---
|
||||
|
||||
Run Semgrep static analysis on changed files to detect security vulnerabilities, dangerous patterns, and framework-specific issues.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check if semgrep is installed:
|
||||
|
||||
```bash
|
||||
semgrep --version
|
||||
```
|
||||
|
||||
If not installed, skip this check and report: "Skipped: semgrep not installed. Install with: `pip3 install semgrep`"
|
||||
|
||||
2. Identify changed files (`.ts`, `.js`, `.vue`) from the diff.
|
||||
If none are found, skip and report: "Skipped: no changed JS/TS/Vue files."
|
||||
|
||||
3. Run semgrep against changed files:
|
||||
|
||||
```bash
|
||||
semgrep --config=auto --json --quiet <changed_files>
|
||||
```
|
||||
|
||||
4. Parse the JSON output (`.results[]` array). For each finding, map severity:
|
||||
- Semgrep `ERROR` → `critical`
|
||||
- Semgrep `WARNING` → `major`
|
||||
- Semgrep `INFO` → `minor`
|
||||
|
||||
5. Report each finding with:
|
||||
- The semgrep rule ID (`check_id`)
|
||||
- File path and line number (`path`, `start.line`)
|
||||
- The message from `extra.message`
|
||||
- A fix suggestion from `extra.fix` if available, otherwise general remediation advice
|
||||
|
||||
## What Semgrep Catches
|
||||
|
||||
With `--config=auto`, Semgrep loads community-maintained rules for:
|
||||
|
||||
- **Security vulnerabilities:** injection, XSS, SSRF, path traversal, open redirect
|
||||
- **Dangerous patterns:** eval(), innerHTML, dangerouslySetInnerHTML, exec()
|
||||
- **Crypto issues:** weak hashing, hardcoded secrets, insecure random
|
||||
- **Best practices:** missing security headers, unsafe deserialization
|
||||
- **Framework-specific:** Express, React, Vue security patterns
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If semgrep config download fails, skip and report the error.
|
||||
- If semgrep fails to parse a specific file, skip that file and continue with others.
|
||||
- If semgrep produces no findings, report "No issues found."
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
name: sonarjs-lint
|
||||
description: Runs SonarQube-grade static analysis using eslint-plugin-sonarjs
|
||||
severity-default: high
|
||||
tools: [Bash, Read]
|
||||
---
|
||||
|
||||
Run eslint-plugin-sonarjs analysis on changed files to detect bugs, code smells, and security patterns without needing a SonarQube server.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Check if eslint is available:
|
||||
|
||||
```bash
|
||||
pnpm dlx eslint --version
|
||||
```
|
||||
|
||||
If pnpm dlx or eslint is unavailable, skip this check and report: "Skipped: eslint not available. Ensure Node.js and pnpm dlx are installed."
|
||||
|
||||
2. Identify changed files (`.ts`, `.js`, `.vue`) from the diff.
|
||||
|
||||
3. Determine eslint config to use. This check uses a **strict sonarjs-specific config** (not the project's own eslint config, which is less strict):
|
||||
- Look for the colocated strict config at `.agents/checks/eslint.strict.config.js`
|
||||
- If found, run with `--config .agents/checks/eslint.strict.config.js`
|
||||
- **Fallback:** if the strict config cannot be found or fails to load, skip this check and report: "Skipped: .agents/checks/eslint.strict.config.js missing; SonarJS rules require explicit config."
|
||||
|
||||
4. Run eslint against changed files:
|
||||
|
||||
```bash
|
||||
# Use the strict config
|
||||
pnpm dlx --yes --package eslint-plugin-sonarjs eslint --no-config-lookup --config .agents/checks/eslint.strict.config.js --format json <changed_files> 2>/dev/null || true
|
||||
```
|
||||
|
||||
5. Parse the JSON array of file results. For each eslint message, map severity:
|
||||
- `severity 2` (error) → `major`
|
||||
- `severity 1` (warning) → `minor`
|
||||
|
||||
6. Categorize findings by rule ID:
|
||||
- Rule IDs starting with `sonarjs/no-` → category: `logic`
|
||||
- Rule IDs containing `cognitive-complexity` → category: `dx`
|
||||
- Other sonarjs rules → category: `style`
|
||||
|
||||
7. Report each finding with:
|
||||
- The rule ID
|
||||
- File path and line number
|
||||
- The message from eslint
|
||||
- A fix suggestion based on the rule
|
||||
|
||||
## What This Catches
|
||||
|
||||
- **Bug detection:** duplicated branches, element overwrite, identical conditions/expressions, one-iteration loops, empty return values
|
||||
- **Code smells:** cognitive complexity (threshold: 15), duplicate strings, redundant booleans, small switches
|
||||
- **Security patterns:** via sonarjs recommended ruleset
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If eslint fails to parse a Vue file, skip that file and continue with others.
|
||||
- If the plugin fails to install, skip and report the error.
|
||||
- If eslint produces no output or errors, report "No issues found."
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: test-quality
|
||||
description: Reviews test code for quality issues and coverage gaps
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a test quality reviewer. Evaluate the tests included with (or missing from) this code change.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Missing tests** - new behavior without test coverage, modified logic without updated tests
|
||||
2. **Change-detector tests** - tests that assert implementation details instead of behavior (testing that a function was called, not what it produces)
|
||||
3. **Mock-heavy tests** - tests with so many mocks they don't test real behavior
|
||||
4. **Snapshot abuse** - large snapshots that no one reviews, snapshots of implementation details
|
||||
5. **Fragile assertions** - tests that break on unrelated changes, order-dependent tests
|
||||
6. **Missing edge cases** - happy path only, no empty/null/error scenarios tested
|
||||
7. **Test readability** - unclear test names, complex setup that obscures intent, shared mutable state between tests
|
||||
8. **Test isolation** - tests depending on execution order, shared state, external services without mocking
|
||||
|
||||
Rules:
|
||||
|
||||
- Focus on test quality and coverage gaps, not production code bugs
|
||||
- "Major" for missing tests on critical logic, "minor" for missing edge case tests
|
||||
- A change that adds no tests is only an issue if the change adds behavior
|
||||
- Refactors without behavior changes don't need new tests
|
||||
- Prefer behavioral tests: test inputs and outputs, not internal implementation
|
||||
- This repo uses **colocated tests**: `.test.ts` files live next to their source files (e.g., `MyComponent.test.ts` beside `MyComponent.vue`). When checking for missing tests, look for a colocated `.test.ts` file, not a separate `tests/` directory
|
||||
|
||||
## Repo-Specific Testing Conventions
|
||||
|
||||
- Tests use **Vitest** (not Jest) — run with `pnpm test:unit`
|
||||
- Test files are **colocated**: `MyComponent.test.ts` next to `MyComponent.vue`
|
||||
- Use `@vue/test-utils` for component testing, `@pinia/testing` (`createTestingPinia`) for store tests
|
||||
- Browser/E2E tests use **Playwright** in `browser_tests/` — run with `pnpm test:browser:local`
|
||||
- Mock composables using the singleton factory pattern inside `vi.mock()` — see `docs/testing/unit-testing.md` for the pattern
|
||||
- Never use `any` in test code either — proper typing applies to tests too
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: vue-patterns
|
||||
description: Reviews Vue 3.5+ code for framework-specific anti-patterns
|
||||
severity-default: medium
|
||||
tools: [Read, Grep]
|
||||
---
|
||||
|
||||
You are a Vue 3.5 framework specialist reviewing a code diff. Focus on Vue-specific patterns, anti-patterns, and missed framework features.
|
||||
|
||||
Check for:
|
||||
|
||||
1. **Options API in new files** - new .vue files using Options API instead of Composition API with `<script setup>`. Modifications to existing Options API files are fine.
|
||||
2. **Reactivity anti-patterns** - destructuring reactive objects losing reactivity, using `ref()` for objects that should be `reactive()`, accessing `.value` inside templates, incorrectly using `toRefs`/`toRef`
|
||||
3. **Watch/watchEffect cleanup** - watchers without cleanup functions when they set up side effects (timers, listeners, subscriptions)
|
||||
4. **Flush timing issues** - DOM access in watch callbacks without `{ flush: 'post' }`, `nextTick` misuse, accessing template refs before mount
|
||||
5. **defineEmits typing** - using array syntax `defineEmits(['event'])` instead of TypeScript syntax `defineEmits<{...}>()`
|
||||
6. **defineExpose misuse** - exposing internal state via `defineExpose` when events would be more appropriate (expose is for imperative methods: validate, focus, open)
|
||||
7. **Prop drilling** - passing props through 3+ component levels where provide/inject would be cleaner
|
||||
8. **VueUse opportunities** - manual implementations of common composables that VueUse already provides (useLocalStorage, useEventListener, useDebounceFn, useIntersectionObserver, etc.)
|
||||
9. **Computed vs method** - methods used in templates for derived state that should be computed properties, or computed properties that have side effects
|
||||
10. **PrimeVue usage in new code** - New components must NOT use PrimeVue. This project is migrating to shadcn-vue (Reka UI primitives). If new code imports from `primevue/*`, flag it and suggest the shadcn-vue equivalent.
|
||||
|
||||
Available shadcn-vue replacements in `src/components/ui/`:
|
||||
|
||||
- `button/` — Button, variants
|
||||
- `select/` — Select, SelectTrigger, SelectContent, SelectItem
|
||||
- `textarea/` — Textarea
|
||||
- `toggle-group/` — ToggleGroup, ToggleGroupItem
|
||||
- `slider/` — Slider
|
||||
- `skeleton/` — Skeleton
|
||||
- `stepper/` — Stepper
|
||||
- `tags-input/` — TagsInput
|
||||
- `search-input/` — SearchInput
|
||||
- `Popover.vue` — Popover
|
||||
|
||||
For Reka UI primitives not yet wrapped, create a new component in `src/components/ui/` following the pattern in existing components (see `src/components/ui/AGENTS.md`): use `useForwardProps`, `cn()`, design tokens.
|
||||
|
||||
Modifications to existing PrimeVue-based components are acceptable but should note the migration opportunity.
|
||||
|
||||
Rules:
|
||||
|
||||
- Only review .vue and composable .ts files — skip stores, services, utils
|
||||
- Do NOT flag existing Options API files being modified (only flag NEW files)
|
||||
- Flag new PrimeVue imports — the project is migrating to shadcn-vue/Reka UI
|
||||
- When suggesting shadcn-vue alternatives, reference `src/components/ui/AGENTS.md` for the component creation pattern
|
||||
- Use Iconify icons (`<i class="icon-[lucide--check]" />`) not PrimeIcons
|
||||
- "Major" for reactivity bugs and flush timing, "minor" for API style and VueUse opportunities, "nitpick" for preference-level patterns
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
name: regenerating-screenshots
|
||||
description: 'Creates a PR to regenerate Playwright screenshot expectations. Use when screenshot tests are failing on main or PRs due to stale golden images. Triggers on: regen screenshots, regenerate screenshots, update expectations, fix screenshot tests.'
|
||||
---
|
||||
|
||||
# Regenerating Playwright Screenshot Expectations
|
||||
|
||||
Automates the process of triggering the `PR: Update Playwright Expectations`
|
||||
GitHub Action by creating a labeled PR from `origin/main`.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Fetch latest main**
|
||||
|
||||
```bash
|
||||
git fetch origin main
|
||||
```
|
||||
|
||||
2. **Create a timestamped branch** from `origin/main`
|
||||
|
||||
Format: `regen-screenshots/YYYY-MM-DDTHH` (hour resolution, local time)
|
||||
|
||||
```bash
|
||||
git checkout -b regen-screenshots/<datetime> origin/main
|
||||
```
|
||||
|
||||
3. **Create an empty commit**
|
||||
|
||||
```bash
|
||||
git commit --allow-empty -m "test: regenerate screenshot expectations"
|
||||
```
|
||||
|
||||
4. **Push the branch**
|
||||
|
||||
```bash
|
||||
git push origin regen-screenshots/<datetime>
|
||||
```
|
||||
|
||||
5. **Generate a poem** about regenerating screenshots. Be creative — a
|
||||
new, unique poem every time. Short (4–8 lines). Can be funny, wistful,
|
||||
epic, haiku-style, limerick, sonnet fragment — vary the form.
|
||||
|
||||
6. **Create the PR** with the poem as the body (no label yet).
|
||||
|
||||
Write the poem to a temp file and use `--body-file`:
|
||||
|
||||
```bash
|
||||
# Write poem to temp file
|
||||
# Create PR:
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head regen-screenshots/<datetime> \
|
||||
--title "test: regenerate screenshot expectations" \
|
||||
--body-file <temp-file>
|
||||
```
|
||||
|
||||
7. **Add the label** as a separate step to trigger the GitHub Action.
|
||||
|
||||
The `labeled` event only fires when a label is added after PR
|
||||
creation, not when applied during creation via `--label`.
|
||||
|
||||
Use the GitHub API directly (`gh pr edit --add-label` fails due to
|
||||
deprecated Projects Classic GraphQL errors):
|
||||
|
||||
```bash
|
||||
gh api repos/{owner}/{repo}/issues/<pr-number>/labels \
|
||||
-f "labels[]=New Browser Test Expectations" --method POST
|
||||
```
|
||||
|
||||
8. **Report the result** to the user:
|
||||
- PR URL
|
||||
- Branch name
|
||||
- Note that the GitHub Action will run automatically and commit
|
||||
updated screenshots to the branch.
|
||||
|
||||
## Notes
|
||||
|
||||
- The `New Browser Test Expectations` label triggers the
|
||||
`pr-update-playwright-expectations.yaml` workflow.
|
||||
- The workflow runs Playwright with `--update-snapshots`, commits results
|
||||
back to the PR branch, then removes the label.
|
||||
- This is fire-and-forget — no need to wait for or monitor the Action.
|
||||
- Always return to the original branch/worktree state after pushing.
|
||||
@@ -5,10 +5,3 @@ reviews:
|
||||
high_level_summary: false
|
||||
auto_review:
|
||||
drafts: true
|
||||
ignore_title_keywords:
|
||||
- '[release]'
|
||||
- '[backport'
|
||||
ignore_usernames:
|
||||
- comfy-pr-bot
|
||||
- github-actions
|
||||
- github-actions[bot]
|
||||
|
||||
@@ -38,9 +38,6 @@ TEST_COMFYUI_DIR=/home/ComfyUI
|
||||
ALGOLIA_APP_ID=4E0RO38HS8
|
||||
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
|
||||
|
||||
# Enable PostHog debug logging in the browser console.
|
||||
# VITE_POSTHOG_DEBUG=true
|
||||
|
||||
# Sentry ENV vars replace with real ones for debugging
|
||||
# SENTRY_AUTH_TOKEN=private-token # get from sentry
|
||||
# SENTRY_ORG=comfy-org
|
||||
|
||||
8
.github/AGENTS.md
vendored
@@ -13,11 +13,3 @@ This automated review performs comprehensive analysis:
|
||||
- Integration concerns
|
||||
|
||||
For implementation details, see `.claude/commands/comprehensive-pr-review.md`.
|
||||
|
||||
## GitHub Actions: Fork PR Permissions
|
||||
|
||||
Fork PRs get a **read-only `GITHUB_TOKEN`** — no PR comments, no secret access, no pushing.
|
||||
|
||||
Any workflow that needs write access must use the **two-workflow split**: a `pull_request`-triggered `ci-*.yaml` uploads artifacts (including PR metadata), then a `workflow_run`-triggered `pr-*.yaml` downloads them and posts comments with write permissions. See `ci-size-data` → `pr-size-report` or `ci-perf-report` → `pr-perf-report`. Use `.github/actions/post-pr-report-comment` for the comment step.
|
||||
|
||||
Never write PR comments directly from `pull_request` workflows or use `pull_request_target` to run untrusted code.
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Post PR Report Comment
|
||||
description: Reads a markdown report file and posts/updates a single idempotent comment on a PR.
|
||||
|
||||
inputs:
|
||||
pr-number:
|
||||
description: PR number to comment on
|
||||
required: true
|
||||
report-file:
|
||||
description: Path to the markdown report file
|
||||
required: true
|
||||
comment-marker:
|
||||
description: HTML comment marker for idempotent matching
|
||||
required: true
|
||||
token:
|
||||
description: GitHub token with pull-requests write permission
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Read report
|
||||
id: report
|
||||
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
|
||||
with:
|
||||
path: ${{ inputs.report-file }}
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
number: ${{ inputs.pr-number }}
|
||||
body: |
|
||||
${{ steps.report.outputs.content }}
|
||||
${{ inputs.comment-marker }}
|
||||
body-include: ${{ inputs.comment-marker }}
|
||||
3
.github/license-clarifications.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"posthog-js@*": { "licenses": "Apache-2.0" }
|
||||
}
|
||||
19
.github/workflows/ci-dist-telemetry-scan.yaml
vendored
@@ -79,22 +79,3 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo '✅ No Mixpanel references found'
|
||||
|
||||
- name: Scan dist for PostHog telemetry references
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo '🔍 Scanning for PostHog references...'
|
||||
if rg --no-ignore -n \
|
||||
-g '*.html' \
|
||||
-g '*.js' \
|
||||
-e '(?i)posthog\.init' \
|
||||
-e '(?i)posthog\.capture' \
|
||||
-e 'PostHogTelemetryProvider' \
|
||||
-e 'ph\.comfy\.org' \
|
||||
-e 'posthog-js' \
|
||||
dist; then
|
||||
echo '❌ ERROR: PostHog references found in dist assets!'
|
||||
echo 'PostHog must be properly tree-shaken from OSS builds.'
|
||||
exit 1
|
||||
fi
|
||||
echo '✅ No PostHog references found'
|
||||
|
||||
@@ -100,7 +100,6 @@ jobs:
|
||||
--production \
|
||||
--summary \
|
||||
--excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \
|
||||
--clarificationsFile .github/license-clarifications.json \
|
||||
--onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then
|
||||
echo ''
|
||||
echo '✅ All production dependency licenses are approved!'
|
||||
|
||||
64
.github/workflows/ci-perf-report.yaml
vendored
@@ -14,6 +14,7 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
perf-tests:
|
||||
@@ -44,7 +45,7 @@ jobs:
|
||||
- name: Run performance tests
|
||||
id: perf
|
||||
continue-on-error: true
|
||||
run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=3
|
||||
run: pnpm exec playwright test --project=performance --workers=1
|
||||
|
||||
- name: Upload perf metrics
|
||||
if: always()
|
||||
@@ -55,16 +56,55 @@ jobs:
|
||||
retention-days: 30
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Save PR metadata
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
mkdir -p temp/perf-meta
|
||||
echo "${{ github.event.number }}" > temp/perf-meta/number.txt
|
||||
echo "${{ github.event.pull_request.base.ref }}" > temp/perf-meta/base.txt
|
||||
report:
|
||||
needs: perf-tests
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@v6
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
name: perf-meta
|
||||
path: temp/perf-meta/
|
||||
node-version: 22
|
||||
|
||||
- name: Download PR perf metrics
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: perf-metrics
|
||||
path: test-results/
|
||||
|
||||
- name: Download baseline perf metrics
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: ci-perf-report.yaml
|
||||
event: push
|
||||
name: perf-metrics
|
||||
path: temp/perf-baseline/
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate perf report
|
||||
run: npx --yes tsx scripts/perf-report.ts > perf-report.md
|
||||
|
||||
- name: Read perf report
|
||||
id: perf-report
|
||||
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
|
||||
with:
|
||||
path: ./perf-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
${{ steps.perf-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_PERF -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_PERF -->'
|
||||
|
||||
4
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
62
.github/workflows/cloud-dispatch-build.yaml
vendored
@@ -1,62 +0,0 @@
|
||||
---
|
||||
# Dispatches a frontend-asset-build event to the cloud repo on push to
|
||||
# cloud/* branches and main. The cloud repo handles the actual build,
|
||||
# GCS upload, and secret management (Sentry, Algolia, GCS creds).
|
||||
#
|
||||
# This is fire-and-forget — it does NOT wait for the cloud workflow to
|
||||
# complete. Status is visible in the cloud repo's Actions tab.
|
||||
|
||||
name: Cloud Frontend Build Dispatch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'cloud/*'
|
||||
- 'main'
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: cloud-dispatch-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
# Fork guard: prevent forks from dispatching to the cloud repo.
|
||||
# For pull_request events, only dispatch when the 'preview' label is added.
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
(github.event_name != 'pull_request' ||
|
||||
github.event.label.name == 'preview')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build client payload
|
||||
id: payload
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
if [ "${EVENT_NAME}" = "pull_request" ]; then
|
||||
REF="${PR_HEAD_SHA}"
|
||||
BRANCH="${PR_HEAD_REF}"
|
||||
else
|
||||
REF="${GITHUB_SHA}"
|
||||
BRANCH="${GITHUB_REF_NAME}"
|
||||
fi
|
||||
payload="$(jq -nc \
|
||||
--arg ref "${REF}" \
|
||||
--arg branch "${BRANCH}" \
|
||||
'{ref: $ref, branch: $branch}')"
|
||||
echo "json=${payload}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Dispatch to cloud repo
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CLOUD_DISPATCH_TOKEN }}
|
||||
repository: Comfy-Org/cloud
|
||||
event-type: frontend-asset-build
|
||||
client-payload: ${{ steps.payload.outputs.json }}
|
||||
102
.github/workflows/pr-perf-report.yaml
vendored
@@ -1,102 +0,0 @@
|
||||
name: 'PR: Performance Report'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI: Performance Report']
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Download PR metadata
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
name: perf-meta
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
path: temp/perf-meta/
|
||||
|
||||
- name: Resolve and validate PR metadata
|
||||
id: pr-meta
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const artifactPr = Number(fs.readFileSync('temp/perf-meta/number.txt', 'utf8').trim());
|
||||
const artifactBase = fs.readFileSync('temp/perf-meta/base.txt', 'utf8').trim();
|
||||
|
||||
// Resolve PR from trusted workflow context
|
||||
let pr = context.payload.workflow_run.pull_requests?.[0];
|
||||
if (!pr) {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.payload.workflow_run.head_sha,
|
||||
});
|
||||
pr = prs.find(p => p.state === 'open');
|
||||
}
|
||||
|
||||
if (!pr) {
|
||||
core.setFailed('Unable to resolve PR from workflow_run context.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(pr.number) !== artifactPr) {
|
||||
core.setFailed(`Artifact PR number (${artifactPr}) does not match trusted context (${pr.number}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const trustedBase = pr.base?.ref;
|
||||
if (!trustedBase || artifactBase !== trustedBase) {
|
||||
core.setFailed(`Artifact base (${artifactBase}) does not match trusted context (${trustedBase}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('number', String(pr.number));
|
||||
core.setOutput('base', trustedBase);
|
||||
|
||||
- name: Download PR perf metrics
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
name: perf-metrics
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
path: test-results/
|
||||
|
||||
- name: Download baseline perf metrics
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: ${{ steps.pr-meta.outputs.base }}
|
||||
workflow: ci-perf-report.yaml
|
||||
event: push
|
||||
name: perf-metrics
|
||||
path: temp/perf-baseline/
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate perf report
|
||||
run: npx --yes tsx scripts/perf-report.ts > perf-report.md
|
||||
|
||||
- name: Post PR comment
|
||||
uses: ./.github/actions/post-pr-report-comment
|
||||
with:
|
||||
pr-number: ${{ steps.pr-meta.outputs.number }}
|
||||
report-file: ./perf-report.md
|
||||
comment-marker: '<!-- COMFYUI_FRONTEND_PERF -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
100
.github/workflows/pr-size-report.yaml
vendored
@@ -45,76 +45,28 @@ jobs:
|
||||
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
|
||||
path: temp/size
|
||||
|
||||
- name: Resolve and validate PR metadata
|
||||
id: pr-meta
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
// workflow_dispatch: validate artifact metadata against API-resolved PR
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
const pullNumber = Number('${{ inputs.pr_number }}');
|
||||
const { data: dispatchPr } = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullNumber,
|
||||
});
|
||||
|
||||
const artifactPr = Number(fs.readFileSync('temp/size/number.txt', 'utf8').trim());
|
||||
const artifactBase = fs.readFileSync('temp/size/base.txt', 'utf8').trim();
|
||||
|
||||
if (artifactPr !== dispatchPr.number) {
|
||||
core.setFailed(`Artifact PR number (${artifactPr}) does not match dispatch PR (${dispatchPr.number}).`);
|
||||
return;
|
||||
}
|
||||
if (artifactBase !== dispatchPr.base.ref) {
|
||||
core.setFailed(`Artifact base (${artifactBase}) does not match dispatch PR base (${dispatchPr.base.ref}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('number', String(dispatchPr.number));
|
||||
core.setOutput('base', dispatchPr.base.ref);
|
||||
return;
|
||||
}
|
||||
|
||||
// workflow_run: validate artifact metadata against trusted context
|
||||
const artifactPr = Number(fs.readFileSync('temp/size/number.txt', 'utf8').trim());
|
||||
const artifactBase = fs.readFileSync('temp/size/base.txt', 'utf8').trim();
|
||||
|
||||
let pr = context.payload.workflow_run.pull_requests?.[0];
|
||||
if (!pr) {
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.payload.workflow_run.head_sha,
|
||||
});
|
||||
pr = prs.find(p => p.state === 'open');
|
||||
}
|
||||
|
||||
if (!pr) {
|
||||
core.setFailed('Unable to resolve PR from workflow_run context.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(pr.number) !== artifactPr) {
|
||||
core.setFailed(`Artifact PR number (${artifactPr}) does not match trusted context (${pr.number}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const trustedBase = pr.base?.ref;
|
||||
if (!trustedBase || artifactBase !== trustedBase) {
|
||||
core.setFailed(`Artifact base (${artifactBase}) does not match trusted context (${trustedBase}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('number', String(pr.number));
|
||||
core.setOutput('base', trustedBase);
|
||||
- name: Set base branch
|
||||
id: pr-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=main" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download previous size data
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
with:
|
||||
branch: ${{ steps.pr-meta.outputs.base }}
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: ci-size-data.yaml
|
||||
event: push
|
||||
name: size-data
|
||||
@@ -124,10 +76,18 @@ jobs:
|
||||
- name: Generate size report
|
||||
run: node scripts/size-report.js > size-report.md
|
||||
|
||||
- name: Post PR comment
|
||||
uses: ./.github/actions/post-pr-report-comment
|
||||
- name: Read size report
|
||||
id: size-report
|
||||
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
|
||||
with:
|
||||
path: ./size-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
||||
with:
|
||||
pr-number: ${{ steps.pr-meta.outputs.number }}
|
||||
report-file: ./size-report.md
|
||||
comment-marker: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_SIZE -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
@@ -26,7 +26,6 @@ dist-ssr
|
||||
.claude/*.local.json
|
||||
.claude/*.local.md
|
||||
.claude/*.local.txt
|
||||
.claude/worktrees
|
||||
CLAUDE.local.md
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
],
|
||||
"no-control-regex": "off",
|
||||
"no-eval": "error",
|
||||
"no-eval": "off",
|
||||
"no-redeclare": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
|
||||
<div class="p-terminal size-full rounded-none p-2">
|
||||
<div ref="terminalEl" class="terminal-host h-full" />
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
||||
>
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div ref="terminalEl" class="h-full terminal-host" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.left="{
|
||||
@@ -13,7 +16,7 @@
|
||||
size="small"
|
||||
:class="
|
||||
cn('absolute top-2 right-8 transition-opacity', {
|
||||
'pointer-events-none opacity-0 select-none': !isHovered
|
||||
'opacity-0 pointer-events-none select-none': !isHovered
|
||||
})
|
||||
"
|
||||
:aria-label="tooltipText"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8 select-none">
|
||||
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
|
||||
<!-- Installation Path Section -->
|
||||
<div class="flex grow flex-col gap-6 text-neutral-300">
|
||||
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
|
||||
<div class="grow flex flex-col gap-6 text-neutral-300">
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
{{ $t('install.locationPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<p class="px-12 text-center text-neutral-400">
|
||||
<p class="text-center text-neutral-400 px-12">
|
||||
{{ $t('install.locationPicker.subtitle') }}
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<InputText
|
||||
v-model="installPath"
|
||||
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
||||
class="flex-1 border-neutral-700 bg-neutral-800/50 text-neutral-200 placeholder:text-neutral-500"
|
||||
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
|
||||
:class="{ 'p-invalid': pathError }"
|
||||
@update:model-value="validatePath"
|
||||
@focus="onFocus"
|
||||
@@ -23,7 +23,7 @@
|
||||
<Button
|
||||
icon="pi pi-folder-open"
|
||||
severity="secondary"
|
||||
class="border-0 bg-neutral-700 hover:bg-neutral-600"
|
||||
class="bg-neutral-700 hover:bg-neutral-600 border-0"
|
||||
@click="browsePath"
|
||||
/>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
<Message
|
||||
v-if="pathError"
|
||||
severity="error"
|
||||
class="w-full whitespace-pre-line"
|
||||
class="whitespace-pre-line w-full"
|
||||
>
|
||||
{{ pathError }}
|
||||
</Message>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="size-full object-contain px-4 pt-4 opacity-25"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<i
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 z-10 col-span-full row-span-full text-[4rem] text-green-500 opacity-100 transition-opacity [text-shadow:0.25rem_0_0.5rem_black] group-hover/task-card:opacity-20"
|
||||
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 col-span-full row-span-full z-10 text-[4rem] text-green-500 opacity-100 transition-opacity group-hover/task-card:opacity-20 [text-shadow:0.25rem_0_0.5rem_black]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="w-full text-center text-neutral-400">
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
@@ -45,8 +45,7 @@ import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
@@ -56,7 +55,6 @@ import type {
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
@@ -82,7 +80,8 @@ const executeTask = async (task: MaintenanceTask) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription')
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex size-full flex-col justify-between rounded-lg p-6">
|
||||
<h1 class="m-0 font-inter text-xl font-semibold italic">
|
||||
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
|
||||
<h1 class="font-inter font-semibold text-xl m-0 italic">
|
||||
{{ $t(`desktopDialogs.${id}.title`, title) }}
|
||||
</h1>
|
||||
<p class="whitespace-pre-wrap">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div
|
||||
class="grid h-screen w-screen items-center justify-around overflow-y-auto"
|
||||
class="h-screen w-screen grid items-center justify-around overflow-y-auto"
|
||||
>
|
||||
<div class="relative m-8 text-center">
|
||||
<!-- Header -->
|
||||
@@ -13,7 +13,7 @@
|
||||
<span>{{ $t('desktopUpdate.description') }}</span>
|
||||
</div>
|
||||
|
||||
<ProgressSpinner class="m-8 size-48" />
|
||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
||||
|
||||
<!-- Console button -->
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||
<div class="flex size-full flex-col">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<Stepper
|
||||
v-model:value="currentStep"
|
||||
class="flex h-full flex-col"
|
||||
class="flex flex-col h-full"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<!-- Main content area that grows to fill available space -->
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<!-- Install footer with navigation -->
|
||||
<InstallFooter
|
||||
class="mx-auto my-6 w-full max-w-2xl"
|
||||
class="w-full max-w-2xl my-6 mx-auto"
|
||||
:current-step
|
||||
:can-proceed
|
||||
:disable-location-step="noGpu"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div
|
||||
class="dark-theme grid h-screen min-h-full w-screen min-w-full justify-around overflow-y-auto bg-neutral-900 font-sans text-neutral-300"
|
||||
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
|
||||
>
|
||||
<div class="relative m-8 w-screen max-w-(--breakpoint-sm)">
|
||||
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
|
||||
<!-- Header -->
|
||||
<h1 class="backspan pi-wrench text-4xl font-bold">
|
||||
{{ t('maintenance.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex w-full flex-wrap items-center gap-4">
|
||||
<div class="w-full flex flex-wrap gap-4 items-center">
|
||||
<span class="grow">
|
||||
{{ t('maintenance.status') }}:
|
||||
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
|
||||
</span>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<SelectButton
|
||||
v-model="displayAsList"
|
||||
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
|
||||
@@ -56,10 +56,10 @@
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="mb-1 block">
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="mb-1 block">
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
@@ -71,13 +71,13 @@
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-x-0 border-y border-solid border-neutral-700"
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
:filter
|
||||
:display-as-list
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
<div class="flex justify-between gap-4 flex-row">
|
||||
<Button
|
||||
:label="t('maintenance.consoleLogs')"
|
||||
icon="pi pi-desktop"
|
||||
@@ -189,7 +189,8 @@ const completeValidation = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('maintenance.error.cannotContinue')
|
||||
detail: t('maintenance.error.cannotContinue'),
|
||||
life: 5_000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark hide-language-selector>
|
||||
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
|
||||
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
|
||||
<div
|
||||
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
|
||||
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
|
||||
>
|
||||
<h2 class="text-3xl font-semibold text-neutral-100">
|
||||
{{ $t('install.helpImprove') }}
|
||||
@@ -15,7 +15,7 @@
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 underline hover:text-blue-300"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
{{ $t('install.privacyPolicy') }} </a
|
||||
>.
|
||||
@@ -33,7 +33,7 @@
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end pt-6">
|
||||
<div class="flex pt-6 justify-end">
|
||||
<Button
|
||||
:label="$t('g.ok')"
|
||||
icon="pi pi-check"
|
||||
@@ -72,7 +72,8 @@ const updateConsent = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail')
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/>
|
||||
|
||||
<div class="no-drag sad-text flex items-center">
|
||||
<div class="flex min-w-110 flex-col gap-8 p-8">
|
||||
<div class="flex flex-col gap-8 p-8 min-w-110">
|
||||
<!-- Header -->
|
||||
<h1 class="text-4xl font-bold text-red-500">
|
||||
{{ $t('notSupported.title') }}
|
||||
@@ -20,7 +20,7 @@
|
||||
<p class="text-xl">
|
||||
{{ $t('notSupported.message') }}
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-neutral-800">
|
||||
<ul class="list-disc list-inside space-y-1 text-neutral-800">
|
||||
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
|
||||
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
|
||||
</ul>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<BaseViewTemplate dark>
|
||||
<div class="relative min-h-screen">
|
||||
<!-- Terminal Background Layer (always visible during loading) -->
|
||||
<div v-if="!isError" class="fixed inset-0 z-0 overflow-hidden">
|
||||
<div class="size-full">
|
||||
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
||||
<div class="h-full w-full">
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Semi-transparent overlay -->
|
||||
<div v-if="!isError" class="fixed inset-0 z-5 bg-neutral-900/80"></div>
|
||||
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
||||
|
||||
<!-- Smooth radial gradient overlay -->
|
||||
<div
|
||||
@@ -45,9 +45,9 @@
|
||||
<!-- Error Section (positioned at bottom) -->
|
||||
<div
|
||||
v-if="isError"
|
||||
class="absolute inset-x-0 bottom-20 flex flex-col items-center gap-4"
|
||||
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div class="flex gap-4 justify-center">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
:label="$t('serverStart.reportIssue')"
|
||||
@@ -71,10 +71,10 @@
|
||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||
<div
|
||||
v-if="terminalVisible && isError"
|
||||
class="absolute inset-x-4 bottom-4 z-10 mx-auto max-w-4xl"
|
||||
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
|
||||
>
|
||||
<div
|
||||
class="h-[300px] rounded-lg border border-neutral-700 bg-neutral-900/95 p-4"
|
||||
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
|
||||
>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 50],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": null },
|
||||
{ "name": "MASK", "type": "MASK", "links": null }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "KSampler",
|
||||
"pos": [500, 50],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": null },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": null }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": null }],
|
||||
"properties": { "Node name for S&R": "KSampler" },
|
||||
"widgets_values": [0, "randomize", 20, 8, "euler", "normal", 1]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": { "offset": [0, 0], "scale": 1 }
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,760 +0,0 @@
|
||||
{
|
||||
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
|
||||
"revision": 0,
|
||||
"last_node_id": 11,
|
||||
"last_link_id": 18,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PreviewAny",
|
||||
"pos": [1031, 434],
|
||||
"size": [250, 178],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": [null, null, null]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"pos": [788, 433.5],
|
||||
"size": [225, 380],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [5]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["3", "string_a"],
|
||||
["4", "value"],
|
||||
["6", "value"],
|
||||
["6", "value_1"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [548, 451],
|
||||
"size": [225, 142],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [4]
|
||||
}
|
||||
],
|
||||
"title": "Outer",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Outer\n"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[4, 1, 0, 5, 0, "STRING"],
|
||||
[5, 5, 0, 2, 0, "STRING"]
|
||||
],
|
||||
"groups": [],
|
||||
"definitions": {
|
||||
"subgraphs": [
|
||||
{
|
||||
"id": "1e38d8ea-45e1-48a5-aa20-966584201867",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 0",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [351, 432.5, 120, 120]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1352, 294.5, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "7bf3e1d4-0521-4b5c-92f5-47ca598b7eb4",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [1],
|
||||
"localized_name": "string_a",
|
||||
"pos": [451, 452.5]
|
||||
},
|
||||
{
|
||||
"id": "5fb3dcf7-9bfd-4b3c-a1b9-750b4f3edf19",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [13],
|
||||
"pos": [451, 472.5]
|
||||
},
|
||||
{
|
||||
"id": "55d24b8a-7c82-4b02-8e3d-ff31ffb8aa13",
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [16],
|
||||
"pos": [451, 492.5]
|
||||
},
|
||||
{
|
||||
"id": "c1fe7cc3-547e-4fb0-b763-61888558d4bd",
|
||||
"name": "value_1_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [18],
|
||||
"pos": [451, 512.5]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1372, 314.5]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [504, 437],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 13
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2]
|
||||
}
|
||||
],
|
||||
"title": "Inner 1",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 1\n"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [743, 325],
|
||||
"size": [347, 231],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"pos": [1115, 301],
|
||||
"size": [210, 196],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 7
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 16
|
||||
},
|
||||
{
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value_1"
|
||||
},
|
||||
"link": 18
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [9]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["5", "string_a"],
|
||||
["11", "value"],
|
||||
["9", "value"],
|
||||
["10", "string_a"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 2,
|
||||
"origin_id": 4,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 3,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 3,
|
||||
"origin_slot": 0,
|
||||
"target_id": 6,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": 6,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 4,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 6,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 3,
|
||||
"target_id": 6,
|
||||
"target_slot": 2,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 1",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [180, 739, 120, 100]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1246, 612, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [4],
|
||||
"localized_name": "string_a",
|
||||
"pos": [280, 759]
|
||||
},
|
||||
{
|
||||
"id": "d50f6a62-0185-43d4-a174-a8a94bd8f6e7",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [14],
|
||||
"pos": [280, 779]
|
||||
},
|
||||
{
|
||||
"id": "6b78450e-5986-49cd-b743-c933e5a34a69",
|
||||
"name": "value_1",
|
||||
"type": "STRING",
|
||||
"linkIds": [17],
|
||||
"pos": [280, 799]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [12],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1266, 632]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 11,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [334, 742],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 14
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [7]
|
||||
}
|
||||
],
|
||||
"title": "Inner 2",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 2\n"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [581, 637],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [11]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"pos": [1004, 613],
|
||||
"size": [210, 142],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 11
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 17
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [12]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"proxyWidgets": [
|
||||
["7", "string_a"],
|
||||
["8", "value"]
|
||||
]
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 4,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"origin_id": 11,
|
||||
"origin_slot": 0,
|
||||
"target_id": 10,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"origin_id": 10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 9,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"origin_id": 9,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 11,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 2,
|
||||
"target_id": 9,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
},
|
||||
{
|
||||
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
|
||||
"version": 1,
|
||||
"state": {
|
||||
"lastGroupId": 0,
|
||||
"lastNodeId": 11,
|
||||
"lastLinkId": 18,
|
||||
"lastRerouteId": 0
|
||||
},
|
||||
"revision": 0,
|
||||
"config": {},
|
||||
"name": "Sub 2",
|
||||
"inputNode": {
|
||||
"id": -10,
|
||||
"bounding": [262, 1222, 120, 80]
|
||||
},
|
||||
"outputNode": {
|
||||
"id": -20,
|
||||
"bounding": [1123.089999999999, 1125.1999999999998, 120, 60]
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"linkIds": [9],
|
||||
"localized_name": "string_a",
|
||||
"pos": [362, 1242]
|
||||
},
|
||||
{
|
||||
"id": "3a545207-7202-42a9-a82f-3b62e1b0f459",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"linkIds": [15],
|
||||
"pos": [362, 1262]
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"linkIds": [10],
|
||||
"localized_name": "STRING",
|
||||
"pos": [1143.089999999999, 1145.1999999999998]
|
||||
}
|
||||
],
|
||||
"widgets": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 8,
|
||||
"type": "PrimitiveStringMultiline",
|
||||
"pos": [412.96000000000004, 1228.2399999999996],
|
||||
"size": [210, 88],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "value",
|
||||
"name": "value",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "value"
|
||||
},
|
||||
"link": 15
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [8]
|
||||
}
|
||||
],
|
||||
"title": "Inner 3",
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveStringMultiline"
|
||||
},
|
||||
"widgets_values": ["Inner 3\n"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "StringConcatenate",
|
||||
"pos": [686.08, 1132.38],
|
||||
"size": [400, 200],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "string_a",
|
||||
"name": "string_a",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_a"
|
||||
},
|
||||
"link": 9
|
||||
},
|
||||
{
|
||||
"localized_name": "string_b",
|
||||
"name": "string_b",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "string_b"
|
||||
},
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [10]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "StringConcatenate"
|
||||
},
|
||||
"widgets_values": ["", "", ""]
|
||||
}
|
||||
],
|
||||
"groups": [],
|
||||
"links": [
|
||||
{
|
||||
"id": 8,
|
||||
"origin_id": 8,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 1,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 0,
|
||||
"target_id": 7,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"origin_id": 7,
|
||||
"origin_slot": 0,
|
||||
"target_id": -20,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"origin_id": -10,
|
||||
"origin_slot": 1,
|
||||
"target_id": 8,
|
||||
"target_slot": 0,
|
||||
"type": "STRING"
|
||||
}
|
||||
],
|
||||
"extra": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [-412, 11]
|
||||
},
|
||||
"frontendVersion": "1.41.7"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -36,7 +36,14 @@
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.65"
|
||||
"ver": "0.3.65",
|
||||
"models": [
|
||||
{
|
||||
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
||||
"directory": "checkpoints"
|
||||
}
|
||||
]
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
},
|
||||
|
||||
@@ -206,7 +206,9 @@ export class ComfyPage {
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.runButton = page.getByTestId(TestIds.topbar.queueButton)
|
||||
this.runButton = page
|
||||
.getByTestId(TestIds.topbar.queueButton)
|
||||
.getByRole('button', { name: 'Run' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
@@ -430,10 +432,7 @@ export const comfyPageFixture = base.extend<{
|
||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||
// Disable toast warning about version compatibility, as they may or
|
||||
// may not appear - depending on upstream ComfyUI dependencies
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true,
|
||||
// Browser tests should opt into missing-model warnings explicitly so
|
||||
// workflows do not render differently based on models present on disk.
|
||||
'Comfy.Workflow.ShowMissingModelsWarning': false
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -172,19 +172,6 @@ export class VueNodeHelpers {
|
||||
async enterSubgraph(nodeId?: string): Promise<void> {
|
||||
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
|
||||
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
|
||||
|
||||
// The footer tab button extends below the node body (visible area),
|
||||
// but its bounding box center overlaps the node body div.
|
||||
// Click at the bottom 25% of the button which is the genuinely visible
|
||||
// and unobstructed area outside the node body boundary.
|
||||
const box = await editButton.boundingBox()
|
||||
if (!box) {
|
||||
throw new Error(
|
||||
'subgraph-enter-button has no bounding box: element may be hidden or not in DOM'
|
||||
)
|
||||
}
|
||||
await editButton.click({
|
||||
position: { x: box.width / 2, y: box.height * 0.75 }
|
||||
})
|
||||
await editButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,22 +55,15 @@ export class ComfyNodeSearchBox {
|
||||
|
||||
async fillAndSelectFirstNode(
|
||||
nodeName: string,
|
||||
options?: { suggestionIndex?: number; exact?: boolean }
|
||||
options?: { suggestionIndex: number }
|
||||
) {
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
if (options?.exact) {
|
||||
await this.dropdown
|
||||
.locator(`li[aria-label="${nodeName}"]`)
|
||||
.first()
|
||||
.click()
|
||||
} else {
|
||||
await this.dropdown
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
await this.dropdown
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
|
||||
@@ -33,7 +33,6 @@ export const TestIds = {
|
||||
},
|
||||
topbar: {
|
||||
queueButton: 'queue-button',
|
||||
queueModeMenuTrigger: 'queue-mode-menu-trigger',
|
||||
saveButton: 'save-workflow-button'
|
||||
},
|
||||
nodeLibrary: {
|
||||
|
||||
@@ -29,10 +29,8 @@ class ComfyQueueButton {
|
||||
public readonly dropdownButton: Locator
|
||||
constructor(public readonly actionbar: ComfyActionbar) {
|
||||
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
|
||||
this.primaryButton = this.root
|
||||
this.dropdownButton = actionbar.root.getByTestId(
|
||||
TestIds.topbar.queueModeMenuTrigger
|
||||
)
|
||||
this.primaryButton = this.root.locator('.p-splitbutton-button')
|
||||
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
|
||||
}
|
||||
|
||||
public async toggleOptions() {
|
||||
|
||||
@@ -89,17 +89,6 @@ test.describe('Execution error', () => {
|
||||
})
|
||||
|
||||
test.describe('Missing models warning', () => {
|
||||
test('Should be disabled by default in browser tests', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('missing/missing_models')
|
||||
|
||||
const dialogTitle = comfyPage.page.getByText(
|
||||
'This workflow is missing models'
|
||||
)
|
||||
await expect(dialogTitle).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,58 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe(
|
||||
'Image paste priority over stale node metadata',
|
||||
{ tag: ['@node'] },
|
||||
() => {
|
||||
test('Should not paste copied node when a LoadImage node is selected and clipboard has stale node metadata', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/load_image_with_ksampler')
|
||||
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(initialCount).toBe(2)
|
||||
|
||||
// Copy the KSampler node (puts data-metadata in clipboard)
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
await ksamplerNodes[0].copy()
|
||||
|
||||
// Select the LoadImage node
|
||||
const loadImageNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
||||
await loadImageNodes[0].click('title')
|
||||
|
||||
// Simulate pasting when clipboard has stale node metadata (text/html
|
||||
// with data-metadata) but no image file items. This replicates the bug
|
||||
// scenario: user copied a node, then copied a web image (which replaces
|
||||
// clipboard files but may leave stale text/html with node metadata).
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const nodeData = { nodes: [{ type: 'KSampler', id: 99 }] }
|
||||
const base64 = btoa(JSON.stringify(nodeData))
|
||||
const html =
|
||||
'<meta charset="utf-8"><div><span data-metadata="' +
|
||||
base64 +
|
||||
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
|
||||
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.setData('text/html', html)
|
||||
|
||||
const event = new ClipboardEvent('paste', {
|
||||
clipboardData: dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node count should remain the same — stale node metadata should NOT
|
||||
// be deserialized when a media node is selected.
|
||||
const finalCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(finalCount).toBe(initialCount)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -171,9 +171,6 @@ test.describe('Node Interaction', () => {
|
||||
|
||||
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
|
||||
await comfyPage.nodeOps.dragTextEncodeNode2()
|
||||
// Move mouse away to avoid hover highlight on the node at the drop position.
|
||||
await comfyPage.canvasOps.moveMouseToEmptyArea()
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
|
||||
})
|
||||
|
||||
@@ -819,13 +816,16 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
const activeWorkflowName =
|
||||
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
||||
const workflowPathA = `${workflowA}.json`
|
||||
const workflowPathB = `${workflowB}.json`
|
||||
|
||||
expect(openWorkflows).toEqual(
|
||||
expect.arrayContaining([workflowA, workflowB])
|
||||
expect.arrayContaining([workflowPathA, workflowPathB])
|
||||
)
|
||||
expect(openWorkflows.indexOf(workflowA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowB)
|
||||
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowPathB)
|
||||
)
|
||||
expect(activeWorkflowName).toEqual(workflowB)
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -29,27 +29,12 @@ test.describe(
|
||||
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
|
||||
// 'workflow.avif'
|
||||
]
|
||||
const filesWithUpload = new Set(['no_workflow.webp'])
|
||||
|
||||
fileNames.forEach(async (fileName) => {
|
||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const shouldUpload = filesWithUpload.has(fileName)
|
||||
const uploadRequestPromise = shouldUpload
|
||||
? comfyPage.page.waitForRequest((req) =>
|
||||
req.url().includes('/upload/')
|
||||
)
|
||||
: null
|
||||
|
||||
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||
|
||||
if (uploadRequestPromise) {
|
||||
const request = await uploadRequestPromise
|
||||
expect(request.url()).toContain('/upload/')
|
||||
} else {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
}
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
@@ -110,9 +110,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
|
||||
await comfyPage.canvasOps.disconnectEdge()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
await comfyPage.page.locator('.p-chip-remove-icon').click()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler', {
|
||||
exact: true
|
||||
})
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'added-node-no-connection.png'
|
||||
)
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 93 KiB |
@@ -67,66 +67,4 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
|
||||
recordMeasurement(m)
|
||||
console.log(`Clipping: ${m.layouts} forced layouts`)
|
||||
})
|
||||
|
||||
test('subgraph idle style recalculations', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-idle')
|
||||
recordMeasurement(m)
|
||||
console.log(
|
||||
`Subgraph idle: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
|
||||
)
|
||||
})
|
||||
|
||||
test('subgraph mouse interaction style recalculations', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
const canvas = comfyPage.canvas
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not available')
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await comfyPage.page.mouse.move(
|
||||
box.x + (box.width * i) / 100,
|
||||
box.y + (box.height * (i % 3)) / 3
|
||||
)
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-mouse-sweep')
|
||||
recordMeasurement(m)
|
||||
console.log(
|
||||
`Subgraph mouse sweep: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
|
||||
)
|
||||
})
|
||||
|
||||
test('subgraph DOM widget clipping during node selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
|
||||
await comfyPage.perf.startMeasuring()
|
||||
|
||||
const canvas = comfyPage.canvas
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) throw new Error('Canvas bounding box not available')
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await comfyPage.page.mouse.click(
|
||||
box.x + box.width / 3 + (i % 5) * 30,
|
||||
box.y + box.height / 3 + (i % 4) * 30
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
|
||||
const m = await comfyPage.perf.stopMeasuring('subgraph-dom-widget-clipping')
|
||||
recordMeasurement(m)
|
||||
console.log(`Subgraph clipping: ${m.layouts} forced layouts`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
|
||||
})
|
||||
|
||||
test('loads from inserted workflow', async ({ comfyPage }) => {
|
||||
const workflowName = 'single_connected_reroute_node'
|
||||
const workflowName = 'single_connected_reroute_node.json'
|
||||
await comfyPage.workflow.setupWorkflowsDirectory({
|
||||
[`${workflowName}.json`]: `links/${workflowName}.json`
|
||||
[workflowName]: 'links/single_connected_reroute_node.json'
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
|
||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
@@ -21,12 +21,14 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can create new blank workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow',
|
||||
'*Unsaved Workflow (2)'
|
||||
'*Unsaved Workflow.json',
|
||||
'*Unsaved Workflow (2).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -39,37 +41,37 @@ test.describe('Workflows sidebar', () => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await tab.open()
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1', 'workflow2'])
|
||||
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
|
||||
)
|
||||
})
|
||||
|
||||
test('Can duplicate workflow', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
|
||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||
expect.arrayContaining(['workflow1'])
|
||||
expect.arrayContaining(['workflow1.json'])
|
||||
)
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)',
|
||||
'*workflow1 (Copy) (2)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json'
|
||||
])
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow1',
|
||||
'*workflow1 (Copy)',
|
||||
'*workflow1 (Copy) (2)',
|
||||
'*workflow1 (Copy) (3)'
|
||||
'workflow1.json',
|
||||
'*workflow1 (Copy).json',
|
||||
'*workflow1 (Copy) (2).json',
|
||||
'*workflow1 (Copy) (3).json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -83,12 +85,12 @@ test.describe('Workflows sidebar', () => {
|
||||
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
|
||||
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
|
||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getNodeCount())
|
||||
.toEqual(originalNodeCount + 1)
|
||||
|
||||
await tab.getPersistedItem('workflow1').click()
|
||||
await tab.getPersistedItem('workflow1.json').click()
|
||||
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
|
||||
})
|
||||
|
||||
@@ -111,22 +113,22 @@ test.describe('Workflows sidebar', () => {
|
||||
const openedWorkflow = tab.getOpenedItem('foo/bar')
|
||||
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow',
|
||||
'foo/baz'
|
||||
'*Unsaved Workflow.json',
|
||||
'foo/baz.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can save workflow as', async ({ comfyPage }) => {
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', 'workflow3'])
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
|
||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
||||
})
|
||||
|
||||
test('Exported workflow does not contain localized slot names', async ({
|
||||
@@ -182,15 +184,15 @@ test.describe('Workflows sidebar', () => {
|
||||
})
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5'
|
||||
'workflow5.json'
|
||||
])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5'
|
||||
'workflow5.json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -210,25 +212,25 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
|
||||
const topbar = comfyPage.menu.topbar
|
||||
await topbar.saveWorkflow('workflow1')
|
||||
await topbar.saveWorkflowAs('workflow2')
|
||||
await topbar.saveWorkflow('workflow1.json')
|
||||
await topbar.saveWorkflowAs('workflow2.json')
|
||||
await comfyPage.nextFrame()
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow1', 'workflow2'])
|
||||
.toEqual(['workflow1.json', 'workflow2.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow2')
|
||||
.toEqual('workflow2.json')
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1')
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
// The old workflow1 should be deleted and the new one should be saved.
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['workflow2', 'workflow1'])
|
||||
.toEqual(['workflow2.json', 'workflow1.json'])
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||
.toEqual('workflow1')
|
||||
.toEqual('workflow1.json')
|
||||
})
|
||||
|
||||
test('Does not report warning when switching between opened workflows', async ({
|
||||
@@ -264,15 +266,17 @@ test.describe('Workflows sidebar', () => {
|
||||
)
|
||||
await closeButton.click()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can close saved workflow with command', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.workflowsTab
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
||||
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||
@@ -280,7 +284,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18'
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
@@ -291,14 +295,14 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows', async ({ comfyPage }) => {
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18'
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
@@ -310,7 +314,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow'
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -322,11 +326,16 @@ test.describe('Workflows sidebar', () => {
|
||||
const { workflowsTab } = comfyPage.menu
|
||||
await workflowsTab.open()
|
||||
|
||||
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
|
||||
await workflowsTab
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.contextMenu.clickMenuItem('Duplicate')
|
||||
await expect
|
||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
@@ -338,7 +347,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
// Wait for workflow to appear in Browse section after sync
|
||||
const workflowItem =
|
||||
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
|
||||
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
|
||||
await expect(workflowItem).toBeVisible({ timeout: 3000 })
|
||||
|
||||
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
@@ -355,7 +364,7 @@ test.describe('Workflows sidebar', () => {
|
||||
}
|
||||
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
|
||||
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
|
||||
'#graph-canvas',
|
||||
{ targetPosition }
|
||||
)
|
||||
|
||||
@@ -555,74 +555,6 @@ test.describe(
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Nested Promoted Widget Disabled State', () => {
|
||||
test('Externally linked promoted widget is disabled, unlinked ones are not', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-nested-promotion'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node 5 (Sub 0) has 4 promoted widgets. The first (string_a) has its
|
||||
// slot connected externally from the Outer node, so it should be
|
||||
// disabled. The remaining promoted textarea widgets (value, value_1)
|
||||
// are unlinked and should be enabled.
|
||||
const promotedNames = await getPromotedWidgetNames(comfyPage, '5')
|
||||
expect(promotedNames).toContain('string_a')
|
||||
expect(promotedNames).toContain('value')
|
||||
|
||||
const disabledState = await comfyPage.page.evaluate(() => {
|
||||
const node = window.app!.canvas.graph!.getNodeById('5')
|
||||
return (node?.widgets ?? []).map((w) => ({
|
||||
name: w.name,
|
||||
disabled: !!w.computedDisabled
|
||||
}))
|
||||
})
|
||||
|
||||
const linkedWidget = disabledState.find((w) => w.name === 'string_a')
|
||||
expect(linkedWidget?.disabled).toBe(true)
|
||||
|
||||
const unlinkedWidgets = disabledState.filter(
|
||||
(w) => w.name !== 'string_a'
|
||||
)
|
||||
for (const w of unlinkedWidgets) {
|
||||
expect(w.disabled).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
test('Unlinked promoted textarea widgets are editable on the subgraph exterior', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'subgraphs/subgraph-nested-promotion'
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// The promoted textareas that are NOT externally linked should be
|
||||
// fully opaque and interactive.
|
||||
const textareas = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.domWidgetTextarea
|
||||
)
|
||||
await expect(textareas.first()).toBeVisible()
|
||||
|
||||
const count = await textareas.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const textarea = textareas.nth(i)
|
||||
const wrapper = textarea.locator('..')
|
||||
const opacity = await wrapper.evaluate(
|
||||
(el) => getComputedStyle(el).opacity
|
||||
)
|
||||
|
||||
if (opacity === '1' && (await textarea.isEditable())) {
|
||||
const testContent = `nested-promotion-edit-${i}`
|
||||
await textarea.fill(testContent)
|
||||
await expect(textarea).toHaveValue(testContent)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Promotion Cleanup', () => {
|
||||
test('Removing subgraph node clears promotion store entries', async ({
|
||||
comfyPage
|
||||
|
||||
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -22,10 +22,8 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'Load Checkpoint' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
@@ -43,14 +41,8 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
||||
|
||||
const checkpointNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'Load Checkpoint' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const ksamplerNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'KSampler' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |