Compare commits

..

5 Commits

Author SHA1 Message Date
kishore
6898665823 feat: polish OAuth consent UI and align with workspace switcher design
- Workspace picker is now an inline radio list (matches the cloud
  workspace switcher) instead of a dropdown — uses WorkspaceProfilePic
  avatars so OAuth consent feels native to the cloud app.
- Cap list at max-h-72 with scrollbar-custom so 10+ workspaces stay
  discoverable.
- Resume cloud calls are now relative (same-origin via Vite proxy or
  prod reverse proxy) — fixes the cross-origin cookie loss that would
  bounce the consent challenge back to login.
- Map 400 / 403 / 404 cloud errors to user-facing messages
  (expired / scope_broadening / feature_unavailable).
- Surface registered redirect_uri and RFC 7591 application_type so
  users can verify the loopback destination before granting.
2026-05-11 19:04:26 -07:00
kishore
607bd8a309 fix: proxy registry API in local cloud dev
Route /customers and /releases to the Comfy registry host when the backend is loopback ingest; those paths are not implemented on ingest.

Stub /api/i18n for cloud dev so bootstrap does not hard-fail against OSS-only routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-08 00:31:19 -07:00
kishore
101f054395 fix: disable Mixpanel in local cloud dev
Avoid sending local OAuth browser-test telemetry to the cloud Mixpanel proxy when the cloud frontend is pointed at a loopback backend.
2026-05-08 00:27:26 -07:00
kishore
cb042bee24 fix: use local API base in cloud dev
Keep local cloud development on the Vite proxy path so user-scoped customer requests do not leak to staging.
2026-05-08 00:02:34 -07:00
kishore
e3d2ae3cef feat: add OAuth frontend consent flow
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 23:33:12 -07:00
227 changed files with 2089 additions and 19492 deletions

View File

