Compare commits

..

1 Commits

Author SHA1 Message Date
bymyself
c4468a14c5 fix(cloud): disable legacy node templates feature on cloud
The legacy node templates feature (accessed via canvas right-click menu)
causes false-positive errors on cloud. This change conditionally loads
the extension only for non-cloud builds.

Refs: COM-14105
Amp-Thread-ID: https://ampcode.com/threads/T-019c0c5f-e384-7733-9081-9460dc8debf5
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 18:07:18 -08:00
1857 changed files with 29693 additions and 168051 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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."

View File

@@ -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.

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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.*'
]
}
]

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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 |

View 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

View File

@@ -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."

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -391,7 +391,7 @@ echo "Last stable release: $LAST_STABLE"
```bash
# Trigger the workflow
gh workflow run release-version-bump.yaml -f version_type=${VERSION_TYPE}
gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE}
# Workflow runs quickly - usually creates PR within 30 seconds
echo "Workflow triggered. Waiting for PR creation..."
@@ -443,21 +443,28 @@ echo "Workflow triggered. Waiting for PR creation..."
gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \
echo "ERROR: Release label missing! Add it immediately!"
```
2. Verify version number in package.json
3. Review all changed files
4. Ensure no unintended changes included
5. Wait for required PR checks:
2. Check for update-locales commits:
```bash
# WARNING: update-locales may add [skip ci] which blocks release workflow!
gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \
echo "WARNING: [skip ci] detected - release workflow may not trigger!"
```
3. Verify version number in package.json
4. Review all changed files
5. Ensure no unintended changes included
6. Wait for required PR checks:
```bash
gh pr checks ${PR_NUMBER} --watch
```
6. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
### Step 12: Pre-Merge Validation
1. **Review Requirements**: Release PRs require approval
2. Monitor CI checks
3. Check no new commits to main since PR creation
4. **DEPLOYMENT READINESS**: Ready to merge?
2. Monitor CI checks - watch for update-locales
3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger!
4. Check no new commits to main since PR creation
5. **DEPLOYMENT READINESS**: Ready to merge?
### Step 13: Execute Release

View File

@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(pnpx vitest run --testPathPattern=\"draftCacheV2.property\")",
"Bash(pnpx vitest run \"draftCacheV2.property\")",
"Bash(node -e \"const fc = require\\(''fast-check''\\); console.log\\(Object.keys\\(fc\\).filter\\(k => k.includes\\(''string''\\)\\).join\\('', ''\\)\\)\")"
]
}
}

View File

@@ -1,150 +0,0 @@
---
name: backport-management
description: Manages cherry-pick backports across stable release branches. Discovers candidates from Slack/git, analyzes dependencies, resolves conflicts via worktree, and logs results. Use when asked to backport, cherry-pick to stable, manage release branches, do stable branch maintenance, or run a backport session.
---
# Backport Management
Cherry-pick backport management for Comfy-Org/ComfyUI_frontend stable release branches.
## Quick Start
1. **Discover** — Collect candidates from Slack bot + git log gap (`reference/discovery.md`)
2. **Analyze** — Categorize MUST/SHOULD/SKIP, check deps (`reference/analysis.md`)
3. **Plan** — Order by dependency (leaf fixes first), group into waves per branch
4. **Execute** — Label-driven automation → worktree fallback for conflicts (`reference/execution.md`)
5. **Verify** — After each wave, verify branch integrity before proceeding
6. **Log & Report** — Generate session report with mermaid diagram (`reference/logging.md`)
## System Context
| Item | Value |
| -------------- | ------------------------------------------------- |
| Repo | `~/ComfyUI_frontend` (Comfy-Org/ComfyUI_frontend) |
| Merge strategy | Squash merge (`gh pr merge --squash --admin`) |
| Automation | `pr-backport.yaml` GitHub Action (label-driven) |
| Tracking dir | `~/temp/backport-session/` |
## Branch Scope Rules
**Critical: Match PRs to the correct target branches.**
| Branch prefix | Scope | Example |
| ------------- | ------------------------------ | ----------------------------------------- |
| `cloud/*` | Cloud-hosted ComfyUI only | App mode, cloud auth, cloud-specific UI |
| `core/*` | Local/self-hosted ComfyUI only | Core editor, local workflows, node system |
**⚠️ NEVER backport cloud-only PRs to `core/*` branches.** Cloud-only changes (app mode, cloud auth, cloud billing UI, cloud-specific API calls) are irrelevant to local users and waste effort. Before backporting any PR to a `core/*` branch, check:
- Does the PR title/description mention "app mode", "cloud", or cloud-specific features?
- Does the PR only touch files like `appModeStore.ts`, cloud auth, or cloud-specific components?
- If yes → skip for `core/*` branches (may still apply to `cloud/*` branches)
## ⚠️ Gotchas (Learn from Past Sessions)
### Use `gh api` for Labels — NOT `gh pr edit`
`gh pr edit --add-label` triggers Projects Classic deprecation errors. Always use:
```bash
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$PR/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH"
```
### Automation Over-Reports Conflicts
The `pr-backport.yaml` action reports more conflicts than reality. `git cherry-pick -m 1` with git auto-merge handles many cases the automation can't. Always attempt manual cherry-pick before skipping.
### Never Skip Based on Conflict File Count
12 or 27 conflicting files can be trivial (snapshots, new files). **Categorize conflicts first**, then decide. See Conflict Triage below.
## Conflict Triage
**Always categorize before deciding to skip. High conflict count ≠ hard conflicts.**
| Type | Symptom | Resolution |
| ---------------------------- | ------------------------------------ | --------------------------------------------------------------- |
| **Binary snapshots (PNGs)** | `.png` files in conflict list | `git checkout --theirs $FILE && git add $FILE` — always trivial |
| **Modify/delete (new file)** | PR introduces files not on target | `git add $FILE` — keep the new file |
| **Modify/delete (removed)** | Target removed files the PR modifies | `git rm $FILE` — file no longer relevant |
| **Content conflicts** | Marker-based (`<<<<<<<`) | Accept theirs via python regex (see below) |
| **Add/add** | Both sides added same file | Accept theirs, verify no logic conflict |
| **Locale/JSON files** | i18n key additions | Accept theirs, validate JSON after |
```python
# Accept theirs for content conflicts
import re
pattern = r'<<<<<<< HEAD\n(.*?)=======\n(.*?)>>>>>>> [^\n]+\n?'
content = re.sub(pattern, r'\2', content, flags=re.DOTALL)
```
### Escalation Triggers (Flag for Human)
- **Package.json/lockfile changes** → skip on stable (transitive dep regression risk)
- **Core type definition changes** → requires human judgment
- **Business logic conflicts** (not just imports/exports) → requires domain knowledge
- **Admin-merged conflict resolutions** → get human review of the resolution before continuing the wave
## Auto-Skip Categories
Skip these without discussion:
- **Dep refresh PRs** — Risk of transitive dep regressions on stable. Cherry-pick individual CVE fixes instead.
- **CI/tooling changes** — Not user-facing
- **Test-only / lint rule changes** — Not user-facing
- **Revert pairs** — If PR A reverted by PR B, skip both. If fixed version (PR C) exists, backport only C.
- **Features not on target branch** — e.g., Painter, GLSLShader, appModeStore on core/1.40
- **Cloud-only PRs on core/\* branches** — App mode, cloud auth, cloud billing. These only affect cloud-hosted ComfyUI.
## Wave Verification
After merging each wave of PRs to a target branch, verify branch integrity before proceeding:
```bash
# Fetch latest state of target branch
git fetch origin TARGET_BRANCH
# Quick smoke check: does the branch build?
git worktree add /tmp/verify-TARGET origin/TARGET_BRANCH
cd /tmp/verify-TARGET
source ~/.nvm/nvm.sh && nvm use 24 && pnpm install && pnpm typecheck
git worktree remove /tmp/verify-TARGET --force
```
If typecheck fails, stop and investigate before continuing. A broken branch after wave N means all subsequent waves will compound the problem.
## Continuous Backporting Recommendation
Large backport sessions (50+ PRs) are expensive and error-prone. Prefer continuous backporting:
- Backport bug fixes as they merge to main (same day or next day)
- Use the automation labels immediately after merge
- Reserve session-style bulk backporting for catching up after gaps
- When a release branch is created, immediately start the continuous process
## Quick Reference
### Label-Driven Automation (default path)
```bash
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$PR/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH"
# Wait 3 min, check: gh pr list --base TARGET_BRANCH --state open
```
### Manual Worktree Cherry-Pick (conflict fallback)
```bash
git worktree add /tmp/backport-$BRANCH origin/$BRANCH
cd /tmp/backport-$BRANCH
git checkout -b backport-$PR-to-$BRANCH origin/$BRANCH
git cherry-pick -m 1 $MERGE_SHA
# Resolve conflicts, push, create PR, merge
```
### PR Title Convention
```
[backport TARGET_BRANCH] Original Title (#ORIGINAL_PR)
```

View File

@@ -1,68 +0,0 @@
# Analysis & Decision Framework
## Categorization
| Category | Criteria | Action |
| -------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **MUST** | User-facing bug, crash, data corruption, security. Clear breakage that users will hit. | Backport (with deps if needed) |
| **SHOULD** | UX improvement, minor bug, small dep chain. No user-visible breakage if skipped, but improves experience. | Backport if clean cherry-pick; defer if conflict resolution is non-trivial |
| **SKIP** | CI/tooling, test-only, lint rules, cosmetic, dep refresh | Skip with documented reason |
| **NEEDS DISCUSSION** | Large dep chain, unclear risk/benefit, touches core types | Flag for human |
### MUST vs SHOULD Decision Guide
When unsure, ask: "If a user on this stable branch reports this issue, would we consider it a bug?"
- **Yes** → MUST. The fix addresses broken behavior.
- **No, but it's noticeably better** → SHOULD. The fix is a quality-of-life improvement.
- **No, and it's cosmetic or internal** → SKIP.
For SHOULD items with conflicts: if conflict resolution requires more than trivial accept-theirs patterns (content conflicts in business logic, not just imports), downgrade to SKIP or escalate to NEEDS DISCUSSION.
## Branch Scope Filtering
**Before categorizing, filter by branch scope:**
| Target branch | Skip if PR is... |
| ------------- | ------------------------------------------------------------------- |
| `core/*` | Cloud-only (app mode, cloud auth, cloud billing, cloud-specific UI) |
| `cloud/*` | Local-only features not present on cloud branch |
Cloud-only PRs backported to `core/*` are wasted effort — `core/*` branches serve local/self-hosted users who never see cloud features. Check PR titles, descriptions, and files changed for cloud-specific indicators.
## Features Not on Stable Branches
Check before backporting — these don't exist on older branches:
- **Painter** (`src/extensions/core/painter.ts`) — not on core/1.40
- **GLSLShader** — not on core/1.40
- **App builder** — check per branch
- **appModeStore.ts** — not on core/1.40
## Dep Refresh PRs
Always SKIP on stable branches. Risk of transitive dependency regressions outweighs audit cleanup benefit. If a specific CVE fix is needed, cherry-pick that individual fix instead.
## Revert Pairs
If PR A is reverted by PR B:
- Skip BOTH A and B
- If a fixed version exists (PR C), backport only C
## Dependency Analysis
```bash
# Find other PRs that touched the same files
gh pr view $PR --json files --jq '.files[].path' | while read f; do
git log --oneline origin/TARGET..$MERGE_SHA -- "$f"
done
```
## Human Review Checkpoint
Present decisions.md before execution. Include:
1. All MUST/SHOULD/SKIP categorizations with rationale
2. Questions for human (feature existence, scope, deps)
3. Estimated effort per branch

View File

@@ -1,42 +0,0 @@
# Discovery — Candidate Collection
## Source 1: Slack Backport-Checker Bot
Use `slackdump` skill to export `#frontend-releases` channel (C09K9TPU2G7):
```bash
slackdump export -o ~/slack-exports/frontend-releases.zip C09K9TPU2G7
```
Parse bot messages for PRs flagged "Might need backport" per release version.
## Source 2: Git Log Gap Analysis
```bash
# Count gap
git log --oneline origin/TARGET..origin/main | wc -l
# List gap commits
git log --oneline origin/TARGET..origin/main
# Check if a PR is already on target
git log --oneline origin/TARGET --grep="#PR_NUMBER"
# Check for existing backport PRs
gh pr list --base TARGET --state all --search "backport PR_NUMBER"
```
## Source 3: GitHub PR Details
```bash
# Get merge commit SHA
gh pr view $PR --json mergeCommit,title --jq '"Title: \(.title)\nMerge: \(.mergeCommit.oid)"'
# Get files changed
gh pr view $PR --json files --jq '.files[].path'
```
## Output: candidate_list.md
Table per target branch:
| PR# | Title | Category | Flagged by Bot? | Decision |

