mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-12 02:08:25 +00:00
Compare commits
7 Commits
fix/workfl
...
refactor/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26cef70453 | ||
|
|
c57d4f5da8 | ||
|
|
695ec64752 | ||
|
|
7cd10ccd88 | ||
|
|
6da7c896c9 | ||
|
|
37c6ddfcd9 | ||
|
|
55c42ee484 |
150
.claude/skills/backport-management/SKILL.md
Normal file
150
.claude/skills/backport-management/SKILL.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
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)
|
||||
```
|
||||
68
.claude/skills/backport-management/reference/analysis.md
Normal file
68
.claude/skills/backport-management/reference/analysis.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
42
.claude/skills/backport-management/reference/discovery.md
Normal file
42
.claude/skills/backport-management/reference/discovery.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# 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 |
|
||||
150
.claude/skills/backport-management/reference/execution.md
Normal file
150
.claude/skills/backport-management/reference/execution.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 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.
|
||||
96
.claude/skills/backport-management/reference/logging.md
Normal file
96
.claude/skills/backport-management/reference/logging.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 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/`.
|
||||
@@ -177,6 +177,7 @@ import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||
import { setNodeLocatorResolver } from '@/stores/nodeImageStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
@@ -510,6 +511,8 @@ onMounted(async () => {
|
||||
)
|
||||
}
|
||||
|
||||
setNodeLocatorResolver(workflowStore.nodeToNodeLocatorId)
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
await comfyApp.setup(canvasRef.value)
|
||||
canvasStore.canvas = comfyApp.canvas
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
import { getActivePinia } from 'pinia'
|
||||
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
@@ -9,6 +10,7 @@ import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { useNodeImageStore } from '@/stores/nodeImageStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
@@ -985,6 +987,13 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
// Install property projection so node.imgs, node.imageIndex, etc.
|
||||
// delegate to the centralized NodeImageStore.
|
||||
// Guarded because Pinia may not be initialized in unit tests.
|
||||
if (getActivePinia()) {
|
||||
useNodeImageStore().installPropertyProjection(node)
|
||||
}
|
||||
|
||||
this._nodes.push(node)
|
||||
this._nodes_by_id[node.id] = node
|
||||
|
||||
|
||||
@@ -51,6 +51,31 @@ describe('defaultSearcher', () => {
|
||||
const result = await defaultSearcher('xyz', items)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('matches against label when provided', async () => {
|
||||
const itemsWithLabels = [
|
||||
createItem('model_v1.safetensors', 'My Cool Model'),
|
||||
createItem('lora_v2.safetensors', 'Style Transfer LoRA'),
|
||||
createItem('checkpoint.ckpt', 'Realistic Vision')
|
||||
]
|
||||
const result = await defaultSearcher('cool', itemsWithLabels)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe('model_v1.safetensors')
|
||||
})
|
||||
|
||||
it('matches by label case-insensitively', async () => {
|
||||
const itemsWithLabels = [createItem('file.safetensors', 'My Cool Model')]
|
||||
const result = await defaultSearcher('MY COOL', itemsWithLabels)
|
||||
expect(result).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('matches when word is in name or label', async () => {
|
||||
const itemsWithLabels = [
|
||||
createItem('sd_v15.safetensors', 'Stable Diffusion 1.5')
|
||||
]
|
||||
const result = await defaultSearcher('stable', itemsWithLabels)
|
||||
expect(result).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDefaultSortOptions', () => {
|
||||
|
||||
@@ -12,7 +12,8 @@ export async function defaultSearcher(
|
||||
const words = query.trim().toLowerCase().split(' ')
|
||||
return items.filter((item) => {
|
||||
const name = item.name.toLowerCase()
|
||||
return words.every((word) => name.includes(word))
|
||||
const label = item.label?.toLowerCase() ?? ''
|
||||
return words.every((word) => name.includes(word) || label.includes(word))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
350
src/stores/nodeImageStore.test.ts
Normal file
350
src/stores/nodeImageStore.test.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { setNodeLocatorResolver, useNodeImageStore } from './nodeImageStore'
|
||||
|
||||
const mockNodeToNodeLocatorId = vi.fn()
|
||||
|
||||
function createMockNode(overrides: Record<string, unknown> = {}): LGraphNode {
|
||||
return {
|
||||
id: 1,
|
||||
type: 'TestNode',
|
||||
...overrides
|
||||
} as Partial<LGraphNode> as LGraphNode
|
||||
}
|
||||
|
||||
describe(useNodeImageStore, () => {
|
||||
let store: ReturnType<typeof useNodeImageStore>
|
||||
const locatorA = '42' as NodeLocatorId
|
||||
const locatorB = 'abc-123:42' as NodeLocatorId
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useNodeImageStore()
|
||||
vi.clearAllMocks()
|
||||
setNodeLocatorResolver(mockNodeToNodeLocatorId)
|
||||
})
|
||||
|
||||
describe('getState', () => {
|
||||
it('returns default state for new locatorId', () => {
|
||||
const state = store.getState(locatorA)
|
||||
expect(state).toEqual({
|
||||
imgs: [],
|
||||
imageIndex: null,
|
||||
imageRects: [],
|
||||
pointerDown: null,
|
||||
overIndex: null
|
||||
})
|
||||
})
|
||||
|
||||
it('returns same state for same locatorId', () => {
|
||||
const first = store.getState(locatorA)
|
||||
first.overIndex = 42
|
||||
const second = store.getState(locatorA)
|
||||
expect(second.overIndex).toBe(42)
|
||||
})
|
||||
|
||||
it('returns different references for different locatorIds', () => {
|
||||
const a = store.getState(locatorA)
|
||||
const b = store.getState(locatorB)
|
||||
expect(a).not.toBe(b)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearState', () => {
|
||||
it('removes entry for locatorId', () => {
|
||||
const state = store.getState(locatorA)
|
||||
state.overIndex = 5
|
||||
store.clearState(locatorA)
|
||||
|
||||
const fresh = store.getState(locatorA)
|
||||
expect(fresh.overIndex).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearAll', () => {
|
||||
it('removes all entries', () => {
|
||||
store.getState(locatorA).overIndex = 1
|
||||
store.getState(locatorB).overIndex = 2
|
||||
store.clearAll()
|
||||
|
||||
expect(store.getState(locatorA).overIndex).toBeNull()
|
||||
expect(store.getState(locatorB).overIndex).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('installPropertyProjection', () => {
|
||||
it('projects imageRects reads/writes to store', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
node.imageRects = [[10, 20, 30, 40]]
|
||||
expect(store.getState(locatorA).imageRects).toEqual([[10, 20, 30, 40]])
|
||||
expect(node.imageRects).toEqual([[10, 20, 30, 40]])
|
||||
})
|
||||
|
||||
it('projects pointerDown reads/writes to store', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
node.pointerDown = { index: 3, pos: [100, 200] }
|
||||
expect(store.getState(locatorA).pointerDown).toEqual({
|
||||
index: 3,
|
||||
pos: [100, 200]
|
||||
})
|
||||
expect(node.pointerDown).toEqual({ index: 3, pos: [100, 200] })
|
||||
})
|
||||
|
||||
it('projects overIndex reads/writes to store', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
node.overIndex = 7
|
||||
expect(store.getState(locatorA).overIndex).toBe(7)
|
||||
expect(node.overIndex).toBe(7)
|
||||
})
|
||||
|
||||
it('projects imageIndex reads/writes to store', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
node.imageIndex = 5
|
||||
expect(store.getState(locatorA).imageIndex).toBe(5)
|
||||
expect(node.imageIndex).toBe(5)
|
||||
})
|
||||
|
||||
it('preserves existing values when installing projection', () => {
|
||||
const node = createMockNode({ overIndex: 3, imageIndex: 2 })
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.overIndex).toBe(3)
|
||||
expect(node.imageIndex).toBe(2)
|
||||
expect(store.getState(locatorA).overIndex).toBe(3)
|
||||
expect(store.getState(locatorA).imageIndex).toBe(2)
|
||||
})
|
||||
|
||||
it('returns undefined when node has no locatorId', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(undefined)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.overIndex).toBeUndefined()
|
||||
expect(node.imageIndex).toBeUndefined()
|
||||
})
|
||||
|
||||
it('silently drops writes when node has no locatorId', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(undefined)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
node.overIndex = 5
|
||||
|
||||
expect(node.overIndex).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is idempotent when called twice on the same node', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
node.overIndex = 3
|
||||
node.imageIndex = 7
|
||||
node.imgs = [new Image()]
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.overIndex).toBe(3)
|
||||
expect(node.imageIndex).toBe(7)
|
||||
expect(node.imgs).toHaveLength(1)
|
||||
expect(store.getState(locatorA).overIndex).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('imgs projection', () => {
|
||||
it('returns undefined when store array is empty', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.imgs).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns the array when store has images', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
const img = new Image()
|
||||
node.imgs = [img]
|
||||
expect(node.imgs).toEqual([img])
|
||||
expect(store.getState(locatorA).imgs).toEqual([img])
|
||||
})
|
||||
|
||||
it('converts undefined assignment to empty array in store', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
node.imgs = [new Image()]
|
||||
node.imgs = undefined
|
||||
expect(node.imgs).toBeUndefined()
|
||||
expect(store.getState(locatorA).imgs).toEqual([])
|
||||
})
|
||||
|
||||
it('preserves existing imgs when installing projection', () => {
|
||||
const img = new Image()
|
||||
const node = createMockNode({ imgs: [img] })
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.imgs).toEqual([img])
|
||||
expect(store.getState(locatorA).imgs).toEqual([img])
|
||||
})
|
||||
|
||||
it('supports optional chaining pattern (node.imgs?.length)', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.imgs?.length).toBeUndefined()
|
||||
|
||||
node.imgs = [new Image(), new Image()]
|
||||
expect(node.imgs?.length).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('subgraph isolation', () => {
|
||||
it('isolates image state across subgraph instances', () => {
|
||||
const locator1 = 'uuid-instance-1:42' as NodeLocatorId
|
||||
const locator2 = 'uuid-instance-2:42' as NodeLocatorId
|
||||
|
||||
const img1 = new Image()
|
||||
const img2 = new Image()
|
||||
|
||||
store.getState(locator1).imgs = [img1]
|
||||
store.getState(locator2).imgs = [img2]
|
||||
|
||||
expect(store.getState(locator1).imgs).toEqual([img1])
|
||||
expect(store.getState(locator2).imgs).toEqual([img2])
|
||||
})
|
||||
|
||||
it('isolates imageIndex across subgraph instances', () => {
|
||||
const locator1 = 'uuid-instance-1:42' as NodeLocatorId
|
||||
const locator2 = 'uuid-instance-2:42' as NodeLocatorId
|
||||
|
||||
store.getState(locator1).imageIndex = 0
|
||||
store.getState(locator2).imageIndex = 3
|
||||
|
||||
expect(store.getState(locator1).imageIndex).toBe(0)
|
||||
expect(store.getState(locator2).imageIndex).toBe(3)
|
||||
})
|
||||
|
||||
it('projects to correct store entry based on locatorId', () => {
|
||||
const locator1 = 'uuid-instance-1:42' as NodeLocatorId
|
||||
const locator2 = 'uuid-instance-2:42' as NodeLocatorId
|
||||
|
||||
const node1 = createMockNode({ id: 42, _locator: locator1 })
|
||||
const node2 = createMockNode({ id: 42, _locator: locator2 })
|
||||
|
||||
mockNodeToNodeLocatorId.mockImplementation(
|
||||
(n: Record<string, unknown>) => n._locator
|
||||
)
|
||||
|
||||
store.installPropertyProjection(node1)
|
||||
store.installPropertyProjection(node2)
|
||||
|
||||
node1.overIndex = 1
|
||||
node2.overIndex = 9
|
||||
|
||||
expect(store.getState(locator1).overIndex).toBe(1)
|
||||
expect(store.getState(locator2).overIndex).toBe(9)
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiple nodes have independent state', () => {
|
||||
it('imageIndex is independent per node', () => {
|
||||
const nodeA = createMockNode({ id: 1, _locator: '1' })
|
||||
const nodeB = createMockNode({ id: 2, _locator: '2' })
|
||||
const locA = '1' as NodeLocatorId
|
||||
const locB = '2' as NodeLocatorId
|
||||
|
||||
mockNodeToNodeLocatorId.mockImplementation(
|
||||
(n: Record<string, unknown>) => n._locator
|
||||
)
|
||||
|
||||
store.installPropertyProjection(nodeA)
|
||||
store.installPropertyProjection(nodeB)
|
||||
|
||||
nodeA.imageIndex = 0
|
||||
nodeB.imageIndex = 5
|
||||
|
||||
expect(nodeA.imageIndex).toBe(0)
|
||||
expect(nodeB.imageIndex).toBe(5)
|
||||
expect(store.getState(locA).imageIndex).toBe(0)
|
||||
expect(store.getState(locB).imageIndex).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DEFAULT_STATE immutability', () => {
|
||||
it('default imageRects is frozen and cannot be mutated', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
// Read default imageRects without triggering state creation
|
||||
const nodeB = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorB)
|
||||
store.installPropertyProjection(nodeB)
|
||||
|
||||
// Default arrays should be frozen (no state entry exists yet)
|
||||
expect(() => {
|
||||
;(nodeB.imageRects as unknown[]).push([0, 0, 10, 10])
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('null-to-null transitions', () => {
|
||||
it('imageIndex null → null works', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.imageIndex).toBeNull()
|
||||
node.imageIndex = null
|
||||
expect(node.imageIndex).toBeNull()
|
||||
})
|
||||
|
||||
it('pointerDown null → null works', () => {
|
||||
const node = createMockNode()
|
||||
mockNodeToNodeLocatorId.mockReturnValue(locatorA)
|
||||
|
||||
store.installPropertyProjection(node)
|
||||
|
||||
expect(node.pointerDown).toBeNull()
|
||||
node.pointerDown = null
|
||||
expect(node.pointerDown).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
155
src/stores/nodeImageStore.ts
Normal file
155
src/stores/nodeImageStore.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { Point, Rect } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
interface PointerDownState {
|
||||
index: number | null
|
||||
pos: Point
|
||||
}
|
||||
|
||||
interface NodeImageState {
|
||||
imgs: HTMLImageElement[]
|
||||
imageIndex: number | null
|
||||
imageRects: Rect[]
|
||||
pointerDown: PointerDownState | null
|
||||
overIndex: number | null
|
||||
}
|
||||
|
||||
function createDefaultState(): NodeImageState {
|
||||
return {
|
||||
imgs: [],
|
||||
imageIndex: null,
|
||||
imageRects: [],
|
||||
pointerDown: null,
|
||||
overIndex: null
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: Readonly<NodeImageState> = Object.freeze({
|
||||
...createDefaultState(),
|
||||
imgs: Object.freeze([]) as unknown as HTMLImageElement[],
|
||||
imageRects: Object.freeze([]) as unknown as Rect[]
|
||||
})
|
||||
|
||||
/**
|
||||
* Module-scoped resolver for converting nodes to locator IDs.
|
||||
* Set once during app bootstrap via {@link setNodeLocatorResolver} to
|
||||
* avoid a circular dependency: LGraph → nodeImageStore → workflowStore → app → litegraph.
|
||||
*/
|
||||
let _nodeLocatorResolver:
|
||||
| ((node: LGraphNode) => NodeLocatorId | undefined)
|
||||
| undefined
|
||||
|
||||
export function setNodeLocatorResolver(
|
||||
resolver: (node: LGraphNode) => NodeLocatorId | undefined
|
||||
): void {
|
||||
_nodeLocatorResolver = resolver
|
||||
}
|
||||
|
||||
function getNodeLocatorId(node: LGraphNode): NodeLocatorId | undefined {
|
||||
return _nodeLocatorResolver?.(node)
|
||||
}
|
||||
|
||||
export const useNodeImageStore = defineStore('nodeImage', () => {
|
||||
const state = new Map<NodeLocatorId, NodeImageState>()
|
||||
|
||||
function getState(locatorId: NodeLocatorId): NodeImageState {
|
||||
const existing = state.get(locatorId)
|
||||
if (existing) return existing
|
||||
|
||||
const entry = createDefaultState()
|
||||
state.set(locatorId, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
function peekState(locatorId: NodeLocatorId): NodeImageState | undefined {
|
||||
return state.get(locatorId)
|
||||
}
|
||||
|
||||
function clearState(locatorId: NodeLocatorId): void {
|
||||
state.delete(locatorId)
|
||||
}
|
||||
|
||||
function clearAll(): void {
|
||||
state.clear()
|
||||
}
|
||||
|
||||
function setStateProperty<K extends keyof NodeImageState>(
|
||||
locatorId: NodeLocatorId,
|
||||
prop: K,
|
||||
value: NodeImageState[K]
|
||||
): void {
|
||||
getState(locatorId)[prop] = value
|
||||
}
|
||||
|
||||
function installPropertyProjection(node: LGraphNode): void {
|
||||
const simpleProperties: (keyof NodeImageState)[] = [
|
||||
'imageRects',
|
||||
'pointerDown',
|
||||
'overIndex',
|
||||
'imageIndex'
|
||||
]
|
||||
|
||||
const nodeRecord = node as unknown as Record<string, unknown>
|
||||
|
||||
for (const prop of simpleProperties) {
|
||||
const existingValue = nodeRecord[prop]
|
||||
|
||||
Object.defineProperty(node, prop, {
|
||||
get() {
|
||||
const locatorId = getNodeLocatorId(node)
|
||||
if (!locatorId) return undefined
|
||||
return (peekState(locatorId) ?? DEFAULT_STATE)[prop]
|
||||
},
|
||||
set(value: unknown) {
|
||||
const locatorId = getNodeLocatorId(node)
|
||||
if (!locatorId) return
|
||||
setStateProperty(
|
||||
locatorId,
|
||||
prop,
|
||||
value as NodeImageState[typeof prop]
|
||||
)
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
|
||||
if (existingValue !== undefined) {
|
||||
nodeRecord[prop] = existingValue
|
||||
}
|
||||
}
|
||||
|
||||
// imgs needs special handling: return undefined when empty to preserve
|
||||
// node.imgs?.length optional chaining semantics
|
||||
const existingImgs = node.imgs
|
||||
|
||||
Object.defineProperty(node, 'imgs', {
|
||||
get() {
|
||||
const locatorId = getNodeLocatorId(node)
|
||||
if (!locatorId) return undefined
|
||||
const s = peekState(locatorId)
|
||||
return s?.imgs.length ? s.imgs : undefined
|
||||
},
|
||||
set(value: HTMLImageElement[] | undefined) {
|
||||
const locatorId = getNodeLocatorId(node)
|
||||
if (!locatorId) return
|
||||
getState(locatorId).imgs = value ?? []
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
|
||||
if (existingImgs !== undefined) {
|
||||
node.imgs = existingImgs
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getState,
|
||||
clearState,
|
||||
clearAll,
|
||||
installPropertyProjection
|
||||
}
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { clone } from '@/scripts/utils'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { useNodeImageStore } from '@/stores/nodeImageStore'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { isAnimatedOutput, isVideoNode } from '@/utils/litegraphUtil'
|
||||
import {
|
||||
@@ -359,6 +360,8 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
delete nodePreviewImages.value[nodeLocatorId]
|
||||
}
|
||||
|
||||
useNodeImageStore().clearState(nodeLocatorId)
|
||||
|
||||
return hadOutputs
|
||||
}
|
||||
|
||||
@@ -407,6 +410,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
app.nodeOutputs = {}
|
||||
nodeOutputs.value = {}
|
||||
revokeAllPreviews()
|
||||
useNodeImageStore().clearAll()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user