@@ -1,88 +0,0 @@
# Description: Extension API test suite (I-TF) + compat-floor gate (I-TF.7)
#
# Runs on any PR touching extension-api declaration files, extension-api-v2
# implementation/tests, or the touch-point DB/rollup (blast-radius changes).
#
# Two jobs:
# test — vitest run against src/extension-api-v2/__tests__/
# compat-floor — python scripts/check-compat-floor.py (exits 1 if any
# blast_radius ≥ 2.0 category is missing a stub triple)
#
# The compat-floor job is the CI enforcement of PLAN.md §Compat-floor:
# "Every blast_radius ≥ 2.0 pattern MUST pass v1 + v2 + migration before v2 ships."
name: 'CI: Tests Extension API'
on:
push:
branches: [main, master, dev*, core/*, extension-v2*]
paths:
- 'src/extension-api/**'
- 'src/extension-api-v2/**'
- 'packages/extension-api/**'
- 'vitest.extension-api.config.mts'
- 'research/touch-points/rollup.yaml'
- 'research/touch-points/behavior-categories.yaml'
- 'scripts/check-compat-floor.py'
- 'pnpm-lock.yaml'
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths:
- 'src/extension-api/**'
- 'src/extension-api-v2/**'
- 'packages/extension-api/**'
- 'vitest.extension-api.config.mts'
- 'research/touch-points/rollup.yaml'
- 'research/touch-points/behavior-categories.yaml'
- 'scripts/check-compat-floor.py'
- 'pnpm-lock.yaml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Extension API tests (vitest)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run extension-api test suite
run: pnpm test:extension-api
- name: Run with coverage (push only)
if: github.event_name == 'push'
run: pnpm test:extension-api:coverage
- name: Upload coverage to Codecov
if: github.event_name == 'push'
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
files: coverage/lcov.info
flags: extension-api
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
compat-floor:
name: Compat-floor gate (blast_radius ≥ 2.0)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.11'
- name: Install PyYAML
run: pip install pyyaml
- name: Check compat floor
run: python3 scripts/check-compat-floor.py
# Exits 1 if any blast_radius ≥ 2.0 behavior category is missing
# any of its three stub files (v1/v2/migration). Enforces PLAN.md §Compat-floor.

View File

@@ -1,97 +0,0 @@
# Description: Publish @comfyorg/extension-api to npm with provenance attestation.
#
# Triggered by a tag push matching 'extension-api-v*' (e.g. extension-api-v0.1.0).
# Also supports workflow_dispatch for a manual dry-run (set dry_run: true).
#
# Prerequisites (one-time human setup):
# - NPM_TOKEN secret must be set in the repo/org settings with publish
# access to the @comfyorg scope on npmjs.com.
# - The @comfyorg npm scope already exists (used by @comfyorg/comfyui-frontend).
#
# PKG4.D4 (MIG1 / Phase A — surface-only shim)
name: 'Extension API: Publish'
on:
push:
tags:
- 'extension-api-v*'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run — build and verify without publishing'
required: false
default: true
type: boolean
permissions:
contents: write # needed to create GitHub Release
id-token: write # needed for npm provenance via OIDC
jobs:
publish:
name: Publish @comfyorg/extension-api
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # full history for release notes
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Setup npm registry
uses: actions/setup-node@v6
with:
registry-url: 'https://registry.npmjs.org/'
- name: Build package
run: pnpm --filter @comfyorg/extension-api build
- name: Typecheck package
run: pnpm --filter @comfyorg/extension-api typecheck
- name: Verify package version matches tag
if: github.event_name == 'push'
run: |
TAG="${GITHUB_REF_NAME}" # e.g. extension-api-v0.1.0
PKG_VERSION=$(node -p "require('./packages/extension-api/package.json').version")
TAG_VERSION="${TAG#extension-api-v}" # strip prefix → 0.1.0
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
echo "::error::Tag '$TAG' implies version '$TAG_VERSION' but packages/extension-api/package.json has '$PKG_VERSION'. Update the package.json before tagging."
exit 1
fi
echo "Version check passed: $PKG_VERSION"
- name: Publish to npm (with provenance)
if: github.event_name == 'push' || !inputs.dry_run
run: |
cd packages/extension-api
npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Dry-run report
if: inputs.dry_run
run: |
echo "=== DRY RUN — would publish ==="
cd packages/extension-api
npm pack --dry-run
echo "=== End dry run ==="
- name: Create GitHub Release
if: github.event_name == 'push'
uses: actions/github-script@v8
with:
script: |
const tag = context.ref.replace('refs/tags/', '')
const { data: release } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: tag,
generate_release_notes: true,
draft: false,
prerelease: context.ref.includes('-alpha') || context.ref.includes('-beta') || context.ref.includes('-rc')
})
console.log(`Release created: ${release.html_url}`)

View File

@@ -1,65 +0,0 @@
# Description: Typecheck and build the @comfyorg/extension-api package.
# Runs on PRs and pushes touching the public type surface, the core .v2.ts
# implementations, or the package scaffold — so regressions in the published
# contract are caught before merge.
#
# PKG4.D3 (MIG1 / Phase A — surface-only shim)
name: 'Extension API: Typecheck'
on:
push:
branches: [main, master, dev*, core/*, extension-v2*]
paths:
- 'src/extension-api/**'
- 'src/extensions/core/*.v2.ts'
- 'src/services/extension-api-service.ts'
- 'packages/extension-api/**'
- '.github/workflows/extension-api-*.yml'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths:
- 'src/extension-api/**'
- 'src/extensions/core/*.v2.ts'
- 'src/services/extension-api-service.ts'
- 'packages/extension-api/**'
- '.github/workflows/extension-api-*.yml'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
typecheck:
name: Build + typecheck @comfyorg/extension-api
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build package (emit declarations)
run: pnpm --filter @comfyorg/extension-api build
- name: Typecheck package
run: pnpm --filter @comfyorg/extension-api typecheck
- name: Smoke-test consumer (tsc --noEmit on minimal extension)
# Verifies the published types are consumable from an external module
# that imports from '@comfyorg/extension-api'. Uses a minimal fixture
# checked in to packages/extension-api/test/smoke/.
run: |
cd packages/extension-api
if [ -d test/smoke ]; then
pnpm exec tsc --noEmit --project test/smoke/tsconfig.json
else
echo "No smoke test found — skipping (add packages/extension-api/test/smoke/ to enable)"
fi

View File

@@ -6,7 +6,6 @@
"trailingComma": "none",
"printWidth": 80,
"ignorePatterns": [
"packages/extension-api/build/**",
"packages/registry-types/src/comfyRegistryTypes.ts",
"public/materialdesignicons.min.css",
"src/types/generatedManagerTypes.ts",

View File

@@ -32,34 +32,16 @@ test.describe('Careers page @smoke', () => {
}
})
test('clicking a department button scrolls to and activates that section', async ({
test('ENGINEERING category filter narrows the role list', async ({
page
}) => {
const rolesSection = page.getByTestId('careers-roles')
await rolesSection.scrollIntoViewIfNeeded()
await expect(rolesSection).toBeVisible()
const allCount = await page.getByTestId('careers-role-link').count()
const engineeringButton = page.getByRole('button', {
name: 'ENGINEERING',
exact: true
})
// RolesSection is hydrated via `client:visible`. Once the button responds
// to a click by flipping aria-pressed, Vue is hydrated and the rest of
// the locator logic is in effect.
await expect(async () => {
await engineeringButton.click()
await expect(engineeringButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 1_000
})
}).toPass({ timeout: 10_000 })
const engineeringSection = page.locator('#careers-dept-engineering')
await expect(engineeringSection).toBeInViewport()
expect(await page.getByTestId('careers-role-link').count()).toBe(allCount)
await page.getByRole('button', { name: 'ENGINEERING', exact: true }).click()
const engineeringLocator = page.getByTestId('careers-role-link')
await expect(engineeringLocator.first()).toBeVisible()
const engineeringCount = await engineeringLocator.count()
expect(engineeringCount).toBeLessThanOrEqual(allCount)
expect(engineeringCount).toBeGreaterThan(0)
})
})

View File

@@ -1,61 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
const M4_PRO_14_INCH_VIEWPORT = { width: 2016, height: 1310 }
const LAST_SECTION_HASH = '#contact'
test.describe(
'ContentSection scroll-spy @smoke',
{
annotation: [
{
type: 'issue',
description:
'https://linear.app/comfyorg/issue/FE-604/bug-bottom-badge-not-activating-on-scroll-at-high-resolution-3024x1964'
},
{
type: 'environment',
description:
'14" MacBook M4 Pro logical viewport reported in FE-604; /privacy-policy reproduces because of its short trailing sections'
}
]
},
() => {
test.use({ viewport: M4_PRO_14_INCH_VIEWPORT })
test('activates the last badge when user scrolls to the bottom', async ({
page
}) => {
await page.goto('/privacy-policy')
const sidebarNav = page.getByRole('navigation', {
name: 'Category filter'
})
const badges = sidebarNav.getByRole('button')
const lastBadge = badges.last()
await expect(badges.first()).toHaveAttribute('aria-pressed', 'true')
await expect(lastBadge).toHaveAttribute('aria-pressed', 'false')
await page.evaluate(() =>
window.scrollTo(0, document.documentElement.scrollHeight)
)
await expect(lastBadge).toHaveAttribute('aria-pressed', 'true')
})
test('activates the last badge when page mounts already at the bottom via trailing hash', async ({
page
}) => {
await page.goto(`/privacy-policy${LAST_SECTION_HASH}`)
const sidebarNav = page.getByRole('navigation', {
name: 'Category filter'
})
const lastBadge = sidebarNav.getByRole('button').last()
await expect(lastBadge).toHaveAttribute('aria-pressed', 'true')
})
}
)

View File

@@ -1,71 +1,27 @@
import { expect, test } from '@playwright/test'
import { demos, getNextDemo } from '../src/config/demos'
import { t } from '../src/i18n/translations'
const escapeRegExp = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
test.describe('Demo pages @smoke', () => {
for (const demo of demos) {
const nextDemo = getNextDemo(demo.slug)
test('demo detail page renders hero and embed', async ({ page }) => {
await page.goto('/demos/image-to-video')
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
await expect(page.getByRole('heading', { level: 1 })).toContainText(
'Create a Video from an Image'
)
const iframe = page.locator('iframe[title*="Interactive demo"]')
await expect(iframe).toBeAttached()
})
test(`/demos/${demo.slug} renders hero, embed, transcript, and next-demo nav`, async ({
page
}) => {
await page.goto(`/demos/${demo.slug}`)
test('demo detail page has transcript section', async ({ page }) => {
await page.goto('/demos/image-to-video')
await expect(
page.getByRole('button', { name: /demo transcript/i })
).toBeVisible()
})
const heading = page.getByRole('heading', { level: 1 })
await expect(heading).toBeVisible()
await expect(heading).toContainText(t(demo.title, 'en'))
const ogImage = page.locator('head meta[property="og:image"]')
await expect(ogImage).toHaveAttribute(
'content',
new RegExp(`${escapeRegExp(demo.slug)}-og\\.png`)
)
const iframe = page.locator(
`iframe[title*="${t('demos.embed.label', 'en')}"]`
)
await expect(iframe).toBeAttached()
await expect(iframe).toHaveAttribute(
'src',
new RegExp(escapeRegExp(demo.arcadeId))
)
await expect(
page.getByRole('button', { name: /demo transcript/i })
).toBeVisible()
await expect(
page.getByText(t(nextDemo.title, 'en')).first()
).toBeVisible()
const nextThumb = page.locator(`img[src="${nextDemo.thumbnail}"]`).first()
await expect(nextThumb).toBeAttached()
await expect(nextThumb).toBeVisible()
const naturalWidth = await nextThumb.evaluate(
(img) => (img as HTMLImageElement).naturalWidth
)
expect(naturalWidth).toBeGreaterThan(1)
})
test(`/zh-CN/demos/${demo.slug} renders localized content`, async ({
page
}) => {
await page.goto(`/zh-CN/demos/${demo.slug}`)
await expect(page).toHaveURL(/\/zh-CN\/demos\//)
const heading = page.getByRole('heading', { level: 1 })
await expect(heading).toContainText(t(demo.title, 'zh-CN'))
await expect(heading).toContainText(/[\u4E00-\u9FFF]/)
await expect(
page.getByText(t(nextDemo.title, 'zh-CN')).first()
).toBeVisible()
})
}
test('demo detail page has next demo navigation', async ({ page }) => {
await page.goto('/demos/image-to-video')
await expect(page.getByText(/what's next/i)).toBeVisible()
})
test('demo library page renders', async ({ page }) => {
await page.goto('/demos')
@@ -76,4 +32,13 @@ test.describe('Demo pages @smoke', () => {
const response = await page.goto('/demos/nonexistent')
expect(response?.status()).toBe(404)
})
test('zh-CN demo page renders localized content', async ({ page }) => {
await page.goto('/zh-CN/demos/image-to-video')
await expect(page.getByRole('heading', { level: 1 })).toContainText(
'从图片创建视频'
)
const nextDemoLink = page.locator('a[href*="/zh-CN/demos/"]').first()
await expect(nextDemoLink).toBeAttached()
})
})

View File

@@ -1,4 +1,3 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test } from './fixtures/blockExternalMedia'
@@ -48,105 +47,4 @@ test.describe('Mobile layout @mobile', () => {
const mobileContainer = page.getByTestId('social-proof-mobile')
await expect(mobileContainer).toBeVisible()
})
test.describe('SocialProofBar seamless marquee', () => {
test.use({ contextOptions: { reducedMotion: 'no-preference' } })
test('mobile forward marquee loops seamlessly', async ({ page }) => {
const geometry = await measureMarqueeLoopGeometry(
page,
'[data-testid="social-proof-mobile"] .animate-marquee'
)
expectSeamlessForwardLoop(geometry)
})
test('mobile reverse marquee loops seamlessly', async ({ page }) => {
const geometry = await measureMarqueeLoopGeometry(
page,
'[data-testid="social-proof-mobile"] .animate-marquee-reverse'
)
expectSeamlessReverseLoop(geometry)
})
})
})
test.describe('Desktop SocialProofBar @smoke', () => {
test.use({ contextOptions: { reducedMotion: 'no-preference' } })
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('desktop marquee loops seamlessly', async ({ page }) => {
const geometry = await measureMarqueeLoopGeometry(
page,
'[data-testid="social-proof-desktop"] .animate-marquee'
)
expectSeamlessForwardLoop(geometry)
})
})
type MarqueeGeometry = {
copyWidths: number[]
startPositions: number[]
endPositions: number[]
}
async function measureMarqueeLoopGeometry(
page: Page,
selector: string
): Promise<MarqueeGeometry> {
await page.locator(selector).first().waitFor()
return page.evaluate((sel) => {
const tracks = Array.from(
document.querySelectorAll<HTMLElement>(sel)
).slice(0, 2)
const firstAnimation = tracks[0]?.getAnimations()[0]
if (!firstAnimation) {
throw new Error(`No CSS animation found on ${sel}`)
}
const duration = firstAnimation.effect?.getTiming().duration
if (typeof duration !== 'number' || duration <= 1) {
throw new Error(
`Animation on ${sel} has unusable duration: ${String(duration)}`
)
}
const setAllTimes = (time: number) => {
for (const track of tracks) {
for (const anim of track.getAnimations()) {
anim.currentTime = time
}
}
void document.body.offsetWidth
}
const readX = () => tracks.map((track) => track.getBoundingClientRect().x)
setAllTimes(0)
const startPositions = readX()
const copyWidths = tracks.map(
(track) => track.getBoundingClientRect().width
)
setAllTimes(duration - 0.1)
const endPositions = readX()
return { copyWidths, startPositions, endPositions }
}, selector)
}
function expectTwoMatchingCopies(geometry: MarqueeGeometry) {
const { copyWidths } = geometry
expect(copyWidths.length, 'expected two duplicate marquee tracks').toBe(2)
expect(copyWidths[0]).toBeGreaterThan(0)
expect(copyWidths[1]).toBeCloseTo(copyWidths[0], 0)
}
function expectSeamlessForwardLoop(geometry: MarqueeGeometry) {
expectTwoMatchingCopies(geometry)
// Copy 2 ends the cycle exactly where copy 1 started, so the restart
// (when copy 1 jumps back to its start position) is visually indistinguishable.
expect(geometry.endPositions[1]).toBeCloseTo(geometry.startPositions[0], 0)
}
function expectSeamlessReverseLoop(geometry: MarqueeGeometry) {
expectTwoMatchingCopies(geometry)
// Reverse marquee: copy 1 ends the cycle where copy 2 started.
expect(geometry.endPositions[0]).toBeCloseTo(geometry.startPositions[1], 0)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,13 +1,10 @@
<script setup lang="ts">
import { useEventListener, useTemplateRefsList } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { computed, ref } from 'vue'
import type { Department } from '../../data/roles'
import type { Locale } from '../../i18n/translations'
import { prefersReducedMotion } from '../../composables/useReducedMotion'
import { t } from '../../i18n/translations'
import { scrollTo } from '../../scripts/smoothScroll'
import CategoryNav from '../common/CategoryNav.vue'
import SectionLabel from '../common/SectionLabel.vue'
@@ -16,72 +13,24 @@ const { locale = 'en', departments = [] } = defineProps<{
departments?: readonly Department[]
}>()
const activeCategory = ref('all')
const visibleDepartments = computed(() =>
departments.filter((d) => d.roles.length > 0)
)
const categories = computed(() =>
visibleDepartments.value.map((d) => ({ label: d.name, value: d.key }))
const categories = computed(() => [
{ label: 'ALL', value: 'all' },
...visibleDepartments.value.map((d) => ({ label: d.name, value: d.key }))
])
const filteredDepartments = computed(() =>
activeCategory.value === 'all'
? visibleDepartments.value
: visibleDepartments.value.filter((d) => d.key === activeCategory.value)
)
const hasRoles = computed(() => visibleDepartments.value.length > 0)
const activeCategory = ref('')
const sectionRefs = useTemplateRefsList<HTMLElement>()
let isScrolling = false
let pendingFrame = 0
const HEADER_OFFSET = -144
const ACTIVATION_OFFSET = 300
const deptElementId = (key: string) => `careers-dept-${key}`
function pickActiveSection() {
pendingFrame = 0
if (isScrolling) return
const sections = sectionRefs.value as HTMLElement[]
if (sections.length === 0) return
let active = sections[0]
for (const el of sections) {
if (el.getBoundingClientRect().top - ACTIVATION_OFFSET <= 0) {
active = el
} else {
break
}
}
activeCategory.value = active.id.replace(/^careers-dept-/, '')
}
function scheduleUpdate() {
if (pendingFrame !== 0) return
pendingFrame = requestAnimationFrame(pickActiveSection)
}
onMounted(pickActiveSection)
useEventListener('scroll', scheduleUpdate, { passive: true })
useEventListener('resize', scheduleUpdate, { passive: true })
function scrollToDepartment(deptKey: string) {
activeCategory.value = deptKey
isScrolling = true
const el = document.getElementById(deptElementId(deptKey))
if (!el) {
isScrolling = false
return
}
scrollTo(el, {
offset: HEADER_OFFSET,
duration: 0.8,
immediate: prefersReducedMotion(),
onComplete: () => {
isScrolling = false
pickActiveSection()
}
})
}
</script>
<template>
@@ -99,10 +48,9 @@ function scrollToDepartment(deptKey: string) {
</h2>
<CategoryNav
v-if="hasRoles"
v-model="activeCategory"
:categories="categories"
:model-value="activeCategory"
class="mt-4"
@update:model-value="scrollToDepartment"
/>
</div>
</div>
@@ -117,11 +65,9 @@ function scrollToDepartment(deptKey: string) {
</p>
<div
v-for="dept in visibleDepartments"
:id="deptElementId(dept.key)"
:ref="sectionRefs.set"
v-for="dept in filteredDepartments"
:key="dept.key"
class="mb-12 scroll-mt-24 last:mb-0 md:scroll-mt-36"
class="mb-12 last:mb-0"
>
<SectionLabel>
{{ dept.name }}

View File

@@ -1,11 +1,7 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import {
useEventListener,
useIntersectionObserver,
useTemplateRefsList
} from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { useIntersectionObserver, useTemplateRefsList } from '@vueuse/core'
import { computed, ref } from 'vue'
import type { Locale, TranslationKey } from '../../i18n/translations'
@@ -44,25 +40,13 @@ const activeSection = ref(sections[0]?.id ?? '')
const sectionRefs = useTemplateRefsList<HTMLElement>()
let isScrolling = false
let scrollSafetyTimer: ReturnType<typeof setTimeout> | undefined
const HEADER_OFFSET = -144
const BOTTOM_THRESHOLD_PX = 4
const SCROLL_SAFETY_MS = 1500
function clearScrollLock() {
isScrolling = false
if (scrollSafetyTimer !== undefined) {
clearTimeout(scrollSafetyTimer)
scrollSafetyTimer = undefined
}
}
useIntersectionObserver(
sectionRefs,
(entries) => {
if (isScrolling) return
if (isAtBottom()) return
let best: IntersectionObserverEntry | null = null
for (const entry of entries) {
if (!entry.isIntersecting) continue
@@ -74,39 +58,22 @@ useIntersectionObserver(
{ rootMargin: '-20% 0px -60% 0px' }
)
function isAtBottom(): boolean {
const scrollBottom = window.scrollY + window.innerHeight
return (
scrollBottom >= document.documentElement.scrollHeight - BOTTOM_THRESHOLD_PX
)
}
function activateLastIfAtBottom() {
if (isScrolling) return
if (!isAtBottom()) return
const lastId = sections[sections.length - 1]?.id
if (lastId) activeSection.value = lastId
}
onMounted(activateLastIfAtBottom)
useEventListener('scroll', activateLastIfAtBottom, { passive: true })
function scrollToSection(id: string) {
activeSection.value = id
clearScrollLock()
isScrolling = true
scrollSafetyTimer = setTimeout(clearScrollLock, SCROLL_SAFETY_MS)
const el = document.getElementById(id)
if (el) {
scrollTo(el, {
offset: HEADER_OFFSET,
duration: 0.8,
immediate: prefersReducedMotion(),
onComplete: clearScrollLock
onComplete: () => {
isScrolling = false
}
})
return
}
clearScrollLock()
isScrolling = false
}
</script>

View File

@@ -14,28 +14,23 @@ const logos = [
'Ubisoft'
]
const mobileRow1Logos = logos.slice(0, 6)
const mobileRow2Logos = logos.slice(6)
const desktopLogos = Array.from({ length: 4 }, () => logos).flat()
const row1 = logos.slice(0, 6)
const mobileRow1 = [...row1, ...row1]
const row2 = logos.slice(6)
const mobileRow2 = [...row2, ...row2]
</script>
<template>
<section class="overflow-hidden py-12">
<!-- Single row on desktop -->
<div data-testid="social-proof-desktop" class="hidden w-max gap-2 md:flex">
<div class="animate-marquee hidden items-center gap-2 md:flex">
<div
v-for="copy in 2"
:key="copy"
class="animate-marquee flex shrink-0 items-center gap-2"
style="--marquee-gap: 0.5rem"
:aria-hidden="copy === 2 ? 'true' : undefined"
v-for="(logo, i) in desktopLogos"
:key="`${logo}-${i}`"
class="flex h-20 w-50 shrink-0 items-center justify-center"
>
<div
v-for="logo in logos"
:key="logo"
class="flex h-20 w-50 shrink-0 items-center justify-center"
>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
</div>
@@ -44,38 +39,22 @@ const mobileRow2Logos = logos.slice(6)
data-testid="social-proof-mobile"
class="flex flex-col gap-8 md:hidden"
>
<div class="flex w-max gap-8">
<div class="animate-marquee flex items-center gap-8">
<div
v-for="copy in 2"
:key="copy"
class="animate-marquee flex shrink-0 items-center gap-8"
style="--marquee-gap: 2rem"
:aria-hidden="copy === 2 ? 'true' : undefined"
v-for="(logo, i) in mobileRow1"
:key="`${logo}-${i}`"
class="flex h-14 w-40 shrink-0 items-center justify-center"
>
<div
v-for="logo in mobileRow1Logos"
:key="logo"
class="flex h-14 w-40 shrink-0 items-center justify-center"
>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
</div>
<div class="flex w-max gap-8">
<div class="animate-marquee-reverse flex items-center gap-8">
<div
v-for="copy in 2"
:key="copy"
class="animate-marquee-reverse flex shrink-0 items-center gap-8"
style="--marquee-gap: 2rem"
:aria-hidden="copy === 2 ? 'true' : undefined"
v-for="(logo, i) in mobileRow2"
:key="`${logo}-${i}`"
class="flex h-14 w-40 shrink-0 items-center justify-center"
>
<div
v-for="logo in mobileRow2Logos"
:key="logo"
class="flex h-14 w-40 shrink-0 items-center justify-center"
>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
<img :src="`/icons/clients/${logo}.svg`" :alt="logo" />
</div>
</div>
</div>

View File

@@ -8,12 +8,10 @@ import { t } from '../../i18n/translations'
const {
arcadeId,
title,
aspectRatio = 16 / 9,
locale = 'en'
} = defineProps<{
arcadeId: string
title: string
aspectRatio?: number
locale?: Locale
}>()
@@ -26,8 +24,7 @@ const loaded = ref(false)
:aria-label="t('demos.embed.label', locale)"
>
<div
class="relative mx-auto max-w-6xl overflow-hidden rounded-4xl border border-white/10"
:style="{ aspectRatio }"
class="relative mx-auto aspect-video max-w-6xl overflow-hidden rounded-4xl border border-white/10"
>
<div
v-if="!loaded"

View File

@@ -276,6 +276,29 @@ onUnmounted(() => {
fill="#211927"
/>
</g>
<!-- Left-edge fade -->
<rect
x="300"
y="150"
width="250"
height="900"
fill="url(#localHeroFadeLeft)"
/>
<defs>
<linearGradient
id="localHeroFadeLeft"
x1="550"
y1="600"
x2="300"
y2="600"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#211927" stop-opacity="0" />
<stop offset="1" stop-color="#211927" />
</linearGradient>
</defs>
</svg>
</div>

View File

@@ -15,14 +15,6 @@ interface Demo {
readonly transcript?: TranslationKey
readonly publishedDate: string
readonly modifiedDate: string
/**
* Width / height of the Arcade demo's source recording (e.g. 1.93 for a
* landscape screencast). Sizes the embed container to match so rounded
* corners hug the content instead of empty letterbox space. Source from
* Arcade's `_serializablePublicFlow.aspectRatio` (which is height/width —
* invert it). Defaults to 16/9 if omitted.
*/
readonly aspectRatio?: number
}
export const demos: readonly Demo[] = [
@@ -40,8 +32,7 @@ export const demos: readonly Demo[] = [
difficulty: 'beginner',
tags: ['templates', 'image', 'video'],
publishedDate: '2026-04-19',
modifiedDate: '2026-04-19',
aspectRatio: 1.931
modifiedDate: '2026-04-19'
},
{
slug: 'workflow-templates',
@@ -57,25 +48,7 @@ export const demos: readonly Demo[] = [
difficulty: 'beginner',
tags: ['getting-started', 'templates', 'workflow'],
publishedDate: '2026-04-19',
modifiedDate: '2026-04-19',
aspectRatio: 1.931
},
{
slug: 'community-workflows',
arcadeId: 'mqZh17oWDuWIyhK0xwEV',
category: 'demos.category.gettingStarted',
title: 'demos.community-workflows.title',
description: 'demos.community-workflows.description',
transcript: 'demos.community-workflows.transcript',
ogImage: '/images/demos/community-workflows-og.png',
thumbnail: '/images/demos/community-workflows-thumb.webp',
estimatedTime: 'demos.duration.2min',
durationIso: 'PT2M',
difficulty: 'beginner',
tags: ['getting-started', 'community', 'workflow', 'hub'],
publishedDate: '2026-05-04',
modifiedDate: '2026-05-04',
aspectRatio: 1.931
modifiedDate: '2026-04-19'
}
]

View File

@@ -3570,20 +3570,6 @@ const translations = {
'<ol><li><strong>打开模板浏览器</strong> — 点击 ComfyUI 侧栏中的模板图标。</li><li><strong>浏览分类</strong> — 模板按任务分类:图像生成、视频、放大等。</li><li><strong>预览模板</strong> — 将鼠标悬停在模板上查看预览。</li><li><strong>加载并自定义</strong> — 点击加载模板,然后修改参数。</li></ol>'
},
'demos.community-workflows.title': {
en: 'Explore and Use a Community Workflow from the Hub',
'zh-CN': '探索并使用社区工作流'
},
'demos.community-workflows.description': {
en: 'Discover how to find and get started with popular community workflows for generative AI projects.',
'zh-CN': '了解如何查找并使用流行的社区工作流来构建生成式 AI 项目。'
},
'demos.community-workflows.transcript': {
en: '<ol><li><strong>Open the Workflow Hub</strong> — From the ComfyUI sidebar, navigate to the community Workflow Hub to browse curated and trending workflows shared by the community.</li><li><strong>Browse popular workflows</strong> — Explore featured projects sorted by popularity, recency, and category to find one that matches your goal.</li><li><strong>Preview a workflow</strong> — Click a workflow card to see example outputs, required models, and a description of what it produces.</li><li><strong>Open in ComfyUI</strong> — Use the "Get Started" action to load the selected community workflow directly onto your canvas.</li><li><strong>Run and customize</strong> — Queue the workflow to generate your first result, then tweak prompts, models, and parameters to make it your own.</li></ol>',
'zh-CN':
'<ol><li><strong>打开工作流中心</strong> — 在 ComfyUI 侧栏中,进入社区工作流中心,浏览社区分享的精选和热门工作流。</li><li><strong>浏览热门工作流</strong> — 按热度、时间和分类浏览精选项目,找到符合需求的工作流。</li><li><strong>预览工作流</strong> — 点击工作流卡片,查看示例输出、所需模型和功能描述。</li><li><strong>在 ComfyUI 中打开</strong> — 使用"开始使用"按钮,将选中的社区工作流直接加载到画布。</li><li><strong>运行并自定义</strong> — 排队执行工作流以生成首个结果,然后调整提示词、模型和参数。</li></ol>'
},
'demos.nav.nextDemo': { en: "What's Next", 'zh-CN': '下一个演示' },
'demos.nav.viewDemo': { en: 'View Demo', 'zh-CN': '查看演示' },
'demos.nav.allDemos': { en: 'All Demos', 'zh-CN': '所有演示' },

View File

@@ -121,7 +121,6 @@ const breadcrumbJsonLd = {
<ArcadeEmbed
arcadeId={demo.arcadeId}
title={title}
aspectRatio={demo.aspectRatio}
client:load
/>

View File

@@ -122,7 +122,6 @@ const breadcrumbJsonLd = {
<ArcadeEmbed
arcadeId={demo.arcadeId}
title={title}
aspectRatio={demo.aspectRatio}
locale="zh-CN"
client:load
/>

View File

@@ -101,13 +101,13 @@
transform: translateX(0);
}
100% {
transform: translateX(calc(-100% - var(--marquee-gap, 0px)));
transform: translateX(-50%);
}
}
@keyframes marquee-reverse {
0% {
transform: translateX(calc(-100% - var(--marquee-gap, 0px)));
transform: translateX(-50%);
}
100% {
transform: translateX(0);
@@ -115,15 +115,11 @@
}
@utility animate-marquee {
@media (prefers-reduced-motion: no-preference) {
animation: marquee 30s linear infinite;
}
animation: marquee 30s linear infinite;
}
@utility animate-marquee-reverse {
@media (prefers-reduced-motion: no-preference) {
animation: marquee-reverse 30s linear infinite;
}
animation: marquee-reverse 30s linear infinite;
}
@keyframes ripple-effect {

View File

@@ -1,42 +0,0 @@
{
"last_node_id": 10,
"last_link_id": 10,
"nodes": [
{
"id": 10,
"type": "LoadImage",
"pos": [50, 50],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["this-image-does-not-exist-deadbeef.png", "image"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -82,7 +82,7 @@ export class Topbar {
}
getSaveDialog(): Locator {
return this.page.getByRole('dialog').getByRole('textbox')
return this.page.locator('.p-dialog-content input')
}
saveWorkflow(workflowName: string): Promise<void> {
@@ -116,9 +116,9 @@ export class Topbar {
// Check if a confirmation dialog appeared (e.g., "Overwrite existing file?")
// If so, return early to let the test handle the confirmation
const confirmationDialog = this.page
.getByRole('dialog')
.filter({ hasText: 'Overwrite' })
const confirmationDialog = this.page.locator(
'.p-dialog:has-text("Overwrite")'
)
if (await confirmationDialog.isVisible()) {
return
}

View File

@@ -127,7 +127,9 @@ export class BuilderSelectHelper {
await popoverTrigger.click()
await this.page.getByText('Rename', { exact: true }).click()
const dialogInput = this.page.getByRole('dialog').getByRole('textbox')
const dialogInput = this.page.locator(
'.p-dialog-content input[type="text"]'
)
await dialogInput.fill(newName)
await this.page.keyboard.press('Enter')
await dialogInput.waitFor({ state: 'hidden' })

View File

@@ -1,10 +1,6 @@
import type { WebSocketRoute } from '@playwright/test'
import type {
NodeError,
NodeProgressState,
PromptResponse
} from '@/schemas/apiSchema'
import type { NodeError, PromptResponse } from '@/schemas/apiSchema'
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
@@ -234,16 +230,6 @@ export class ExecutionHelper {
)
}
/** Send `progress_state` WS event with per-node execution state. */
progressState(jobId: string, nodes: Record<string, NodeProgressState>): void {
this.requireWs().send(
JSON.stringify({
type: 'progress_state',
data: { prompt_id: jobId, nodes }
})
)
}
/**
* Complete a job by adding it to mock history, sending execution_success,
* and triggering a history refresh via a status event.

View File

@@ -18,7 +18,9 @@ export class NodeOperationsHelper {
public readonly promptDialogInput: Locator
constructor(private comfyPage: ComfyPage) {
this.promptDialogInput = this.page.getByRole('dialog').getByRole('textbox')
this.promptDialogInput = this.page.locator(
'.p-dialog-content input[type="text"]'
)
}
private get page() {

View File

@@ -14,7 +14,6 @@ export class VueNodeFixture {
public readonly collapseIcon: Locator
public readonly root: Locator
public readonly widgets: Locator
public readonly imagePreview: Locator
constructor(private readonly locator: Locator) {
this.header = locator.locator('[data-testid^="node-header-"]')
@@ -26,7 +25,6 @@ export class VueNodeFixture {
this.collapseIcon = this.collapseButton.locator('i')
this.root = locator
this.widgets = this.locator.locator('.lg-node-widget')
this.imagePreview = locator.locator('.image-preview')
}
async getTitle(): Promise<string> {

View File

@@ -229,9 +229,9 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
// The dialog appearing proves the keybinding was intercepted by the app.
await comfyPage.keyboard.press('Control+s')
// The Save As dialog should appear
const saveDialog = comfyPage.page.getByRole('dialog')
await expect(saveDialog).toBeVisible()
// The Save As dialog should appear (p-dialog overlay)
const dialogOverlay = comfyPage.page.locator('.p-dialog-mask')
await expect(dialogOverlay).toBeVisible()
// Dismiss the dialog
await comfyPage.keyboard.press('Escape')

View File

@@ -16,9 +16,9 @@ async function saveAndOpenPublishDialog(
workflowName: string
): Promise<void> {
await comfyPage.menu.topbar.saveWorkflow(workflowName)
const overwriteDialog = comfyPage.page
.getByRole('dialog')
.filter({ hasText: 'Overwrite' })
const overwriteDialog = comfyPage.page.locator(
'.p-dialog:has-text("Overwrite")'
)
// Bounded wait: point-in-time isVisible() can miss dialogs that open
// slightly after saveWorkflow() resolves.
try {

View File

@@ -8,9 +8,6 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const DEPRECATED_NODE_TYPE = 'ImageBatch'
const API_NODE_TYPE = 'FluxProUltraImageNode'
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
@@ -144,73 +141,3 @@ test.describe(
})
}
)
for (const vueEnabled of [false, true] as const) {
const renderer = vueEnabled ? 'vue' : 'classic'
const tag = vueEnabled
? ['@vue-nodes', '@screenshot', '@node']
: ['@screenshot', '@node']
test.describe(`Node lifecycle badge (${renderer})`, { tag }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Graph.CanvasInfo', false)
})
for (const mode of [NodeBadgeMode.ShowAll, NodeBadgeMode.None] as const) {
test(`renders deprecated node with mode=${mode}`, async ({
comfyPage
}) => {
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeLifeCycleBadgeMode',
mode
)
await comfyPage.nodeOps.clearGraph()
await comfyPage.nodeOps.addNode(DEPRECATED_NODE_TYPE, undefined, {
x: 100,
y: 100
})
await comfyPage.canvasOps.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`node-lifecycle-${mode}-${renderer}.png`
)
})
}
})
test.describe(`API pricing badge (${renderer})`, { tag }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Graph.CanvasInfo', false)
await comfyPage.page.evaluate((type) => {
const registered = window.LiteGraph!.registered_node_types[type] as {
nodeData?: { price_badge?: unknown }
}
if (!registered?.nodeData) throw new Error(`No nodeData for ${type}`)
registered.nodeData.price_badge = {
engine: 'jsonata',
expr: "{'type': 'text', 'text': '99.9 credits/Run'}",
depends_on: { widgets: [], inputs: [], input_groups: [] }
}
}, API_NODE_TYPE)
})
for (const enabled of [true, false] as const) {
test(`renders api node with showApiPricing=${enabled}`, async ({
comfyPage
}) => {
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.ShowApiPricing',
enabled
)
await comfyPage.nodeOps.clearGraph()
await comfyPage.nodeOps.addNode(API_NODE_TYPE, undefined, {
x: 100,
y: 100
})
await comfyPage.canvasOps.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`api-pricing-${enabled ? 'on' : 'off'}-${renderer}.png`
)
})
}
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -21,8 +21,9 @@ test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
})
const nodeId = String(loadImageNode.id)
const { imagePreview } =
await comfyPage.vueNodes.getFixtureByTitle('Load Image')
const imagePreview = comfyPage.vueNodes
.getNodeLocator(nodeId)
.locator('.image-preview')
await expect(imagePreview).toBeVisible()
await expect(imagePreview.locator('img')).toBeVisible({ timeout: 30_000 })
@@ -43,25 +44,6 @@ test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
await expect(comfyPage.page.locator('.mask-editor-dialog')).toBeVisible()
})
test('hides mask and download buttons when image is missing', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'widgets/load_image_widget_missing_file'
)
const { imagePreview } =
await comfyPage.vueNodes.getFixtureByTitle('Load Image')
await expect(imagePreview).toBeVisible()
await expect(imagePreview.getByTestId('error-loading-image')).toBeVisible()
await imagePreview.getByRole('region').hover()
await expect(imagePreview.getByLabel('Edit or mask image')).toHaveCount(0)
await expect(imagePreview.getByLabel('Download image')).toHaveCount(0)
})
test('shows image context menu options', async ({ comfyPage }) => {
const { nodeId } = await loadImageOnNode(comfyPage)

View File

@@ -1,211 +0,0 @@
import type { WebSocketRoute } from '@playwright/test'
import { mergeTests } from '@playwright/test'
import type { z } from 'zod'
import {
comfyExpect as expect,
comfyPageFixture
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'
import { webSocketFixture } from '@e2e/fixtures/ws'
import type {
RawJobListItem,
zJobsListResponse
} from '@/platform/remote/comfyui/jobs/jobTypes'
type JobsListResponse = z.infer<typeof zJobsListResponse>
const test = mergeTests(comfyPageFixture, webSocketFixture)
const KSAMPLER_NODE = '3'
const EXECUTING_CLASS = /outline-node-stroke-executing/
const QUEUE_ROUTE = /\/api\/jobs\?[^/]*status=in_progress,pending/
const HISTORY_ROUTE = /\/api\/jobs\?[^/]*status=completed/
function jobsResponse(jobs: RawJobListItem[]): JobsListResponse {
return {
jobs,
pagination: { offset: 0, limit: 200, total: jobs.length, has_more: false }
}
}
async function mockJobsRoute(
comfyPage: ComfyPage,
pattern: RegExp,
body: string,
status: number = 200
): Promise<() => number> {
let count = 0
await comfyPage.page.route(pattern, async (route) => {
count += 1
await route.fulfill({
status,
contentType: 'application/json',
body
})
})
return () => count
}
const emptyJobsBody = JSON.stringify(jobsResponse([]))
type Scenario = {
name: string
/** Built per-test so it can incorporate the runtime-assigned jobId. */
queueBody: (jobId: string) => string
/** Whether the active job state should still be reflected after reconnect. */
expectsActiveAfter: boolean
}
const scenarios: Scenario[] = [
{
name: 'clears stale active job when queue is empty after reconnect',
queueBody: () => emptyJobsBody,
expectsActiveAfter: false
},
{
name: 'preserves active job when the job is still in the queue',
queueBody: (jobId) =>
JSON.stringify(
jobsResponse([
{ id: jobId, status: 'in_progress', create_time: Date.now() }
])
),
expectsActiveAfter: true
}
]
/**
* Stub the queue/history endpoints per `scenario`, close the WS, and wait
* for the auto-reconnect to issue a fresh queue fetch.
*/
async function triggerReconnect(
comfyPage: ComfyPage,
ws: WebSocketRoute,
scenario: Scenario,
jobId: string
): Promise<void> {
await mockJobsRoute(comfyPage, HISTORY_ROUTE, emptyJobsBody)
const queueFetches = await mockJobsRoute(
comfyPage,
QUEUE_ROUTE,
scenario.queueBody(jobId)
)
const fetchesBeforeClose = queueFetches()
await ws.close()
await expect.poll(queueFetches).toBeGreaterThan(fetchesBeforeClose)
}
test.describe('WebSocket reconnect with stale job', { tag: '@ui' }, () => {
test.describe('app mode skeleton', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.appMode.enterAppModeWithInputs([[KSAMPLER_NODE, 'seed']])
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
})
for (const scenario of scenarios) {
test(scenario.name, async ({ comfyPage, getWebSocket }) => {
const ws = await getWebSocket()
const exec = new ExecutionHelper(comfyPage, ws)
const jobId = await exec.run()
exec.executionStart(jobId)
// Skeleton visibility is the deterministic sync point: it appears
// once both `storeJob` (HTTP) and `executionStart` (WS) have been
// processed, regardless of arrival order.
const firstSkeleton = comfyPage.appMode.outputHistory.skeletons.first()
await expect(firstSkeleton).toBeVisible()
await triggerReconnect(comfyPage, ws, scenario, jobId)
if (scenario.expectsActiveAfter) {
await expect(firstSkeleton).toBeVisible()
} else {
await expect(comfyPage.appMode.outputHistory.skeletons).toHaveCount(0)
}
})
}
test('preserves active job when the queue endpoint fails on reconnect', async ({
comfyPage,
getWebSocket
}) => {
const ws = await getWebSocket()
const exec = new ExecutionHelper(comfyPage, ws)
const jobId = await exec.run()
exec.executionStart(jobId)
const firstSkeleton = comfyPage.appMode.outputHistory.skeletons.first()
await expect(firstSkeleton).toBeVisible()
await mockJobsRoute(comfyPage, HISTORY_ROUTE, emptyJobsBody)
// Prime queueStore.runningTasks with the active job — a WS status
// event drives GraphView.onStatus -> queueStore.update().
const primer = await mockJobsRoute(
comfyPage,
QUEUE_ROUTE,
JSON.stringify(
jobsResponse([
{ id: jobId, status: 'in_progress', create_time: Date.now() }
])
)
)
exec.status(1)
await expect.poll(primer).toBeGreaterThanOrEqual(1)
// Swap to a failing handler so the reconnect-driven fetch 500s.
// The fix should preserve runningTasks from the priming call rather
// than overwriting it with empty/error state.
await comfyPage.page.unroute(QUEUE_ROUTE)
const failed = await mockJobsRoute(comfyPage, QUEUE_ROUTE, '{}', 500)
const before = failed()
await ws.close()
await expect.poll(failed).toBeGreaterThan(before)
await expect(firstSkeleton).toBeVisible()
})
})
test.describe('vue node executing class', { tag: '@vue-nodes' }, () => {
for (const scenario of scenarios) {
test(scenario.name, async ({ comfyPage, getWebSocket }) => {
const ws = await getWebSocket()
const exec = new ExecutionHelper(comfyPage, ws)
// The executing outline lives on the outer `[data-node-id]`
// container, not the inner wrapper.
const ksamplerNode = comfyPage.vueNodes.getNodeLocator(KSAMPLER_NODE)
await expect(ksamplerNode).toBeVisible()
const jobId = await exec.run()
exec.executionStart(jobId)
exec.progressState(jobId, {
[KSAMPLER_NODE]: {
value: 0,
max: 1,
state: 'running',
node_id: KSAMPLER_NODE,
display_node_id: KSAMPLER_NODE,
prompt_id: jobId
}
})
await expect(ksamplerNode).toHaveClass(EXECUTING_CLASS)
await triggerReconnect(comfyPage, ws, scenario, jobId)
if (scenario.expectsActiveAfter) {
await expect(ksamplerNode).toHaveClass(EXECUTING_CLASS)
} else {
await expect(ksamplerNode).not.toHaveClass(EXECUTING_CLASS)
}
})
}
})
})

View File

@@ -1,108 +0,0 @@
# 10. Deprecate Node-Level Serialization Control
Date: 2026-05-12
## Status
Accepted
## Context
The v2 extension API initially included `node.on('beforeSerialize', handler)` as a migration path from v1 patterns like `node.onSerialize` and `nodeType.prototype.serialize` patching. This allowed extensions to:
1. **Append extra fields** to the serialized node object
2. **Transform the entire serialized object** via a replace function
However, during design review (PR #12142), we questioned whether node-level serialization control is the right abstraction:
### The Problem
Node-level serialization control is fundamentally **wrong-layered**:
- **Extension state should live in widgets**, not as arbitrary fields on the node
- Widget-level `beforeSerialize` already handles all legitimate use cases
- Node-level hooks encourage storing extension state in ad-hoc `node.properties` or custom fields, which:
- Breaks the clean separation between framework concerns and extension concerns
- Creates hidden dependencies between serialization format and extension behavior
- Makes migration and format evolution harder
### v1 Usage Analysis
Touch-point audit of `nodeType.prototype.serialize` and `node.onSerialize` patterns in the wild:
| Use Case | Proper v2 Alternative |
| --------------------------- | --------------------------------------------------- |
| Store extension state | Use widget values with `beforeSerialize` |
| Persist per-instance config | Use `widget.setOption()``widget_options` sidecar |
| Add metadata for export | Use a dedicated extension state widget |
| Transform output format | Framework concern, not extension concern |
No use case requires node-level control that can't be better served by widget-level APIs.
## Decision
**Deprecate `node.on('beforeSerialize')`** — mark as `@deprecated` with clear guidance pointing to widget-level alternatives. Remove in v1.0.
Widget-level serialization control (`widget.on('beforeSerialize')`) remains fully supported as the correct abstraction.
### Migration Path
Extensions currently using `node.on('beforeSerialize')` should:
1. **Store state in widgets** instead of arbitrary node fields
2. **Use `widget.on('beforeSerialize')`** to control serialization per-widget
3. **Use `widget.setOption()`** for per-instance configuration
Example migration:
```ts
// BEFORE (v1 / deprecated v2)
node.on('beforeSerialize', (e) => {
e.data['my_extension_state'] = computeState()
})
// AFTER (recommended v2)
const stateWidget = node.addWidget('STRING', '_my_state', '', {
hidden: true,
serialize: true
})
stateWidget.on('beforeSerialize', (e) => {
e.setSerializedValue(JSON.stringify(computeState()))
})
```
### Implementation Steps
1. Add `@deprecated` tag to `node.on('beforeSerialize')` with migration guidance
2. Add console.warn when the deprecated event is used (dev mode only)
3. Update documentation to recommend widget-level patterns
4. Remove `NodeBeforeSerializeEvent` type and handler in v1.0
## Consequences
### Positive
- **Cleaner architecture**: Extension state flows through widgets, the designed data channel
- **Better debuggability**: Widget values are visible in workflow JSON at predictable locations
- **Easier migration**: Future format changes only need to consider widget serialization
- **Reduced API surface**: One less event type to maintain and document
### Negative
- **Migration burden**: Extensions using node-level serialization must refactor
- **Potential edge cases**: Some exotic use cases may require workarounds
### Risk Mitigation
- Deprecation warning gives extension authors runway to migrate
- Widget-level APIs are already more capable than node-level alternatives
- The `@deprecated` tag and docs provide clear migration path
## Notes
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 11 for the full discussion thread.
Related decisions:
- Widget-level `beforeSerialize` remains the primary extension serialization hook
- `setSerializeEnabled()` remains for simple static opt-out cases

View File

@@ -1,111 +0,0 @@
# 11. Immutability Enforcement via Fresh Copies
Date: 2026-05-12
## Status
Accepted
## Context
The extension API exposes collection-returning methods like `widgets()`, `inputs()`, `outputs()`, and object-returning methods like `getProperties()`. These methods need immutability guarantees to prevent extensions from accidentally or intentionally mutating internal state.
### The Problem
Without runtime immutability enforcement:
- Extensions could push items into `widgets()` array, corrupting internal state
- Mutations to returned objects would silently affect internal data
- Debugging would be difficult — state corruption could surface far from the mutation site
- Internal framework code might inadvertently rely on returned arrays being stable
TypeScript's `readonly` modifier and JSDoc annotations provide compile-time protection, but:
- JavaScript consumers have no protection
- Type assertions can bypass readonly
- Agent-generated code may not respect type hints
### Options Considered
| Option | Pros | Cons |
| ------------------------ | --------------------------------------------------------- | -------------------------------------------------------- |
| **1. `Object.freeze()`** | Runtime immutability, throws on mutation | Performance overhead, nested objects need deep freeze |
| **2. Return fresh copy** | Simple, functional style, no mutation affects source | Slight memory overhead, multiple calls = multiple arrays |
| **3. Proxy wrapper** | Helpful error messages, can intercept specific operations | Complexity, performance overhead, harder to debug |
| **4. TypeScript only** | Zero runtime cost | No protection for JS consumers, can be bypassed |
| **5. Private fields** | True encapsulation | Blocks read access too, not suitable for APIs |
## Decision
**Return fresh copies** (Option 2) for all collection-returning and object-returning methods in the extension API.
### Implementation Pattern
```ts
// CORRECT: Return fresh copy
widgets(): readonly WidgetHandle[] {
const container = world.getComponent(nodeId, WidgetComponentContainer)
return (container?.widgetIds ?? []).map(createWidgetHandle)
// Each call creates new array — mutations don't affect internal state
}
getProperties(): Record<string, unknown> {
return { ...world.getComponent(nodeId, NodeTypeKey)?.properties }
// Shallow copy — mutations don't affect source
}
```
### Scope
Apply this pattern to:
- `NodeHandle.widgets()` — returns fresh `WidgetHandle[]`
- `NodeHandle.inputs()` — returns fresh `SlotInfo[]`
- `NodeHandle.outputs()` — returns fresh `SlotInfo[]`
- `NodeHandle.getProperties()` — returns fresh `Record<string, unknown>`
- `WidgetHandle` methods that return objects (if any)
- Any future collection/object-returning methods
### Internal Callers
Framework-internal code must also use mutation APIs rather than mutating returned collections:
```ts
// WRONG: Mutating returned array
const widgets = node.widgets()
widgets.push(newWidget) // No effect on node!
// CORRECT: Use mutation API
node.addWidget(type, name, value, options)
```
## Consequences
### Positive
- **True immutability**: Mutations to returned data never affect internal state
- **Predictable behavior**: Each call returns fresh data reflecting current state
- **Simple mental model**: "This is your copy, do what you want with it"
- **JavaScript-safe**: Works regardless of TypeScript types
### Negative
- **Memory overhead**: Multiple calls create multiple arrays (usually negligible)
- **No mutation detection**: Extensions silently get isolated copies, won't know their mutations are ignored
- **Fresh reference each call**: Cannot use `===` to detect changes (use deep comparison or events)
### Mitigations
- Document that returned collections are snapshots
- Use events (`valueChange`, `propertyChange`) to observe changes
- The memory overhead is negligible for typical widget/slot counts
## Notes
This decision was made during design review of PR #12142 (ext-api foundation). See `design-review-12142.md` Topic 14 for the full discussion thread.
The alternative of `Object.freeze()` was rejected because:
- It requires deep freezing for nested objects
- Performance overhead for each call
- Fresh copies achieve the same goal more simply

View File

@@ -1,138 +0,0 @@
# 12. Pure Function Loader Pattern for Extension Registration
Date: 2026-05-12
## Status
Accepted
## Context
The v2 extension API needs a mechanism for extensions to register themselves with the runtime. Two broad approaches exist:
### Side-Effect Registration (Vue 2 Plugin Pattern)
```ts
// Extension self-registers at import time
import { app } from '@comfyorg/core'
app.use({
install(app) {
app.component('MyWidget', MyWidget)
app.directive('my-directive', myDirective)
}
})
```
Problems:
- **Import order matters**: If extension A depends on extension B being registered first, import order must be carefully managed
- **Hard to test**: Side effects at import time make mocking difficult; tests must manipulate module cache
- **Hard to tree-shake**: Bundlers can't eliminate unused extensions — the import executes
- **Timing coupling**: Registration and activation are conflated; can't collect extensions first, then activate later
### Pure Function + Loader Pattern
```ts
// Extension declares intent — no side effects
export default defineNode({
name: 'my-extension',
nodeTypes: ['MyNode'],
nodeCreated(handle) {
// ...
}
})
// App bootstrap activates all registered extensions
startExtensionSystem()
```
## Decision
**Adopt the pure function + loader pattern** for v2 extension registration.
### Implementation
```ts
// Extension Registry (data collection only)
const nodeExtensions: NodeExtensionOptions[] = []
export function defineNode(options: NodeExtensionOptions): void {
nodeExtensions.push(options)
}
// Loader (activation)
export function startExtensionSystem(): void {
const world = getWorld()
watch(
() => world.entitiesWith(NodeTypeKey),
(nodeEntityIds) => {
for (const id of nodeEntityIds) {
mountExtensionsForNode(id)
}
},
{ immediate: true }
)
}
```
### Key Properties
1. **Pure registration**: `defineNode()` has no side effects beyond pushing to an array. It doesn't touch the World, DOM, or any reactive state.
2. **Centralized activation**: `startExtensionSystem()` is called exactly once during app bootstrap. This single entry point controls when the extension system "goes live".
3. **Reactive mounting**: The loader watches the World for entity changes. Extensions are mounted/unmounted in response to ECS state, not imperative calls.
4. **Order independence**: Extensions can be defined in any order. The loader sorts by name (lexicographic, see D10b) for deterministic execution.
### Registration Flow
```
Extension files App bootstrap World
| | |
| defineNode({...}) | |
|--------------------->| |
| (push to array) | |
| | |
| | startExtensionSystem()
| |------------------>|
| | (watch for NodeType entities)
| | |
| | NodeType added |
| |<------------------|
| | |
| | mountExtensionsForNode(id)
| | (runs setup) |
```
## Consequences
### Positive
- **Testability**: Extensions are plain objects; tests can construct them without side effects. `_clearExtensionsForTesting()` resets state between tests.
- **Tree-shakeable**: Bundlers can eliminate unused extension files if their exports are never referenced.
- **Order independent**: No import order bugs — the loader handles activation order.
- **Lazy activation**: Registration is instant; activation only happens when `startExtensionSystem()` is called.
- **SSR friendly**: Pure functions don't execute browser-only code at import time.
### Negative
- **Manual bootstrap**: App must call `startExtensionSystem()` — forgetting it silently disables extensions.
- **Two-step mental model**: Developers must understand "register" vs "activate" phases.
### Mitigations
- App bootstrap is a well-defined location; the call is hard to miss.
- Clear documentation and starter templates include the bootstrap call.
- Dev-mode warnings if extensions are defined but the system never starts.
## Notes
This pattern aligns with modern framework conventions:
- **Vite plugins**: `vite.config.ts` collects plugins as an array; Vite activates them at build time.
- **Vue 3 Composition API**: `setup()` returns reactive state; the framework activates it.
- **React hooks**: Pure functions declare effects; React schedules them.
The key insight is separating **declaration** (what do I want?) from **execution** (make it happen). This separation enables testing, lazy loading, and predictable behavior.

View File

@@ -8,19 +8,16 @@ An Architecture Decision Record captures an important architectural decision mad
## ADR Index
| ADR | Title | Status | Date |
| ---------------------------------------------------------- | ------------------------------------------ | -------- | ---------- |
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
| [0010](0010-deprecate-node-level-serialization-control.md) | Deprecate Node-Level Serialization Control | Accepted | 2026-05-12 |
| [0011](0011-immutability-via-fresh-copies.md) | Immutability Enforcement via Fresh Copies | Accepted | 2026-05-12 |
| [0012](0012-pure-function-loader-pattern.md) | Pure Function Loader Pattern | Accepted | 2026-05-12 |
| ADR | Title | Status | Date |
| -------------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
## Creating a New ADR

View File

@@ -1,93 +0,0 @@
# Research: Canvas vs Client/Pixel Coordinate Usage
Date: 2026-05-12
## Question
How should the extension API handle coordinate systems? Should it expose canvas coordinates, screen/client coordinates, or both?
## Coordinate Systems in ComfyUI
### 1. Canvas Space (Logical Units)
Node positions and sizes are in canvas logical units:
- Independent of zoom/pan
- `[0, 0]` is the canvas origin
- Moving a node to `[100, 200]` places it at canvas position (100, 200) regardless of viewport state
### 2. Screen/Client Space (Pixels)
DOM elements use pixel coordinates relative to the viewport:
- Affected by zoom/pan/scroll
- `clientX`/`clientY` from mouse events
- `getBoundingClientRect()` returns pixel values
### 3. Widget Height (Pixels)
DOM widgets reserve height in pixels:
```ts
addDOMWidget({ name: 'preview', element: img, height: 200 }) // 200px
```
## Current Extension API
| Method | Coordinate System | Notes |
| -------------------------- | ----------------- | ----------------------------------------- |
| `getPosition()` | Canvas | Returns `[x, y]` in canvas units |
| `setPosition()` | Canvas | Accepts `[x, y]` in canvas units |
| `getSize()` | Canvas | Returns `[width, height]` in canvas units |
| `setSize()` | Canvas | Accepts `[width, height]` in canvas units |
| `addDOMWidget({ height })` | Pixels | Reserved height in pixels |
| `widget.setHeight(px)` | Pixels | Widget height in pixels |
## Analysis
### When Extensions Need Canvas Coordinates
1. **Node positioning**: Placing nodes relative to each other
2. **Layout algorithms**: Auto-arranging nodes in a pattern
3. **Collision detection**: Checking if nodes overlap
### When Extensions Need Screen Coordinates
1. **Custom overlays**: Drawing UI at a specific screen location
2. **Drag-and-drop from external sources**: Converting mouse position to canvas position
3. **Context menus**: Positioning menus near the cursor
### Current State
The extension API currently exposes:
- **Canvas coordinates** for node position/size — appropriate, as these are logical values
- **Pixel values** for DOM widget height — appropriate, as these are DOM measurements
**Missing**: No conversion helpers between canvas and screen coordinates.
## Recommendation
**The current approach is appropriate.** Extensions that manipulate node positions should work in canvas space. This is the natural abstraction — extensions shouldn't need to account for zoom/pan when laying out nodes.
### For Advanced Cases
Extensions needing coordinate conversion (e.g., custom overlays) should either:
1. **Use LiteGraph's existing transform utilities** (available on `app.canvas`)
2. **Access the transform state** via a future canvas API (not part of node/widget handles)
### Why Not Expose Conversion Helpers on NodeHandle?
- **Wrong abstraction level**: Coordinate conversion is a canvas concern, not a node concern
- **State dependency**: Conversion requires current zoom/pan state, which changes frequently
- **Rare use case**: Most extensions work entirely in canvas space
## Future Considerations
If multiple extensions need coordinate conversion, consider:
1. **Canvas API**: `canvas.screenToCanvas(point)` / `canvas.canvasToScreen(point)`
2. **Events with both coordinates**: `positionChanged` could include both canvas and screen positions
For now, no changes are needed — the current API serves the common cases well.

View File

@@ -1,93 +0,0 @@
# Research: DOM Widget Convergence with Base Widget
Date: 2026-05-12
## Question
Should DOM widgets be unified with base widgets, or kept as a separate concept?
## Current State
### Creation APIs
- `node.addWidget(type, name, value, options)` — creates a standard widget
- `node.addDOMWidget({ name, element, height })` — creates a DOM-backed widget
### Internal Implementation
Both use the same underlying `CreateWidget` command:
```ts
addWidget(type, name, defaultValue, options) {
return dispatch({ type: 'CreateWidget', widgetType: type, ... })
}
addDOMWidget(opts) {
return dispatch({ type: 'CreateWidget', widgetType: 'DOM', ... })
}
```
DOM widgets are just widgets with `widgetType: 'DOM'` and an element reference.
### Shared WidgetHandle Interface
Both widget types share the same `WidgetHandle` interface:
| Method | Standard Widget | DOM Widget |
| -------------------------------- | --------------- | ----------------------- |
| `entityId`, `name`, `widgetType` | ✓ | ✓ |
| `getValue()` / `setValue()` | ✓ (scalar) | ✓ (often unused) |
| `isHidden()` / `setHidden()` | ✓ | ✓ |
| `isDisabled()` / `setDisabled()` | ✓ | ✓ |
| `setHeight(px)` | no-op | ✓ (updates reservation) |
| `on('valueChange')` | ✓ | ✓ |
| `getOption()` / `setOption()` | ✓ | ✓ |
## Analysis
### Arguments FOR Full Convergence
1. **Single mental model**: Extensions learn one widget concept, not two.
2. **Consistent behavior**: All widgets appear in `node.widgets()`, serialize the same way.
3. **Simpler API surface**: Fewer methods to document and maintain.
### Arguments FOR Keeping Separate APIs
1. **Different ergonomics**: Standard widgets are data-driven (name, value, options); DOM widgets are element-driven (pass an HTMLElement).
2. **Type safety**: `addDOMWidget` can require `element: HTMLElement` at compile time; merging would make it optional with runtime checks.
3. **Clear intent**: Separate APIs signal different use cases.
## Recommendation
**Keep the current partial convergence.** The implementation is unified (`CreateWidget` command), but the creation APIs remain separate for ergonomic reasons.
### Rationale
1. **Creation differs, usage is unified.** Extensions create DOM widgets differently (need an element), but interact with them the same way (via `WidgetHandle`).
2. **Type safety is valuable.** `addDOMWidget({ element })` is clearer than `addWidget('DOM', name, null, { element })`.
3. **Already well-integrated.** DOM widgets appear in `node.widgets()`, get the same events, and use the same serialization infrastructure.
### What "Convergence" Means Here
The widgets are already converged at:
- **Entity level**: Same `WidgetEntityId` brand
- **Interface level**: Same `WidgetHandle` type
- **Command level**: Same `CreateWidget` command internally
The APIs are intentionally separate at:
- **Creation level**: `addWidget` vs `addDOMWidget`
This is the right split — unified where it matters (runtime behavior), separate where it improves DX (creation ergonomics).
## Future Considerations
If we add more widget creation patterns (e.g., `addCanvasWidget`, `addThreeJSWidget`), we might consider:
1. **Factory pattern**: `node.widgets.create('DOM', { element })` / `node.widgets.create('INT', { min, max })`
2. **Builder pattern**: `node.addWidget('DOM').withElement(el).withHeight(200).build()`
For now, two explicit methods (`addWidget`, `addDOMWidget`) serve the common cases well.

View File

@@ -1,112 +0,0 @@
# Research: Identity Encapsulation in the Extension API
Date: 2026-05-12
## Question
When do extensions need access to raw entity IDs (`NodeEntityId`, `WidgetEntityId`, `SlotEntityId`)? Should these be exposed or hidden?
## Current State
The v2 extension API exposes entity IDs as read-only properties:
```ts
interface NodeHandle {
readonly entityId: NodeEntityId
// ...
}
interface WidgetHandle {
readonly entityId: WidgetEntityId
// ...
}
interface SlotInfo {
readonly entityId: SlotEntityId
// ...
}
```
All IDs are **branded types** to prevent accidental mixing at compile time.
## Use Cases for Raw Entity IDs
### 1. Per-Instance State Mapping
Extensions maintaining external state per node:
```ts
const nodeCache = new Map<NodeEntityId, CachedData>()
defineNode({
name: 'my-cache-extension',
nodeCreated(handle) {
nodeCache.set(handle.entityId, computeExpensiveData())
onNodeRemoved(() => nodeCache.delete(handle.entityId))
}
})
```
### 2. Logging and Debugging
```ts
node.on('executed', (e) => {
console.log(`[${node.entityId}] Output:`, e.output)
})
```
### 3. Inter-Extension Communication
Extensions that need to coordinate across multiple nodes:
```ts
// Extension A stores data
globalState.set(nodeA.entityId, data)
// Extension B retrieves it
const data = globalState.get(nodeB.entityId)
```
### 4. External System Interop
Extensions integrating with analytics, debugging tools, or external services that need stable node identifiers.
## Analysis
### Arguments FOR Exposing Entity IDs
1. **Legitimate need exists** — The use cases above are real and common.
2. **Branded types prevent misuse** — Can't accidentally use `NodeEntityId` where `WidgetEntityId` is expected.
3. **Read-only access** — Extensions can't mutate the ID or corrupt internal state.
4. **Opaque value** — The format (`node:<graphUuid>:<localId>`) is an implementation detail; extensions should treat it as an opaque string.
### Arguments AGAINST Exposing Entity IDs
1. **Format coupling** — Extensions might parse the ID string and break if format changes.
2. **Internal detail leakage** — Knowing the ID scheme reveals ECS architecture.
3. **Future migration friction** — Changing ID representation requires careful deprecation.
### Mitigations
- **Document as opaque**: JSDoc clearly states IDs are opaque, not to be parsed.
- **Branded types**: TypeScript prevents misuse across entity categories.
- **Phase A format**: Current format includes graph UUID + local ID; this can evolve via semver.
## Recommendation
**Keep exposing entity IDs.** The use cases are legitimate, the branded types provide safety, and the read-only nature limits risk. Document that IDs are opaque strings — extensions should never parse or construct them.
### Guidelines for Extension Authors
1. **Use IDs only for keying** — Maps, Sets, logging, external system references.
2. **Never parse IDs** — The format is an implementation detail subject to change.
3. **Prefer handles over IDs** — When passing references between functions, use the handle object, not the raw ID.
4. **Clean up on removal** — Always use `onNodeRemoved()` to clean up Maps keyed by entityId.
## Future Considerations
If the ID format needs to change significantly, the branded types allow us to:
1. Introduce a new branded type (e.g., `NodeEntityIdV2`)
2. Deprecate the old ID with migration guidance
3. Keep both supported during a transition period

View File

@@ -1,121 +0,0 @@
# Research: Serialization Context Simplification
Date: 2026-05-12
## Question
Can the serialization context be simplified from 4 values to fewer?
Current contexts:
- `'workflow'` — saving workflow to disk
- `'prompt'` — queueing a run (API call)
- `'clone'` — copy/paste operation
- `'subgraph-promote'` — widget becoming subgraph IO
## Use Case Analysis
### Context: 'workflow'
**Purpose**: Full persistence of user's work.
**What extensions need**: Serialize everything the user configured.
**Example**: A widget storing user preferences needs to include all settings.
### Context: 'prompt'
**Purpose**: Sending data to the backend for execution.
**What extensions need**:
- Transform values (dynamic prompts → resolved text)
- Skip preview-only widgets
- Materialize async sources (webcam → frame data)
**Example**:
```ts
widget.on('beforeSerialize', async (e) => {
if (e.context === 'prompt') {
e.setSerializedValue(await captureFrame())
}
})
```
### Context: 'clone'
**Purpose**: Copy/paste should yield independent copy.
**What extensions need**: Reset instance-specific state while keeping user settings.
**Example**: A random seed widget might want a new seed on paste.
### Context: 'subgraph-promote'
**Purpose**: Widget becomes an input/output on a subgraph.
**What extensions need**: Convert internal representation to subgraph IO format.
**Example**: Internal state becomes an exposed parameter.
## Simplification Options
### Option A: Keep All 4 (Current State)
| Pro | Con |
| ---------------------------------------- | ----------------- |
| Each context has distinct semantics | 4 cases to handle |
| Type system enforces valid values | More complex API |
| Clear intent for each serialization path | |
### Option B: Collapse to 2 ('persist' | 'execute')
```ts
context: 'persist' | 'execute'
// 'persist' = workflow, clone, subgraph-promote
// 'execute' = prompt
```
| Pro | Con |
| ------------------------------------------ | ------------------------------- |
| Simpler mental model | Loses clone/promote distinction |
| Most extensions only care about this split | Can't reset seed on clone |
### Option C: Remove Context Entirely
Extensions always transform regardless of context. The framework handles differences.
| Pro | Con |
| ---------------------------- | ---------------------------------------------- |
| Simplest API | Loses control for edge cases |
| Framework handles all nuance | Some extensions need context-specific behavior |
## Recommendation
**Keep all 4 contexts.** The use cases are genuinely different:
1. **workflow vs prompt**: Very common distinction. Dynamic prompts only process on prompt; preview widgets skip prompt. This is the most important split.
2. **clone**: Less common, but needed for stateful widgets (random seeds, generated IDs, captured frames).
3. **subgraph-promote**: Specialized, but necessary for the subgraph feature to work correctly.
### Rationale
- Extensions that don't care can ignore the context.
- Extensions that do care have the information they need.
- The 4 values map to 4 distinct operations in the framework.
- Collapsing contexts would remove functionality with no real simplification gain.
### Mitigation for Complexity
- Document common patterns clearly
- Most extensions only need: `if (context === 'prompt')`
- Provide examples in JSDoc
## Note on Deprecation
The `NodeBeforeSerializeEvent` is deprecated (ADR-0010). The `WidgetBeforeSerializeEvent` remains supported and uses the same 4 contexts.
Since node-level serialization is being removed, this research applies to widget-level serialization only.

View File

@@ -1,148 +0,0 @@
# Widget State Categories
Date: 2026-05-12
## Overview
Widget state in the v2 extension API is organized into distinct categories, each with different characteristics for mutability, persistence, and event handling.
## Categories
### 1. Identity (Read-Only Invariants)
Set at construction, never change.
| Property | Type | Notes |
| ------------ | ---------------- | ------------------------------------ |
| `entityId` | `WidgetEntityId` | Branded, stable for widget lifetime |
| `name` | `string` | Widget name as registered |
| `widgetType` | `string` | e.g., `'INT'`, `'STRING'`, `'COMBO'` |
| `label` | `string` | Display label, defaults to `name` |
**Constraints:**
- No setters exist for these properties
- Extensions cannot modify identity after creation
- Attempting to change identity is a design error
### 2. Value (First-Class, Every Widget)
The primary user-edited data.
| Method | Notes |
| ------------------- | ----------------------------------- |
| `getValue()` | Returns current value |
| `setValue(v)` | Dispatches `SetWidgetValue` command |
| `on('valueChange')` | Fires on value mutation |
**Constraints:**
- Type varies by widget type (`number` for INT, `string` for STRING, etc.)
- Persisted to `widgets_values` in workflow JSON
- Included in API prompt by default (unless `setSerializeEnabled(false)`)
- Changes are undo-able via command dispatch
### 3. Properties (First-Class, Every Widget)
Common properties all widgets share.
| Property | Getter | Setter | Event |
| ----------- | ---------------------- | ------------------------ | ---------------- |
| `hidden` | `isHidden()` | `setHidden(b)` | `propertyChange` |
| `disabled` | `isDisabled()` | `setDisabled(b)` | `propertyChange` |
| `serialize` | `isSerializeEnabled()` | `setSerializeEnabled(b)` | `propertyChange` |
**Constraints:**
- Boolean values only
- `hidden` affects UI visibility, not serialization
- `disabled` makes widget read-only in UI
- `serialize` controls inclusion in workflow/prompt output
- Changes fire `propertyChange`, not `valueChange`
### 4. Options Bag (Type-Specific)
Per-instance overrides for type-specific configuration.
| Method | Notes |
| ----------------------- | ---------------------------------------------- |
| `getOption(key)` | Returns per-instance override or class default |
| `setOption(key, value)` | Persists to `widget_options` sidecar |
| `on('optionChange')` | Fires on option mutation |
**Common options by widget type:**
| Widget Type | Options |
| ----------- | ---------------------------------- |
| INT, FLOAT | `min`, `max`, `step`, `precision` |
| STRING | `multiline`, `placeholder`, `rows` |
| COMBO | `values` |
**Constraints:**
- Options are JSON-serializable values
- Persisted separately from `widgets_values` (additive, backward-compatible)
- Extensions can add custom options
- Option keys should be documented per widget type
### 5. DOM-Specific
Properties unique to DOM widgets.
| Method | Notes |
| --------------- | ------------------------------------------ |
| `setHeight(px)` | Updates reserved height, triggers relayout |
**Constraints:**
- Only meaningful for `addDOMWidget()` widgets
- No-op for non-DOM widgets
- Measured in pixels (screen space)
- No event fired; relayout is automatic
## Category Interaction Rules
### Event Separation
Each category has its own event:
| Category | Event |
| ---------- | ---------------- |
| Value | `valueChange` |
| Properties | `propertyChange` |
| Options | `optionChange` |
**Rule**: Events do not cross categories. Changing `hidden` does not fire `valueChange`.
### Serialization Behavior
| Category | Serialization |
| ---------- | ---------------------------------------------------------------- |
| Identity | Not serialized (derived from node type) |
| Value | `widgets_values` array |
| Properties | `hidden`/`disabled` not persisted; `serialize` affects inclusion |
| Options | `widget_options` sidecar object |
### Mutability Summary
| Category | Mutable | Undo-able | Fires Event |
| ---------- | ------- | --------- | ---------------- |
| Identity | ✗ | — | — |
| Value | ✓ | ✓ | `valueChange` |
| Properties | ✓ | ✓ | `propertyChange` |
| Options | ✓ | ✓ | `optionChange` |
| DOM Height | ✓ | ✗ | — |
## Agent Implementation Notes
Agents working with widget state should:
1. **Respect category boundaries**: Don't try to `setValue()` to change visibility; use `setHidden()`.
2. **Use appropriate events**: Listen to `propertyChange` for UI state, `valueChange` for data.
3. **Handle type-specific options carefully**: Check widget type before accessing type-specific options.
4. **Preserve identity invariants**: Never try to change `entityId`, `name`, `widgetType`, or `label`.
5. **Consider serialization context**: Options persist to a sidecar; values persist to the main array.

View File

@@ -103,9 +103,7 @@ export default defineConfig([
projectService: {
allowDefaultProject: [
'vite.electron.config.mts',
'vite.types.config.mts',
'packages/extension-api/scripts/build-docs.ts',
'packages/extension-api/vite.config.mts'
'vite.types.config.mts'
]
}
}

1
global.d.ts vendored
View File

@@ -5,6 +5,7 @@ declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_CONFIG__: boolean
declare const __DEV_SERVER_COMFYUI_URL__: string
interface ImpactQueueFunction {
(...args: unknown[]): void

View File

@@ -9,10 +9,6 @@ const config: KnipConfig = {
'src/assets/css/style.css',
'src/scripts/ui/menu/index.ts',
'src/types/index.ts',
// Public extension API surface — published package entry point.
// Per AGENTS.md, this barrel is the explicit exception to the
// no-barrel-files-in-src rule because it IS the package entry.
'src/extension-api/index.ts',
'src/storybook/mocks/**/*.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}', '!.claude/**']
@@ -36,12 +32,6 @@ const config: KnipConfig = {
'packages/ingest-types': {
project: ['src/**/*.{js,ts}']
},
'packages/extension-api': {
// Build output is committed for npm package visibility
ignore: ['build/**'],
// typedoc is invoked via execSync in scripts/build-docs.ts
ignoreDependencies: ['typedoc']
},
'apps/website': {
entry: ['src/scripts/**/*.ts']
}
@@ -70,30 +60,7 @@ const config: KnipConfig = {
// Agent review check config, not part of the build
'.agents/checks/eslint.strict.config.js',
// Devtools extensions, included dynamically
'tools/devtools/web/**',
// Deprecated stub re-exporting from `@/extension-api`. Will be removed
// once PKG2 (`@comfyorg/extension-api`) ships and downstream imports
// migrate to the package path.
'src/types/extensionV2.ts',
// D18 Phase 1 scaffolding — empty registries the loader will populate
// in Phase 2 once side-effect registration moves out of
// extension-api-service. See decisions/D18-pure-functions-loader-registration.md.
'src/services/registries/**',
// D18 Phase 1 — brand symbol + isBrandedExtension guard. Currently
// consumed only by the define* call sites inside extension-api-service;
// the type-guard and getBrandKind are exported for the Phase 2 loader.
'src/extension-api/brand.ts',
// Strangler-pattern v2 conversions of core extensions. Not yet wired
// into the bootstrap (registration lands in a follow-up PR alongside
// the v1→v2 cut-over). Tracked by I-EXT (#12144).
'src/extensions/core/noteNode.v2.ts',
'src/extensions/core/rerouteNode.v2.ts',
'src/extensions/core/slotDefaults.v2.ts',
// W6.P3.D — defineWidget+mount showcase port (D-widget-converge / A12).
'src/extensions/core/webcamCapture.v2.ts',
// W6.P4.D — canvas-units canary + escape-hatch annotation example
// (D-coord-space / A13).
'src/extensions/core/coordSpaceDemo.v2.ts'
'tools/devtools/web/**'
],
vite: {
config: ['vite?(.*).config.mts']
@@ -112,15 +79,7 @@ const config: KnipConfig = {
tags: [
'-knipIgnoreUnusedButUsedByCustomNodes',
'-knipIgnoreUnusedButUsedByVueNodesBranch',
'-knipIgnoreUsedByStackedPR',
// Public API surface consumed externally by extension authors and the
// TypeDoc docgen pipeline (PKG2). Mark exports with @publicAPI when they
// are part of `@comfyorg/extension-api` but not internally referenced.
'-publicAPI',
// Per D20, the three *EntityId brand re-exports in src/extension-api/{node,widget}.ts
// are demoted to @internal — they stay available for internal package modules
// but are removed from the public barrel and from TypeDoc output.
'-internal'
'-knipIgnoreUsedByStackedPR'
]
}

View File

@@ -31,12 +31,7 @@ export default {
}
function formatAndEslint(fileNames: string[]) {
// Exclude package build directories from linting
const filtered = fileNames.filter(
(f) => !f.includes('/packages/') || !f.includes('/build/')
)
if (filtered.length === 0) return []
const joinedPaths = toJoinedRelativePaths(filtered)
const joinedPaths = toJoinedRelativePaths(fileNames)
return [
`pnpm exec oxfmt --write ${joinedPaths}`,
`pnpm exec oxlint --fix ${joinedPaths}`,

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.45.4",
"version": "1.45.1",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
@@ -48,7 +48,6 @@
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
"test:coverage": "vitest run --coverage",
"test:unit": "nx run test",
"test:extension-api": "[ -f vitest.extension-api.config.mts ] && vitest run --config vitest.extension-api.config.mts || echo 'SKIP: vitest.extension-api.config.mts not found'",
"typecheck": "vue-tsc --noEmit",
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
"typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck",

View File

@@ -1,2 +0,0 @@
docs-build/
node_modules/

View File

@@ -1,9 +0,0 @@
src/
scripts/
tsconfig*.json
typedoc.json
docs-build/
*.test.ts
*.spec.ts
__tests__/
node_modules/

View File

@@ -1,50 +0,0 @@
# @comfyorg/extension-api
> **Status**: scaffolded. Package implementation pending PKG3 — see
> `../../../plans/P2-extension-api-package.md` and
> `../../../plans/prompts/PKG3-npm-package.md` in the workspace root.
The official TypeScript declaration package for ComfyUI extensions. This
package replaces the practice of vendoring `comfy.d.ts` files in custom
node repos.
## Install (post-publish)
```bash
pnpm add -D @comfyorg/extension-api
```
```ts
import { defineExtension } from '@comfyorg/extension-api'
export default defineExtension({
name: 'MyExtension',
setup(ctx) {
ctx.onNodeMounted((node) => {
// ...
})
}
})
```
## Source
This package is built from the source-of-truth folder
`../../src/extension-api/`. Do not edit the package's `build/` output
directly.
## Versioning
- `0.x.y` — experimental during parallel-paths transition (D6 Phase A).
- `1.0.0` — first stable release once D5/D6/D7/D8 are accepted and the
surface has stabilized.
- Breaking changes follow semver strictly from `1.0.0` onward.
## Cross-references
- `decisions/D6-parallel-paths-migration.md` — versioning rationale
- `plans/P2-extension-api-package.md` — package structure plan
- `plans/prompts/PKG3-npm-package.md` — implementation prompt
- `plans/prompts/PKG4-ci-workflows.md` — publish workflow
- `plans/prompts/PKG5-docgen-mdx.md` — docgen pipeline
- `plans/prompts/PKG6-docs-comfy-org.md` — docs.comfy.org integration

View File

@@ -1,2 +0,0 @@
index.js
index.js.map

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
{
"name": "@comfyorg/extension-api",
"version": "0.1.0",
"description": "Official TypeScript extension API for ComfyUI custom nodes",
"files": [
"build",
"README.md"
],
"type": "module",
"types": "./build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"import": "./build/index.js",
"default": "./build/index.js"
}
},
"scripts": {
"typecheck": "vite build --logLevel warn",
"build": "vite build",
"docs:build": "tsx scripts/build-docs.ts",
"docs:watch": "tsx scripts/build-docs.ts --watch"
},
"devDependencies": {
"tsx": "catalog:",
"typedoc": "0.28.19",
"typedoc-plugin-markdown": "^4.6.3",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-dts": "catalog:"
},
"peerDependencies": {
"vue": "catalog:"
},
"nx": {
"tags": [
"scope:shared",
"type:api"
]
}
}

View File

@@ -1,495 +0,0 @@
#!/usr/bin/env tsx
/* eslint-disable no-console -- CLI build script; stdout progress is intentional */
/**
* PKG5 docgen pipeline: TypeDoc → Mintlify MDX
*
* Steps:
* 1. Run TypeDoc with typedoc-plugin-markdown to emit raw markdown into docs-build/raw/
* 2. Post-process each markdown file:
* - Add Mintlify frontmatter (title, description, sidebarTitle, icon)
* - Convert ``` fences without lang tag → ```ts
* - Replace raw [TypeName] cross-refs with MDX relative links
* - Wrap @example blocks in proper code fences
* 3. Write final .mdx files to docs-build/mintlify/
* 4. Emit docs-build/mintlify/nav-snippet.json — merges into docs.comfy.org mint.json
*
* Run: pnpm --filter @comfyorg/extension-api docs:build
*/
import { execSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const pkgRoot = path.resolve(__dirname, '..')
const rawDir = path.join(pkgRoot, 'docs-build', 'raw')
const mintlifyDir = path.join(pkgRoot, 'docs-build', 'mintlify')
const watchMode = process.argv.includes('--watch')
// ── Page metadata ────────────────────────────────────────────────────────────
// Controls frontmatter for each generated page. Key = TypeDoc output filename
// stem (lowercased). Unrecognised files get generic metadata.
interface PageMeta {
title: string
sidebarTitle?: string
description: string
icon?: string
group: 'core' | 'handles' | 'events' | 'shell' | 'identity' | 'root'
order: number
}
const PAGE_META: Record<string, PageMeta> = {
// Top-level overview
index: {
title: 'Extension API Overview',
description: 'TypeScript API reference for ComfyUI custom node extensions.',
icon: 'puzzle-piece',
group: 'root',
order: 0
},
// Lifecycle / registration
defineextension: {
title: 'defineExtension',
description:
'Register an app-scoped extension for init, setup, and shell UI contributions.',
icon: 'code',
group: 'core',
order: 1
},
definenodeextension: {
title: 'defineNodeExtension',
description:
'Register a node-scoped extension reacting to node lifecycle events.',
icon: 'code',
group: 'core',
order: 2
},
definewidgetextension: {
title: 'defineWidgetExtension',
description: 'Register a custom widget type with its own DOM rendering.',
icon: 'code',
group: 'core',
order: 3
},
extensionoptions: {
title: 'ExtensionOptions',
description:
'Options object for defineExtension — app-wide lifecycle and shell UI.',
group: 'core',
order: 4
},
nodeextensionoptions: {
title: 'NodeExtensionOptions',
description:
'Options object for defineNodeExtension — node lifecycle hooks.',
group: 'core',
order: 5
},
widgetextensionoptions: {
title: 'WidgetExtensionOptions',
description:
'Options object for defineWidgetExtension — custom widget rendering.',
group: 'core',
order: 6
},
onnoderemoved: {
title: 'onNodeRemoved',
sidebarTitle: 'onNodeRemoved',
description:
'Implicit-context lifecycle hook: fires when a node is removed from the graph.',
group: 'core',
order: 7
},
onnodemounted: {
title: 'onNodeMounted',
sidebarTitle: 'onNodeMounted',
description:
'Implicit-context lifecycle hook: fires when a node is fully mounted.',
group: 'core',
order: 8
},
// Handles
nodehandle: {
title: 'NodeHandle',
description:
'Controlled access to node state, mutations, slots, and events.',
icon: 'circle-nodes',
group: 'handles',
order: 10
},
widgethandle: {
title: 'WidgetHandle',
description: 'Controlled access to widget state, mutations, and events.',
icon: 'sliders',
group: 'handles',
order: 11
},
slotinfo: {
title: 'SlotInfo',
description: 'Read-only snapshot of a node slot (input or output).',
group: 'handles',
order: 12
},
// Events
nodeexecutedevent: {
title: 'NodeExecutedEvent',
description: 'Payload fired when a node finishes execution.',
group: 'events',
order: 20
},
nodeconnectedevent: {
title: 'NodeConnectedEvent',
description: 'Payload fired when a slot connection is made.',
group: 'events',
order: 21
},
nodedisconnectedevent: {
title: 'NodeDisconnectedEvent',
description: 'Payload fired when a slot connection is removed.',
group: 'events',
order: 22
},
nodepositionchangedevent: {
title: 'NodePositionChangedEvent',
description: 'Payload fired when a node is moved on the canvas.',
group: 'events',
order: 23
},
nodesizechangedevent: {
title: 'NodeSizeChangedEvent',
description: 'Payload fired when a node is resized.',
group: 'events',
order: 24
},
nodemodechangedevent: {
title: 'NodeModeChangedEvent',
description: 'Payload fired when a node execution mode changes.',
group: 'events',
order: 25
},
nodebeforeserializeevent: {
title: 'NodeBeforeSerializeEvent',
description: 'Pre-serialization hook payload — override or skip node data.',
group: 'events',
order: 26
},
widgetvaluechangeevent: {
title: 'WidgetValueChangeEvent',
description: 'Payload fired when a widget value changes.',
group: 'events',
order: 27
},
widgetbeforeserializeevent: {
title: 'WidgetBeforeSerializeEvent',
description:
'Pre-serialization hook payload — override or skip widget value.',
group: 'events',
order: 28
},
widgetbeforequeueevent: {
title: 'WidgetBeforeQueueEvent',
description:
'Pre-queue validation payload — call reject() to cancel queue.',
group: 'events',
order: 29
},
// Shell UI
sidebartabextension: {
title: 'SidebarTabExtension',
description: 'Register a custom sidebar tab.',
group: 'shell',
order: 40
},
bottompanelextension: {
title: 'BottomPanelExtension',
description: 'Register a custom bottom panel tab.',
group: 'shell',
order: 41
},
toastmanager: {
title: 'ToastManager',
description: 'Show toast notifications to the user.',
group: 'shell',
order: 42
},
commandmanager: {
title: 'CommandManager',
description: 'Register keyboard shortcuts and command palette entries.',
group: 'shell',
order: 43
},
extensionmanager: {
title: 'ExtensionManager',
description: 'Access shell UI registration APIs.',
group: 'shell',
order: 44
},
// Identity
nodelocatorid: {
title: 'NodeLocatorId',
description:
'Branded string ID that uniquely locates a node across graph snapshots.',
group: 'identity',
order: 50
},
nodeexecutionid: {
title: 'NodeExecutionId',
description: 'Branded string ID for a specific node execution run.',
group: 'identity',
order: 51
}
}
const GROUP_LABELS: Record<PageMeta['group'], string> = {
root: 'Extensions API',
core: 'Registration',
handles: 'Handles',
events: 'Events',
shell: 'Shell UI',
identity: 'Identity'
}
// ── Utilities ────────────────────────────────────────────────────────────────
function slug(stem: string): string {
return stem
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
function metaFor(stem: string): PageMeta {
const key = stem.toLowerCase().replace(/[^a-z]/g, '')
return (
PAGE_META[key] ?? {
title: stem,
description: `API reference for ${stem}.`,
group: 'core',
order: 99
}
)
}
/** Convert TypeDoc raw markdown to Mintlify-compatible MDX. */
function toMintlifyMdx(raw: string, stem: string): string {
const meta = metaFor(stem)
// Build frontmatter
const fm: string[] = [
`---`,
`title: "${meta.title}"`,
...(meta.sidebarTitle ? [`sidebarTitle: "${meta.sidebarTitle}"`] : []),
`description: "${meta.description}"`,
...(meta.icon ? [`icon: "${meta.icon}"`] : []),
`---`
]
let body = raw
// Strip TypeDoc breadcrumb header lines (e.g. "[**@comfyorg/...**](../index.md)\n\n***\n\n[@comfyorg...]...")
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\)\n+\*+\n+/gm, '')
body = body.replace(/^\[.*?\]\(\.\.\/index\.md\).*\n+/gm, '')
// Remove the TypeDoc-generated H1 (we use frontmatter title instead)
body = body.replace(/^# .+\n+/, '')
// Ensure opening code fences that have no lang tag get `ts`
// Only match a ``` that is immediately followed by a newline (opening fence),
// not a closing fence (which also has just ``` + newline but we can detect
// by context: opening fences follow non-fence lines; closing fences follow content).
// Simpler heuristic: replace ``` at start of line only when not already closing a block.
// We track state via a flag pass instead of a single regex.
let inBlock = false
body = body
.split('\n')
.map((line) => {
if (inBlock) {
if (line.trim() === '```') {
inBlock = false
return line
}
return line
}
if (line.startsWith('```')) {
if (line.trim() === '```') {
// bare opening fence → add ts
inBlock = true
return '```ts'
}
// has a lang tag already
inBlock = true
return line
}
return line
})
.join('\n')
// TypeDoc emits `typescript` lang tag; normalize to `ts`
body = body.replace(/^```typescript\b/gm, '```ts')
// Fix TypeDoc cross-ref links: [TypeName](../type-alias/TypeName.md) → relative MDX paths
// Pattern: [Label](../category/FileName.md) → [Label](./filename)
body = body.replace(
/\[([^\]]+)\]\(\.\.\/([\w-]+)\/([\w-]+)\.md\)/g,
(_match, label, _category, file) => `[${label}](./${slug(file)})`
)
// Same-dir links
body = body.replace(
/\[([^\]]+)\]\(([\w-]+)\.md\)/g,
(_match, label, file) => `[${label}](./${slug(file)})`
)
// TypeDoc wraps @example content in a "## Example" heading; Mintlify prefers
// code examples to be directly under prose without a sub-heading.
// Flatten "## Example\n\n```ts" → "```ts"
body = body.replace(/^## Example\s*\n+/gm, '')
// Stability tags: render as a <Tip> callout
body = body.replace(
/\*\*Stability\*\*: `(stable|experimental|deprecated)`/g,
(_match, level) => {
const label =
level === 'stable'
? '<Tip>**Stability:** Stable — part of the public API contract.</Tip>'
: level === 'experimental'
? '<Warning>**Stability:** Experimental — may change before 1.0.</Warning>'
: '<Warning>**Stability:** Deprecated — will be removed. See migration guide.</Warning>'
return label
}
)
// @stability TSDoc tag (appears as plain text after TypeDoc strips tags)
body = body.replace(
/^Stability: (stable|experimental|deprecated)\s*$/gm,
(_match, level) => {
if (level === 'stable') return '<Tip>**Stability:** Stable</Tip>'
if (level === 'experimental')
return '<Warning>**Stability:** Experimental</Warning>'
return '<Warning>**Stability:** Deprecated</Warning>'
}
)
return [...fm, '', body.trim(), ''].join('\n')
}
// ── Nav snippet builder ───────────────────────────────────────────────────────
interface NavPage {
group?: string
pages: (string | NavPage)[]
}
function buildNavSnippet(stems: string[]): NavPage {
// Sort stems by order then group by category
const sortedStems = stems
.slice()
.sort((a, b) => metaFor(a).order - metaFor(b).order)
const sortedByGroup: Record<string, string[]> = {}
for (const stem of sortedStems) {
const group = metaFor(stem).group
if (!sortedByGroup[group]) sortedByGroup[group] = []
sortedByGroup[group].push(`extensions/api/${slug(stem)}`)
}
const groupOrder: PageMeta['group'][] = [
'root',
'core',
'handles',
'events',
'shell',
'identity'
]
const pages: (string | NavPage)[] = []
// Overview at top level
if (sortedByGroup['root']) {
for (const p of sortedByGroup['root']) pages.push(p)
}
for (const grp of groupOrder) {
if (grp === 'root') continue
const grpPages = sortedByGroup[grp]
if (!grpPages?.length) continue
pages.push({ group: GROUP_LABELS[grp], pages: grpPages })
}
return { group: 'Extensions API', pages }
}
// ── Main pipeline ────────────────────────────────────────────────────────────
function runTypedoc(): void {
console.log('▶ Running TypeDoc...')
execSync(
`pnpm exec typedoc --options ${path.join(pkgRoot, 'typedoc.json')} --out ${rawDir}`,
{ cwd: pkgRoot, stdio: 'inherit' }
)
}
function processFiles(): void {
if (!fs.existsSync(rawDir)) {
throw new Error(`TypeDoc output directory not found: ${rawDir}`)
}
fs.mkdirSync(mintlifyDir, { recursive: true })
const mdFiles = fs
.readdirSync(rawDir, { recursive: true })
.filter((f): f is string => typeof f === 'string' && f.endsWith('.md'))
const stems: string[] = []
for (const relPath of mdFiles) {
const src = path.join(rawDir, relPath)
const stem = path.basename(relPath, '.md')
const raw = fs.readFileSync(src, 'utf8')
const mdx = toMintlifyMdx(raw, stem)
const destName = slug(stem) + '.mdx'
const dest = path.join(mintlifyDir, destName)
fs.writeFileSync(dest, mdx)
console.log(`${relPath} → mintlify/${destName}`)
stems.push(stem)
}
// Write nav snippet
const nav = buildNavSnippet(stems)
const navDest = path.join(mintlifyDir, 'nav-snippet.json')
fs.writeFileSync(navDest, JSON.stringify(nav, null, 2) + '\n')
console.log(` ✔ nav-snippet.json`)
console.log(`\n✅ Mintlify MDX written to: ${mintlifyDir}`)
console.log(` ${stems.length} pages + nav-snippet.json`)
}
function run(): void {
runTypedoc()
processFiles()
}
if (watchMode) {
// Simple watch: re-run on change to source files
console.log('👁 Watch mode — watching src/extension-api/**')
const srcDir = path.resolve(pkgRoot, '../../src/extension-api')
let debounce: ReturnType<typeof setTimeout> | null = null
run()
fs.watch(srcDir, { recursive: true }, () => {
if (debounce) clearTimeout(debounce)
debounce = setTimeout(() => {
console.log('\n🔄 Source changed — rebuilding...')
try {
run()
} catch (e) {
console.error(e)
}
}, 500)
})
} else {
run()
}

View File

@@ -1,17 +0,0 @@
/**
* @comfyorg/extension-api — Public Extension API for ComfyUI
*
* This is the package entry point compiled to `build/index.js` + `build/index.d.ts`.
* It is a single re-export of the canonical surface defined in
* `src/extension-api/index.ts` in the main app — that file is the one source
* of truth for what is part of the stable, semver-versioned public contract.
*
* Do NOT add exports here. Add them to `src/extension-api/index.ts` and they
* will flow through this barrel automatically.
*
* The tsconfig.json `paths` alias `@/*` → `../../src/*` resolves the import
* below at both typecheck and build time.
*
* @packageDocumentation
*/
export * from '@/extension-api/index'

View File

@@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2023", "ES2023.Array", "DOM", "DOM.Iterable"],
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"declaration": true,
"declarationMap": false,
"noEmit": false,
"outDir": "./build",
"paths": {
"@/*": ["../../src/*"],
"@/utils/formatUtil": [
"../../packages/shared-frontend-utils/src/formatUtil.ts"
],
"@/utils/networkUtil": [
"../../packages/shared-frontend-utils/src/networkUtil.ts"
]
}
},
"include": [
"../../src/**/*.ts",
"../../src/types/litegraph-augmentation.d.ts",
"../../global.d.ts"
],
"exclude": [
"../../src/**/*.test.ts",
"../../src/**/*.spec.ts",
"../../src/**/*.vue",
"**/*.test.ts",
"**/*.spec.ts",
"scripts/**"
]
}

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["../../src/*"]
}
},
"include": ["../../src/extension-api/**/*.ts"],
"exclude": [
"../../src/**/*.test.ts",
"../../src/**/*.spec.ts",
"../../src/**/*.vue"
]
}

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"noEmit": true,
"declaration": false,
"declarationMap": false
}
}

View File

@@ -1,45 +0,0 @@
{
"entryPoints": ["../../src/extension-api/index.ts"],
"tsconfig": "./tsconfig.docs.json",
"out": "./docs-build/raw",
"plugin": ["typedoc-plugin-markdown"],
"excludeInternal": true,
"excludePrivate": true,
"excludeProtected": true,
"readme": "none",
"skipErrorChecking": true,
"githubPages": false,
"blockTags": [
"@stability",
"@packageDocumentation",
"@example",
"@typeParam",
"@returns",
"@deprecated",
"@remarks"
],
"hideGenerator": true,
"useCodeBlocks": true,
"flattenOutputFiles": false,
"entryFileName": "index",
"fileExtension": ".md",
"outputFileStrategy": "members",
"hidePageHeader": false,
"hideBreadcrumbs": false,
"useHTMLAnchors": false,
"sanitizeComments": true,
"expandObjects": false,
"parametersFormat": "table",
"propertiesFormat": "table",
"typeDeclarationFormat": "table",
"indexFormat": "table",
"tableColumnSettings": {
"hideDefaults": false,
"hideInherited": false,
"hideModifiers": false,
"hideOverrides": false,
"hideSources": true,
"hideValues": false,
"leftAlignHeaders": false
}
}

View File

@@ -1,74 +0,0 @@
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
const here = fileURLToPath(new URL('.', import.meta.url))
const repoRoot = resolve(here, '..', '..')
const repoSrc = resolve(repoRoot, 'src')
const surfaceRoot = resolve(repoSrc, 'extension-api')
/**
* Library build for `@comfyorg/extension-api`.
*
* Per ADR D17 (PKG2 build strategy), the package is built from the canonical
* surface defined in the main app at `src/extension-api/index.ts`. Vite
* resolves the `@/*` aliases against the main app's `src/` directory and
* emits a single bundled `index.js` plus a single bundled `index.d.ts`.
*
* The package barrel at `packages/extension-api/src/index.ts` is the
* Vite entry point and re-exports `@/extension-api/index` — preserving
* "the barrel is the source of truth in main app `src/extension-api/`"
* intent in `packages/extension-api/AGENTS.md`.
*
* Vue is externalized as a peer dependency (per D6.1 Phase A — extension
* authors share the host app's Vue runtime).
*/
export default defineConfig({
resolve: {
alias: {
'@/utils/formatUtil': resolve(
repoRoot,
'packages/shared-frontend-utils/src/formatUtil.ts'
),
'@/utils/networkUtil': resolve(
repoRoot,
'packages/shared-frontend-utils/src/networkUtil.ts'
),
'@': repoSrc
}
},
build: {
outDir: resolve(here, 'build'),
emptyOutDir: true,
sourcemap: true,
target: 'es2022',
minify: false,
lib: {
// Build directly from the canonical surface in the main app — the
// package's own `src/index.ts` exists only as a documented entry
// point that re-exports the same surface, but we point Vite at the
// canonical file so dts paths line up cleanly with the JS bundle.
entry: resolve(surfaceRoot, 'index.ts'),
formats: ['es'],
fileName: () => 'index.js'
},
rollupOptions: {
// Vue is provided by the host app at runtime.
external: ['vue', /^@vue\//]
}
},
plugins: [
dts({
// Bundle all types into a single index.d.ts. This ensures the package
// is self-contained and doesn't reference paths outside build/.
rollupTypes: true,
outDir: resolve(here, 'build'),
tsconfigPath: resolve(here, 'tsconfig.build.json'),
logLevel: 'warn',
// Only include the extension-api surface, not the entire app
include: [resolve(surfaceRoot, '**/*.ts')]
})
]
})

View File

@@ -523,6 +523,10 @@ export type ImportPublishedAssetsRequest = {
* IDs of published assets (inputs and models) to import.
*/
published_asset_ids: Array<string>
/**
* The share ID of the published workflow these assets belong to. Required for authorization.
*/
share_id: string
}
/**

View File

@@ -310,7 +310,8 @@ export const zImportPublishedAssetsResponse = z.object({
* Request body for importing assets from a published workflow.
*/
export const zImportPublishedAssetsRequest = z.object({
published_asset_ids: z.array(z.string())
published_asset_ids: z.array(z.string().min(1).max(64)).max(1000),
share_id: z.string().min(1).max(64)
})
/**

View File

@@ -10057,8 +10057,6 @@ export interface components {
};
progress: number;
create_time: number;
/** @description Actual credits consumed by the task. Present once status is finalized; 0 for failed tasks. */
consumed_credit?: number;
};
TripoSuccessTask: {
/** @enum {integer} */

View File

@@ -3,14 +3,12 @@ import { describe, expect, it } from 'vitest'
import {
appendWorkflowJsonExt,
ensureWorkflowSuffix,
getFilePathSeparatorVariants,
getFilenameDetails,
getMediaTypeFromFilename,
getPathDetails,
highlightQuery,
isCivitaiModelUrl,
isPreviewableMediaType,
joinFilePath,
truncateFilename
} from './formatUtil'
@@ -85,11 +83,9 @@ describe('formatUtil', () => {
describe('video files', () => {
it('should identify video extensions correctly', () => {
expect(getMediaTypeFromFilename('video.mp4')).toBe('video')
expect(getMediaTypeFromFilename('apple.m4v')).toBe('video')
expect(getMediaTypeFromFilename('clip.webm')).toBe('video')
expect(getMediaTypeFromFilename('movie.mov')).toBe('video')
expect(getMediaTypeFromFilename('film.avi')).toBe('video')
expect(getMediaTypeFromFilename('episode.mkv')).toBe('video')
})
})
@@ -303,42 +299,6 @@ describe('formatUtil', () => {
})
})
describe('joinFilePath', () => {
it('joins subfolder and filename with normalized slash separators', () => {
expect(joinFilePath('nested\\folder', 'child\\file.png')).toBe(
'nested/folder/child/file.png'
)
})
it('trims boundary separators without changing the filename body', () => {
expect(joinFilePath('/nested/folder/', '/file.png')).toBe(
'nested/folder/file.png'
)
})
it('returns the normalized filename when no subfolder is provided', () => {
expect(joinFilePath('', 'nested\\file.png')).toBe('nested/file.png')
})
it('returns the normalized subfolder without a trailing slash when no filename is provided', () => {
expect(joinFilePath('nested\\folder', '')).toBe('nested/folder')
expect(joinFilePath('nested\\folder', null)).toBe('nested/folder')
})
})
describe('getFilePathSeparatorVariants', () => {
it('returns slash and backslash variants for nested paths', () => {
expect(getFilePathSeparatorVariants('nested\\folder/file.png')).toEqual([
'nested/folder/file.png',
'nested\\folder\\file.png'
])
})
it('returns a single value when no separator is present', () => {
expect(getFilePathSeparatorVariants('file.png')).toEqual(['file.png'])
})
})
describe('appendWorkflowJsonExt', () => {
it('appends .app.json when isApp is true', () => {
expect(appendWorkflowJsonExt('test', true)).toBe('test.app.json')

View File

@@ -256,31 +256,6 @@ export function isValidUrl(url: string): boolean {
}
}
export function joinFilePath(
subfolder: string | null | undefined,
filename: string | null | undefined
): string {
const normalizedSubfolder = normalizeFilePathSeparators(
subfolder ?? ''
).replace(/^\/+|\/+$/g, '')
const normalizedFilename = normalizeFilePathSeparators(
filename ?? ''
).replace(/^\/+/g, '')
if (!normalizedSubfolder) return normalizedFilename
if (!normalizedFilename) return normalizedSubfolder
return `${normalizedSubfolder}/${normalizedFilename}`
}
export function getFilePathSeparatorVariants(filepath: string): string[] {
const slashPath = normalizeFilePathSeparators(filepath)
const backslashPath = slashPath.replace(/\//g, '\\')
return slashPath === backslashPath ? [slashPath] : [slashPath, backslashPath]
}
function normalizeFilePathSeparators(filepath: string): string {
return filepath.replace(/[\\/]+/g, '/')
}
/**
* Parses a filepath into its filename and subfolder components.
*
@@ -299,7 +274,8 @@ export function parseFilePath(filepath: string): {
} {
if (!filepath?.trim()) return { filename: '', subfolder: '' }
const normalizedPath = normalizeFilePathSeparators(filepath)
const normalizedPath = filepath
.replace(/[\\/]+/g, '/') // Normalize path separators
.replace(/^\//, '') // Remove leading slash
.replace(/\/$/, '') // Remove trailing slash
@@ -581,7 +557,7 @@ const IMAGE_EXTENSIONS = [
'tiff',
'svg'
] as const
const VIDEO_EXTENSIONS = ['mp4', 'm4v', 'webm', 'mov', 'avi', 'mkv'] as const
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi'] as const
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac'] as const
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb', 'usdz'] as const
const TEXT_EXTENSIONS = [

420
pnpm-lock.yaml generated
View File

@@ -650,7 +650,7 @@ importers:
version: 22.6.1(@babel/traverse@7.29.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.6.1)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
'@nx/vite':
specifier: 'catalog:'
version: 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)
version: 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)
'@pinia/testing':
specifier: 'catalog:'
version: 1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))
@@ -662,7 +662,7 @@ importers:
version: 4.6.0
'@storybook/addon-docs':
specifier: 'catalog:'
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@storybook/addon-mcp':
specifier: 'catalog:'
version: 0.1.6(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
@@ -671,10 +671,10 @@ importers:
version: 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.13(typescript@5.9.3))
'@storybook/vue3-vite':
specifier: 'catalog:'
version: 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
version: 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@testing-library/jest-dom':
specifier: 'catalog:'
version: 6.9.1
@@ -704,7 +704,7 @@ importers:
version: 0.170.0
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
version: 6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.0.16(vitest@4.0.16)
@@ -842,19 +842,19 @@ importers:
version: 11.1.0
vite:
specifier: ^8.0.0
version: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
version: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-dts:
specifier: 'catalog:'
version: 4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-html:
specifier: 'catalog:'
version: 3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-vue-devtools:
specifier: 'catalog:'
version: 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
version: 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
vitest:
specifier: 'catalog:'
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue-component-type-helpers:
specifier: 'catalog:'
version: 3.2.6
@@ -912,10 +912,10 @@ importers:
devDependencies:
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
version: 6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
dotenv:
specifier: 'catalog:'
version: 16.6.1
@@ -927,13 +927,13 @@ importers:
version: 30.0.0(@babel/parser@7.29.0)(vue@3.5.13(typescript@5.9.3))
vite:
specifier: ^8.0.0
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-html:
specifier: 'catalog:'
version: 3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-vue-devtools:
specifier: 'catalog:'
version: 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
version: 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
vue-tsc:
specifier: 'catalog:'
version: 3.2.5(typescript@5.9.3)
@@ -982,16 +982,16 @@ importers:
version: 0.9.8(prettier@3.7.4)(typescript@5.9.3)
'@astrojs/vue':
specifier: 'catalog:'
version: 5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)
version: 5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.8.2)
'@playwright/test':
specifier: 'catalog:'
version: 1.58.1
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
version: 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
astro:
specifier: 'catalog:'
version: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0)
version: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2)
tailwindcss:
specifier: 'catalog:'
version: 4.2.0
@@ -1003,7 +1003,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
packages/design-system:
dependencies:
@@ -1030,31 +1030,6 @@ importers:
specifier: 'catalog:'
version: 5.9.3
packages/extension-api:
dependencies:
vue:
specifier: 'catalog:'
version: 3.5.13(typescript@5.9.3)
devDependencies:
tsx:
specifier: 'catalog:'
version: 4.19.4
typedoc:
specifier: 0.28.19
version: 0.28.19(typescript@5.9.3)
typedoc-plugin-markdown:
specifier: ^4.6.3
version: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
typescript:
specifier: 'catalog:'
version: 5.9.3
vite:
specifier: ^8.0.0
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-plugin-dts:
specifier: 'catalog:'
version: 4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
packages/ingest-types:
dependencies:
zod:
@@ -2456,9 +2431,6 @@ packages:
'@formkit/auto-animate@0.9.0':
resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==}
'@gerrit0/mini-shiki@3.23.0':
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==}
'@grpc/grpc-js@1.9.15':
resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==}
engines: {node: ^8.13.0 || >=10.10.0}
@@ -5481,10 +5453,6 @@ packages:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@@ -7656,9 +7624,6 @@ packages:
resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==}
engines: {node: '>=16.14'}
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -7899,10 +7864,6 @@ packages:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
@@ -9382,19 +9343,6 @@ packages:
typed-binary@4.3.2:
resolution: {integrity: sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ==}
typedoc-plugin-markdown@4.11.0:
resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==}
engines: {node: '>= 18'}
peerDependencies:
typedoc: 0.28.x
typedoc@0.28.19:
resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==}
engines: {node: '>= 18', pnpm: '>= 10'}
hasBin: true
peerDependencies:
typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x
typegpu@0.8.2:
resolution: {integrity: sha512-wkMJWhJE0pSkw2G/FesjqjbtHkREyOKu1Zmyj19xfmaX5+65YFwgfQNKSK8CxqN4kJkP7JFelLDJTSYY536TYg==}
engines: {node: '>=12.20.0'}
@@ -10238,11 +10186,6 @@ packages:
engines: {node: '>= 14.6'}
hasBin: true
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -10524,14 +10467,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@astrojs/vue@5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.9.0)':
'@astrojs/vue@5.1.4(@types/node@25.0.3)(astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2))(esbuild@0.27.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(vue@3.5.13(typescript@5.9.3))(yaml@2.8.2)':
dependencies:
'@vitejs/plugin-vue': 5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
'@vitejs/plugin-vue': 5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vue/compiler-sfc': 3.5.28
astro: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-plugin-vue-devtools: 7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
astro: 5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-vue-devtools: 7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
vue: 3.5.13(typescript@5.9.3)
transitivePeerDependencies:
- '@nuxt/kit'
@@ -11920,14 +11863,6 @@ snapshots:
'@formkit/auto-animate@0.9.0': {}
'@gerrit0/mini-shiki@3.23.0':
dependencies:
'@shikijs/engine-oniguruma': 3.23.0
'@shikijs/langs': 3.23.0
'@shikijs/themes': 3.23.0
'@shikijs/types': 3.23.0
'@shikijs/vscode-textmate': 10.0.2
'@grpc/grpc-js@1.9.15':
dependencies:
'@grpc/proto-loader': 0.7.13
@@ -12316,14 +12251,6 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor-model@7.33.1(@types/node@25.0.3)':
dependencies:
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.0
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.57.2(@types/node@24.10.4)':
dependencies:
'@microsoft/api-extractor-model': 7.33.1(@types/node@24.10.4)
@@ -12343,25 +12270,6 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.57.2(@types/node@25.0.3)':
dependencies:
'@microsoft/api-extractor-model': 7.33.1(@types/node@25.0.3)
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.0
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
'@rushstack/rig-package': 0.7.1
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
'@rushstack/ts-command-line': 5.3.1(@types/node@25.0.3)
diff: 8.0.3
lodash: 4.17.23
minimatch: 10.2.1
resolve: 1.22.11
semver: 7.5.4
source-map: 0.6.1
typescript: 5.8.2
transitivePeerDependencies:
- '@types/node'
'@microsoft/tsdoc-config@0.18.0':
dependencies:
'@microsoft/tsdoc': 0.16.0
@@ -12587,11 +12495,11 @@ snapshots:
- typescript
- verdaccio
'@nx/vite@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)':
'@nx/vite@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)':
dependencies:
'@nx/devkit': 22.6.1(nx@22.6.1)
'@nx/js': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)
'@nx/vitest': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)
'@nx/vitest': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)
'@phenomnomnominal/tsquery': 6.1.4(typescript@5.9.3)
ajv: 8.18.0
enquirer: 2.3.6
@@ -12599,8 +12507,8 @@ snapshots:
semver: 7.7.4
tsconfig-paths: 4.2.0
tslib: 2.8.1
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -12611,7 +12519,7 @@ snapshots:
- typescript
- verdaccio
'@nx/vitest@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vitest@4.0.16)':
'@nx/vitest@22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16)':
dependencies:
'@nx/devkit': 22.6.1(nx@22.6.1)
'@nx/js': 22.6.1(@babel/traverse@7.29.0)(nx@22.6.1)
@@ -12619,8 +12527,8 @@ snapshots:
semver: 7.7.4
tslib: 2.8.1
optionalDependencies:
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -13234,27 +13142,10 @@ snapshots:
optionalDependencies:
'@types/node': 24.10.4
'@rushstack/node-core-library@5.20.1(@types/node@25.0.3)':
dependencies:
ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0)
ajv-formats: 3.0.1(ajv@8.13.0)
fs-extra: 11.3.2
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.11
semver: 7.5.4
optionalDependencies:
'@types/node': 25.0.3
'@rushstack/problem-matcher@0.2.1(@types/node@24.10.4)':
optionalDependencies:
'@types/node': 24.10.4
'@rushstack/problem-matcher@0.2.1(@types/node@25.0.3)':
optionalDependencies:
'@types/node': 25.0.3
'@rushstack/rig-package@0.7.1':
dependencies:
resolve: 1.22.11
@@ -13268,14 +13159,6 @@ snapshots:
optionalDependencies:
'@types/node': 24.10.4
'@rushstack/terminal@0.22.1(@types/node@25.0.3)':
dependencies:
'@rushstack/node-core-library': 5.20.1(@types/node@25.0.3)
'@rushstack/problem-matcher': 0.2.1(@types/node@25.0.3)
supports-color: 8.1.1
optionalDependencies:
'@types/node': 25.0.3
'@rushstack/ts-command-line@5.3.1(@types/node@24.10.4)':
dependencies:
'@rushstack/terminal': 0.22.1(@types/node@24.10.4)
@@ -13285,15 +13168,6 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@rushstack/ts-command-line@5.3.1(@types/node@25.0.3)':
dependencies:
'@rushstack/terminal': 0.22.1(@types/node@25.0.3)
'@types/argparse': 1.0.38
argparse: 1.0.10
string-argv: 0.3.2
transitivePeerDependencies:
- '@types/node'
'@sec-ant/readable-stream@0.4.1': {}
'@sentry-internal/browser-utils@10.32.1':
@@ -13443,10 +13317,10 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@storybook/addon-docs@10.2.10(@types/react@19.1.9)(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@mdx-js/react': 3.1.1(@types/react@19.1.9)(react@19.2.4)
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@storybook/react-dom-shim': 10.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
react: 19.2.4
@@ -13472,25 +13346,25 @@ snapshots:
- '@tmcp/auth'
- typescript
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@storybook/builder-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
'@storybook/csf-plugin': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
ts-dedent: 2.2.0
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- esbuild
- rollup
- webpack
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@storybook/csf-plugin@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.27.3
rollup: 4.53.5
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@storybook/global@5.0.0': {}
@@ -13515,14 +13389,14 @@ snapshots:
react-dom: 19.2.4(react@19.2.4)
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@storybook/vue3-vite@10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
'@storybook/builder-vite': 10.2.10(esbuild@0.27.3)(rollup@4.53.5)(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@storybook/vue3': 10.2.10(storybook@10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(vue@3.5.13(typescript@5.9.3))
magic-string: 0.30.21
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
typescript: 5.9.3
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue-component-meta: 2.2.12(typescript@5.9.3)
vue-docgen-api: 4.79.2(vue@3.5.13(typescript@5.9.3))
transitivePeerDependencies:
@@ -13604,19 +13478,19 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.0
'@tailwindcss/oxide-win32-x64-msvc': 4.2.0
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@tailwindcss/node': 4.2.0
'@tailwindcss/oxide': 4.2.0
tailwindcss: 4.2.0
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@tailwindcss/vite@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@tailwindcss/node': 4.2.0
'@tailwindcss/oxide': 4.2.0
tailwindcss: 4.2.0
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@tanstack/virtual-core@3.13.12': {}
@@ -14209,32 +14083,32 @@ snapshots:
vue: 3.5.13(typescript@5.9.3)
vue-router: 4.4.3(vue@3.5.13(typescript@5.9.3))
'@vitejs/plugin-vue-jsx@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vitejs/plugin-vue-jsx@4.2.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@rolldown/pluginutils': 1.0.0-rc.9
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.29.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue: 3.5.13(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vitejs/plugin-vue@5.2.4(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue: 3.5.13(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue: 3.5.13(typescript@5.9.3)
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.53
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue: 3.5.13(typescript@5.9.3)
'@vitest/coverage-v8@4.0.16(vitest@4.0.16)':
@@ -14250,7 +14124,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -14271,21 +14145,21 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.16
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))':
'@vitest/mocker@4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 4.0.16
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -14321,7 +14195,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -14496,38 +14370,38 @@ snapshots:
dependencies:
'@vue/devtools-kit': 7.7.9
'@vue/devtools-core@7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vue/devtools-core@7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@vue/devtools-kit': 7.7.9
'@vue/devtools-shared': 7.7.9
mitt: 3.0.1
nanoid: 5.1.5
pathe: 2.0.3
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vue: 3.5.13(typescript@5.9.3)
transitivePeerDependencies:
- vite
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@vue/devtools-kit': 8.0.5
'@vue/devtools-shared': 8.0.5
mitt: 3.0.1
nanoid: 5.1.5
pathe: 2.0.3
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vue: 3.5.13(typescript@5.9.3)
transitivePeerDependencies:
- vite
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))':
'@vue/devtools-core@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))':
dependencies:
'@vue/devtools-kit': 8.0.5
'@vue/devtools-shared': 8.0.5
mitt: 3.0.1
nanoid: 5.1.5
pathe: 2.0.3
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vue: 3.5.13(typescript@5.9.3)
transitivePeerDependencies:
- vite
@@ -14935,7 +14809,7 @@ snapshots:
astral-regex@2.0.0: {}
astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.9.0):
astro@5.18.1(@types/node@25.0.3)(jiti@2.6.1)(rollup@4.53.5)(terser@5.39.2)(tsx@4.19.4)(typescript@5.9.3)(yaml@2.8.2):
dependencies:
'@astrojs/compiler': 2.13.1
'@astrojs/internal-helpers': 0.7.6
@@ -14992,8 +14866,8 @@ snapshots:
unist-util-visit: 5.1.0
unstorage: 1.17.4
vfile: 6.0.3
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitefu: 1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
yocto-spinner: 0.2.3
@@ -15186,10 +15060,6 @@ snapshots:
dependencies:
balanced-match: 4.0.3
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.3
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@@ -17593,8 +17463,6 @@ snapshots:
lru-cache@8.0.5: {}
lunr@2.3.9: {}
lz-string@1.5.0: {}
lz-utils@2.1.0: {}
@@ -18030,10 +17898,6 @@ snapshots:
dependencies:
brace-expansion: 5.0.2
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.6
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.12
@@ -19963,19 +19827,6 @@ snapshots:
typed-binary@4.3.2: {}
typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)):
dependencies:
typedoc: 0.28.19(typescript@5.9.3)
typedoc@0.28.19(typescript@5.9.3):
dependencies:
'@gerrit0/mini-shiki': 3.23.0
lunr: 2.3.9
markdown-it: 14.1.1
minimatch: 10.2.5
typescript: 5.9.3
yaml: 2.9.0
typegpu@0.8.2:
dependencies:
tinyest: 0.1.2
@@ -20260,27 +20111,27 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
birpc: 2.9.0
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-dev-rpc@1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
birpc: 2.9.0
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-hot-client: 2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-hot-client@2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-hot-client@2.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-hot-client@2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-hot-client@2.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-dts@4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-dts@4.5.4(@types/node@24.10.4)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@microsoft/api-extractor': 7.57.2(@types/node@24.10.4)
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
@@ -20293,32 +20144,13 @@ snapshots:
magic-string: 0.30.21
typescript: 5.9.3
optionalDependencies:
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
- rollup
- supports-color
vite-plugin-dts@4.5.4(@types/node@25.0.3)(rollup@4.53.5)(typescript@5.9.3)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
dependencies:
'@microsoft/api-extractor': 7.57.2(@types/node@25.0.3)
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
'@volar/typescript': 2.4.28
'@vue/language-core': 2.2.0(typescript@5.9.3)
compare-versions: 6.1.1
debug: 4.4.3
kolorist: 1.8.0
local-pkg: 1.1.2
magic-string: 0.30.21
typescript: 5.9.3
optionalDependencies:
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
transitivePeerDependencies:
- '@types/node'
- rollup
- supports-color
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
@@ -20332,9 +20164,9 @@ snapshots:
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-html@3.2.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
@@ -20348,9 +20180,9 @@ snapshots:
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-inspect@0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-inspect@0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
@@ -20361,12 +20193,12 @@ snapshots:
perfect-debounce: 1.0.0
picocolors: 1.1.1
sirv: 3.0.2
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- rollup
- supports-color
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
ansis: 4.2.0
debug: 4.4.3
@@ -20376,12 +20208,12 @@ snapshots:
perfect-debounce: 2.0.0
sirv: 3.0.2
unplugin-utils: 0.3.1
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
transitivePeerDependencies:
- supports-color
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-inspect@11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
ansis: 4.2.0
debug: 4.4.3
@@ -20391,56 +20223,56 @@ snapshots:
perfect-debounce: 2.0.0
sirv: 3.0.2
unplugin-utils: 0.3.1
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-dev-rpc: 1.1.0(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
transitivePeerDependencies:
- supports-color
vite-plugin-vue-devtools@7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
vite-plugin-vue-devtools@7.7.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-core': 7.7.9(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-kit': 7.7.9
'@vue/devtools-shared': 7.7.9
execa: 9.6.1
sirv: 3.0.2
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-plugin-inspect: 0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-inspect: 0.8.9(rollup@4.53.5)(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
transitivePeerDependencies:
- '@nuxt/kit'
- rollup
- supports-color
- vue
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-kit': 8.0.5
'@vue/devtools-shared': 8.0.5
sirv: 3.0.2
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
transitivePeerDependencies:
- '@nuxt/kit'
- supports-color
- vue
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3)):
vite-plugin-vue-devtools@8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3)):
dependencies:
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-core': 8.0.5(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
'@vue/devtools-kit': 8.0.5
'@vue/devtools-shared': 8.0.5
sirv: 3.0.2
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vite-plugin-inspect: 11.3.3(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
vite-plugin-vue-inspector: 5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
transitivePeerDependencies:
- '@nuxt/kit'
- supports-color
- vue
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
@@ -20451,11 +20283,11 @@ snapshots:
'@vue/compiler-dom': 3.5.28
kolorist: 1.8.0
magic-string: 0.30.21
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vite-plugin-vue-inspector@5.3.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0)
@@ -20466,11 +20298,11 @@ snapshots:
'@vue/compiler-dom': 3.5.28
kolorist: 1.8.0
magic-string: 0.30.21
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
'@oxc-project/runtime': 0.115.0
lightningcss: 1.32.0
@@ -20485,9 +20317,9 @@ snapshots:
jiti: 2.6.1
terser: 5.39.2
tsx: 4.19.4
yaml: 2.9.0
yaml: 2.8.2
vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
'@oxc-project/runtime': 0.115.0
lightningcss: 1.32.0
@@ -20502,16 +20334,16 @@ snapshots:
jiti: 2.6.1
terser: 5.39.2
tsx: 4.19.4
yaml: 2.9.0
yaml: 2.8.2
vitefu@1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)):
vitefu@1.1.2(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)):
optionalDependencies:
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.16
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.16
'@vitest/runner': 4.0.16
'@vitest/snapshot': 4.0.16
@@ -20528,7 +20360,7 @@ snapshots:
tinyexec: 1.0.4
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -20550,10 +20382,10 @@ snapshots:
- tsx
- yaml
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0):
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.16
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
'@vitest/mocker': 4.0.16(vite@8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
'@vitest/pretty-format': 4.0.16
'@vitest/runner': 4.0.16
'@vitest/snapshot': 4.0.16
@@ -20570,7 +20402,7 @@ snapshots:
tinyexec: 1.0.4
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0)
vite: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -20980,7 +20812,7 @@ snapshots:
yaml-eslint-parser@1.3.0:
dependencies:
eslint-visitor-keys: 3.4.3
yaml: 2.9.0
yaml: 2.8.2
yaml-language-server@1.20.0:
dependencies:
@@ -21002,8 +20834,6 @@ snapshots:
yaml@2.8.2: {}
yaml@2.9.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:

View File

@@ -1,915 +0,0 @@
meta:
schema_version: 1
generated_from:
- database.yaml
- rollup.yaml
- star-cache.yaml
generated_by: scripts/build-behavior-categories.py (I-TF.1)
source_pattern_count: 62
category_count: 37
usage_weight_formula: sum_over_members(blast_radius * occurrences)
exemplar_ranking: repo_stars desc, then pattern blast_radius desc; distinct (repo, pattern_id)
notes:
- Categories cluster by intent, not by surface_family. S2 is split into creation / teardown / hydration / interaction /
drawing / connection / serialization / properties.
- S1 hooks are merged with their prototype-patching equivalents where intent matches (BC.20 node-type reg, BC.22 menus,
BC.03 hydration).
- S8.P1 isVirtualNode is a registration-time flag, so it lives in BC.20 alongside the node-type registration hooks.
- S10.D3 setSize and S15.OS1 dynamic outputs join S10.D1 in BC.09 dynamic-slot-mutation since they all describe runtime
topology mutation.
- S14.ID1 NodeLocatorId joins S11.G2 graph enumeration in BC.29 because both are about cross-scope node identity/resolution.
- S11.G1/G3/G4 (version, batching, setDirtyCanvas) collapse into BC.30 graph change-tracking — the v2 reactivity story replaces
all three.
- BC.21 (S1.H2 getCustomWidgets) has only 2 evidence rows in database.yaml; this is the 'small family — 2 + 1 minor variant'
acceptance carve-out. The two exemplars are kept as-is, no synthetic third row.
- BC.31 and BC.32 added 2026-05-08 from Notion API usage research (notion-api-research-evidence.yaml staging).
S16 is a new surface family (DOM injection) not previously tracked. S16.VUE1 grouped with BC.32 (embedded runtimes).
S3.C2 (ContextMenu replacement) added to BC.06 member list.
- Notion source also upgrades occurrence signal on BC.01/BC.02/BC.04/BC.06/BC.07/BC.09/BC.26/BC.29/BC.30 — reflected
in staging file; usage_weight values below are NOT yet updated (need re-run of rollup-blast-radius.py after merge).
- BC.33 (cross-ext DOM widget obs), BC.34 (settings dialog), BC.35 (pre-queue validation) added 2026-05-08 from Notion COM-3668.
- BC.36 (PrimeVue widget API surface) added 2026-05-08 from Notion Widget Component APIs page; was erroneously numbered BC.33 — corrected.
- BC.37 (VueNode bridge timing) added 2026-05-08 from Notion Frontend Architecture page (3536d73d). Captures the
nodeCreated→VueNode-not-yet-mounted hazard and the waitForLoad3d deferral pattern as a concrete test fixture.
categories:
- category_id: BC.01
name: 'Node lifecycle: creation'
intent: Hooks fired when a node is constructed or attached to the graph (per-instance setup).
notes: >-
nodeCreated fires BEFORE the VueNode Vue component mounts. Extensions that need to access
VueNode-backed state (DOM widgets, Three.js renderers, etc.) must defer to onNodeMounted
(v2) or waitForLoad3d-style callbacks (v1). See BC.37 for the deferred-mount bridge pattern.
Source: Notion Frontend Architecture page (2026-05-08).
member_pattern_ids:
- S2.N1
- S2.N8
usage_weight: 37.56
exemplars:
- pattern_id: S2.N1
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/saveImageExtraOutput.ts#L31
stars: 1787
- pattern_id: S2.N8
repo: Azornes/Comfyui-LayerForge
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/src/CanvasView.ts#L1401
stars: 313
- pattern_id: S2.N1
repo: SKBv0/ComfyUI_SpideyReroute
url: https://github.com/SKBv0/ComfyUI_SpideyReroute/blob/main/js/SpideyReroute.js#L41
stars: 13
- category_id: BC.02
name: 'Node lifecycle: teardown'
intent: Single de-facto teardown surface for cleaning up DOM widgets, intervals, and observers when a node is removed.
member_pattern_ids:
- S2.N4
usage_weight: 29.35
exemplars:
- pattern_id: S2.N4
repo: Lightricks/ComfyUI-LTXVideo
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L137
stars: 3581
- pattern_id: S2.N4
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js#L348
stars: 2568
- pattern_id: S2.N4
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/docs/architecture/ecs-migration-plan.md#L587
stars: 1787
- category_id: BC.03
name: 'Node lifecycle: hydration from saved workflows'
intent: React when a node is rehydrated from a stored workflow; the working replacement for the unused loadedGraphNode hook.
member_pattern_ids:
- S1.H1
- S2.N7
usage_weight: 15.42
exemplars:
- pattern_id: S1.H1
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
stars: 1787
- pattern_id: S1.H1
repo: sofakid/dandy
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L114
stars: 54
- pattern_id: S2.N7
repo: akawana/ComfyUI-Folded-Prompts
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1265
stars: 4
- category_id: BC.04
name: 'Node interaction: pointer, selection, resize'
intent: 'User-driven per-node events: mouse down for custom click regions, selection focus, and resize feedback for relayout.'
member_pattern_ids:
- S2.N10
- S2.N17
- S2.N19
usage_weight: 38.07
exemplars:
- pattern_id: S2.N10
repo: diodiogod/TTS-Audio-Suite
url: https://github.com/diodiogod/TTS-Audio-Suite/blob/main/web/chatterbox_voice_capture.js#L202
stars: 906
- pattern_id: S2.N10
repo: melMass/comfy_mtb
url: https://github.com/melMass/comfy_mtb/blob/main/web/comfy_shared.js#L1047
stars: 702
- pattern_id: S2.N10
repo: pixaroma/ComfyUI-Pixaroma
url: https://github.com/pixaroma/ComfyUI-Pixaroma/blob/main/js/compare/index.js#L360
stars: 137
- category_id: BC.05
name: Custom DOM widgets and node sizing
intent: Contribute DOM-backed widgets and override computeSize so the node reserves the right area for them.
member_pattern_ids:
- S4.W2
- S2.N11
usage_weight: 33.35
exemplars:
- pattern_id: S4.W2
repo: Lightricks/ComfyUI-LTXVideo
url: https://github.com/Lightricks/ComfyUI-LTXVideo/blob/main/web/js/sparse_track_editor.js#L218
stars: 3581
- pattern_id: S4.W2
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/editors/editor_base.js#L511
stars: 2568
- pattern_id: S2.N11
repo: o-l-l-i/ComfyUI-Olm-ImageAdjust
url: https://github.com/o-l-l-i/ComfyUI-Olm-ImageAdjust/blob/main/web/olm_imageadjust.js#L319
stars: 45
- category_id: BC.06
name: Custom canvas drawing (per-node and canvas-level)
intent:
Per-node onDrawForeground and full LGraphCanvas.prototype overrides for badges, indicators, keyboard, and custom
render passes. Includes global ContextMenu replacement (S3.C2) as the most destructive canvas-level override.
v1_scope_note: >-
Simon Tranter (COM-3668, 2025-05-12) explicitly vetoed canvas drawing overrides as "too hacky/specific
to implement APIs for". Confirmed out of v2 v1 scope. S3.C* patterns remain in DB for blast-radius
tracking and strangler-fig planning but v2 need not replace them 1:1. Supports D9 Phase C deferral.
member_pattern_ids:
- S2.N9
- S3.C1
- S3.C2
usage_weight: 58.97
exemplars:
- pattern_id: S3.C1
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1256
stars: 2568
- pattern_id: S3.C1
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/simpleTouchSupport.ts#L174
stars: 1787
- pattern_id: S3.C1
repo: melMass/comfy_mtb
url: https://github.com/melMass/comfy_mtb/blob/main/web/note_plus.js#L1
stars: 702
- category_id: BC.07
name: Connection observation, intercept, and veto
intent:
Subscribe to link connect/disconnect events on a node and intercept incoming/outgoing connections before they are
wired to refuse them, mutate slots, or coerce types.
member_pattern_ids:
- S2.N3
- S2.N12
- S2.N13
usage_weight: 51.08
exemplars:
- pattern_id: S2.N13
repo: rgthree/rgthree-comfy
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/node_mode_relay.js#L90
stars: 3049
- pattern_id: S2.N12
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/jsnodes.js#L152
stars: 2568
- pattern_id: S2.N12
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/core/graph/widgets/dynamicWidgets.ts#L539
stars: 1787
- category_id: BC.08
name: Programmatic linking
intent: Extensions wire connections from code (workflow templates, auto-routing).
member_pattern_ids:
- S10.D2
usage_weight: 11.81
exemplars:
- pattern_id: S10.D2
repo: MockbaTheBorg/ComfyUI-Mockba
url: https://github.com/MockbaTheBorg/ComfyUI-Mockba/blob/main/js/slider.js#L1
stars: 1
- pattern_id: S10.D2
repo: vjumpkung/comfyui-infinitetalk-native-sampler
url: https://github.com/vjumpkung/comfyui-infinitetalk-native-sampler/blob/main/README.md#L1
stars: 1
- pattern_id: S10.D2
repo: goodtab/ComfyUI-Custom-Scripts
url: https://github.com/goodtab/ComfyUI-Custom-Scripts/blob/main/web/js/quickNodes.js#L138
stars: 0
- category_id: BC.09
name: Dynamic slot and output mutation
intent: Grow/shrink inputs and outputs at runtime, with the obligatory computeSize+setSize reflow that follows.
member_pattern_ids:
- S10.D1
- S10.D3
- S15.OS1
usage_weight: 38.63
exemplars:
- pattern_id: S10.D1
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/lib/litegraph/src/canvas/LinkConnector.core.test.ts#L121
stars: 1787
- pattern_id: S10.D1
repo: r-vage/ComfyUI_Eclipse
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-mode-nodes.js#L42
stars: 19
- pattern_id: S15.OS1
repo: yorkane/ComfyUI-KYNode
url: https://github.com/yorkane/ComfyUI-KYNode/blob/main/web/python-editor.js#L243
stars: 10
- category_id: BC.10
name: Widget value subscription
intent: Subscribe to widget value changes either at the widget (callback chain) or node (onWidgetChanged) level.
member_pattern_ids:
- S4.W1
- S2.N14
usage_weight: 32.22
exemplars:
- pattern_id: S2.N14
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/widgetInputs.ts#L317
stars: 1787
- pattern_id: S4.W1
repo: crom8505/ComfyUI-Dynamic-Sigmas
url: https://github.com/crom8505/ComfyUI-Dynamic-Sigmas/blob/main/web/js/graph_sigmas.js#L79
stars: 8
- pattern_id: S4.W1
repo: 834t/ComfyUI_834t_scene_composer
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L148
stars: 5
- category_id: BC.11
name: Widget imperative state writes
intent: Imperatively mutate widget value, COMBO option lists, or the node.widgets array (insert/remove/reorder).
member_pattern_ids:
- S4.W4
- S4.W5
- S2.N16
usage_weight: 28.42
exemplars:
- pattern_id: S2.N16
repo: r-vage/ComfyUI_Eclipse
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-set-get.js#L9
stars: 19
- pattern_id: S4.W4
repo: EnragedAntelope/EA_LMStudio
url: https://github.com/EnragedAntelope/EA_LMStudio/blob/main/web/ea_lmstudio.js#L11
stars: 7
- pattern_id: S4.W4
repo: zzggi2024/shaobkj
url: https://github.com/zzggi2024/shaobkj/blob/main/js/dynamic_inputs.js#L374
stars: 1
- category_id: BC.12
name: Per-widget serialization transform
intent: Transform a widget's value at workflow-serialization time (dynamic prompts, hidden state, expand-on-save).
notes: >-
widget.options.serialize===false widgets (e.g. control_after_generate) still occupy a widgets_values
slot and still fire serializeValue — excluded only from the backend prompt by graphToPrompt(). Test
triple must cover this case explicitly. PR #10392 widgets_values_named is the v2 migration path;
WidgetHandle identity must be by name not position. See research/architecture/widget-serialization-historical-analysis.md.
member_pattern_ids:
- S4.W3
usage_weight: 27.94
exemplars:
- pattern_id: S4.W3
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/helpers/painter.ts#L70
stars: 1787
- pattern_id: S4.W3
repo: Raykosan/ComfyUI_RaykoStudio
url: https://github.com/Raykosan/ComfyUI_RaykoStudio/blob/main/web/rayko_lora_widget.js#L31
stars: 45
- pattern_id: S4.W3
repo: 834t/ComfyUI_834t_scene_composer
url: https://github.com/834t/ComfyUI_834t_scene_composer/blob/main/js/b34t_scene_composer.js#L135
stars: 5
- category_id: BC.13
name: Per-node serialization interception
intent: Intercept node-level serialize/onSerialize to inject custom workflow JSON fields.
notes: >-
Root cause: widgets_values is positional — prototype.serialize patchers consume/produce this array
directly. Three index-drift sources: control_after_generate slot occupancy, extension-injected
widgets, V3 IO.MultiType topology-dependent widget count. NaN→null pipeline produces silent
corruption (backend crash is first visible symptom). Test triple must cover: (a) positional v1
compat, (b) named-map v2 round-trip parity, (c) null-in-numeric-widget logs warning + substitutes
default. PR #11884 guard, PR #10392 named map. See research/architecture/widget-serialization-historical-analysis.md.
member_pattern_ids:
- S2.N6
- S2.N15
usage_weight: 47.07
exemplars:
- pattern_id: S2.N15
repo: Azornes/Comfyui-LayerForge
url: https://github.com/Azornes/Comfyui-LayerForge/blob/main/js/CanvasView.js#L1438
stars: 313
- pattern_id: S2.N15
repo: IAMCCS/IAMCCS-nodes
url: https://github.com/IAMCCS/IAMCCS-nodes/blob/main/web/iamccs_wan_motion_presets.js#L598
stars: 92
- pattern_id: S2.N15
repo: DazzleNodes/ComfyUI-Smart-Resolution-Calc
url: https://github.com/DazzleNodes/ComfyUI-Smart-Resolution-Calc/blob/main/web/utils/serialization.js#L32
stars: 7
- category_id: BC.14
name: Workflow → API serialization interception (graphToPrompt)
intent: Patch app.graphToPrompt to resolve virtual nodes, inject custom metadata, or rewrite the API payload before submit.
member_pattern_ids:
- S6.A1
usage_weight: 46.66
exemplars:
- pattern_id: S6.A1
repo: Comfy-Org/ComfyUI-Manager
url: https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/js/components-manager.js#L781
stars: 14554
- pattern_id: S6.A1
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
stars: 2568
- pattern_id: S6.A1
repo: m3rr/h4_Live
url: https://github.com/m3rr/h4_Live/blob/main/js/h4_datastream.js#L23
stars: 2
- category_id: BC.15
name: Workflow loading into the editor
intent: External/embed scenario where a workflow JSON is pushed into the running editor via app.loadGraphData.
member_pattern_ids:
- S6.A2
usage_weight: 20.31
exemplars:
- pattern_id: S6.A2
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/fixtures/helpers/WorkflowHelper.ts#L215
stars: 1787
- pattern_id: S6.A2
repo: BennyKok/comfyui-deploy
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/workflow-list.js#L456
stars: 1507
- pattern_id: S6.A2
repo: ketle-man/ComfyUI-Workflow-Studio
url: https://github.com/ketle-man/ComfyUI-Workflow-Studio/blob/main/static/js/workflow-tab.js#L67
stars: 2
- category_id: BC.16
name: Execution output consumption (per-node)
intent: Consume backend execution output on a specific node (text, JSON, image) to drive display.
member_pattern_ids:
- S2.N2
usage_weight: 5.74
exemplars:
- pattern_id: S2.N2
repo: andreszs/ComfyUI-Ultralytics-Studio
url: https://github.com/andreszs/ComfyUI-Ultralytics-Studio/blob/main/js/show_string.js#L9
stars: 3
- pattern_id: S2.N2
repo: AlexZ1967/ComfyUI_ALEXZ_tools
url: https://github.com/AlexZ1967/ComfyUI_ALEXZ_tools/blob/main/web/show_json.js#L49
stars: 0
- pattern_id: S2.N2
repo: becky3/comfyui-workspace
url: https://github.com/becky3/comfyui-workspace/blob/main/custom_nodes/ComfyUI-Becky3-Common/js/show_text.js#L33
stars: 0
- category_id: BC.17
name: Backend execution lifecycle and progress events
intent: Subscribe to api.addEventListener for execution_*, progress, status, and reconnecting events.
member_pattern_ids:
- S5.A1
- S5.A2
- S5.A3
usage_weight: 51.25
exemplars:
- pattern_id: S5.A2
repo: AIGODLIKE/AIGODLIKE-ComfyUI-Studio
url: https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Studio/blob/main/loader/components/public/iconRenderer.js#L39
stars: 405
- pattern_id: S5.A3
repo: kyuz0/amd-strix-halo-comfyui-toolboxes
url: https://github.com/kyuz0/amd-strix-halo-comfyui-toolboxes/blob/main/scripts/benchmark_workflows.py#L52
stars: 109
- pattern_id: S5.A1
repo: ShakerSmith/ShakerNodesSuite
url: https://github.com/ShakerSmith/ShakerNodesSuite/blob/main/js/shaker_preview_ui.js#L58
stars: 8
- category_id: BC.18
name: Backend HTTP calls
intent: Call ComfyAPI.fetchApi as the canonical authenticated path to backend HTTP endpoints.
member_pattern_ids:
- S6.A3
usage_weight: 22.74
exemplars:
- pattern_id: S6.A3
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/components/common/BackgroundImageUpload.vue#L61
stars: 1787
- pattern_id: S6.A3
repo: akawana/ComfyUI-Folded-Prompts
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L1227
stars: 4
- pattern_id: S6.A3
repo: zhupeter010903/ComfyUI-XYZ-prompt-library
url: https://github.com/zhupeter010903/ComfyUI-XYZ-prompt-library/blob/main/js/prompt_library_window.js#L1379
stars: 1
- category_id: BC.19
name: Workflow execution trigger
intent: Trigger or intercept queuePrompt for sidebar Run buttons, auth tokens, or payload mutation.
member_pattern_ids:
- S6.A4
usage_weight: 12.65
exemplars:
- pattern_id: S6.A4
repo: MajoorWaldi/ComfyUI-Majoor-AssetsManager
url: https://github.com/MajoorWaldi/ComfyUI-Majoor-AssetsManager/blob/main/js/features/viewer/workflowSidebar/sidebarRunButton.js#L317
stars: 97
- pattern_id: S6.A4
repo: gigici/ComfyUI_BlendPack
url: https://github.com/gigici/ComfyUI_BlendPack/blob/main/js/ui/NodeUI.js#L99
stars: 1
- pattern_id: S6.A4
repo: rohapa/comfyui-replay
url: https://github.com/rohapa/comfyui-replay/blob/main/README.md#L497
stars: 0
- category_id: BC.20
name: Custom node-type registration (frontend-only / virtual)
intent: Register pure-frontend or fully virtual node types and mark them with isVirtualNode so the backend ignores them.
member_pattern_ids:
- S1.H5
- S1.H6
- S8.P1
usage_weight: 27.49
exemplars:
- pattern_id: S1.H6
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/rerouteNode.ts
stars: 1787
- pattern_id: S1.H5
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
stars: 1787
- pattern_id: S1.H6
repo: sofakid/dandy
url: https://github.com/sofakid/dandy/blob/main/web/main.js#L111
stars: 54
- category_id: BC.21
name: Custom widget-type registration
intent: Register new widget types (color picker, file uploader, custom inputs) via getCustomWidgets.
member_pattern_ids:
- S1.H2
usage_weight: 7.17
exemplars:
- pattern_id: S1.H2
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
stars: 1787
- pattern_id: S1.H2
repo: haohaocreates/PR-rk-comfy-nodes-36d8f0a5
url: https://github.com/haohaocreates/PR-rk-comfy-nodes-36d8f0a5/blob/main/web/rk_nodes.ts#L22
stars: 0
- category_id: BC.22
name: Context menu contributions (node and canvas)
intent:
Contribute right-click menu items at both the node and canvas scope, including legacy prototype patches and the
supported v1 hooks.
member_pattern_ids:
- S2.N5
- S1.H3
- S1.H4
usage_weight: 19.53
exemplars:
- pattern_id: S1.H3
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
stars: 1787
- pattern_id: S1.H4
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/
stars: 1787
- pattern_id: S1.H3
repo: r-vage/ComfyUI_Eclipse
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-canvas-utils.js#L2
stars: 19
- category_id: BC.23
name: Node property bag mutations
intent: React to mutations of node.properties — the persistent property bag that survives serialization.
member_pattern_ids:
- S2.N18
usage_weight: 14.42
exemplars:
- pattern_id: S2.N18
repo: rgthree/rgthree-comfy
url: https://github.com/rgthree/rgthree-comfy/blob/main/src_web/comfyui/seed.ts#L78
stars: 3049
- pattern_id: S2.N18
repo: rgthree/rgthree-comfy
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/seed.js#L26
stars: 3049
- pattern_id: S2.N18
repo: rgthree/rgthree-comfy
url: https://github.com/rgthree/rgthree-comfy/blob/main/web/comfyui/power_primitive.js#L142
stars: 3049
- category_id: BC.24
name: Node-def schema inspection
intent: Branch on ComfyNodeDef shape (input.required/optional/hidden, output, output_node, category) to drive UI.
member_pattern_ids:
- S13.SC1
usage_weight: 22.43
exemplars:
- pattern_id: S13.SC1
repo: BennyKok/comfyui-deploy
url: https://github.com/BennyKok/comfyui-deploy/blob/main/web-plugin/index.js#L1
stars: 1507
- pattern_id: S13.SC1
repo: StableLlama/ComfyUI-basic_data_handling
url: https://github.com/StableLlama/ComfyUI-basic_data_handling/blob/main/web/js/dynamicnode.js#L1
stars: 43
- pattern_id: S13.SC1
repo: xeinherjer-dev/ComfyUI-XENodes
url: https://github.com/xeinherjer-dev/ComfyUI-XENodes/blob/main/web/js/combo_selector.js#L1
stars: 1
- category_id: BC.25
name: Shell UI registration (commands, sidebars, toasts)
intent: Declarative shell-UI contributions through extensionManager / commandManager / sidebarTab / bottomPanel.
member_pattern_ids:
- S12.UI1
usage_weight: 10.98
exemplars:
- pattern_id: S12.UI1
repo: robertvoy/ComfyUI-Distributed
url: https://github.com/robertvoy/ComfyUI-Distributed/blob/main/web/main.js#L269
stars: 544
- pattern_id: S12.UI1
repo: maxi45274/ComfyUI_LinkFX
url: https://github.com/maxi45274/ComfyUI_LinkFX/blob/main/js/LinkFX.js#L707
stars: 3
- pattern_id: S12.UI1
repo: criskb/Comfypencil
url: https://github.com/criskb/Comfypencil/blob/main/web/comfy_pencil_extension.js#L955
stars: 0
- category_id: BC.26
name: Globals as ABI (window.LiteGraph, window.comfyAPI)
intent: Reach into the global namespace for LiteGraph constructors/enums or the module-as-global comfyAPI registry.
member_pattern_ids:
- S7.G1
usage_weight: 27.0
exemplars:
- pattern_id: S7.G1
repo: ryanontheinside/ComfyUI_RyanOnTheInside
url: https://github.com/ryanontheinside/ComfyUI_RyanOnTheInside/blob/main/web/js/index.js#L1
stars: 801
- pattern_id: S7.G1
repo: ArtHommage/HommageTools
url: https://github.com/ArtHommage/HommageTools/blob/main/web/js/index.js#L1
stars: 4
- pattern_id: S7.G1
repo: PROJECTMAD/PROJECT-MAD-NODES
url: https://github.com/PROJECTMAD/PROJECT-MAD-NODES/blob/main/web/js/index.js#L1
stars: 4
- category_id: BC.27
name: LiteGraph entity direct manipulation (reroute, group, link, slot)
intent: Direct read/mutation of reroutes, groups, links, and slots — no public extension API exists today.
member_pattern_ids:
- S9.R1
- S9.G1
- S9.L1
- S9.S1
usage_weight: 39.37
exemplars:
- pattern_id: S9.R1
repo: nodetool-ai/nodetool
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
stars: 330
- pattern_id: S9.S1
repo: nodetool-ai/nodetool
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L267
stars: 330
- pattern_id: S9.S1
repo: Stibo/comfyui-nifty-nodes
url: https://github.com/Stibo/comfyui-nifty-nodes/blob/main/js/nifty_nodes.js#L112
stars: 3
- category_id: BC.28
name: Subgraph fan-out via set/get virtual nodes
intent: Fan out a single named value across the graph without explicit links (KJNodes-style Set/Get nodes).
member_pattern_ids:
- S9.SG1
usage_weight: 16.89
exemplars:
- pattern_id: S9.SG1
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/setgetnodes.js#L1406
stars: 2568
- pattern_id: S9.SG1
repo: krismasdev/ComfyUI-Flux-Continuum
url: https://github.com/krismasdev/ComfyUI-Flux-Continuum/blob/main/web/hint.js#L1
stars: 0
- pattern_id: S9.SG1
repo: SpaceWarpStudio/ComfyUI-SetInputGetOutput
url: https://github.com/SpaceWarpStudio/ComfyUI-SetInputGetOutput/blob/main/web/js/setinputgetoutput.js#L1
stars: 0
- category_id: BC.29
name: Graph enumeration, mutation, and cross-scope identity
intent:
Enumerate or mutate the node set (graph.add/remove/findNodesByType/serialize/configure) and resolve cross-subgraph
references via NodeLocatorId / NodeExecutionId.
member_pattern_ids:
- S11.G2
- S14.ID1
usage_weight: 23.56
exemplars:
- pattern_id: S11.G2
repo: yolain/ComfyUI-Easy-Use
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easyExtraMenu.js#L439
stars: 2503
- pattern_id: S11.G2
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/tests/workflowPersistence.spec.ts#L351
stars: 1787
- pattern_id: S11.G2
repo: r-vage/ComfyUI_Eclipse
url: https://github.com/r-vage/ComfyUI_Eclipse/blob/main/js/eclipse-ui-enhancements.js#L29
stars: 19
- category_id: BC.30
name: Graph change tracking, batching, and reactivity flush
intent:
'Coordinate graph-level change: graph._version monotonic counter, beforeChange/afterChange batching, and the imperative
setDirtyCanvas redraw flush.'
member_pattern_ids:
- S11.G1
- S11.G3
- S11.G4
usage_weight: 34.38
exemplars:
- pattern_id: S11.G3
repo: nodetool-ai/nodetool
url: https://github.com/nodetool-ai/nodetool/blob/main/subgraphs.md#L1
stars: 330
- pattern_id: S11.G4
repo: akawana/ComfyUI-Folded-Prompts
url: https://github.com/akawana/ComfyUI-Folded-Prompts/blob/main/js/FPFoldedPrompts.js#L776
stars: 4
- pattern_id: S11.G3
repo: linjm8780860/ljm_comfyui
url: https://github.com/linjm8780860/ljm_comfyui/blob/main/src/utils/vintageClipboard.ts#L1
stars: 0
- category_id: BC.31
name: DOM injection and style management
intent:
Extensions add UI chrome, toolbars, and style overrides directly into the document outside any provided API —
style tags into head, arbitrary elements into body, innerHTML rendering, and external script loading.
member_pattern_ids:
- S16.DOM1
- S16.DOM2
- S16.DOM3
- S16.DOM4
usage_weight: 0.0
notes:
'usage_weight pending rollup-blast-radius.py re-run after database.yaml merge (I-N4.1). Notion counts: DOM1=354
occ, DOM2=364 occ, DOM3=443 occ, DOM4=232 occ across ~81 packages — among the highest raw occurrence counts in
the entire dataset. v2 replacements: injectStyles(), addPanel(), addToolbarItem(), safe HTML rendering API.'
exemplars:
- pattern_id: S16.DOM1
repo: kijai/ComfyUI-KJNodes
url: https://github.com/kijai/ComfyUI-KJNodes/blob/main/web/js/help_popup.js
stars: 2568
- pattern_id: S16.DOM2
repo: yolain/ComfyUI-Easy-Use
url: https://github.com/yolain/ComfyUI-Easy-Use/blob/main/web_version/v1/js/easy/easy.js
stars: 2503
- pattern_id: S16.DOM3
repo: '(aggregate — Notion §2.3)'
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
stars: 0
- category_id: BC.32
name: Embedded framework runtimes and Vue widget bundling
intent:
Extensions bundle their own copy of Vue (or another framework) inside a DOM widget, bypassing the host app
instance and losing access to shared stores, i18n, and theme.
member_pattern_ids:
- S16.VUE1
usage_weight: 0.0
notes:
'usage_weight pending rollup-blast-radius.py re-run. 9 packages confirmed (Notion §2.9). v2 replacement:
registerVueWidget(nodeType, name, Component) sharing host Vue instance — already in plans/P1 §5 Custom widget type.
This BC provides the evidence base for that P1 design decision.'
exemplars:
- pattern_id: S16.VUE1
repo: ComfyUI-NKD-Sigmas-Curve
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
stars: 0
- pattern_id: S16.VUE1
repo: '(aggregate — 9 packages, Notion §2.9)'
url: https://www.notion.so/comfy-org/ComfyUI-Custom-Node-Frontend-API-Usage-Research-3356d73d365080dbaacafe8e52d52692
stars: 0
# ── Categories added 2026-05-08 from Notion COM-3668 (Simon Tranter, Custom Scripts API requirements) ──
- category_id: BC.33
name: Cross-extension DOM widget creation observation
intent:
An extension observes when *any* DOM widget is created (by any other extension) so it can attach its own
listeners — the mechanism the Autocomplete extension needs to wire its input handler to every text widget.
member_pattern_ids:
- S4.W6
usage_weight: 0.0
notes: >-
Identified from COM-3668. Distinct from BC.05 (creating DOM widgets) and BC.10 (subscribing to value changes).
Gap: no v1 hook fires for cross-extension widget creation observation. v2 shape: onDOMWidgetCreated(handler)
in defineExtension setup context. usage_weight pending blast-radius re-run.
source: notion-COM-3668
exemplars:
- pattern_id: S4.W6
repo: goodtab/ComfyUI-Custom-Scripts
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
stars: 0
- category_id: BC.34
name: Settings-panel custom dialog integration
intent: Extensions open custom modal dialogs triggered from the settings panel, rather than injecting raw DOM.
member_pattern_ids:
- S12.UI3
usage_weight: 0.0
notes: >-
Identified from COM-3668. Currently worked around via S16.DOM3 innerHTML injection. Distinct from S12.UI1
(sidebar/command registration) — this is about dialog lifecycle tied to settings entries. v2 shape:
app.ui.openDialog(Component) or settings entry type 'dialog-trigger'. usage_weight pending blast-radius re-run.
source: notion-COM-3668
exemplars:
- pattern_id: S12.UI3
repo: goodtab/ComfyUI-Custom-Scripts
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
stars: 0
- category_id: BC.35
name: Pre-queue widget validation
intent:
Validate widget values before a workflow is submitted and surface typed errors to the user — rejecting
the queue rather than silently mutating or failing.
member_pattern_ids:
- S6.A5
usage_weight: 0.0
notes: >-
Identified from COM-3668. Currently worked around via S6.A4 queuePrompt monkey-patching (silent_breakage=true
when multiple extensions patch). Distinct from D5 beforeSerialize (transforms values) and BC.19 (triggers
execution). v2 needs explicit beforeQueue event with event.reject(message). usage_weight pending re-run.
source: notion-COM-3668
exemplars:
- pattern_id: S6.A5
repo: goodtab/ComfyUI-Custom-Scripts
url: https://github.com/goodtab/ComfyUI-Custom-Scripts
stars: 0
- category_id: BC.36
name: PrimeVue widget component API surface
intent: >-
Custom node authors configuring widget behavior via per-component prop subsets — the v2 replacement
for direct widget.options mutation (S4.W4, S4.W1) and DOM widget construction (S4.W5, S4.W6).
15 PrimeVue components are the authoritative widget-kind enumeration for v2.
member_pattern_ids:
- S4.W1
- S4.W4
- S4.W5
notes: >-
Source: Notion page "Widget Component APIs" (2026-05-08). 15 components: Button, InputText, Select,
ColorPicker, MultiSelect, SelectButton, Slider, Textarea, ToggleSwitch, Chart, Image, ImageCompare,
Galleria, FileUpload, TreeSelect. Exclusion rule (Pablo): strip style/class/dt/pt/*Class/*Style.
ToggleSwitch is the only component with completed Pick<> types so far (WIP).
Informs: D7 typed options bags (future pivot), I-TF.2 widget-kind test triples,
PKG2 WidgetHandle.getOption key surface. disabled/readonly map to D7 first-class fields,
not options bag.
usage_weight: 0.0
exemplars:
- pattern_id: S4.W4
repo: '(see database.yaml S4.W4 exemplars — widget.options.values mutation)'
url: https://www.notion.so/comfy-org/Widget-Component-APIs-2126d73d365080b0bf30f241c09dd756
stars: 0
- category_id: BC.37
name: VueNode bridge timing — deferred mount access
intent: >-
Extensions that register in nodeCreated but need to access Vue-component-backed state
(Three.js renderer, DOM widget, ComponentWidgetImpl value) must defer until the Vue
component's onMounted fires. The v1 pattern is waitForLoad3d(node, cb); the v2 pattern
is onNodeMounted(() => { ... }) inside defineNodeExtension.
member_pattern_ids:
- S4.W5
notes: >-
Source: Notion Frontend Architecture page 3536d73d (2026-05-08). nodeCreated gives the
LiteGraph node; the VueNode Vue component has NOT mounted yet. waitForLoad3d in
src/extensions/core/Load3D is the canonical v1 fixture. ComponentWidgetImpl dual-identity:
LiteGraph side (value/callback/name) vs Vue side (props/emits/lifecycle).
v2 contract: onNodeMounted() hook fires after Vue component mount — this is the correct
timing for accessing VueNode-backed resources.
Informs: I-SR.2.B2 (NodeInstanceScope must not sync-access VueNode at setup time),
I-TF.3.C1 (harness must simulate two-phase mount), I-TF.2 test triple for BC.37.
D8 relevance: app.rootGraph is not reactive (confirmed by this doc) — the exact gap D8 solves.
usage_weight: 0.0
source: notion-frontend-architecture-3536d73d
exemplars:
- pattern_id: S4.W5
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/extensions/core/load3d.ts
stars: 1787
- category_id: BC.38
name: Canvas mode observation
intent: >-
Detect and react to ComfyUI canvas mode transitions (graph / app / builder:inputs /
builder:outputs / builder:arrange). Custom nodes that adapt rendering, widget resize
behavior, or read-only state across modes need a stable event — not polling or heuristics
against internal Pinia store state.
member_pattern_ids:
- S17.AM1
mechanism: absent-api
notes: >-
appModeStore is a Pinia composable; JS extensions cannot use Vue composables. v2 gap:
no node.on('canvasModeChanged') exists yet in node.ts — distinct from NodeModeChangedEvent
(execution mode only). v2 contract: app-level or node-level canvasModeChanged event.
Flagged: Terry DX walkthrough A.1. Informs: node.ts overloads (add canvasModeChanged
or document as known gap), I-TF.2 test triple for BC.38.
usage_weight: 0.0
source: notion-pain-point-assessment
exemplars:
- pattern_id: S17.AM1
repo: (first-principles assessment — Terry Jia)
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
stars: 0
- category_id: BC.39
name: Subgraph boundary event propagation
intent: >-
Custom node callbacks (onExecuted, MatchType, autogrow onConnectionsChange, promoted widget
callbacks) that must propagate across subgraph boundaries. Four distinct silent-failure modes
when custom nodes are placed inside subgraphs.
member_pattern_ids:
- S17.SB1
mechanism: absent-api
notes: >-
Requires D9 Phase B (post-Alex rebase on #11939). ECS substrate must forward SubgraphNode
execution events from internal nodes. MatchType and autogrow propagation require subgraph
boundary awareness in World dispatcher. Blocked: I-PG.B1. Short-term: @experimental on
affected NodeHandle events; subgraphCompatible flag in NodeExtensionOptions.
Intersects: ADR 0006 (I-NEW.1), Austin's fix-linked-widget-promotion.
Flagged: Terry DX walkthrough A.2.
usage_weight: 0.0
source: notion-pain-point-assessment
exemplars:
- pattern_id: S17.SB1
repo: (first-principles assessment — Terry Jia)
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
stars: 0
- category_id: BC.40
name: File upload and asset URL construction
intent: >-
Upload files to ComfyUI backend and construct retrieval URLs. 32+ packages duplicate this
pattern from scratch — FormData construction, fetchApi('/upload/image'), /view?filename URL
assembly. A helper API would collapse this to comfyAPI.uploadFile() + comfyAPI.getFileUrl().
member_pattern_ids:
- S17.FA1
mechanism: absent-api
notes: >-
Out of scope for @comfyorg/extension-api (node extension surface). Belongs in future
@comfyorg/comfy-api package. 32+ packages affected; 9 implement video upload variants.
Upload timeout hardcoded 120s; large 3D/video fail silently. No temp file lifecycle.
Document as known gap in src/extension-api/README.md.
Flagged: Terry DX walkthrough A.3.
usage_weight: 0.0
source: notion-pain-point-assessment
exemplars:
- pattern_id: S17.FA1
repo: (first-principles assessment — Terry Jia)
url: https://www.notion.so/comfy-org/Develop-a-custom-node-from-scratch-pain-point-assessment-33c6d73d365080f49126c0b5affa7559
stars: 0
- category_id: BC.41
name: Widget values positional serialization fragility
intent: >-
Widget values serialized as positional array [v1, v2, v3] instead of named dict.
Any input definition change (add, reorder, rename, remove, required→optional) silently
misaligns values when loading existing workflows. Root cause of #1 user complaint:
"my workflow broke after I updated the custom node."
member_pattern_ids:
- S17.WV1
mechanism: positional-array
notes: >-
Blocked on workflow-schema-migration (out of v2 surface scope). D7 Part 4 (4→2
serialization collapse) + beforeSerialize as partial mitigation. Long-term fix: named dict
format { widgetName: value } — breaking JSON schema change requiring versioning +
migrateWidgetValues() callback. PR #10392 added widgets_values_named opt-in; PR #11884
null guard. v2 contract: name-keyed identity (WidgetHandle by name not position).
Intersects: ADR 0006, widget-serialization-historical-analysis.md, Austin's work.
Flagged: Terry DX walkthrough A.4.
usage_weight: 0.0
source: notion-pain-point-assessment
exemplars:
- pattern_id: S17.WV1
repo: Comfy-Org/ComfyUI_frontend
url: https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/utils/nodeDefOrderingUtil.ts
stars: 1787

File diff suppressed because it is too large Load Diff

View File

@@ -1,117 +0,0 @@
#!/usr/bin/env python3
"""
Compat-floor gate: Verify all high-impact behavior categories have test triples.
Per PLAN.md §Compat-floor: "Every blast_radius ≥ 2.0 pattern MUST pass v1 + v2 +
migration tests before v2 ships."
This script:
1. Reads research/touch-points/behavior-categories.yaml
2. Finds all categories with usage_weight >= 2.0 (blast_radius threshold)
3. Checks that each has all three test files: bc-XX.v1.test.ts, bc-XX.v2.test.ts, bc-XX.migration.test.ts
4. Exits 0 if all present, exits 1 if any missing (fails CI)
Usage: python3 scripts/check-compat-floor.py
"""
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("ERROR: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr)
sys.exit(1)
COMPAT_FLOOR_THRESHOLD = 2.0
BEHAVIOR_CATEGORIES_PATH = Path("research/touch-points/behavior-categories.yaml")
TESTS_DIR = Path("src/extension-api-v2/__tests__")
def main():
# Check that behavior-categories.yaml exists
if not BEHAVIOR_CATEGORIES_PATH.exists():
print(f"ERROR: {BEHAVIOR_CATEGORIES_PATH} not found", file=sys.stderr)
print(" Run scripts/build-behavior-categories.py first or copy from workspace", file=sys.stderr)
sys.exit(1)
# Skip check if tests directory doesn't exist (tests only in tf branch)
if not TESTS_DIR.exists():
print(f"SKIP: {TESTS_DIR} not found — compat-floor tests not yet added to this branch")
print(" The compat-floor gate only enforces on branches with extension-api-v2 tests.")
sys.exit(0)
# Load categories
with open(BEHAVIOR_CATEGORIES_PATH, "r") as f:
data = yaml.safe_load(f)
categories = data.get("categories", [])
# Find categories above compat floor
above_floor = []
for cat in categories:
cat_id = cat.get("category_id", "")
usage_weight = cat.get("usage_weight", 0)
if usage_weight >= COMPAT_FLOOR_THRESHOLD:
above_floor.append({
"id": cat_id,
"name": cat.get("name", ""),
"usage_weight": usage_weight
})
print(f"Compat-floor check: {len(above_floor)} categories with usage_weight >= {COMPAT_FLOOR_THRESHOLD}")
print()
# Check each category for test triples
missing = []
for cat in above_floor:
cat_id = cat["id"]
# Extract number from BC.XX
num_str = cat_id.replace("BC.", "").zfill(2)
required_files = [
f"bc-{num_str}.v1.test.ts",
f"bc-{num_str}.v2.test.ts",
f"bc-{num_str}.migration.test.ts"
]
cat_missing = []
for fname in required_files:
fpath = TESTS_DIR / fname
if not fpath.exists():
cat_missing.append(fname)
if cat_missing:
missing.append({
"category": cat_id,
"name": cat["name"],
"usage_weight": cat["usage_weight"],
"missing": cat_missing
})
status = "❌ MISSING"
else:
status = ""
print(f" {cat_id} ({cat['usage_weight']:.2f}) {cat['name'][:40]:<40} {status}")
if cat_missing:
for m in cat_missing:
print(f" └─ {m}")
print()
if missing:
print(f"FAIL: {len(missing)} categories missing test files", file=sys.stderr)
print()
print("Per PLAN.md §Compat-floor, all blast_radius >= 2.0 categories", file=sys.stderr)
print("must have complete test triples (v1, v2, migration) before v2 ships.", file=sys.stderr)
print()
print("Missing files:", file=sys.stderr)
for m in missing:
for f in m["missing"]:
print(f" - {TESTS_DIR / f}", file=sys.stderr)
sys.exit(1)
else:
print(f"PASS: All {len(above_floor)} compat-floor categories have test triples")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# PKG5.D6 — Generate TypeDoc → Mintlify MDX for @comfyorg/extension-api
#
# Output: packages/extension-api/docs-build/mintlify/*.mdx
# packages/extension-api/docs-build/mintlify/nav-snippet.json
#
# Prerequisites: pnpm install must have been run (typedoc, tsx)
# Usage: ./scripts/generate-docs.sh [--watch]
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PKG_DIR="$REPO_ROOT/packages/extension-api"
if [ ! -f "$PKG_DIR/package.json" ]; then
echo "ERROR: $PKG_DIR/package.json not found — run from repo root or ensure packages/extension-api exists." >&2
exit 1
fi
if [ "${1:-}" = "--watch" ]; then
echo "Starting docs watch mode..."
pnpm --filter @comfyorg/extension-api docs:watch
else
echo "Generating extension API docs..."
pnpm --filter @comfyorg/extension-api docs:build
echo ""
echo "Done. MDX files written to: $PKG_DIR/docs-build/mintlify/"
echo "Copy to Comfy-Org/docs: cp -r $PKG_DIR/docs-build/mintlify/* <docs-repo>/extensions/api/"
fi

View File

@@ -11,6 +11,7 @@ declare global {
const __ALGOLIA_APP_ID__: string
const __ALGOLIA_API_KEY__: string
const __USE_PROD_CONFIG__: boolean
const __DEV_SERVER_COMFYUI_URL__: string
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
const __IS_NIGHTLY__: boolean
}
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
__ALGOLIA_APP_ID__: string
__ALGOLIA_API_KEY__: string
__USE_PROD_CONFIG__: boolean
__DEV_SERVER_COMFYUI_URL__: string
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
__IS_NIGHTLY__: boolean
window?: Record<string, unknown>
@@ -37,6 +39,7 @@ globalWithDefines.__SENTRY_DSN__ = ''
globalWithDefines.__ALGOLIA_APP_ID__ = ''
globalWithDefines.__ALGOLIA_API_KEY__ = ''
globalWithDefines.__USE_PROD_CONFIG__ = false
globalWithDefines.__DEV_SERVER_COMFYUI_URL__ = ''
globalWithDefines.__DISTRIBUTION__ = 'localhost'
globalWithDefines.__IS_NIGHTLY__ = false

View File

@@ -11,7 +11,6 @@
<DialogOverlay />
<DialogContent
:size="item.dialogComponentProps.size ?? 'md'"
:class="item.dialogComponentProps.contentClass"
:aria-labelledby="item.key"
@escape-key-down="
(e) =>

View File

@@ -1,11 +1,11 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createPinia, setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComponentProps } from 'vue-component-type-helpers'
import { createI18n } from 'vue-i18n'
import { useDialogStore } from '@/stores/dialogStore'
import ConfirmationDialogContent from './ConfirmationDialogContent.vue'
type Props = ComponentProps<typeof ConfirmationDialogContent>
@@ -13,23 +13,7 @@ type Props = ComponentProps<typeof ConfirmationDialogContent>
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: {
cancel: 'Cancel',
confirm: 'Confirm',
delete: 'Delete',
overwrite: 'Overwrite',
save: 'Save',
no: 'No',
ok: 'OK',
close: 'Close'
},
desktopMenu: {
reinstall: 'Reinstall'
}
}
},
messages: { en: {} },
missingWarn: false,
fallbackWarn: false
})
@@ -40,9 +24,10 @@ describe('ConfirmationDialogContent', () => {
})
function renderComponent(props: Partial<Props> = {}) {
const user = userEvent.setup()
render(ConfirmationDialogContent, {
global: { plugins: [i18n] },
return render(ConfirmationDialogContent, {
global: {
plugins: [PrimeVue, i18n]
},
props: {
message: 'Test message',
type: 'default',
@@ -50,7 +35,6 @@ describe('ConfirmationDialogContent', () => {
...props
} as Props
})
return { user }
}
it('renders long messages without breaking layout', () => {
@@ -60,103 +44,42 @@ describe('ConfirmationDialogContent', () => {
expect(screen.getByText(longFilename)).toBeInTheDocument()
})
it('renders the hint as a status alert when provided', () => {
renderComponent({ hint: 'This action cannot be undone.' })
const status = screen.getByRole('status')
expect(status).toHaveTextContent('This action cannot be undone.')
it('omits the Cancel button when type is dirtyClose', () => {
renderComponent({ type: 'dirtyClose' })
expect(screen.queryByText('g.cancel')).not.toBeInTheDocument()
expect(screen.getByText('g.save')).toBeInTheDocument()
})
it('does not render a status alert when hint is omitted', () => {
renderComponent()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
it('uses the provided denyLabel for the deny button on dirtyClose', () => {
renderComponent({ type: 'dirtyClose', denyLabel: 'Sign out anyway' })
expect(screen.getByText('Sign out anyway')).toBeInTheDocument()
expect(screen.queryByText('g.no')).not.toBeInTheDocument()
})
describe('button surface per type', () => {
it("type='default' renders Cancel and Confirm", () => {
renderComponent({ type: 'default' })
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Confirm' })
).toBeInTheDocument()
})
it("type='delete' renders Cancel and Delete", () => {
renderComponent({ type: 'delete' })
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument()
})
it("type='overwrite' renders Cancel and Overwrite", () => {
renderComponent({ type: 'overwrite' })
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'Overwrite' })
).toBeInTheDocument()
})
it("type='dirtyClose' renders No and Save (no Cancel)", () => {
renderComponent({ type: 'dirtyClose' })
expect(
screen.queryByRole('button', { name: 'Cancel' })
).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
})
it("type='info' renders only OK (no Cancel)", () => {
renderComponent({ type: 'info' })
expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Cancel' })
).not.toBeInTheDocument()
})
})
it('confirm callback receives true and closes the dialog', async () => {
it('calls onConfirm(false) when deny is clicked on dirtyClose', async () => {
const onConfirm = vi.fn()
const { user } = renderComponent({ type: 'default', onConfirm })
const closeSpy = vi.spyOn(useDialogStore(), 'closeDialog')
renderComponent({
type: 'dirtyClose',
denyLabel: 'Close anyway',
onConfirm
})
await user.click(screen.getByRole('button', { name: 'Confirm' }))
await userEvent.click(screen.getByRole('button', { name: 'Close anyway' }))
expect(onConfirm).toHaveBeenCalledWith(false)
})
it('calls onConfirm(true) when save is clicked on dirtyClose', async () => {
const onConfirm = vi.fn()
renderComponent({ type: 'dirtyClose', onConfirm })
await userEvent.click(screen.getByRole('button', { name: 'g.save' }))
expect(onConfirm).toHaveBeenCalledWith(true)
expect(closeSpy).toHaveBeenCalledOnce()
})
describe('dirtyClose deny label', () => {
it('uses the provided denyLabel for the deny button', () => {
renderComponent({ type: 'dirtyClose', denyLabel: 'Sign out anyway' })
expect(screen.getByText('Sign out anyway')).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'No' })
).not.toBeInTheDocument()
})
it('falls back to "No" when denyLabel is not provided', () => {
renderComponent({ type: 'dirtyClose' })
expect(screen.getByRole('button', { name: 'No' })).toBeInTheDocument()
})
it('calls onConfirm(false) when deny is clicked', async () => {
const onConfirm = vi.fn()
const { user } = renderComponent({
type: 'dirtyClose',
denyLabel: 'Close anyway',
onConfirm
})
await user.click(screen.getByRole('button', { name: 'Close anyway' }))
expect(onConfirm).toHaveBeenCalledWith(false)
})
it('calls onConfirm(true) when save is clicked', async () => {
const onConfirm = vi.fn()
const { user } = renderComponent({ type: 'dirtyClose', onConfirm })
await user.click(screen.getByRole('button', { name: 'Save' }))
expect(onConfirm).toHaveBeenCalledWith(true)
})
it('falls back to "no" label when denyLabel is not provided', () => {
renderComponent({ type: 'dirtyClose' })
expect(screen.getByText('g.no')).toBeInTheDocument()
})
})

View File

@@ -9,14 +9,16 @@
{{ item }}
</li>
</ul>
<div
<Message
v-if="hint"
role="status"
class="mt-2 flex items-start gap-2 text-sm text-muted-foreground"
class="mt-2"
icon="pi pi-info-circle"
severity="secondary"
size="small"
variant="simple"
>
<i class="pi pi-info-circle mt-0.5" aria-hidden="true" />
<span>{{ hint }}</span>
</div>
{{ hint }}
</Message>
</div>
<div class="flex shrink-0 flex-wrap justify-end gap-4">
<div
@@ -113,6 +115,7 @@
</template>
<script setup lang="ts">
import Message from 'primevue/message'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -4,7 +4,7 @@ import { computed, reactive, ref, shallowRef, watch } from 'vue'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
@@ -44,24 +44,24 @@ watch(
}
)
function isSectionCollapsed(nodeId: NodeId): boolean {
function isSectionCollapsed(nodeId: string): boolean {
// Defaults to collapsed when not explicitly set by the user
return collapseMap[nodeId] ?? true
}
function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
collapseMap[nodeId] = collapsed
}
const isAllCollapsed = computed({
get() {
return searchedWidgetsSectionDataList.value.every(({ node }) =>
isSectionCollapsed(node.id)
isSectionCollapsed(String(node.id))
)
},
set(collapse: boolean) {
for (const { node } of widgetsSectionDataList.value) {
setSectionCollapsed(node.id, collapse)
setSectionCollapsed(String(node.id), collapse)
}
}
})
@@ -101,7 +101,7 @@ async function searcher(query: string) {
:key="node.id"
:node
:widgets
:collapse="isSectionCollapsed(node.id) && !isSearching"
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
:tooltip="
isSearching || widgets.length
? ''
@@ -109,7 +109,7 @@ async function searcher(query: string) {
"
show-locate-button
class="border-b border-interface-stroke"
@update:collapse="setSectionCollapsed(node.id, $event)"
@update:collapse="setSectionCollapsed(String(node.id), $event)"
/>
</TransitionGroup>
</template>

View File

@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
import { computed, reactive, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -68,19 +68,19 @@ watch(
}
)
function isSectionCollapsed(nodeId: NodeId): boolean {
function isSectionCollapsed(nodeId: string): boolean {
// When not explicitly set, sections are collapsed if multiple nodes are selected
return collapseMap[nodeId] ?? isMultipleNodesSelected.value
}
function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
function setSectionCollapsed(nodeId: string, collapsed: boolean) {
collapseMap[nodeId] = collapsed
}
const isAllCollapsed = computed({
get() {
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
({ node }) => isSectionCollapsed(node.id)
({ node }) => isSectionCollapsed(String(node.id))
)
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
return hasAdvanced
@@ -89,7 +89,7 @@ const isAllCollapsed = computed({
},
set(collapse: boolean) {
for (const { node } of widgetsSectionDataList.value) {
setSectionCollapsed(node.id, collapse)
setSectionCollapsed(String(node.id), collapse)
}
advancedCollapsed.value = collapse
}
@@ -154,7 +154,7 @@ const advancedLabel = computed(() => {
:node
:label
:widgets
:collapse="isSectionCollapsed(node.id) && !isSearching"
:collapse="isSectionCollapsed(String(node.id)) && !isSearching"
:show-locate-button="isMultipleNodesSelected"
:tooltip="
isSearching || widgets.length
@@ -162,7 +162,7 @@ const advancedLabel = computed(() => {
: t('rightSidePanel.inputsNoneTooltip')
"
class="border-b border-interface-stroke"
@update:collapse="setSectionCollapsed(node.id, $event)"
@update:collapse="setSectionCollapsed(String(node.id), $event)"
/>
</TransitionGroup>
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">

View File

@@ -252,20 +252,6 @@ describe('CurrentUserPopoverLegacy', () => {
expect(screen.getByText('Log Out')).toBeInTheDocument()
})
describe('credits help icon (FE-617)', () => {
it('renders the credits help icon as an interactive button with the unified-credits tooltip as its accessible name', () => {
renderComponent()
const helpButton = screen.getByTestId('credits-info-button')
expect(helpButton).toBeInTheDocument()
expect(helpButton.tagName).toBe('BUTTON')
expect(helpButton).toHaveAttribute(
'aria-label',
enMessages.credits.unified.tooltip
)
})
})
it('opens user settings and emits close event when settings item is clicked', async () => {
const { user, onClose } = renderComponent()

View File

@@ -41,16 +41,10 @@
<span v-else class="text-base font-semibold text-base-foreground">{{
formattedBalance
}}</span>
<Button
<i
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
variant="muted-textonly"
size="icon-sm"
class="mr-auto"
:aria-label="$t('credits.unified.tooltip')"
data-testid="credits-info-button"
>
<i class="icon-[lucide--circle-help]" />
</Button>
class="mr-auto icon-[lucide--circle-help] cursor-help text-base text-muted-foreground"
/>
<Button
v-if="isCloud && isFreeTier"
variant="gradient"

View File

@@ -543,7 +543,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
}
])
const verifySpy = vi
.spyOn(missingMediaScan, 'verifyMediaCandidates')
.spyOn(missingMediaScan, 'verifyCloudMediaCandidates')
.mockImplementation(async (candidates) => {
for (const c of candidates) c.isMissing = true
})
@@ -686,7 +686,7 @@ describe('realtime verification staleness guards', () => {
let resolveVerify: (() => void) | undefined
const verifyPromise = new Promise<void>((r) => (resolveVerify = r))
const verifySpy = vi
.spyOn(missingMediaScan, 'verifyMediaCandidates')
.spyOn(missingMediaScan, 'verifyCloudMediaCandidates')
.mockImplementation(async (candidates) => {
await verifyPromise
for (const c of candidates) c.isMissing = true

View File

@@ -28,7 +28,7 @@ import {
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import {
scanNodeMediaCandidates,
verifyMediaCandidates
verifyCloudMediaCandidates
} from '@/platform/missingMedia/missingMediaScan'
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
@@ -209,8 +209,8 @@ function scanSingleNodeErrors(node: LGraphNode): void {
if (confirmedMedia.length) {
useMissingMediaStore().addMissingMedia(confirmedMedia)
}
// Cloud media scans return pending for asset verification. OSS scans only
// return pending for generated output/temp media.
// Cloud media scans always return isMissing: undefined pending
// verification against the input-assets list.
const pendingMedia = mediaCandidates.filter((c) => c.isMissing === undefined)
if (pendingMedia.length) {
void verifyAndAddPendingMedia(pendingMedia)
@@ -282,7 +282,7 @@ async function verifyAndAddPendingMedia(
): Promise<void> {
const rootGraphAtScan = app.rootGraph
try {
await verifyMediaCandidates(pending, { isCloud })
await verifyCloudMediaCandidates(pending)
if (app.rootGraph !== rootGraphAtScan) return
const verified = pending.filter(
(c) => c.isMissing === true && isCandidateStillActive(c.nodeId)

View File

@@ -1,88 +0,0 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useReconnectQueueRefresh } from '@/composables/useReconnectQueueRefresh'
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'
function makeJob(id: string, status: JobListItem['status']): JobListItem {
return {
id,
status,
create_time: 0,
update_time: 0,
last_state_update: 0,
priority: 0
}
}
vi.mock('@/scripts/api', () => ({
api: {
getQueue: vi.fn(),
getHistory: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
apiURL: vi.fn((p: string) => `/api${p}`)
}
}))
describe('useReconnectQueueRefresh', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.restoreAllMocks()
vi.mocked(api.getQueue).mockResolvedValue({ Running: [], Pending: [] })
vi.mocked(api.getHistory).mockResolvedValue([])
})
it('forwards running+pending job ids to clearActiveJobIfStale', async () => {
vi.mocked(api.getQueue).mockResolvedValue({
Running: [makeJob('run-1', 'in_progress')],
Pending: [makeJob('pend-1', 'pending'), makeJob('pend-2', 'pending')]
})
const executionStore = useExecutionStore()
const clearSpy = vi
.spyOn(executionStore, 'clearActiveJobIfStale')
.mockImplementation(() => {})
const refresh = useReconnectQueueRefresh()
await refresh()
expect(clearSpy).toHaveBeenCalledTimes(1)
expect(clearSpy).toHaveBeenCalledWith(
new Set(['run-1', 'pend-1', 'pend-2'])
)
})
it('passes an empty set when the queue is genuinely empty', async () => {
const executionStore = useExecutionStore()
const clearSpy = vi
.spyOn(executionStore, 'clearActiveJobIfStale')
.mockImplementation(() => {})
const refresh = useReconnectQueueRefresh()
await refresh()
expect(clearSpy).toHaveBeenCalledWith(new Set())
})
it('reuses the prior queue snapshot when the fetch fails, so a still-running job is not falsely cleared', async () => {
vi.mocked(api.getQueue)
.mockResolvedValueOnce({
Running: [makeJob('run-1', 'in_progress')],
Pending: []
})
.mockRejectedValueOnce(new Error('network down'))
const executionStore = useExecutionStore()
const clearSpy = vi
.spyOn(executionStore, 'clearActiveJobIfStale')
.mockImplementation(() => {})
const refresh = useReconnectQueueRefresh()
await refresh() // primes the store with run-1
await refresh() // network failure here — store must not go empty
expect(clearSpy).toHaveBeenLastCalledWith(new Set(['run-1']))
})
})

View File

@@ -1,25 +0,0 @@
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
/**
* After a WebSocket reconnect, refresh the queue from the server and clear
* any active job that finished during the disconnect window. Returns the
* handler so the caller can wire it to the `reconnected` api event.
*
* `update()` preserves the previous queue snapshot when the fetch fails, so
* if the network is still flaky we reconcile against the last known good
* state rather than an empty (and falsely "stale") set.
*/
export function useReconnectQueueRefresh() {
const queueStore = useQueueStore()
const executionStore = useExecutionStore()
return async function refreshOnReconnect() {
await queueStore.update()
const activeJobIds = new Set([
...queueStore.runningTasks.map((t) => t.jobId),
...queueStore.pendingTasks.map((t) => t.jobId)
])
executionStore.clearActiveJobIfStale(activeJobIds)
}
}

View File

@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { getComfyApiBaseUrlForEnvironment } from '@/config/comfyApi'
describe('comfy api config', () => {
it('uses same-origin API calls for cloud local development', () => {
expect(
getComfyApiBaseUrlForEnvironment({
isCloudDistribution: true,
isDev: true,
devServerComfyUIUrl: 'http://127.0.0.1:8188',
useProdConfig: false
})
).toBe('')
})
it('keeps staging API for non-local staging builds', () => {
expect(
getComfyApiBaseUrlForEnvironment({
isCloudDistribution: true,
isDev: false,
useProdConfig: false
})
).toBe('https://stagingapi.comfy.org')
})
})

View File

@@ -10,34 +10,68 @@ const STAGING_API_BASE_URL = 'https://stagingapi.comfy.org'
const PROD_PLATFORM_BASE_URL = 'https://platform.comfy.org'
const STAGING_PLATFORM_BASE_URL = 'https://stagingplatform.comfy.org'
const BUILD_TIME_API_BASE_URL = __USE_PROD_CONFIG__
? PROD_API_BASE_URL
: STAGING_API_BASE_URL
type ComfyApiEnvironment = {
isCloudDistribution: boolean
isDev: boolean
devServerComfyUIUrl?: string
useProdConfig: boolean
}
const BUILD_TIME_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
? PROD_PLATFORM_BASE_URL
: STAGING_PLATFORM_BASE_URL
const localOriginPattern =
/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?(?:\/|$)/
export function getComfyApiBaseUrl(): string {
if (!isCloud) {
return BUILD_TIME_API_BASE_URL
function buildTimeApiBaseUrl(useProdConfig: boolean): string {
return useProdConfig ? PROD_API_BASE_URL : STAGING_API_BASE_URL
}
function buildTimePlatformBaseUrl(useProdConfig: boolean): string {
return useProdConfig ? PROD_PLATFORM_BASE_URL : STAGING_PLATFORM_BASE_URL
}
function isLocalDevServer(url?: string): boolean {
return url ? localOriginPattern.test(url) : false
}
export function getComfyApiBaseUrlForEnvironment({
isCloudDistribution,
isDev,
devServerComfyUIUrl,
useProdConfig
}: ComfyApiEnvironment): string {
const buildTimeApiBaseUrlValue = buildTimeApiBaseUrl(useProdConfig)
if (!isCloudDistribution) {
return buildTimeApiBaseUrlValue
}
if (isDev && isLocalDevServer(devServerComfyUIUrl)) {
return ''
}
return configValueOrDefault(
remoteConfig.value,
'comfy_api_base_url',
BUILD_TIME_API_BASE_URL
buildTimeApiBaseUrlValue
)
}
export function getComfyApiBaseUrl(): string {
return getComfyApiBaseUrlForEnvironment({
isCloudDistribution: isCloud,
isDev: import.meta.env.DEV,
devServerComfyUIUrl: __DEV_SERVER_COMFYUI_URL__,
useProdConfig: __USE_PROD_CONFIG__
})
}
export function getComfyPlatformBaseUrl(): string {
const buildTimePlatformBaseUrlValue =
buildTimePlatformBaseUrl(__USE_PROD_CONFIG__)
if (!isCloud) {
return BUILD_TIME_PLATFORM_BASE_URL
return buildTimePlatformBaseUrlValue
}
return configValueOrDefault(
remoteConfig.value,
'comfy_platform_base_url',
BUILD_TIME_PLATFORM_BASE_URL
buildTimePlatformBaseUrlValue
)
}

View File

@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import {
getFirebaseConfigForEnvironment,
getFirebaseAuthEmulatorUrl
} from '@/config/firebase'
describe('firebase config', () => {
it('uses the explicit local project id when the auth emulator is enabled', () => {
const config = getFirebaseConfigForEnvironment({
isCloudBuild: false,
useProdConfig: false,
authEmulatorHost: '127.0.0.1:9099',
localProjectId: 'demo-cloud'
})
expect(config.projectId).toBe('demo-cloud')
expect(config.authDomain).toBe('demo-cloud.firebaseapp.com')
})
it('fails fast when the auth emulator is enabled without a local project id', () => {
expect(() =>
getFirebaseConfigForEnvironment({
isCloudBuild: false,
useProdConfig: false,
authEmulatorHost: '127.0.0.1:9099'
})
).toThrow('VITE_FIREBASE_PROJECT_ID is required')
})
it('does not connect to the emulator without the explicit host flag', () => {
expect(getFirebaseAuthEmulatorUrl(undefined)).toBeNull()
expect(getFirebaseAuthEmulatorUrl('127.0.0.1:9099')).toBe(
'http://127.0.0.1:9099'
)
})
})

View File

@@ -25,7 +25,54 @@ const PROD_CONFIG: FirebaseOptions = {
measurementId: 'G-3ZBD3MBTG4'
}
const BUILD_TIME_CONFIG = __USE_PROD_CONFIG__ ? PROD_CONFIG : DEV_CONFIG
type FirebaseEnvironment = {
isCloudBuild: boolean
useProdConfig: boolean
authEmulatorHost?: string
localProjectId?: string
}
function buildLocalEmulatorConfig(
buildTimeConfig: FirebaseOptions,
localProjectId: string | undefined
): FirebaseOptions {
if (!localProjectId) {
throw new Error(
'VITE_FIREBASE_PROJECT_ID is required when VITE_FIREBASE_AUTH_EMULATOR_HOST is set'
)
}
return {
...buildTimeConfig,
projectId: localProjectId,
authDomain: `${localProjectId}.firebaseapp.com`
}
}
export function getFirebaseAuthEmulatorUrl(
host: string | undefined
): string | null {
return host ? `http://${host}` : null
}
export function getFirebaseConfigForEnvironment({
isCloudBuild,
useProdConfig,
authEmulatorHost,
localProjectId
}: FirebaseEnvironment): FirebaseOptions {
const buildTimeConfig = useProdConfig ? PROD_CONFIG : DEV_CONFIG
if (authEmulatorHost) {
return buildLocalEmulatorConfig(buildTimeConfig, localProjectId)
}
if (!isCloudBuild) {
return buildTimeConfig
}
const runtimeConfig = remoteConfig.value.firebase_config
return runtimeConfig ?? buildTimeConfig
}
/**
* Returns the Firebase configuration for the current environment.
@@ -33,10 +80,10 @@ const BUILD_TIME_CONFIG = __USE_PROD_CONFIG__ ? PROD_CONFIG : DEV_CONFIG
* - OSS / localhost builds fall back to the build-time config determined by __USE_PROD_CONFIG__
*/
export function getFirebaseConfig(): FirebaseOptions {
if (!isCloud) {
return BUILD_TIME_CONFIG
}
const runtimeConfig = remoteConfig.value.firebase_config
return runtimeConfig ?? BUILD_TIME_CONFIG
return getFirebaseConfigForEnvironment({
isCloudBuild: isCloud,
useProdConfig: __USE_PROD_CONFIG__,
authEmulatorHost: import.meta.env.VITE_FIREBASE_AUTH_EMULATOR_HOST,
localProjectId: import.meta.env.VITE_FIREBASE_PROJECT_ID
})
}

View File

@@ -1,68 +0,0 @@
# Extension API — Public Source of Truth
> **Status**: Implemented (Phase A). Runtime backed by stub ECS components;
> full ECS integration lands with #11939.
This folder is the single source of truth for the public ComfyUI extension
API. Every file here is part of the published `@comfyorg/extension-api`
npm package. Do not re-export from `/src` — this barrel is the **published
package entry point**, which is the explicit exception to the project's
"no barrel files in /src" rule (root AGENTS.md rule #19).
## File structure
```
extension-api/
├── index.ts ← barrel — package entry point
├── node.ts ← NodeHandle interface + node event payload types
├── widget.ts ← WidgetHandle interface + widget event payload types
├── types.ts ← ExtensionOptions, NodeExtensionOptions, WidgetExtensionOptions
├── events.ts ← Handler<E>, AsyncHandler<E>, Unsubscribe
├── lifecycle.ts ← onNodeMounted, onNodeRemoved hooks + rationale docs
├── shell.ts ← SidebarTabExtension, BottomPanelExtension, CommandManager, etc.
├── identifiers.ts ← NodeLocatorId, NodeExecutionId + parsers/type guards
└── README.md ← this file
```
## What about v1?
v1 (`ComfyExtension` interface in `../types/comfy.ts`, `app.registerExtension(...)`
runtime entry point in `../scripts/app.ts`) **stays in its current locations**.
Custom extensions in the wild consume the runtime entry point, not the type
file — moving the type file would churn ~30 internal imports for zero runtime
benefit. The v1↔v2 distinction is at the entry point, not the folder.
## Authoring rules
1. **Hand-authored**, not generated. This is a public API; we own the shape.
2. **No `any`, no `as any`, no `@ts-expect-error`.** If you need an escape
hatch, the type is wrong.
3. Every public type has a TSDoc block with at minimum:
- 1-line summary
- `@stability` tag (`stable` | `experimental` | `deprecated`)
- `@example` block (where applicable)
4. Naming follows conventions:
- Read-only invariants (set at construction): `readonly` property
- Read-only state (changes over time): method (`getValue()`)
- Mutating actions: method (`setValue(v)`)
- Boolean predicates: method (`isHidden()`)
5. Events: typed payloads, no `Function`, split-channel events
(`valueChange` / `optionChange` / `propertyChange`).
6. No internal types (`World`, `Component<T>`, branded `EntityId` internals)
leak through this barrel.
## Key design decisions
| ADR | Decision |
| ----------------------------------------------------------------------------- | --------------------------------------------------------- |
| [ADR-0008](../../docs/adr/0008-entity-component-system.md) | Entity Component System architecture |
| [ADR-0010](../../docs/adr/0010-deprecate-node-level-serialization-control.md) | Deprecate `node.on('beforeSerialize')` — use widget-level |
| [ADR-0011](../../docs/adr/0011-immutability-via-fresh-copies.md) | Return fresh copies from collection methods |
| [ADR-0012](../../docs/adr/0012-pure-function-loader-pattern.md) | Pure function registration + loader activation |
## Related research
- [Identity encapsulation](../../docs/research/identity-encapsulation.md) — when extensions need raw entity IDs
- [Coordinate systems](../../docs/research/coordinate-systems.md) — canvas vs screen coordinates
- [Widget state categories](../../docs/research/widget-state-categories.md) — value/properties/options/DOM
- [Serialization context](../../docs/research/serialization-context.md) — workflow/prompt/clone/subgraph-promote

View File

@@ -1,61 +0,0 @@
/**
* D18 Phase 1 — brand symbols for `define*` outputs.
*
* Per D18, `defineNode` / `defineWidget` / `defineExtension` will become
* pure functions whose return values are recognized at registration time
* by a loader that walks module exports and dispatches based on the brand.
*
* Phase 1 (this file) introduces the brand symbol and the `isBrandedExtension`
* type-guard. The `define*` functions stamp the brand on their returned
* options so a future loader can identify them. Side-effect registration
* remains unchanged in Phase 1; Phase 2 removes it.
*
* The brand is a `Symbol.for(...)` so HMR + duplicate-package scenarios still
* resolve to the same identity (per realm / per JS context).
*
* @internal — not re-exported from `@comfyorg/extension-api/index.ts`. The
* loader lives inside the runtime, not in the published package.
*/
export const EXTENSION_BRAND = Symbol.for('@comfyorg/extension-api:brand')
export type ExtensionKind = 'node' | 'widget' | 'app'
export interface Branded {
readonly [EXTENSION_BRAND]: ExtensionKind
}
/**
* Stamp a brand on an options object and freeze it. Returned reference is
* the same object — branding is non-enumerable so JSON serialization,
* spread operations, and shallow-equal comparisons are unaffected.
*/
export function stampBrand<T extends object>(
options: T,
kind: ExtensionKind
): T & Branded {
Object.defineProperty(options, EXTENSION_BRAND, {
value: kind,
enumerable: false,
writable: false,
configurable: false
})
return Object.freeze(options) as T & Branded
}
/**
* Type-guard for branded extension options. The loader uses this to
* decide whether a module export is a `defineX(...)` result.
*
* Unbranded values (utility exports, constants, helper functions) return
* `false` and are silently ignored by the loader.
*/
export function isBrandedExtension(value: unknown): value is Branded {
if (value === null || typeof value !== 'object') return false
const kind = (value as Record<symbol, unknown>)[EXTENSION_BRAND]
return kind === 'node' || kind === 'widget' || kind === 'app'
}
export function getBrandKind(value: Branded): ExtensionKind {
return value[EXTENSION_BRAND]
}

View File

@@ -1,277 +0,0 @@
/**
* Shared event infrastructure for the ComfyUI extension API.
*
* @packageDocumentation
*/
/**
* A typed event handler function.
*
* @typeParam E - The event payload type.
* @example
* ```ts
* const handler: Handler<WidgetValueChangeEvent<number>> = (e) => {
* console.log(e.oldValue, '->', e.newValue)
* }
* ```
*/
export type Handler<E> = (event: E) => void
/**
* A typed async-capable event handler. Only valid for events that explicitly
* support async handling (currently only `beforeSerialize`).
*
* @typeParam E - The event payload type.
* @example
* ```ts
* import type { AsyncHandler, WidgetBeforeSerializeEvent } from '@comfyorg/extension-api'
*
* const handler: AsyncHandler<WidgetBeforeSerializeEvent> = async (e) => {
* const frame = await captureFrame()
* e.setSerializedValue(frame)
* }
* ```
*/
export type AsyncHandler<E> = (event: E) => void | Promise<void>
/**
* Cleanup function returned by `on()` — call to remove the listener.
*
* @example
* ```ts
* const off = node.on('executed', handler)
* // later:
* off()
* ```
*/
export type Unsubscribe = () => void
// Event-namespace facades
//
// Four typed event-namespace handles (`graph` / `execution` / `server` /
// `workbench`) replace the ad-hoc `api.addEventListener('execution_start', ...)`
// pattern documented in 360+ ecosystem call sites. Each namespace is a
// module-level singleton (SD-4 (a), handoff-11) — call from any setup() body
// or hook closure. Subscriptions registered inside a setup context auto-dispose
// when the surrounding instance is unmounted (Vue-style; subscription is added
// to the context's unmountHooks). Outside a setup context, the returned
// `Unsubscribe` is the caller's responsibility.
//
// Payload typing (SD-5 (b)): each `on()` accepts a string event name and
// returns `Handler<EventPayloadMap[ns][evt]>`. The maps default to `unknown`
// today and are tightened by D5 module augmentation in a follow-on PR. Authors
// get autocomplete on canonical event names; payload narrowing arrives when
// D5 lands.
import { api } from '@/scripts/api'
import { getCurrentExtensionInstance } from '@/services/extension-api-service'
/**
* Per-namespace event payload map. **Augment via TS module augmentation** to
* narrow payloads for canonical events. Until D5 ships, all payloads default
* to `unknown`.
*
* @example
* ```ts
* declare module '@comfyorg/extension-api' {
* interface ExecutionEventPayloads {
* start: { promptId: string }
* progress: { value: number; max: number }
* }
* }
* ```
*/
export interface GraphEventPayloads {
[event: string]: unknown
}
/**
* See {@link GraphEventPayloads | the augmentation example} —
* augment this interface the same way to narrow `execution.*` payloads.
*/
export interface ExecutionEventPayloads {
[event: string]: unknown
}
/**
* See {@link GraphEventPayloads | the augmentation example} —
* augment this interface the same way to narrow `server.*` payloads.
*/
export interface ServerEventPayloads {
[event: string]: unknown
}
/**
* See {@link GraphEventPayloads | the augmentation example} —
* augment this interface the same way to narrow `workbench.*` payloads.
*/
export interface WorkbenchEventPayloads {
[event: string]: unknown
}
interface EventNamespace<M> {
/**
* Subscribe to an event. Returns an {@link Unsubscribe} function.
*
* Inside a `setup()` body the subscription is also added to the
* surrounding instance's `onUnmounted` queue and auto-disposes when the
* extension/tab/panel is unmounted.
*/
on<K extends keyof M & string>(event: K, handler: Handler<M[K]>): Unsubscribe
/**
* Remove a previously registered handler. Same as the {@link Unsubscribe}
* returned by `on()`. Exposed for symmetry with `addEventListener`/`removeEventListener`.
*/
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void
}
function makeNamespace<M>(rename: (evt: string) => string): EventNamespace<M> {
// ComfyApi extends EventTarget but its addEventListener is strictly typed
// against the validated ApiCalls union. The bootstrap-hooks facade
// accepts any string (custom-node events ride server.on with arbitrary
// names per ADR), so we widen via EventTarget to get the generic overload.
const target = api as unknown as EventTarget
return {
on<K extends keyof M & string>(
event: K,
handler: Handler<M[K]>
): Unsubscribe {
const wireName = rename(event)
// payload arrives as CustomEvent.detail.
const adapter = (e: Event): void => {
const detail = (e as CustomEvent).detail as M[K]
handler(detail)
}
target.addEventListener(wireName, adapter)
const unsubscribe: Unsubscribe = () => {
target.removeEventListener(wireName, adapter)
}
// Auto-dispose inside a setup() context (mirrors Vue's onScopeDispose).
const ctx = getCurrentExtensionInstance()
if (ctx) {
ctx.unmountHooks.push(unsubscribe)
}
return unsubscribe
},
off<K extends keyof M & string>(event: K, handler: Handler<M[K]>): void {
// Note: off() with a raw handler only matches if the caller saved the
// exact adapter reference returned from on(). The recommended path is
// to call the Unsubscribe returned by on(). This off() is retained for
// API symmetry but does NOT round-trip with on() handlers — they wrap
// the user fn in an adapter for CustomEvent unwrap. Authors that need
// explicit off() should use the Unsubscribe handle.
target.removeEventListener(
rename(event),
handler as unknown as EventListener
)
}
}
}
/**
* Graph-mutation events (frontend-dispatched).
*
* @publicAPI
* @stability experimental
* @example
* ```ts
* import { onMounted, graph } from '@comfyorg/extension-api'
*
* defineExtension({
* name: 'my-ext',
* setup() {
* onMounted(() => {
* graph.on('changed', (e) => console.log('graph changed', e))
* })
* }
* })
* ```
*/
export const graph: EventNamespace<GraphEventPayloads> = makeNamespace(
(evt) => `graph:${evt}`
)
/**
* Prompt-run lifecycle events (backend-dispatched).
*
* Canonical events: `'start'`, `'end'`, `'error'`, `'interrupted'`, `'cached'`,
* `'executing'`, `'progress'`, `'preview'`. The wire-name mapping rewrites
* `'start'` → `'execution_start'`, etc., matching the legacy
* `api.addEventListener('execution_start', ...)` shape.
*
* @publicAPI
* @stability experimental
* @example
* ```ts
* import { defineExtension, onMounted, execution } from '@comfyorg/extension-api'
*
* defineExtension({
* name: 'my-ext',
* setup() {
* onMounted(() => {
* execution.on('start', (e) => console.log('run started', e))
* execution.on('progress', (e) => console.log('progress', e))
* execution.on('end', () => console.log('run done'))
* })
* }
* })
* ```
*/
export const execution: EventNamespace<ExecutionEventPayloads> = makeNamespace(
(evt) => `execution_${evt}`
)
/**
* Non-execution backend events + custom-node events.
*
* Canonical events: `'status'`, `'logs'`, `'reconnected'`, `'feature_flags'`,
* `'assets'`. Custom-node events ride this channel with arbitrary string
* (e.g. `server.on('rayko.inspline.show', ...)`). Module-augment
* `ServerEventPayloads` to type custom events.
*
* @publicAPI
* @stability experimental
* @example
* ```ts
* import { defineExtension, onMounted, server } from '@comfyorg/extension-api'
*
* defineExtension({
* name: 'my-ext',
* setup() {
* onMounted(() => {
* server.on('reconnected', () => console.log('server back online'))
* })
* }
* })
* ```
*/
export const server: EventNamespace<ServerEventPayloads> = makeNamespace(
(evt) => evt
)
/**
* UI shell events.
*
* Canonical events today: `'notification'`. Future: `'themeChanged'`,
* `'panelToggled'`, `'commandInvoked'`. NOT a DI container — see
* the bootstrap-hooks design for the "thin event-namespace handle only"
* scope-back.
*
* @publicAPI
* @stability experimental
* @example
* ```ts
* import { defineExtension, onMounted, workbench } from '@comfyorg/extension-api'
*
* defineExtension({
* name: 'my-ext',
* setup() {
* onMounted(() => {
* workbench.on('notification', (e) => console.log('workbench notif', e))
* })
* }
* })
* ```
*/
export const workbench: EventNamespace<WorkbenchEventPayloads> = makeNamespace(
(evt) => `workbench:${evt}`
)

View File

@@ -1,54 +0,0 @@
/**
* Node identity helpers — re-exported from internal `nodeIdentification.ts`.
*
* `NodeLocatorId` and `NodeExecutionId` are the two stable node identity
* primitives in the public API. All extension-facing code that needs to
* reference a node across subgraph boundaries or execution runs should use
* these rather than raw LiteGraph integer node IDs.
*
* @packageDocumentation
*/
export type { NodeLocatorId, NodeExecutionId } from '@/types/nodeIdentification'
/**
* Node identity round-trip helpers. Create/parse branded `NodeLocatorId` and
* `NodeExecutionId` values, or narrow an `unknown` to one with the type
* guards. Use these instead of raw string manipulation so future changes to
* the identity scheme stay transparent.
*
* @example
* ```ts
* import {
* createNodeLocatorId,
* parseNodeLocatorId,
* isNodeLocatorId,
* createNodeExecutionId,
* parseNodeExecutionId,
* isNodeExecutionId
* } from '@comfyorg/extension-api'
*
* // Construct
* const locator = createNodeLocatorId(graphUuid, localId)
* const execId = createNodeExecutionId(locator, runTag)
*
* // Narrow
* if (isNodeLocatorId(maybe)) {
* const parts = parseNodeLocatorId(maybe)
* console.log(parts.graphUuid, parts.localId)
* }
*
* if (isNodeExecutionId(maybe)) {
* const parts = parseNodeExecutionId(maybe)
* console.log(parts.locator, parts.runTag)
* }
* ```
*/
export {
isNodeLocatorId,
isNodeExecutionId,
parseNodeLocatorId,
createNodeLocatorId,
parseNodeExecutionId,
createNodeExecutionId
} from '@/types/nodeIdentification'

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