View File

@@ -1,150 +0,0 @@
# Execution Workflow
## Per-Branch Execution Order
1. Smallest gap first (validation run)
2. Medium gap next (quick win)
3. Largest gap last (main effort)
## Step 1: Label-Driven Automation (Batch)
```bash
# Add labels to all candidates for a target branch
for pr in $PR_LIST; do
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$pr/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH" --silent
sleep 2
done
# Wait 3 minutes for automation
sleep 180
# Check which got auto-PRs
gh pr list --base TARGET_BRANCH --state open --limit 50 --json number,title
```
## Step 2: Review & Merge Clean Auto-PRs
```bash
for pr in $AUTO_PRS; do
# Check size
gh pr view $pr --json title,additions,deletions,changedFiles \
--jq '"Files: \(.changedFiles), +\(.additions)/-\(.deletions)"'
# Admin merge
gh pr merge $pr --squash --admin
sleep 3
done
```
## Step 3: Manual Worktree for Conflicts
```bash
git fetch origin TARGET_BRANCH
git worktree add /tmp/backport-TARGET origin/TARGET_BRANCH
cd /tmp/backport-TARGET
for PR in ${CONFLICT_PRS[@]}; do
# Refresh target ref so each branch is based on current HEAD
git fetch origin TARGET_BRANCH
git checkout origin/TARGET_BRANCH
git checkout -b backport-$PR-to-TARGET origin/TARGET_BRANCH
git cherry-pick -m 1 $MERGE_SHA
# If conflict — NEVER skip based on file count alone!
# Categorize conflicts first: binary PNGs, modify/delete, content, add/add
# See SKILL.md Conflict Triage table for resolution per type.
# Resolve all conflicts, then:
git add .
GIT_EDITOR=true git cherry-pick --continue
git push origin backport-$PR-to-TARGET
NEW_PR=$(gh pr create --base TARGET_BRANCH --head backport-$PR-to-TARGET \
--title "[backport TARGET] TITLE (#$PR)" \
--body "Backport of #$PR..." | grep -oP '\d+$')
gh pr merge $NEW_PR --squash --admin
sleep 3
done
# Cleanup
cd -
git worktree remove /tmp/backport-TARGET --force
```
**⚠️ Human review for conflict resolutions:** When admin-merging a PR where you manually resolved conflicts (especially content conflicts beyond trivial accept-theirs), pause and present the resolution diff to the human for review before merging. Trivial resolutions (binary snapshots, modify/delete, locale key additions) can proceed without review.
## Step 4: Wave Verification
After completing all PRs in a wave for a target branch:
```bash
git fetch origin TARGET_BRANCH
git worktree add /tmp/verify-TARGET origin/TARGET_BRANCH
cd /tmp/verify-TARGET
source ~/.nvm/nvm.sh && nvm use 24 && pnpm install && pnpm typecheck
git worktree remove /tmp/verify-TARGET --force
```
If verification fails, stop and fix before proceeding to the next wave. Do not compound problems across waves.
## Conflict Resolution Patterns
### 1. Content Conflicts (accept theirs)
```python
import re
pattern = r'<<<<<<< HEAD\n(.*?)=======\n(.*?)>>>>>>> [^\n]+\n?'
content = re.sub(pattern, r'\2', content, flags=re.DOTALL)
```
### 2. Modify/Delete (two cases!)
```bash
# Case A: PR introduces NEW files not on target → keep them
git add $FILE
# Case B: Target REMOVED files the PR modifies → drop them
git rm $FILE
```
### 3. Binary Files (snapshots)
```bash
git checkout --theirs $FILE && git add $FILE
```
### 4. Locale Files
Usually adding new i18n keys — accept theirs, validate JSON:
```bash
python3 -c "import json; json.load(open('src/locales/en/main.json'))" && echo "Valid"
```
## Merge Conflicts After Other Merges
When merging multiple PRs to the same branch, later PRs may conflict with earlier merges:
```bash
git fetch origin TARGET_BRANCH
git rebase origin/TARGET_BRANCH
# Resolve new conflicts
git push --force origin backport-$PR-to-TARGET
sleep 20 # Wait for GitHub to recompute merge state
gh pr merge $PR --squash --admin
```
## Lessons Learned
1. **Automation reports more conflicts than reality**`cherry-pick -m 1` with git auto-merge handles many "conflicts" the automation can't
2. **Never skip based on conflict file count** — 12 or 27 conflicts can be trivial (snapshots, new files). Categorize first: binary PNGs, modify/delete, content, add/add.
3. **Modify/delete goes BOTH ways** — if the PR introduces new files (not on target), `git add` them. If target deleted files the PR modifies, `git rm`.
4. **Binary snapshot PNGs** — always `git checkout --theirs && git add`. Never skip a PR just because it has many snapshot conflicts.
5. **Batch label additions need 2s delay** between API calls to avoid rate limits
6. **Merging 6+ PRs rapidly** can cause later PRs to become unmergeable — wait 20-30s for GitHub to recompute merge state
7. **appModeStore.ts, painter files, GLSLShader files** don't exist on core/1.40 — `git rm` these
8. **Always validate JSON** after resolving locale file conflicts
9. **Dep refresh PRs** — skip on stable branches. Risk of transitive dep regressions outweighs audit cleanup. Cherry-pick individual CVE fixes instead.
10. **Verify after each wave** — run `pnpm typecheck` on the target branch after merging a batch. Catching breakage early prevents compounding errors.
11. **Cloud-only PRs don't belong on core/\* branches** — app mode, cloud auth, and cloud-specific UI changes are irrelevant to local users. Always check PR scope against branch scope before backporting.

View File

@@ -1,96 +0,0 @@
# Logging & Session Reports
## During Execution
Maintain `execution-log.md` with per-branch tables:
```markdown
| PR# | Title | Status | Backport PR | Notes |
| ----- | ----- | --------------------------------- | ----------- | ------- |
| #XXXX | Title | ✅ Merged / ⏭️ Skip / ⏸️ Deferred | #YYYY | Details |
```
## Wave Verification Log
Track verification results per wave:
```markdown
## Wave N Verification — TARGET_BRANCH
- PRs merged: #A, #B, #C
- Typecheck: ✅ Pass / ❌ Fail
- Issues found: (if any)
- Human review needed: (list any non-trivial conflict resolutions)
```
## Session Report Template
```markdown
# Backport Session Report
## Summary
| Branch | Candidates | Merged | Skipped | Deferred | Rate |
| ------ | ---------- | ------ | ------- | -------- | ---- |
## Deferred Items (Needs Human)
| PR# | Title | Branch | Issue |
## Conflict Resolutions Requiring Review
| PR# | Branch | Conflict Type | Resolution Summary |
## Automation Performance
| Metric | Value |
| --------------------------- | ----- |
| Auto success rate | X% |
| Manual resolution rate | X% |
| Overall clean rate | X% |
| Wave verification pass rate | X% |
## Process Recommendations
- Were there clusters of related PRs that should have been backported together?
- Any PRs that should have been backported sooner (continuous backporting candidates)?
- Feature branches that need tracking for future sessions?
```
## Final Deliverable: Visual Summary
At session end, generate a **mermaid diagram** showing all backported PRs organized by target branch and category (MUST/SHOULD), plus a summary table. Present this to the user as the final output.
```mermaid
graph TD
subgraph branch1["☁️ cloud/X.XX — N PRs"]
C1["#XXXX title"]
C2["#XXXX title"]
end
subgraph branch2must["🔴 core/X.XX MUST — N PRs"]
M1["#XXXX title"]
end
subgraph branch2should["🟡 core/X.XX SHOULD — N PRs"]
S1["#XXXX-#XXXX N auto-merged"]
S2["#XXXX-#XXXX N manual picks"]
end
classDef cloudStyle fill:#1a3a5c,stroke:#4da6ff,color:#e0f0ff
classDef coreStyle fill:#1a4a2e,stroke:#4dff88,color:#e0ffe8
classDef mustStyle fill:#5c1a1a,stroke:#ff4d4d,color:#ffe0e0
classDef shouldStyle fill:#4a3a1a,stroke:#ffcc4d,color:#fff5e0
```
Use the `mermaid` tool to render this diagram and present it alongside the summary table as the session's final deliverable.
## Files to Track
- `candidate_list.md` — all candidates per branch
- `decisions.md` — MUST/SHOULD/SKIP with rationale
- `wave-plan.md` — execution order
- `execution-log.md` — real-time status
- `backport-session-report.md` — final summary
All in `~/temp/backport-session/`.

View File

@@ -1,179 +0,0 @@
---
name: perf-fix-with-proof
description: 'Ships performance fixes with CI-proven improvement using stacked PRs. PR1 adds a @perf test (establishes baseline on main), PR2 adds the fix (CI shows delta). Use when implementing a perf optimization and wanting to prove it in CI.'
---
# Performance Fix with Proof
Ships perf fixes as two stacked PRs so CI automatically proves the improvement.
## Why Two PRs
The `ci-perf-report.yaml` workflow compares PR metrics against the **base branch baseline**. If you add a new `@perf` test in the same PR as the fix, that test doesn't exist on main yet — no baseline, no delta, no proof. Stacking solves this:
1. **PR1 (test-only)** — adds the `@perf` test that exercises the bottleneck. Merges to main. CI runs it on main → baseline established.
2. **PR2 (fix)** — adds the optimization. CI runs the same test → compares against PR1's baseline → delta shows improvement.
## Workflow
### Step 1: Create the test branch
```bash
git worktree add <worktree-path> -b perf/test-<name> origin/main
```
### Step 2: Write the `@perf` test
Add a test to `browser_tests/tests/performance.spec.ts` (or a new file with `@perf` tag). The test should stress the specific bottleneck.
**Test structure:**
```typescript
test('<descriptive name>', async ({ comfyPage }) => {
// 1. Load a workflow that exercises the bottleneck
await comfyPage.workflow.loadWorkflow('<workflow>')
// 2. Start measuring
await comfyPage.perf.startMeasuring()
// 3. Perform the action that triggers the bottleneck (at scale)
for (let i = 0; i < N; i++) {
// ... stress the hot path ...
await comfyPage.nextFrame()
}
// 4. Stop measuring and record
const m = await comfyPage.perf.stopMeasuring('<metric-name>')
recordMeasurement(m)
console.log(`<name>: ${m.styleRecalcs} recalcs, ${m.layouts} layouts`)
})
```
**Available metrics** (from `PerformanceHelper`):
- `m.styleRecalcs` / `m.styleRecalcDurationMs` — style recalculation count and time
- `m.layouts` / `m.layoutDurationMs` — forced layout count and time
- `m.taskDurationMs` — total main-thread JS execution time
- `m.heapDeltaBytes` — memory pressure delta
**Key helpers** (from `ComfyPage`):
- `comfyPage.perf.startMeasuring()` / `.stopMeasuring(name)` — CDP metrics capture
- `comfyPage.nextFrame()` — wait one animation frame
- `comfyPage.workflow.loadWorkflow(name)` — load a test workflow from `browser_tests/assets/`
- `comfyPage.canvas` — the canvas locator
- `comfyPage.page.mouse.move(x, y)` — mouse interaction
### Step 3: Add test workflow asset (if needed)
If the bottleneck needs a specific workflow (e.g., 50+ nodes, many DOM widgets), add it to `browser_tests/assets/`. Keep it minimal — only the structure needed to trigger the bottleneck.
### Step 4: Verify locally
```bash
pnpm exec playwright test --project=performance --grep "<test name>"
```
Confirm the test runs and produces reasonable metric values.
### Step 5: Create PR1 (test-only)
```bash
pnpm typecheck:browser
pnpm lint
git add browser_tests/
git commit -m "test: add perf test for <bottleneck description>"
git push -u origin perf/test-<name>
gh pr create --title "test: add perf test for <bottleneck>" \
--body "Adds a @perf test to establish a baseline for <bottleneck>.
This is PR 1 of 2. The fix will follow in a separate PR once this baseline is established on main.
## What
Adds \`<test-name>\` to the performance test suite measuring <metric> during <action>.
## Why
Needed to prove the improvement from the upcoming fix for backlog item #<N>." \
--base main
```
### Step 6: Get PR1 merged
Once PR1 merges, CI runs the test on main → baseline artifact saved.
### Step 7: Create PR2 (fix) on top of main
```bash
git worktree add <worktree-path> -b perf/fix-<name> origin/main
```
Implement the fix. The `@perf` test from PR1 is now on main and will run automatically. CI will:
1. Run the test on the PR branch
2. Download the baseline from main (which includes PR1's test results)
3. Post a PR comment showing the delta
### Step 8: Verify the improvement shows in CI
The `ci-perf-report.yaml` posts a comment like:
```markdown
## ⚡ Performance Report
| Metric | Baseline | PR (n=3) | Δ | Sig |
| --------------------- | -------- | -------- | ---- | --- |
| <name>: style recalcs | 450 | 12 | -97% | 🟢 |
```
If Δ is negative for the target metric, the fix is proven.
## Test Design Guidelines
1. **Stress the specific bottleneck** — don't measure everything, isolate the hot path
2. **Use enough iterations** — the test should run long enough that the metric difference is clear (100+ frames for idle tests, 50+ interactions for event tests)
3. **Keep it deterministic** — avoid timing-dependent assertions; measure counts not durations when possible
4. **Match the backlog entry** — reference the backlog item number in the test name or PR description
## Examples
**Testing DOM widget reactive mutations (backlog #8):**
```typescript
test('DOM widget positioning recalculations', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.perf.startMeasuring()
// Idle for 120 frames — DOM widgets update position every frame
for (let i = 0; i < 120; i++) {
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('dom-widget-idle')
recordMeasurement(m)
})
```
**Testing measureText caching (backlog #4):**
```typescript
test('canvas text rendering with many nodes', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('large-workflow-50-nodes')
await comfyPage.perf.startMeasuring()
for (let i = 0; i < 60; i++) {
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('text-rendering-50-nodes')
recordMeasurement(m)
})
```
## Reference
| Resource | Path |
| ----------------- | ----------------------------------------------------- |
| Perf test file | `browser_tests/tests/performance.spec.ts` |
| PerformanceHelper | `browser_tests/fixtures/helpers/PerformanceHelper.ts` |
| Perf reporter | `browser_tests/helpers/perfReporter.ts` |
| CI workflow | `.github/workflows/ci-perf-report.yaml` |
| Report generator | `scripts/perf-report.ts` |
| Stats utilities | `scripts/perf-stats.ts` |
| Backlog | `docs/perf/BACKLOG.md` (local only, not committed) |
| Playbook | `docs/perf/PLAYBOOK.md` (local only, not committed) |

View File

@@ -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 (48 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.

View File

@@ -1,361 +0,0 @@
---
name: ticket-intake
description: 'Parse ticket URL (Notion or GitHub), extract all data, initialize pipeline run. Use when starting work on a new ticket or when asked to pick up a ticket.'
---
# Ticket Intake
Parses a ticket URL from supported sources (Notion or GitHub), extracts all relevant information, and creates a ticket in the pipeline API.
> **🚨 CRITICAL REQUIREMENT**: This skill MUST register the ticket in the Pipeline API and update the source (Notion/GitHub). If these steps are skipped, the entire pipeline breaks. See [Mandatory API Calls](#mandatory-api-calls-execute-all-three) below.
## Supported Sources
| Source | URL Pattern | Provider File |
| ------ | --------------------------------------------------- | --------------------- |
| Notion | `https://notion.so/...` `https://www.notion.so/...` | `providers/notion.md` |
| GitHub | `https://github.com/{owner}/{repo}/issues/{n}` | `providers/github.md` |
## Quick Start
When given a ticket URL:
1. **Detect source type** from URL pattern
2. **Load provider-specific logic** from `providers/` directory
3. Fetch ticket content via appropriate API
4. Extract and normalize properties to common schema
5. **Register ticket in pipeline API** ← MANDATORY
6. **Update source** (Notion status / GitHub comment) ← MANDATORY
7. **Run verification script** to confirm API registration
8. Output summary and handoff to `research-orchestrator`
## Configuration
Uses the **production API** by default. No configuration needed for read operations.
**Defaults (no setup required):**
- API URL: `https://api-gateway-856475788601.us-central1.run.app`
- Read-only endpoints at `/public/*` require no authentication
**For write operations** (transitions, creating tickets), set:
```bash
export PIPELINE_API_KEY="..." # Get from GCP Secret Manager or ask admin
```
**Optional (for local working artifacts):**
```bash
PIPELINE_DIR="${PIPELINE_DIR:-$HOME/repos/ticket-to-pr-pipeline}"
```
## Mandatory API Calls (Execute ALL Three)
**⚠️ These three API calls are the ENTIRE POINT of this skill. Without them, the ticket is invisible to the pipeline, downstream skills will fail, and Notion status won't update.**
**You MUST make these HTTP requests.** Use `curl` from bash — do not just read this as documentation.
### Call 1: Create Ticket
```bash
API_URL="${PIPELINE_API_URL:-https://api-gateway-856475788601.us-central1.run.app}"
API_KEY="${PIPELINE_API_KEY}"
curl -s -X POST "${API_URL}/v1/tickets" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"notion_page_id": "NOTION_PAGE_UUID_HERE",
"title": "TICKET_TITLE_HERE",
"source": "notion",
"metadata": {
"description": "DESCRIPTION_HERE",
"priority": "High",
"labels": [],
"acceptanceCriteria": []
}
}'
```
Save the returned `id` — you need it for the next two calls.
### Call 2: Transition to RESEARCH
```bash
TICKET_ID="id-from-step-1"
curl -s -X POST "${API_URL}/v1/tickets/${TICKET_ID}/transition" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"to_state": "RESEARCH",
"reason": "Intake complete, starting research"
}'
```
### Call 3: Queue Source Update
```bash
curl -s -X POST "${API_URL}/v1/sync/queue" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: ${AGENT_ID:-amp-agent}" \
-d '{
"ticket_id": "TICKET_ID_HERE",
"action": "update_status",
"payload": { "status": "In Progress" },
"priority": "normal"
}'
```
> **Note:** The action MUST be `"update_status"` (not `"UPDATE_NOTION_STATUS"`). Valid actions: `update_status`, `update_pr_url`, `mark_done`.
### TypeScript Equivalent (if using pipeline client)
```typescript
import { PipelineClient } from '@pipeline/client'
const client = new PipelineClient({
apiUrl:
process.env.PIPELINE_API_URL ||
'https://api-gateway-856475788601.us-central1.run.app',
agentId: process.env.AGENT_ID!
})
const ticket = await client.createTicket({
notion_page_id: pageId,
title: ticketTitle,
source: 'notion',
metadata: { description, priority, labels, acceptanceCriteria }
})
await client.transitionState(
ticket.id,
'RESEARCH',
'Intake complete, starting research'
)
await client.queueSync(ticket.id, 'update_status', { status: 'In Progress' })
```
## Workflow
### Step 1: Detect Source Type
Parse the URL to determine source:
```javascript
if (url.includes('notion.so')) {
source = 'notion'
// Load providers/notion.md
} else if (url.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+/)) {
source = 'github'
// Load providers/github.md
} else {
// Error: Unsupported URL format
}
```
### Step 2: Load Provider and Fetch Data
Read the appropriate provider file for source-specific instructions:
- **Notion**: `providers/notion.md` - Uses Notion MCP, handles Slack links
- **GitHub**: `providers/github.md` - Uses `gh` CLI, handles Dosu comments
Follow the provider's instructions for:
- Fetching content
- Extracting properties
- **Updating the source** (Notion status → "In Progress", Assignee → pipeline owner)
### Step 3: Normalize to Common Schema
All providers must extract normalized ticket data following `schema.md`:
```json
{
"id": "abc12345",
"url": "https://...",
"source": "notion | github",
"title": "Ticket title",
"description": "Full description",
"status": "Not Started",
"assignee": "username",
"priority": "High",
"area": "UI",
"labels": ["bug", "frontend"],
"acceptanceCriteria": ["Criterion 1", "Criterion 2"],
"fetchedAt": "2024-01-15T10:30:00Z"
}
```
### Step 4: Register Ticket in Pipeline API (MANDATORY — DO NOT SKIP)
**Execute all three API calls from [Mandatory API Calls](#mandatory-api-calls-execute-all-three) above.**
This is not optional. This is not documentation. You MUST make these HTTP requests right now.
1. `createTicket()` → save the returned ticket ID
2. `transitionState(id, 'RESEARCH')` → confirm state changed
3. `queueSync(id, 'update_status', { status: 'In Progress' })` → confirm queued
**If any call fails**, retry once. If it still fails, report the error prominently — do NOT silently continue.
### Step 5: Run Verification Script
After making the API calls, run the verification script to confirm everything worked:
```bash
bash scripts/verify-intake.sh TICKET_ID_OR_NOTION_PAGE_ID
```
**If the script is not available locally**, verify manually via the public API:
```bash
curl -s "${API_URL}/public/tickets/${TICKET_ID}" | jq '{id, state, title, notion_page_id}'
```
Expected output:
```json
{
"id": "...",
"state": "RESEARCH",
"title": "...",
"notion_page_id": "..."
}
```
**If `state` is not `RESEARCH`, go back to Step 4 and complete the missing calls.**
### Step 6: Output Summary and Handoff
Print a clear summary:
```markdown
## Ticket Intake Complete
**Source:** Notion | GitHub
**Title:** [Ticket title]
**ID:** abc12345
**Status:** In Progress (queued)
**Priority:** High
**Area:** UI
### Description
[Brief description or first 200 chars]
### Acceptance Criteria
- [ ] Criterion 1
- [ ] Criterion 2
### Links
- **Ticket:** [Original URL]
- **Slack:** [Slack thread content fetched via slackdump] (Notion only)
### Pipeline
- **API Ticket ID:** abc12345
- **State:** RESEARCH
- **Verified:** ✅ (via verify-intake.sh or public API)
```
**After printing the summary, immediately handoff** to continue the pipeline. Use the `handoff` tool with all necessary context (ticket ID, source, title, description, slack context if any):
> **Handoff goal:** "Continue pipeline for ticket {ID} ({title}). Ticket is in RESEARCH state. Load skill: `research-orchestrator` to begin research phase. Ticket data: source={source}, notion_page_id={pageId}, priority={priority}. {slack context summary if available}"
**Do NOT wait for human approval to proceed.** The intake phase is complete — handoff immediately.
## Error Handling
### Unsupported URL
```
❌ Unsupported ticket URL format.
Supported formats:
- Notion: https://notion.so/... or https://www.notion.so/...
- GitHub: https://github.com/{owner}/{repo}/issues/{number}
Received: [provided URL]
```
### Provider-Specific Errors
See individual provider files for source-specific error handling:
- `providers/notion.md` - Authentication, page not found
- `providers/github.md` - Auth, rate limits, issue not found
### Missing Properties
Continue with available data and note what's missing:
```
⚠️ Some properties unavailable:
- Priority: not found (using default: Medium)
- Area: not found
Proceeding with available data...
```
### API Call Failures
```
❌ Pipeline API call failed: {method} {endpoint}
Status: {status}
Error: {message}
Retrying once...
❌ Retry also failed. INTAKE IS INCOMPLETE.
The ticket was NOT registered in the pipeline.
Downstream skills will not work until this is fixed.
```
## Notes
- This skill focuses ONLY on intake — it does not do research
- Slack thread content is fetched automatically via the `slackdump` skill — no manual copy-paste needed
- ALL API calls (createTicket, transitionState, queueSync) are MANDATORY — never skip them
- The `queueSync` action must be `"update_status"`, NOT `"UPDATE_NOTION_STATUS"`
- Pipeline state is tracked via the API, not local files
- Working artifacts (research-report.md, plan.md) can be saved locally to `$PIPELINE_DIR/runs/{ticket-id}/`
- The `source` field in the ticket determines which research strategies to use
## API Client Reference
### Available Methods
| Method | Description |
| ----------------------------------------------------------- | ------------------------------------------------------------------- |
| `createTicket({ notion_page_id, title, source, metadata })` | Create a new ticket in the API |
| `getTicket(id)` | Retrieve a ticket by ID |
| `findByNotionId(notionPageId)` | Look up a ticket by its Notion page ID |
| `listTickets({ state, agent_id, limit, offset })` | List tickets with optional filters |
| `transitionState(id, state, reason)` | Move ticket to a new state (e.g., `'RESEARCH'`) |
| `setPRCreated(id, prUrl)` | Mark ticket as having a PR created |
| `queueSync(id, action, payload)` | Queue a sync action (`update_status`, `update_pr_url`, `mark_done`) |
| `registerBranch(id, branch, repo)` | Register working branch for automatic PR detection |
### Error Handling
```typescript
import { PipelineClient, PipelineAPIError } from '@pipeline/client';
try {
await client.createTicket({ ... });
} catch (error) {
if (error instanceof PipelineAPIError) {
console.error(`API Error ${error.status}: ${error.message}`);
}
throw error;
}
```

View File

@@ -1,194 +0,0 @@
# GitHub Provider - Ticket Intake
Provider-specific logic for ingesting tickets from GitHub Issues.
## URL Pattern
```
https://github.com/{owner}/{repo}/issues/{number}
https://www.github.com/{owner}/{repo}/issues/{number}
```
Extract: `owner`, `repo`, `issue_number` from URL.
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
- Access to the repository
## Fetch Issue Content
Use `gh` CLI to fetch issue details:
```bash
# Get issue details in JSON
gh issue view {number} --repo {owner}/{repo} --json title,body,state,labels,assignees,milestone,author,createdAt,comments,linkedPRs
# Get comments separately if needed
gh issue view {number} --repo {owner}/{repo} --comments
```
## Extract Ticket Data
Map GitHub issue fields to normalized ticket data (stored via API):
| GitHub Field | ticket.json Field | Notes |
| ------------ | ----------------- | -------------------------- |
| title | title | Direct mapping |
| body | description | Issue body/description |
| state | status | Map: open → "Not Started" |
| labels | labels | Array of label names |
| assignees | assignee | First assignee login |
| author | author | Issue author login |
| milestone | milestone | Milestone title if present |
| comments | comments | Array of comment objects |
| linkedPRs | linkedPRs | PRs linked to this issue |
### Priority Mapping
Infer priority from labels:
- `priority:critical`, `P0` → "Critical"
- `priority:high`, `P1` → "High"
- `priority:medium`, `P2` → "Medium"
- `priority:low`, `P3` → "Low"
- No priority label → "Medium" (default)
### Area Mapping
Infer area from labels:
- `area:ui`, `frontend`, `component:*` → "UI"
- `area:api`, `backend` → "API"
- `area:docs`, `documentation` → "Docs"
- `bug`, `fix` → "Bug"
- `enhancement`, `feature` → "Feature"
## Update Source
**For GitHub issues, update is optional but recommended.**
Add a comment to indicate work has started:
```bash
gh issue comment {number} --repo {owner}/{repo} --body "🤖 Pipeline started processing this issue."
```
Optionally assign to self:
```bash
gh issue edit {number} --repo {owner}/{repo} --add-assignee @me
```
Log any updates via the Pipeline API:
```typescript
await client.updateTicket(ticketId, {
metadata: {
...ticket.metadata,
githubWrites: [
...(ticket.metadata?.githubWrites || []),
{
action: 'comment',
issueNumber: 123,
at: new Date().toISOString(),
skill: 'ticket-intake',
success: true
}
]
}
})
```
## GitHub-Specific Ticket Fields
Store via API using `client.createTicket()`:
```json
{
"source": "github",
"githubOwner": "Comfy-Org",
"githubRepo": "ComfyUI_frontend",
"githubIssueNumber": 123,
"githubIssueUrl": "https://github.com/Comfy-Org/ComfyUI_frontend/issues/123",
"labels": ["bug", "area:ui", "priority:high"],
"linkedPRs": [456, 789],
"dosuComment": "..." // Extracted Dosu bot analysis if present
}
```
## Dosu Bot Detection
Many repositories use Dosu bot for automated issue analysis. Check comments for Dosu:
```bash
gh issue view {number} --repo {owner}/{repo} --comments | grep -A 100 "dosu"
```
Look for comments from:
- `dosu[bot]`
- `dosu-bot`
Extract Dosu analysis which typically includes:
- Root cause analysis
- Suggested files to modify
- Related issues/PRs
- Potential solutions
Store in ticket data via API:
```json
{
"dosuComment": {
"found": true,
"analysis": "...",
"suggestedFiles": ["src/file1.ts", "src/file2.ts"],
"relatedIssues": [100, 101]
}
}
```
## Extract Linked Issues/PRs
Parse issue body and comments for references:
- `#123` → Issue or PR reference
- `fixes #123`, `closes #123` → Linked issue
- `https://github.com/.../issues/123` → Full URL reference
Store in ticket data via API for research phase:
```json
{
"referencedIssues": [100, 101, 102],
"referencedPRs": [200, 201]
}
```
## Error Handling
### Authentication Error
```
⚠️ GitHub CLI not authenticated.
Run: gh auth login
```
### Issue Not Found
```
❌ GitHub issue not found or inaccessible.
- Check the URL is correct
- Ensure you have access to this repository
- Run: gh auth status
```
### Rate Limiting
```
⚠️ GitHub API rate limited.
Wait a few minutes and try again.
Check status: gh api rate_limit
```

View File

@@ -1,202 +0,0 @@
# Notion Provider - Ticket Intake
Provider-specific logic for ingesting tickets from Notion.
## URL Pattern
```
https://www.notion.so/workspace/Page-Title-abc123def456...
https://notion.so/Page-Title-abc123def456...
https://www.notion.so/abc123def456...
```
Page ID is the 32-character hex string (with or without hyphens).
## Prerequisites
- Notion MCP connected and authenticated
- If not setup: `claude mcp add --transport http notion https://mcp.notion.com/mcp`
- Authenticate via `/mcp` command if prompted
## Fetch Ticket Content
Use `Notion:notion-fetch` with the page URL or ID:
```
Fetch the full page content including all properties
```
## Extract Ticket Data
Extract these properties (names may vary):
| Property | Expected Name | Type |
| ------------- | ------------------------- | ------------ |
| Title | Name / Title | Title |
| Status | Status | Select |
| Assignee | Assignee / Assigned To | Person |
| Description | - | Page content |
| Slack Link | Slack Link / Slack Thread | URL |
| GitHub PR | GitHub PR / PR Link | URL |
| Priority | Priority | Select |
| Area | Area / Category | Select |
| Related Tasks | Related Tasks | Relation |
**If properties are missing**: Note what's unavailable and continue with available data.
## Update Source (REQUIRED)
**⚠️ DO NOT SKIP THIS STEP. This is a required action, not optional.**
**⚠️ Notion Write Safety rules apply (see `$PIPELINE_DIR/docs/notion-write-safety.md` for full reference):**
- **Whitelist**: Only `Status`, `GitHub PR`, and `Assignee` fields may be written
- **Valid transitions**: Not Started → In Progress, In Progress → In Review, In Review → Done
- **Logging**: Every write attempt MUST be logged with timestamp, field, value, previous value, skill name, and success status
Use `Notion:notion-update-page` to update the ticket:
1. **Status**: Set to "In Progress" (only valid from "Not Started")
2. **Assignee**: Assign to pipeline owner (Notion ID: `175d872b-594c-81d4-ba5a-0002911c5966`)
```json
{
"page_id": "{page_id_from_ticket}",
"command": "update_properties",
"properties": {
"Status": "In Progress",
"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"
}
}
```
**After the update succeeds**, log the write via the Pipeline API:
```typescript
await client.updateTicket(ticketId, {
metadata: {
...ticket.metadata,
notionWrites: [
...(ticket.metadata?.notionWrites || []),
{
field: 'Status',
value: 'In Progress',
previousValue: 'Not Started',
at: new Date().toISOString(),
skill: 'ticket-intake',
success: true
}
]
}
})
```
If update fails, log with `success: false` and continue.
## Notion-Specific Ticket Fields
Store via API using `client.createTicket()`:
```json
{
"source": "notion",
"notionPageId": "abc123def456...",
"slackLink": "https://slack.com/...",
"relatedTasks": ["page-id-1", "page-id-2"]
}
```
## Slack Thread Handling
If a Slack link exists, use the `slackdump` skill to fetch the thread content programmatically.
### Slack URL Conversion
Notion stores Slack links in `slackMessage://` format:
```
slackMessage://comfy-organization.slack.com/CHANNEL_ID/THREAD_TS/MESSAGE_TS
```
Convert to browser-clickable format:
```
https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS_NO_DOT
```
**Example:**
- Input: `slackMessage://comfy-organization.slack.com/C075ANWQ8KS/1766022478.450909/1764772881.854829`
- Output: `https://comfy-organization.slack.com/archives/C075ANWQ8KS/p1764772881854829`
(Remove the dot from the last timestamp and prefix with `p`)
### Fetching Thread Content
Load the `slackdump` skill and use the **export-thread** workflow:
```bash
# Export thread by URL
slackdump dump "https://comfy-organization.slack.com/archives/CHANNEL_ID/pMESSAGE_TS"
# Or by colon notation (channel_id:thread_ts)
slackdump dump CHANNEL_ID:THREAD_TS
```
Save the thread content to `$RUN_DIR/slack-context.md` and include it in the ticket metadata.
> **No manual action required.** The slackdump CLI handles authentication via stored credentials at `~/.cache/slackdump/comfy-organization.bin`.
## Database Reference: Comfy Tasks
The "Comfy Tasks" database has these properties (verify via `notion-search`):
- **Status values**: Not Started, In Progress, In Review, Done
- **Team assignment**: "Frontend Team" for unassigned tickets
- **Filtering note**: Team filtering in Notion may have quirks - handle gracefully
### Pipeline Owner Details
When assigning tickets, use these identifiers:
| Platform | Identifier |
| --------------- | -------------------------------------- |
| Notion User ID | `175d872b-594c-81d4-ba5a-0002911c5966` |
| Notion Name | Christian Byrne |
| Notion Email | cbyrne@comfy.org |
| Slack User ID | U087MJCDHHC |
| GitHub Username | christian-byrne |
**To update Assignee**, use the Notion User ID (not name):
```
properties: {"Assignee": "175d872b-594c-81d4-ba5a-0002911c5966"}
```
### Finding Active Tickets
To list your active tickets:
```
Use Notion:notion-search for "Comfy Tasks"
Filter by Assignee = current user OR Team = "Frontend Team"
```
## Error Handling
### Authentication Error
```
⚠️ Notion authentication required.
Run: claude mcp add --transport http notion https://mcp.notion.com/mcp
Then authenticate via /mcp command.
```
### Page Not Found
```
❌ Notion page not found or inaccessible.
- Check the URL is correct
- Ensure you have access to this page
- Try re-authenticating via /mcp
```

View File

@@ -1,81 +0,0 @@
# Ticket Schema
Common schema for normalized ticket data across all sources. This data is stored and retrieved via the Pipeline API, not local files.
## Ticket Data Schema
```json
{
// Required fields (all sources)
"id": "string", // Unique identifier (short form)
"url": "string", // Original URL
"source": "notion | github", // Source type
"title": "string", // Ticket title
"description": "string", // Full description/body
"fetchedAt": "ISO8601", // When ticket was fetched
// Common optional fields
"status": "string", // Current status
"assignee": "string", // Assigned user
"priority": "string", // Priority level
"area": "string", // Category/area
"labels": ["string"], // Tags/labels
"acceptanceCriteria": ["string"] // List of AC items
// Source-specific fields (see providers)
// Notion: notionPageId, slackLink, relatedTasks, notionWrites
// GitHub: githubOwner, githubRepo, githubIssueNumber, linkedPRs, dosuComment, referencedIssues
}
```
## Ticket State Schema (via API)
State is managed via the Pipeline API using `client.transitionState()`:
```json
{
"ticketId": "string",
"state": "intake | research | planning | implementation | pr_created | done | failed",
"stateChangedAt": "ISO8601",
// Timestamps tracked by API
"createdAt": "ISO8601",
"updatedAt": "ISO8601"
}
```
## Priority Normalization
All sources should normalize to these values:
| Normalized | Description |
| ---------- | ------------------------- |
| Critical | Production down, security |
| High | Blocking work, urgent |
| Medium | Normal priority (default) |
| Low | Nice to have, backlog |
## Status Normalization
Pipeline tracks these statuses internally:
| Status | Description |
| -------------- | ---------------------------- |
| research | Gathering context |
| planning | Creating implementation plan |
| implementation | Writing code |
| review | Code review in progress |
| qa | Quality assurance |
| done | PR merged or completed |
## ID Generation
IDs are generated by the API when creating tickets. For reference:
- **Notion**: First 8 characters of page ID
- **GitHub**: `gh-{owner}-{repo}-{issue_number}` (sanitized)
Examples:
- Notion: `abc12345`
- GitHub: `gh-comfy-org-frontend-123`

View File

@@ -1,200 +0,0 @@
---
name: writing-playwright-tests
description: 'Writes Playwright e2e tests for ComfyUI_frontend. Use when creating, modifying, or debugging browser tests. Triggers on: playwright, e2e test, browser test, spec file.'
---
# Writing Playwright Tests for ComfyUI_frontend
## Golden Rules
1. **ALWAYS look at existing tests first.** Search `browser_tests/tests/` for similar patterns before writing new tests.
2. **ALWAYS read the fixture code.** The APIs are in `browser_tests/fixtures/` - read them directly instead of guessing.
3. **Use premade JSON workflow assets** instead of building workflows programmatically.
- Assets live in `browser_tests/assets/`
- Load with `await comfyPage.workflow.loadWorkflow('feature/my_workflow')`
- Create new assets by starting with `browser_tests/assets/default.json` and manually editing the JSON to match your desired graph state
## Vue Nodes vs LiteGraph: Decision Guide
Choose based on **what you're testing**, not personal preference:
| Testing... | Use | Why |
| ---------------------------------------------- | -------------------------------- | ---------------------------------------- |
| Vue-rendered node UI, DOM widgets, CSS states | `comfyPage.vueNodes.*` | Nodes are DOM elements, use locators |
| Canvas interactions, connections, legacy nodes | `comfyPage.nodeOps.*` | Canvas-based, use coordinates/references |
| Both in same test | Pick primary, minimize switching | Avoid confusion |
**Vue Nodes requires explicit opt-in:**
```typescript
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
```
**Vue Node state uses CSS classes:**
```typescript
const BYPASS_CLASS = /before:bg-bypass\/60/
await expect(node).toHaveClass(BYPASS_CLASS)
```
## Common Issues
These are frequent causes of flaky tests - check them first, but investigate if they don't apply:
| Symptom | Common Cause | Typical Fix |
| ---------------------------------- | ------------------------- | -------------------------------------------------------------------------------------- |
| Test passes locally, fails in CI | Missing nextFrame() | Add `await comfyPage.nextFrame()` after canvas ops (not needed after `loadWorkflow()`) |
| Keyboard shortcuts don't work | Missing focus | Add `await comfyPage.canvas.click()` first |
| Double-click doesn't trigger | Timing too fast | Add `{ delay: 5 }` option |
| Elements end up in wrong position | Drag animation incomplete | Use `{ steps: 10 }` not `{ steps: 1 }` |
| Widget value wrong after drag-drop | Upload incomplete | Add `{ waitForUpload: true }` |
| Test fails when run with others | Test pollution | Add `afterEach` with `resetView()` |
| Local screenshots don't match CI | Platform differences | Screenshots are Linux-only, use PR label |
## Test Tags
Add appropriate tags to every test:
| Tag | When to Use |
| ------------- | ----------------------------------------- |
| `@smoke` | Quick essential tests |
| `@slow` | Tests > 10 seconds |
| `@screenshot` | Visual regression tests |
| `@canvas` | Canvas interactions |
| `@node` | Node-related |
| `@widget` | Widget-related |
| `@mobile` | Mobile viewport (runs on Pixel 5 project) |
| `@2x` | HiDPI tests (runs on 2x scale project) |
```typescript
test.describe('Feature', { tag: ['@screenshot', '@canvas'] }, () => {
```
## Retry Patterns
**Never use `waitForTimeout`** - it's always wrong.
| Pattern | Use Case |
| ------------------------ | ---------------------------------------------------- |
| Auto-retrying assertions | `toBeVisible()`, `toHaveText()`, etc. (prefer these) |
| `expect.poll()` | Single value polling |
| `expect().toPass()` | Multiple assertions that must all pass |
```typescript
// Prefer auto-retrying assertions when possible
await expect(node).toBeVisible()
// Single value polling
await expect.poll(() => widget.getValue(), { timeout: 2000 }).toBe(100)
// Multiple conditions
await expect(async () => {
expect(await node1.getValue()).toBe('foo')
expect(await node2.getValue()).toBe('bar')
}).toPass({ timeout: 2000 })
```
## Screenshot Baselines
- **Screenshots are Linux-only.** Don't commit local screenshots.
- **To update baselines:** Add PR label `New Browser Test Expectations`
- **Mask dynamic content:**
```typescript
await expect(comfyPage.canvas).toHaveScreenshot('page.png', {
mask: [page.locator('.timestamp')]
})
```
## CI Debugging
1. Download artifacts from failed CI run
2. Extract and view trace: `npx playwright show-trace trace.zip`
3. CI deploys HTML report to Cloudflare Pages (link in PR comment)
4. Reproduce CI: `CI=true pnpm test:browser`
5. Local runs: `pnpm test:browser:local`
## Anti-Patterns
Avoid these common mistakes:
1. **Arbitrary waits** - Use retrying assertions instead
```typescript
// ❌ await page.waitForTimeout(500)
// ✅ await expect(element).toBeVisible()
```
2. **Implementation-tied selectors** - Use test IDs or semantic selectors
```typescript
// ❌ page.locator('div.container > button.btn-primary')
// ✅ page.getByTestId('submit-button')
```
3. **Missing nextFrame after canvas ops** - Canvas needs sync time
```typescript
await node.drag({ x: 50, y: 50 })
await comfyPage.nextFrame() // Required
```
4. **Shared state between tests** - Tests must be independent
```typescript
// ❌ let sharedData // Outside test
// ✅ Define state inside each test
```
## Quick Start Template
```typescript
// Path depends on test file location - adjust '../' segments accordingly
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('FeatureName', { tag: ['@canvas'] }, () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('should do something', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('myWorkflow')
const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
// ... test logic
await expect(comfyPage.canvas).toHaveScreenshot('expected.png')
})
})
```
## Finding Patterns
```bash
# Find similar tests
grep -r "KSampler" browser_tests/tests/
# Find usage of a fixture method
grep -r "loadWorkflow" browser_tests/tests/
# Find tests with specific tag
grep -r '@screenshot' browser_tests/tests/
```
## Key Files to Read
| Purpose | Path |
| ----------------- | ------------------------------------------ |
| Main fixture | `browser_tests/fixtures/ComfyPage.ts` |
| Helper classes | `browser_tests/fixtures/helpers/` |
| Component objects | `browser_tests/fixtures/components/` |
| Test selectors | `browser_tests/fixtures/selectors.ts` |
| Vue Node helpers | `browser_tests/fixtures/VueNodeHelpers.ts` |
| Test assets | `browser_tests/assets/` |
| Existing tests | `browser_tests/tests/` |
**Read the fixture code directly** - it's the source of truth for available methods.

View File

@@ -1,143 +0,0 @@
---
name: writing-storybook-stories
description: 'Write or update Storybook stories for Vue components in ComfyUI_frontend. Use when adding, modifying, reviewing, or debugging `.stories.ts` files, Storybook docs, component demos, or visual catalog entries in `src/` or `apps/desktop-ui/`.'
---
# Write Storybook Stories for ComfyUI_frontend
## Workflow
1. !!!!IMPORTANT Confirm the worktree is on a `feat/*` or `fix/*` branch. Base PRs on the local `main`, not a fork branch.
2. Read the component source first. Understand props, emits, slots, exposed methods, and any supporting types or composables.
3. Read nearby stories before writing anything.
- Search stories: `rg --files src apps | rg '\.stories\.ts$'`
- Inspect title patterns: `rg -n "title:\\s*'" src apps --glob '*.stories.ts'`
4. If a Figma link is provided, list the states you need to cover before writing stories.
5. Co-locate the story file with the component: `ComponentName.stories.ts`.
6. Add each variation on separate stories, except hover state. this should be automatically applied by the implementation and not require a separate story.
7. Run Storybook and validation checks before handing off.
## Match Local Conventions
- Copy the closest neighboring story instead of forcing one universal template.
- Most repo stories use `@storybook/vue3-vite`. Some stories under `apps/desktop-ui` still use `@storybook/vue3`; keep the local convention for that area.
- Add `tags: ['autodocs']` unless the surrounding stories in that area intentionally omit it.
- Use `ComponentPropsAndSlots<typeof Component>` when it helps with prop and slot typing.
- Keep `render` functions stateful when needed. Use `ref()`, `computed()`, and `toRefs(args)` instead of mutating Storybook args directly.
- Use `args.default` or other slot-shaped args when the component content is provided through slots.
- Use `ComponentExposed` only when a component's exposed API breaks the normal Storybook typing.
- Add decorators for realistic width or background context when the component needs it.
## Title Patterns
Do not invent titles from scratch when a close sibling story already exists. Match the nearest domain pattern.
| Component area | Typical title pattern |
| ------------------------------------------------------- | ------------------------------------ |
| `src/components/ui/button/Button.vue` | `Components/Button/Button` |
| `src/components/ui/input/Input.vue` | `Components/Input` |
| `src/components/ui/search-input/SearchInput.vue` | `Components/Input/SearchInput` |
| `src/components/common/SearchBox.vue` | `Components/Input/SearchBox` |
| `src/renderer/extensions/vueNodes/widgets/components/*` | `Widgets/<WidgetName>` |
| `src/platform/assets/components/*` | `Platform/Assets/<ComponentName>` |
| `apps/desktop-ui/src/components/*` | `Desktop/Components/<ComponentName>` |
| `apps/desktop-ui/src/views/*` | `Desktop/Views/<ViewName>` |
If multiple patterns seem plausible, follow the closest sibling story in the same folder tree.
## Common Story Shapes
### Stateful input or `v-model`
```typescript
export const Default: Story = {
render: (args) => ({
components: { MyComponent },
setup() {
const { disabled, size } = toRefs(args)
const value = ref('Hello world')
return { value, disabled, size }
},
template:
'<MyComponent v-model="value" :disabled="disabled" :size="size" />'
})
}
```
### Slot-driven content
```typescript
const meta: Meta<ComponentPropsAndSlots<typeof Button>> = {
argTypes: {
default: { control: 'text' }
},
args: {
default: 'Button'
}
}
export const SingleButton: Story = {
render: (args) => ({
components: { Button },
setup() {
return { args }
},
template: '<Button v-bind="args">{{ args.default }}</Button>'
})
}
```
### Variants or edge cases grid
```typescript
export const AllVariants: Story = {
render: () => ({
components: { MyComponent },
template: `
<div class="grid gap-4 sm:grid-cols-2">
<MyComponent />
<MyComponent disabled />
<MyComponent loading />
<MyComponent invalid />
</div>
`
})
}
```
## Figma Mapping
- Extract the named states from the design first.
- Prefer explicit prop-driven stories such as `Disabled`, `Loading`, `Invalid`, `WithPlaceholder`, `AllSizes`, or `EdgeCases`.
- Add an aggregate story such as `AllVariants`, `AllSizes`, or `EdgeCases` when side-by-side comparison is useful.
- Use pseudo-state parameters only if the addon is already configured in this repo.
- If a Figma state cannot be represented exactly, capture the closest prop-driven version and explain the gap in the story docs.
## Component-Specific Notes
- Widget components often need a minimal `SimplifiedWidget` object. Build it in `setup()` and use `computed()` when `args` change `widget.options`.
- Input and search components often need a width-constrained wrapper so they render at realistic sizes.
- Asset and platform cards often need background decorators such as `bg-base-background` and fixed-width containers.
- Desktop installer stories may need custom `backgrounds` parameters and may intentionally keep the older Storybook import style used by neighboring files.
- Use semantic tokens such as `bg-base-background` and `bg-node-component-surface` instead of `dark:` variants or hardcoded theme assumptions.
## Checklist
- [ ] Read the component source and any supporting types or composables
- [ ] Match the nearest local title pattern and story style
- [ ] Include a baseline story; name it `Default` only when that matches nearby conventions
- [ ] Add focused stories for meaningful states
- [ ] Add `tags: ['autodocs']`
- [ ] Keep the story co-located with the component
- [ ] Run `pnpm storybook`
- [ ] Run `pnpm typecheck`
- [ ] Run `pnpm lint`
## Avoid
- Do not guess props, emits, slots, or exposed methods.
- Do not force one generic title convention across the repo.
- Do not mutate Storybook args directly for `v-model` components.
- Do not introduce `dark:` Tailwind variants in story wrappers.
- Do not create barrel files.
- Do not assume every story needs `layout: 'centered'` or a `Default` export; follow the nearest existing pattern.

View File

@@ -1,4 +0,0 @@
interface:
display_name: 'ComfyUI Storybook Stories'
short_description: 'Write Vue Storybook stories for ComfyUI'
default_prompt: 'Use $writing-storybook-stories to add or update a Storybook story for this ComfyUI_frontend component.'

View File

@@ -1,25 +0,0 @@
issue_enrichment:
auto_enrich:
enabled: true
reviews:
high_level_summary: false
auto_review:
drafts: true
ignore_title_keywords:
- '[release]'
- '[backport'
ignore_usernames:
- comfy-pr-bot
- github-actions
- github-actions[bot]
pre_merge_checks:
custom_checks:
- name: End-to-end regression coverage for fixes
mode: warning
instructions: |
Pass if at least one of the following is true:
1. Neither the PR title nor any commit subject in the PR uses bug-fix language such as `fix`, `fixed`, `fixes`, `fixing`, `bugfix`, or `hotfix`.
2. The PR changes at least one file under `browser_tests/`.
3. The PR description includes a concrete, non-placeholder explanation of why an end-to-end regression test was not added.
Fail otherwise. When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical.

View File

@@ -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

16
.gitattributes vendored
View File

@@ -1,5 +1,17 @@
# Force all text files to use LF line endings
* text=auto eol=lf
# Default
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.cjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.snap text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

8
.github/AGENTS.md vendored
View File

@@ -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.

View File

@@ -10,7 +10,10 @@ body:
options:
- label: I am running the latest version of ComfyUI
required: true
- label: I have custom nodes enabled
- label: I have searched existing issues to make sure this isn't a duplicate
required: true
- label: I have tested with all custom nodes disabled ([see how](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled))
required: true
- type: textarea
id: description

View File

@@ -4,6 +4,13 @@ labels: []
type: Feature
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the problem you're experiencing, and that it's not addressed in a recent build/commit.
options:
- label: I have searched the existing issues and checked the recent builds/commits
required: true
- type: markdown
attributes:
value: |

View File

@@ -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 }}

View File

@@ -19,7 +19,7 @@ runs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'

View File

@@ -1,3 +0,0 @@
{
"posthog-js@*": { "licenses": "Apache-2.0" }
}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Update electron types

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies

View File

@@ -27,7 +27,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies

View File

@@ -1,100 +0,0 @@
name: 'CI: Dist Telemetry Scan'
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm build
env:
DISTRIBUTION: localhost
- name: Scan dist for GTM telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Google Tag Manager references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e 'Google Tag Manager' \
-e '(?i)\bgtm\.js\b' \
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
dist; then
echo '❌ ERROR: Google Tag Manager references found in dist assets!'
echo 'GTM must be properly tree-shaken from OSS builds.'
exit 1
fi
echo '✅ No GTM references found'
- name: Scan dist for Mixpanel telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Mixpanel references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)mixpanel\.init' \
-e '(?i)mixpanel\.identify' \
-e 'MixpanelTelemetryProvider' \
-e 'mp\.comfy\.org' \
-e 'mixpanel-browser' \
-e '(?i)mixpanel\.track\(' \
dist; then
echo '❌ ERROR: Mixpanel references found in dist assets!'
echo 'Mixpanel must be properly tree-shaken from OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use the TelemetryProvider pattern (see src/platform/telemetry/)'
echo '2. Call telemetry via useTelemetry() hook'
echo '3. Use conditional dynamic imports behind isCloud checks'
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'

View File

@@ -21,19 +21,10 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
token: ${{ !github.event.pull_request.head.repo.fork && secrets.PR_GH_TOKEN || github.token }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Detect browser_tests changes
id: changed-paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
filters: |
browser_tests:
- 'browser_tests/**'
- name: Run ESLint with auto-fix
run: pnpm lint:fix
@@ -61,22 +52,6 @@ jobs:
git commit -m "[automated] Apply ESLint and Oxfmt fixes"
git push
- name: Fail for fork PRs with unfixed lint/format issues
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Linting/formatting issues found. Since this PR is from a fork, auto-fix cannot be applied automatically."
echo ""
echo "Please run these commands locally and push the changes:"
echo " pnpm lint:fix"
echo " pnpm stylelint:fix"
echo " pnpm format"
echo ""
echo "Or set up pre-commit hooks to automatically format on every commit:"
echo " pnpm prepare"
echo ""
echo "See CONTRIBUTING.md for more details."
exit 1
- name: Final validation
run: |
pnpm lint
@@ -84,10 +59,6 @@ jobs:
pnpm format:check
pnpm knip
- name: Typecheck browser tests
if: steps.changed-paths.outputs.browser_tests == 'true'
run: pnpm typecheck:browser
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
@@ -100,3 +71,16 @@ jobs:
repo: context.repo.repo,
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Oxfmt formatting'
})
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
})

View File

@@ -1,119 +0,0 @@
name: 'CI: OSS Assets Validation'
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
push:
branches: [main, dev*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
validate-fonts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm build
env:
DISTRIBUTION: localhost
- name: Check for proprietary fonts in dist
run: |
set -euo pipefail
echo '🔍 Checking dist for proprietary ABCROM fonts...'
if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then
echo '❌ ERROR: dist/ directory missing or empty!'
exit 1
fi
# Check for ABCROM font files
if find dist/ -type f -iname '*abcrom*' \
\( -name '*.woff' -o -name '*.woff2' -o -name '*.ttf' -o -name '*.otf' \) \
-print -quit | grep -q .; then
echo ''
echo '❌ ERROR: Found proprietary ABCROM font files in dist!'
echo ''
find dist/ -type f -iname '*abcrom*' \
\( -name '*.woff' -o -name '*.woff2' -o -name '*.ttf' -o -name '*.otf' \)
echo ''
echo 'ABCROM fonts are proprietary and should not ship to OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use conditional font loading based on isCloud'
echo '2. Ensure fonts are dynamically imported, not bundled'
echo '3. Check vite config for font handling'
exit 1
fi
echo '✅ No proprietary fonts found in dist'
validate-licenses:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate production dependency licenses
run: |
set -euo pipefail
echo '🔍 Checking production dependency licenses...'
# Use license-checker-rseidelsohn (actively maintained fork, handles monorepos)
# Exclude internal @comfyorg packages from license check
# Run in if condition to capture exit code
if npx license-checker-rseidelsohn@4 \
--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!'
else
echo ''
echo '❌ ERROR: Found dependencies with non-approved licenses!'
echo ''
echo 'To fix this:'
echo '1. Check the license of the problematic package'
echo '2. Find an alternative package with an approved license'
echo '3. If the license is safe and OSI-approved, add it to the --onlyAllow list'
echo ''
echo 'For more info on OSI-approved licenses:'
echo 'https://opensource.org/licenses'
exit 1
fi

View File

@@ -1,70 +0,0 @@
name: 'CI: Performance Report'
on:
push:
branches: [main, core/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
concurrency:
group: perf-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
perf-tests:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
- name: Run performance tests
id: perf
continue-on-error: true
run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=3
- name: Upload perf metrics
if: always()
uses: actions/upload-artifact@v6
with:
name: perf-metrics
path: test-results/perf-metrics.json
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
- name: Upload PR metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v6
with:
name: perf-meta
path: temp/perf-meta/

View File

@@ -6,6 +6,9 @@ on:
workflows: ['CI: Tests E2E']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
@@ -60,7 +63,8 @@ jobs:
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting"
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
- name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'

View File

@@ -4,10 +4,8 @@ name: 'CI: Tests E2E'
on:
push:
branches: [main, master, core/*, desktop/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
workflow_dispatch:
concurrency:
@@ -39,7 +37,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.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -87,7 +85,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.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -184,6 +182,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Get start time
id: start-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Post starting comment
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -192,7 +194,8 @@ jobs:
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting"
"starting" \
"${{ steps.start-time.outputs.time }}"
# Deploy and comment for non-forked PRs only
deploy-and-comment:

View File

@@ -6,6 +6,9 @@ on:
workflows: ['CI: Tests Storybook']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
@@ -60,7 +63,8 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting"
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
- name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'

View File

@@ -4,8 +4,6 @@ name: 'CI: Tests Storybook'
on:
workflow_dispatch: # Allow manual triggering
pull_request:
push:
branches: [main]
jobs:
# Post starting comment for non-forked PRs
@@ -26,7 +24,8 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting"
"starting" \
"$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
# Build Storybook for all PRs (free Cloudflare deployment)
storybook-build:
@@ -140,29 +139,6 @@ jobs:
"${{ github.head_ref }}" \
"completed"
# Deploy Storybook to production URL on main branch push
deploy-production:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook
run: pnpm build-storybook
- name: Deploy to Cloudflare Pages (production)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
npx wrangler@^4.0.0 pages deploy storybook-static \
--project-name=comfy-storybook \
--branch=main
# Update comment with Chromatic URLs for version-bump branches
update-comment-with-chromatic:
needs: [chromatic-deployment, deploy-and-comment]

View File

@@ -4,10 +4,8 @@ name: 'CI: Tests Unit'
on:
push:
branches: [main, master, dev*, core/*, desktop/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -1,81 +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, synchronize]
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 for preview labels.
# - labeled: fires when a label is added; check the added label name.
# - synchronize: fires on push; check existing labels on the PR.
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(github.event_name != 'pull_request' ||
(github.event.action == 'labeled' &&
contains(fromJSON('["preview","preview-cpu","preview-gpu"]'), github.event.label.name)) ||
(github.event.action == 'synchronize' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||
contains(github.event.pull_request.labels.*.name, 'preview-gpu'))))
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 }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ACTION: ${{ github.event.action }}
LABEL_NAME: ${{ github.event.label.name }}
PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
run: |
if [ "${EVENT_NAME}" = "pull_request" ]; then
REF="${PR_HEAD_SHA}"
BRANCH="${PR_HEAD_REF}"
# Derive variant from all PR labels (default to cpu for frontend-only previews)
VARIANT="cpu"
echo "${PR_LABELS}" | grep -q '"preview-gpu"' && VARIANT="gpu"
else
REF="${GITHUB_SHA}"
BRANCH="${GITHUB_REF_NAME}"
PR_NUMBER=""
VARIANT=""
fi
payload="$(jq -nc \
--arg ref "${REF}" \
--arg branch "${BRANCH}" \
--arg pr_number "${PR_NUMBER}" \
--arg variant "${VARIANT}" \
'{ref: $ref, branch: $branch, pr_number: $pr_number, variant: $variant}')"
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 }}

View File

@@ -1,39 +0,0 @@
---
# Dispatches a frontend-preview-cleanup event to the cloud repo when a
# frontend PR with a preview label is closed or has its preview label
# removed. The cloud repo handles the actual environment teardown.
#
# 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 Preview Cleanup Dispatch
on:
pull_request:
types: [closed, unlabeled]
permissions: {}
jobs:
dispatch:
# Only dispatch when:
# - PR closed AND had a preview label
# - Preview label specifically removed
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
((github.event.action == 'closed' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||
contains(github.event.pull_request.labels.*.name, 'preview-gpu'))) ||
(github.event.action == 'unlabeled' &&
contains(fromJSON('["preview","preview-cpu","preview-gpu"]'), github.event.label.name)))
runs-on: ubuntu-latest
steps:
- 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-preview-cleanup
client-payload: >-
{"pr_number": "${{ github.event.pull_request.number }}"}

View File

@@ -17,8 +17,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.PR_GH_TOKEN }}
# Setup playwright environment
- name: Setup ComfyUI Frontend

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools

View File

@@ -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-file: '.nvmrc'
- 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 }}

View File

@@ -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 -->'

View File

@@ -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.10
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -109,7 +109,7 @@ jobs:
# Run sharded tests with snapshot updates (browsers pre-installed in container)
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots --grep @screenshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
- name: Stage changed snapshot files
@@ -173,7 +173,6 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
token: ${{ secrets.PR_GH_TOKEN }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
- name: Read desktop-ui version
id: get_version

View File

@@ -91,7 +91,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: 'frontend/.nvmrc'
node-version: lts/*
- name: Install dependencies
working-directory: frontend

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
- name: Check version bump type
id: check_version

View File

@@ -26,7 +26,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Get current version
@@ -50,16 +50,9 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
run: |
pnpm install --frozen-lockfile
# Desktop-specific release artifact with desktop distribution flags.
DISTRIBUTION=desktop pnpm build
pnpm zipdist ./dist ./dist-desktop.zip
# Default release artifact for core/PyPI.
NX_SKIP_NX_CACHE=true pnpm build
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v6
@@ -68,7 +61,6 @@ jobs:
path: |
dist/
dist.zip
dist-desktop.zip
draft_release:
needs: build
@@ -86,7 +78,6 @@ jobs:
with:
files: |
dist.zip
dist-desktop.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: >-

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -22,7 +22,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Get current version

View File

@@ -149,7 +149,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
- name: Bump version
id: bump-version

View File

@@ -58,7 +58,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
cache: 'pnpm'
- name: Bump desktop-ui version

View File

@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools

4
.gitignore vendored
View File

@@ -26,7 +26,6 @@ dist-ssr
.claude/*.local.json
.claude/*.local.md
.claude/*.local.txt
.claude/worktrees
CLAUDE.local.md
# Editor directories and files
@@ -65,7 +64,6 @@ browser_tests/local/
dist.zip
/temp/
/tmp/
# Generated JSON Schemas
/schemas/
@@ -98,5 +96,3 @@ vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
.amp

View File

@@ -1 +0,0 @@
.claude/worktrees

View File

@@ -35,7 +35,7 @@
}
],
"no-control-regex": "off",
"no-eval": "error",
"no-eval": "off",
"no-redeclare": "error",
"no-restricted-imports": [
"error",
@@ -60,6 +60,11 @@
{
"name": "primevue/sidebar",
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
},
{
"name": "@/i18n--to-enable",
"importNames": ["st", "t", "te", "d"],
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
}
]
}
@@ -96,7 +101,6 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"typescript/no-explicit-any": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},
@@ -106,17 +110,6 @@
"rules": {
"no-console": "allow"
}
},
{
"files": ["browser_tests/**/*.ts"],
"rules": {
"typescript/no-explicit-any": "error",
"no-async-promise-executor": "error",
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error"
}
}
]
}

View File

@@ -98,10 +98,12 @@ const config: StorybookConfig = {
},
build: {
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
treeshake: false,
output: {
keepNames: true,
strictExecutionOrder: true
keepNames: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings

View File

@@ -58,7 +58,7 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
document.documentElement.classList.remove('dark-theme')
document.body.classList.remove('dark-theme')
}
document.body.classList.add('font-inter')
document.body.classList.add('[&_*]:!font-inter')
return Story(context.args, context)
}
@@ -90,6 +90,7 @@ const preview: Preview = {
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' }
],
showName: true,
dynamicTitle: true
}
}

View File

@@ -40,27 +40,26 @@
"block-no-empty": true,
"no-descending-specificity": null,
"no-duplicate-at-import-rules": true,
"at-rule-disallowed-list": ["apply"],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"layer",
"config",
"theme",
"reference",
"plugin",
"custom-variant",
"utility",
"source"
"utility"
]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme", "v-bind", "from-folder", "from-json"]
"ignoreFunctions": ["theme", "v-bind"]
}
]
},

View File

@@ -21,7 +21,7 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
- i18n: `src/i18n.ts`,
- Entry Point: `src/main.ts`.
- Tests:
- unit/component in `src/**/*.test.ts`
- unit/component in `tests-ui/` and `src/**/*.test.ts`
- E2E (Playwright) in `browser_tests/**/*.spec.ts`
- Public assets: `public/`
- Build output: `dist/`
@@ -37,10 +37,6 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
The project uses **Nx** for build orchestration and task management
## Package Manager
This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g., `pnpm test:unit`, `pnpm lint`). To run arbitrary packages not in scripts, use `pnpx` or `pnpm dlx` — never `npx`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.
@@ -48,7 +44,7 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
- `pnpm build`: Type-check then production build to `dist/`
- `pnpm preview`: Preview the production build locally
- `pnpm test:unit`: Run Vitest unit tests
- `pnpm test:browser:local`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm typecheck`: Vue TSC type checking
@@ -268,7 +264,7 @@ A particular type of complexity is over-engineering, where developers have made
## Repository Navigation
- Check README files in key folders (browser_tests, composables, etc.)
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)
- Prefer running single tests for performance
- Use --help for unfamiliar CLI tools

View File

@@ -2,57 +2,57 @@
* @Comfy-org/comfy_frontend_devs
# Desktop/Electron
/apps/desktop-ui/ @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/electronDownloadStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/extensions/core/electronAdapter.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/vite.electron.config.mts @benceruleanlu @Comfy-org/comfy_frontend_devs
/apps/desktop-ui/ @benceruleanlu
/src/stores/electronDownloadStore.ts @benceruleanlu
/src/extensions/core/electronAdapter.ts @benceruleanlu
/vite.electron.config.mts @benceruleanlu
# Common UI Components
/src/components/chip/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/card/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/button/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/input/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/chip/ @viva-jinyi
/src/components/card/ @viva-jinyi
/src/components/button/ @viva-jinyi
/src/components/input/ @viva-jinyi
# Topbar
/src/components/topbar/ @pythongosssss @Comfy-org/comfy_frontend_devs
/src/components/topbar/ @pythongosssss
# Thumbnail
/src/renderer/core/thumbnail/ @pythongosssss @Comfy-org/comfy_frontend_devs
/src/renderer/core/thumbnail/ @pythongosssss
# Legacy UI
/scripts/ui/ @pythongosssss @Comfy-org/comfy_frontend_devs
/scripts/ui/ @pythongosssss
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/renderer/core/canvas/links/ @benceruleanlu
# Partner Nodes
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88 @Comfy-org/comfy_frontend_devs
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/services/nodeHelpService.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery @Comfy-org/comfy_frontend_devs
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery @Comfy-org/comfy_frontend_devs
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-org/comfy_frontend_devs
/src/components/load3d/ @jtydhr88 @Comfy-org/comfy_frontend_devs
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata @Comfy-org/comfy_frontend_devs
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (see `.nvmrc` for the required version) and pnpm
- Node.js (v24) and pnpm
- Git for version control
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
@@ -87,10 +87,6 @@ navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to
> ⚠️ IMPORTANT:
> The dev server will NOT load JavaScript extensions from custom nodes. Only core extensions (built into the frontend) will be loaded. This is because the shim system that allows custom node JavaScript to import frontend modules only works in production builds. Python custom nodes still function normally. See [Extension Development Guide](docs/extensions/development.md) for details and workarounds. And See [Extension Overview](docs/extensions/README.md) for extensions overview.
## Troubleshooting
If you run into issues during development (e.g. `pnpm dev` hanging, TypeScript errors after pulling, lock file conflicts), see [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for common fixes.
## Development Workflow
### Architecture Decision Records
@@ -205,7 +201,7 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Tailwind CSS icon classes (`icon-[comfy--template]`) are provided by `@iconify/tailwind4`, configured in `packages/design-system/src/css/style.css`. Custom icons are stored in `packages/design-system/src/icons/` and loaded via `from-folder` at build time.
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).

View File

@@ -1,368 +0,0 @@
# Troubleshooting Guide
This guide helps you resolve common issues when developing ComfyUI Frontend.
## Quick Diagnostic Flowchart
```mermaid
flowchart TD
A[Having Issues?] --> B{What's the problem?}
B -->|Dev server stuck| C[nx serve hangs]
B -->|Build errors| D[Check build issues]
B -->|Lint errors| Q[Check linting issues]
B -->|Dependency issues| E[Package problems]
B -->|Other| F[See FAQ below]
Q --> R{oxlint or ESLint?}
R -->|oxlint| S[Check .oxlintrc.json<br/>and run pnpm lint:fix]
R -->|ESLint| T[Check eslint.config.ts<br/>and run pnpm lint:fix]
S --> L
T --> L
C --> G{Tried quick fixes?}
G -->|No| H[Run: pnpm i]
G -->|Still stuck| I[Run: pnpm clean]
I --> J{Still stuck?}
J -->|Yes| K[Nuclear option:<br/>pnpm dlx rimraf node_modules<br/>&& pnpm i]
J -->|No| L[Fixed!]
H --> L
D --> M[Run: pnpm build]
M --> N{Build succeeds?}
N -->|No| O[Check error messages<br/>in FAQ]
N -->|Yes| L
E --> H
F --> P[Search FAQ or<br/>ask in Discord]
```
## Frequently Asked Questions
### Development Server Issues
#### Q: `pnpm dev` or `nx serve` gets stuck and won't start
**Symptoms:**
- Command hangs on "nx serve"
- Dev server doesn't respond
- Terminal appears frozen
**Solutions (try in order):**
1. **First attempt - Reinstall dependencies:**
```bash
pnpm i
```
2. **Second attempt - Clean build cache:**
```bash
pnpm clean
```
3. **Last resort - Full node_modules reset:**
```bash
pnpm dlx rimraf node_modules && pnpm i
```
**Why this happens:**
- Corrupted dependency cache
- Outdated lock files after branch switching
- Incomplete previous installations
- NX cache corruption
---
#### Q: Port conflicts - "Address already in use"
**Symptoms:**
- Error: `EADDRINUSE` or "port already in use"
- Dev server fails to start
**Solutions:**
1. **Find and kill the process using the port:**
```bash
# On Linux/Mac
lsof -ti:5173 | xargs kill -9
# On Windows
netstat -ano | findstr :5173
taskkill /PID <PID> /F
```
2. **Use a different port** by adding a `port` option to the `server` block in `vite.config.mts`:
```ts
server: {
port: 3000,
// ...existing config
}
```
---
### Build and Type Issues
#### Q: TypeScript errors after pulling latest changes
**Symptoms:**
- Type errors in files you didn't modify
- "Cannot find module" errors
**Solutions:**
1. **Rebuild TypeScript references:**
```bash
pnpm build
```
2. **Clean and reinstall:**
```bash
pnpm clean && pnpm i
```
3. **Restart your IDE's TypeScript server**
- VS Code: `Cmd/Ctrl + Shift + P` → "TypeScript: Restart TS Server"
---
#### Q: "Workspace not found" or monorepo errors
**Symptoms:**
- pnpm can't find workspace packages
- Import errors between packages
**Solutions:**
1. **Verify you're in the project root:**
```bash
pwd # Should be in ComfyUI_frontend/
```
2. **Rebuild workspace:**
```bash
pnpm install
pnpm build
```
---
### Linting Issues (oxlint)
#### Q: `eslint-disable` comment isn't suppressing an oxlint rule
**Symptoms:**
- `// eslint-disable-next-line rule-name` has no effect
- Lint error persists despite the disable comment
**Solution:**
oxlint has its own disable syntax. Use `oxlint-disable` instead:
```ts
// oxlint-disable-next-line no-console
console.log('debug')
```
Check whether the rule is enforced by oxlint (in `.oxlintrc.json`) or ESLint (in `eslint.config.ts`) to pick the right disable comment.
---
#### Q: New lint errors after pulling/upgrading oxlint
**Symptoms:**
- Lint errors in files you didn't change
- Rules you haven't seen before (e.g. `no-immediate-mutation`, `prefer-optional-chain`)
**Solutions:**
1. **Run the auto-fixer first:**
```bash
pnpm lint:fix
```
2. **Review changes carefully** — some oxlint auto-fixes can produce incorrect code. Check the diff before committing.
3. **If a rule seems wrong**, check `.oxlintrc.json` to see if it should be disabled or configured differently.
**Why this happens:** oxlint version bumps often enable new rules by default.
---
#### Q: oxlint fails with TypeScript errors
**Symptoms:**
- `pnpm oxlint` or `pnpm lint` fails with type-related errors
- Errors mention type resolution or missing type information
**Solution:**
oxlint runs with `--type-aware` in this project, which requires valid TypeScript compilation. Fix the TS errors first:
```bash
pnpm typecheck # Identify TS errors
pnpm build # Or do a full build
pnpm lint # Then re-run lint
```
---
#### Q: Duplicate lint errors from both oxlint and ESLint
**Symptoms:**
- Same violation reported twice
- Conflicting auto-fix suggestions
**Solution:**
The project uses `eslint-plugin-oxlint` to automatically disable ESLint rules that oxlint already covers (see `eslint.config.ts`). If you see duplicates:
1. Ensure `.oxlintrc.json` is up to date after adding new oxlint rules
2. Run `pnpm lint` (which runs oxlint then ESLint in sequence) rather than running them individually
---
### Dependency and Package Issues
#### Q: "Package not found" after adding a dependency
**Symptoms:**
- Module not found after `pnpm add`
- Import errors for newly installed packages
**Solutions:**
1. **Ensure you installed in the correct workspace** (see `pnpm-workspace.yaml` for available workspaces):
```bash
# Example: install in a specific workspace
pnpm --filter <workspace-name> add <package>
```
2. **Clear pnpm cache:**
```bash
pnpm store prune
pnpm install
```
---
#### Q: Lock file conflicts after merge/rebase
**Symptoms:**
- Git conflicts in `pnpm-lock.yaml`
- Dependency resolution errors
**Solutions:**
1. **Regenerate lock file:**
```bash
rm pnpm-lock.yaml
pnpm install
```
2. **Or accept upstream lock file:**
```bash
git checkout --theirs pnpm-lock.yaml
pnpm install
```
---
### Testing Issues
#### Q: Tests fail locally but pass in CI
**Symptoms:**
- Flaky tests
- Different results between local and CI
**Solutions:**
1. **Run tests in CI mode:**
```bash
CI=true pnpm test:unit
```
2. **Clear test cache:**
```bash
pnpm test:unit --no-cache
```
3. **Check Node version matches CI** (see `.nvmrc` for the required version):
```bash
node --version
nvm use # If using nvm — reads .nvmrc automatically
```
---
### Git and Branch Issues
#### Q: Changes from another branch appearing in my branch
**Symptoms:**
- Uncommitted changes not related to your work
- Dirty working directory
**Solutions:**
1. **Stash and reinstall:**
```bash
git stash
pnpm install
```
2. **Check for untracked files:**
```bash
git status
git clean -fd # Careful: removes untracked files!
```
---
## Still Having Issues?
1. **Search existing issues:** [GitHub Issues](https://github.com/Comfy-Org/ComfyUI_frontend/issues)
2. **Ask the community:** [Discord](https://discord.com/invite/comfyorg) (navigate to the `#dev-frontend` channel)
3. **Create a new issue:** Include:
- Your OS and Node version (`node --version`)
- Steps to reproduce
- Full error message
- What you've already tried
## Contributing to This Guide
Found a solution to a common problem? Please:
1. Open a PR to add it to this guide
2. Follow the FAQ format above
3. Include the symptoms, solutions, and why it happens
---
**Last Updated:** 2026-03-10

View File

@@ -61,7 +61,8 @@
"^build"
],
"options": {
"command": "vite build --config apps/desktop-ui/vite.config.mts"
"cwd": "apps/desktop-ui",
"command": "vite build --config vite.config.mts"
},
"outputs": [
"{projectRoot}/dist"

View File

@@ -4,39 +4,3 @@
position: absolute;
inset: 0;
}
.p-button-secondary {
border: none;
background-color: var(--color-neutral-600);
color: var(--color-white);
}
.p-button-secondary:hover {
background-color: var(--color-neutral-550);
}
.p-button-secondary:active {
background-color: var(--color-neutral-500);
}
.p-button-danger {
background-color: var(--color-coral-red-600);
}
.p-button-danger:hover {
background-color: var(--color-coral-red-500);
}
.p-button-danger:active {
background-color: var(--color-coral-red-400);
}
.task-div .p-card {
transition: opacity var(--default-transition-duration);
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
}
.task-div .p-card:hover {
opacity: 1;
}

View File

@@ -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"
@@ -98,15 +101,13 @@ onUnmounted(() => {
</script>
<style scoped>
/* xterm renders its internal DOM outside Vue templates, so :deep selectors are
* required to style those generated nodes.
*/
@reference '../../../../assets/css/style.css';
:deep(.p-terminal) .xterm {
overflow: hidden;
@apply overflow-hidden;
}
:deep(.p-terminal) .xterm-screen {
overflow: hidden;
background-color: var(--color-neutral-900);
@apply bg-neutral-900 overflow-hidden;
}
</style>

View File

@@ -7,7 +7,7 @@
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="size"
:size="props.size"
class="language-selector"
@change="onLocaleChange"
>
@@ -36,10 +36,16 @@ import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const { variant = 'dark', size = 'small' } = defineProps<{
variant?: VariantKey
size?: SizeKey
}>()
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
@@ -98,8 +104,10 @@ const VARIANT_PRESETS = {
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[size])
const variantPreset = computed(() => VARIANT_PRESETS[variant])
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const dropdownPt = computed(() => ({
root: {
@@ -187,17 +195,13 @@ async function onLocaleChange(event: SelectChangeEvent) {
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.p-dropdown-panel .p-dropdown-item) {
transition-property: color, background-color, border-color;
transition-duration: var(--default-transition-duration);
@apply transition-colors;
}
:deep(.p-dropdown) {
&:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--color-neutral-900),
0 0 0 4px color-mix(in srgb, var(--color-brand-yellow) 60%, transparent);
}
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
}
</style>

View File

@@ -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>
@@ -269,43 +269,26 @@ const onFocus = async () => {
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.location-picker-accordion) {
padding-inline: calc(var(--spacing) * 12);
@apply px-12;
.p-accordionpanel {
border: 0;
background-color: transparent;
@apply border-0 bg-transparent;
}
.p-accordionheader {
margin-top: calc(var(--spacing) * 2);
border: 0;
border-radius: var(--radius-xl);
background-color: color-mix(
in srgb,
var(--color-neutral-800) 50%,
transparent
);
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
transition:
background-color 0.2s ease,
border-radius 0.5s ease;
&:hover {
background-color: color-mix(
in srgb,
var(--color-neutral-700) 50%,
transparent
);
}
}
/* When panel is expanded, adjust header border radius */
.p-accordionpanel-active {
.p-accordionheader {
border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl);
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
@apply rounded-t-xl rounded-b-none;
}
.p-accordionheader-toggle-icon {
@@ -316,24 +299,11 @@ const onFocus = async () => {
}
.p-accordioncontent {
border: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: var(--radius-xl);
border-bottom-left-radius: var(--radius-xl);
background-color: color-mix(
in srgb,
var(--color-neutral-800) 50%,
transparent
);
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
}
.p-accordioncontent-content {
background-color: transparent;
padding-top: calc(var(--spacing) * 3);
padding-right: calc(var(--spacing) * 5);
padding-bottom: calc(var(--spacing) * 5);
padding-left: calc(var(--spacing) * 5);
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
}
/* Override default chevron icons to use up/down */

View File

@@ -1,20 +1,11 @@
<template>
<div
:class="
cn(
'task-div group/task-card relative grid min-h-52 max-w-48',
isLoading && 'opacity-75'
)
"
class="task-div relative grid min-h-52 max-w-48"
:class="{ 'opacity-75': isLoading }"
>
<Card
:class="
cn(
'relative h-full max-w-48 overflow-hidden',
runner.state !== 'error' && 'opacity-65'
)
"
:pt="cardPt"
class="relative h-full max-w-48 overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
@@ -26,7 +17,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 +43,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="task-card-ok pi pi-check"
/>
</div>
</template>
@@ -64,7 +55,6 @@ import { computed } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { cn } from '@/utils/tailwindUtil'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()
@@ -93,9 +83,51 @@ const reactiveExecuting = computed(() => !!runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
const cardPt = {
header: { class: 'z-0' },
body: { class: 'z-[1] grow justify-between' }
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
.task-card-ok {
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
font-size: 4rem;
text-shadow: 0.25rem 0 0.5rem black;
z-index: 10;
}
.p-card {
@apply transition-opacity;
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
&.opacity-65 {
opacity: 0.4;
}
&:hover {
opacity: 1;
}
}
:deep(.p-card-header) {
z-index: 0;
}
:deep(.p-card-body) {
z-index: 1;
flex-grow: 1;
justify-content: space-between;
}
.task-div {
> i {
pointer-events: none;
}
&:hover > i {
opacity: 0.2;
}
}
</style>

View File

@@ -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
})
}

View File

@@ -1,10 +1,10 @@
<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">
{{ $t(`desktopDialogs.${id}.title`, title) }}
<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">
{{ $t(`desktopDialogs.${id}.message`, message) }}
{{ t(`desktopDialogs.${id}.message`, message) }}
</p>
<div class="flex w-full gap-2">
<Button
@@ -12,7 +12,7 @@
:key="button.label"
class="rounded-lg first:mr-auto"
:label="
$t(
t(
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
button.label
)
@@ -31,6 +31,7 @@ import { useRoute } from 'vue-router'
import { getDialog } from '@/constants/desktopDialogs'
import type { DialogAction } from '@/constants/desktopDialogs'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
const route = useRoute()
@@ -40,3 +41,31 @@ const handleButtonClick = async (button: DialogAction) => {
await electronAPI().Dialog.clickButton(button.returnValue)
}
</script>
<style scoped>
@reference '../assets/css/style.css';
.p-button-secondary {
@apply text-white border-none bg-neutral-600;
}
.p-button-secondary:hover {
@apply bg-neutral-550;
}
.p-button-secondary:active {
@apply bg-neutral-500;
}
.p-button-danger {
@apply bg-coral-red-600;
}
.p-button-danger:hover {
@apply bg-coral-red-500;
}
.p-button-danger:active {
@apply bg-coral-red-400;
}
</style>

View File

@@ -1,25 +1,25 @@
<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 -->
<h1 class="download-bg pi-download text-4xl font-bold">
{{ $t('desktopUpdate.title') }}
{{ t('desktopUpdate.title') }}
</h1>
<div class="m-8">
<span>{{ $t('desktopUpdate.description') }}</span>
<span>{{ t('desktopUpdate.description') }}</span>
</div>
<ProgressSpinner class="m-8 size-48" />
<ProgressSpinner class="m-8 w-48 h-48" />
<!-- Console button -->
<Button
style="transform: translateX(-50%)"
class="fixed bottom-0 left-1/2 my-8"
:label="$t('maintenance.consoleLogs')"
:label="t('maintenance.consoleLogs')"
icon="pi pi-desktop"
icon-pos="left"
severity="secondary"
@@ -28,8 +28,8 @@
<TerminalOutputDrawer
v-model="terminalVisible"
:header="$t('g.terminal')"
:default-message="$t('desktopUpdate.terminalDefaultMessage')"
:header="t('g.terminal')"
:default-message="t('desktopUpdate.terminalDefaultMessage')"
/>
</div>
</div>
@@ -44,6 +44,7 @@ import Toast from 'primevue/toast'
import { onUnmounted, ref } from 'vue'
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
@@ -60,10 +61,10 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
.download-bg::before {
position: absolute;
margin: 0;
color: var(--muted-foreground);
@apply m-0 absolute text-muted;
font-family: 'primeicons', sans-serif;
top: -2rem;
right: 2rem;

View File

@@ -1,8 +1,6 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { nextTick, provide } from 'vue'
import type { ElectronWindow } from '@/utils/envUtil'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
@@ -44,21 +42,16 @@ const meta: Meta<typeof InstallView> = {
const router = createMockRouter()
// Mock electron API
;(window as ElectronWindow).electronAPI = {
;(window as any).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (
_eventName: string,
_data?: Record<string, unknown>
) => {}
trackEvent: (_eventName: string, _data?: any) => {}
},
installComfyUI: (
_options: Parameters<ElectronAPI['installComfyUI']>[0]
) => {},
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
installComfyUI: (_options: any) => {},
changeTheme: (_theme: any) => {},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
@@ -247,8 +240,8 @@ export const DesktopSettings: Story = {
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
@@ -266,8 +259,8 @@ export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
@@ -334,7 +327,7 @@ export const ManualInstall: Story = {
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
@@ -382,7 +375,7 @@ export const ErrorState: Story = {
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
;(window as any).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,

View File

@@ -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"
@@ -183,37 +183,33 @@ onMounted(async () => {
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-steppanel) {
margin-top: calc(var(--spacing) * 8);
display: flex;
justify-content: center;
background-color: transparent;
@apply mt-8 flex justify-center bg-transparent;
}
/* Remove default padding/margin from StepPanels to make scrollbar flush */
:deep(.p-steppanels) {
margin: 0;
padding: 0;
@apply p-0 m-0;
}
/* Ensure StepPanel content container has no top/bottom padding */
:deep(.p-steppanel-content) {
padding: 0;
@apply p-0;
}
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
:deep(.p-steppanels::-webkit-scrollbar) {
width: calc(var(--spacing) * 4);
@apply w-4;
}
:deep(.p-steppanels::-webkit-scrollbar-track) {
background-color: transparent;
@apply bg-transparent;
}
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
border: 4px solid transparent;
border-radius: var(--radius-lg);
background-color: color-mix(in srgb, var(--color-white) 20%, transparent);
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
background-clip: content-box;
}
</style>

View File

@@ -77,7 +77,7 @@ const createMockElectronAPI = () => {
}
const ensureElectronAPI = () => {
const globalWindow = window as { electronAPI?: unknown }
const globalWindow = window as unknown as { electronAPI?: unknown }
if (!globalWindow.electronAPI) {
globalWindow.electronAPI = createMockElectronAPI()
}

View File

@@ -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"
@@ -114,12 +114,12 @@ import Tag from 'primevue/tag'
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import RefreshButton from '@/components/common/RefreshButton.vue'
import StatusTag from '@/components/maintenance/StatusTag.vue'
import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
@@ -129,7 +129,6 @@ import BaseViewTemplate from './templates/BaseViewTemplate.vue'
const electron = electronAPI()
const toast = useToast()
const { t } = useI18n()
const taskStore = useMaintenanceTaskStore()
const { clearResolved, processUpdate, refreshDesktopTasks } = taskStore
@@ -189,7 +188,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
})
}
}
@@ -220,14 +220,14 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-tag) {
--p-tag-gap: 0.375rem;
}
.backspan::before {
position: absolute;
margin: 0;
color: var(--muted-foreground);
@apply m-0 absolute text-muted;
font-family: 'primeicons', sans-serif;
top: -2rem;
right: -2rem;

Some files were not shown because too many files have changed in this diff Show More