Compare commits

..

39 Commits

Author SHA1 Message Date
Comfy Org PR Bot
99f5b36383 [backport cloud/1.36] feat: local/legacy settings dialog fix (#8004)
Backport of #7990 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8004-backport-cloud-1-36-feat-local-legacy-settings-dialog-fix-2e76d73d3650814398aff135890ee1cb)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2026-01-12 22:59:51 -07:00
Comfy Org PR Bot
2da833854a [backport cloud/1.36] Dynamic input fixes (#8008)
Backport of #7837 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8008-backport-cloud-1-36-Dynamic-input-fixes-2e76d73d3650811186daca4b7e20b83a)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-12 22:49:26 -07:00
Comfy Org PR Bot
15955bd940 [backport cloud/1.36] fix: wrap image preview navigation dots when overflowing node width (#8006)
Backport of #7891 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8006-backport-cloud-1-36-fix-wrap-image-preview-navigation-dots-when-overflowing-node-width-2e76d73d3650814ead44ebf4a66d4765)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 22:41:51 -07:00
Comfy Org PR Bot
8e62359af9 [backport cloud/1.36] Fix reactivity washing in refreshNodeSlots (#8019)
Backport of #7802 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8019-backport-cloud-1-36-Fix-reactivity-washing-in-refreshNodeSlots-2e76d73d36508113b3eec5aab31165a7)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-12 22:34:10 -07:00
Comfy Org PR Bot
fca4931f52 [backport cloud/1.36] feat(price-badges): add price badges for Vidu2 nodes (#8017)
Backport of #7927 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8017-backport-cloud-1-36-feat-price-badges-add-price-badges-for-Vidu2-nodes-2e76d73d3650810ea3b1d37744fe0523)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2026-01-12 22:29:36 -07:00
Comfy Org PR Bot
eeb0fd354a [backport cloud/1.36] fix: enable immediate file saving for i18n translations (#8015)
Backport of #7785 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8015-backport-cloud-1-36-fix-enable-immediate-file-saving-for-i18n-translations-2e76d73d365081f6b6aae8ab76c677c5)
by [Unito](https://www.unito.io)

Co-authored-by: sno <snomiao@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-12 22:25:31 -07:00
Comfy Org PR Bot
c28e60e5a8 [backport cloud/1.36] fix: remove negative margin from legacy widget canvas (#8013)
Backport of #7925 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8013-backport-cloud-1-36-fix-remove-negative-margin-from-legacy-widget-canvas-2e76d73d3650818a983bf2ed6095e599)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 22:24:02 -07:00
Comfy Org PR Bot
dc0d85990d [backport cloud/1.36] fix: respect node resizable property in vueNodes mode (#8011)
Backport of #7934 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8011-backport-cloud-1-36-fix-respect-node-resizable-property-in-vueNodes-mode-2e76d73d365081509c9bfe6603052452)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 22:19:50 -07:00
Comfy Org PR Bot
74ebfec582 [backport cloud/1.36] Fix linked asset widget promotion in vue (#8003)
Backport of #7895 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8003-backport-cloud-1-36-Fix-linked-asset-widget-promotion-in-vue-2e76d73d365081758120de9d88ee632a)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-12 22:10:28 -07:00
Comfy Org PR Bot
bc4b3d0a95 [backport cloud/1.36] fix(price-badges): add missing badge for WanReferenceVideoApi node (#8001)
Backport of #7901 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8001-backport-cloud-1-36-fix-price-badges-add-missing-badge-for-WanReferenceVideoApi-node-2e76d73d3650814c9939e8f27c4e48db)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2026-01-12 22:07:00 -07:00
Comfy Org PR Bot
4a6a1287ce [backport cloud/1.36] fix(price-badges): improve Gemini and OpenAI chat nodes (#7999)
Backport of #7900 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7999-backport-cloud-1-36-fix-price-badges-improve-Gemini-and-OpenAI-chat-nodes-2e76d73d36508169a65cce327fd7bdaf)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2026-01-12 22:04:34 -07:00
Comfy Org PR Bot
9bf5176b8f [backport cloud/1.36] fix: prevent image preview resize issues when switching to vueNodes mode (#7997)
Backport of #7868 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7997-backport-cloud-1-36-fix-prevent-image-preview-resize-issues-when-switching-to-vueNodes-2e76d73d36508164be55fceba35aa0d8)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 22:03:39 -07:00
Comfy Org PR Bot
056308a026 [backport cloud/1.36] fix: continue rendering when 3D animation is playing (#7995)
Backport of #7836 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7995-backport-cloud-1-36-fix-continue-rendering-when-3D-animation-is-playing-2e76d73d3650811fb287cc0f31c5301c)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 22:02:46 -07:00
Comfy Org PR Bot
fe48900c56 [backport cloud/1.36] fix: disable frustum culling for SkinnedMesh to prevent clipping during animation (#7989)
Backport of #7856 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7989-backport-cloud-1-36-fix-disable-frustum-culling-for-SkinnedMesh-to-prevent-clipping-du-2e76d73d365081e9b323d462a60cf7dc)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 21:08:23 -07:00
Comfy Org PR Bot
19db196727 [backport cloud/1.36] fix: Improve legacy widget compatibility in vueNodes mode (#7987)
Backport of #7766 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7987-backport-cloud-1-36-fix-Improve-legacy-widget-compatibility-in-vueNodes-mode-2e76d73d365081c7a2e0db7aa23748f5)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 21:02:19 -07:00
Comfy Org PR Bot
4963f59264 [backport cloud/1.36] refactor: simplify asset download state and fix deletion UI (#7991)
Backport of #7974 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7991-backport-cloud-1-36-refactor-simplify-asset-download-state-and-fix-deletion-UI-2e76d73d365081ac8a99c9d4c849325f)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-12 19:59:03 -08:00
Alexander Brown
7f83af391c [backport cloud/1.36] feat: add polling fallback for stale asset downloads (#7981)
Backport of #7926 to cloud/1.36

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7981-backport-cloud-1-36-feat-add-polling-fallback-for-stale-asset-downloads-2e76d73d365081a983a4e5a8683ae2c9)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-12 19:41:57 -08:00
Comfy Org PR Bot
2d04cf4757 [backport cloud/1.36] fix 3d-min-resize (#7985)
Backport of #7815 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7985-backport-cloud-1-36-fix-3d-min-resize-2e76d73d365081f88c7ce59a24f4c3c7)
by [Unito](https://www.unito.io)

Co-authored-by: Kelly Yang <124ykl@gmail.com>
2026-01-12 20:29:43 -07:00
Comfy Org PR Bot
89a00fe459 [backport cloud/1.36] CI: Use custom container for E2E tests (#7983)
Backport of #7625 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7983-backport-cloud-1-36-CI-Use-custom-container-for-E2E-tests-2e76d73d365081608f74da4b8add041e)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-01-12 20:13:28 -07:00
Comfy Org PR Bot
7158e81a4d [backport cloud/1.36] disable workflow validation warnings by default (#7978)
Backport of #7795 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7978-backport-cloud-1-36-disable-workflow-validation-warnings-by-default-2e76d73d365081bb88e9ff3df97a2bf3)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-01-12 20:01:47 -07:00
Comfy Org PR Bot
53d76c42c2 [backport cloud/1.36] fix: PrimitiveNode combo widget value not persisting in vueNodes mode (#7980)
Backport of #7782 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7980-backport-cloud-1-36-fix-PrimitiveNode-combo-widget-value-not-persisting-in-vueNodes-mo-2e76d73d36508169870ec429b84d3765)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-01-12 20:00:23 -07:00
Comfy Org PR Bot
1abe3f2707 [backport cloud/1.36] fix(upload-model): UI/UX improvements for Upload Model Dialog (#7976)
Backport of #7969 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7976-backport-cloud-1-36-fix-upload-model-UI-UX-improvements-for-Upload-Model-Dialog-2e76d73d365081d49320d6edadb06736)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-12 17:58:09 -08:00
Comfy Org PR Bot
e84b6f3696 [backport cloud/1.36] fix(UploadModel): truncate long filenames in wizard (#7944)
Backport of #7939 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7944-backport-cloud-1-36-fix-UploadModel-truncate-long-filenames-in-wizard-2e46d73d365081518cf3f78545ca5ecd)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-10 11:52:18 -07:00
Comfy Org PR Bot
348d674455 [backport cloud/1.36] fix: Model upload UI improvements (#7943)
Backport of #7938 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7943-backport-cloud-1-36-fix-Model-upload-UI-improvements-2e46d73d365081fcadd1f71e7fa012d6)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-10 11:52:09 -07:00
Comfy Org PR Bot
4165f52109 [backport cloud/1.36] fix: UX nits and styles (#7936)
Backport of #7933 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7936-backport-cloud-1-36-fix-UX-nits-and-styles-2e36d73d365081ce89add4b9857df1db)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2026-01-09 16:51:08 -08:00
Simula_r
b53976c775 [backport cloud/1.36] Feat(cloud)/new top up dialog (#7932)
Backport of #7899 to `cloud/1.36`

## Summary
- Implement the new add credits (top up) dialog
- Refactor the subscription dialog to make different credit types easier
to understand

## Conflicts Resolved
- `src/components/dialog/content/TopUpCreditsDialogContent.vue` - Took
PR version (new UI)
- `src/locales/en/main.json` - Added missing `usdAmount` translation key

Automatically created by manual backport process.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7932-backport-cloud-1-36-Feat-cloud-new-top-up-dialog-2e36d73d3650812dba2cd7edcb43259b)
by [Unito](https://www.unito.io)
2026-01-09 15:14:55 -08:00
Comfy Org PR Bot
ec7a3a9e20 [backport cloud/1.36] perf(AssetBrowserModal): virtualize asset grid to reduce network requests (#7922)
Backport of #7919 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7922-backport-cloud-1-36-perf-AssetBrowserModal-virtualize-asset-grid-to-reduce-network-re-2e36d73d3650812ca602d496f4decec4)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-08 19:15:27 -08:00
Comfy Org PR Bot
be8ee3d228 [backport cloud/1.36] fix: Button sizing in modals and asset browser (#7921)
Backport of #7920 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7921-backport-cloud-1-36-fix-Button-sizing-in-modals-and-asset-browser-2e36d73d365081b9a52af74dc2b5e94c)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-08 19:14:14 -08:00
Luke Mino-Altherr
b9e6f3d9fa [backport cloud/1.36] feat: add HoneyToast component for persistent progress notifications (#7918)
Backport of #7902 to cloud/1.36

Original PR: https://github.com/Comfy-Org/ComfyUI_frontend/pull/7902

Cherry-picked merge commit e26e1f0c9e.

## Conflicts resolved
- **pnpm-lock.yaml**: Regenerated with `pnpm install`
-
**tests-ui/tests/components/dialog/footer/ManagerProgressFooter.test.ts**:
Removed (PR deletes this file along with the component it tested)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7918-backport-cloud-1-36-feat-add-HoneyToast-component-for-persistent-progress-notification-2e36d73d3650811a9f57f26c56b84c97)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: sno <snomiao@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-08 17:43:30 -08:00
Comfy Org PR Bot
e912b42fff [backport cloud/1.36] feat: add model download progress dialog (#7917)
Backport of #7897 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7917-backport-cloud-1-36-feat-add-model-download-progress-dialog-2e36d73d365081b18bddeb4835f4d706)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-08 17:29:03 -08:00
Comfy Org PR Bot
aecb841cc0 [backport cloud/1.36] feature: model browser folder grouping (#7916)
Backport of #7892 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7916-backport-cloud-1-36-feature-model-browser-folder-grouping-2e36d73d365081279a6bf032c6e0893d)
by [Unito](https://www.unito.io)

Co-authored-by: Jin Yi <jin12cc@gmail.com>
2026-01-08 17:22:03 -08:00
Comfy Org PR Bot
b4e4cccc31 [backport cloud/1.36] Fix run badge anchoring (#7915)
Backport of #7912 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7915-backport-cloud-1-36-Fix-run-badge-anchoring-2e36d73d36508160a874dea0a732a9af)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-01-08 17:32:03 -07:00
Comfy Org PR Bot
2a15325d81 [backport cloud/1.36] Prevent nav item shrink (#7890)
Backport of #7869 to `cloud/1.36`

Automatically created by backport workflow.

Co-authored-by: Jin Yi <jin12cc@gmail.com>
2026-01-07 16:51:27 -08:00
Comfy Org PR Bot
f1b874eeed [backport cloud/1.36] feat: Stale-while-revalidate pattern for AssetBrowserModal (#7889)
Backport of #7880 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7889-backport-cloud-1-36-feat-Stale-while-revalidate-pattern-for-AssetBrowserModal-2e26d73d365081fb854bfe4189a94bef)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-07 16:42:29 -08:00
Comfy Org PR Bot
1de23b8aa2 [backport cloud/1.36] feat: split asset_update_options_enabled into separate deletion and rename flags (#7888)
Backport of #7864 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7888-backport-cloud-1-36-feat-split-asset_update_options_enabled-into-separate-deletion-and-2e26d73d365081edb755ede860c53f97)
by [Unito](https://www.unito.io)

Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-07 16:28:14 -08:00
Comfy Org PR Bot
e9c47e80b9 [backport cloud/1.36] [feat] Add async model upload with WebSocket progress tracking (#7887)
Backport of #7746 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7887-backport-cloud-1-36-feat-Add-async-model-upload-with-WebSocket-progress-tracking-2e26d73d365081c4a060e6e88f33016d)
by [Unito](https://www.unito.io)

Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-07 16:25:55 -08:00
Comfy Org PR Bot
8eb2ba0745 [backport cloud/1.36] [feat] Filter out nlf model type from Upload Model flow (#7886)
Backport of #7793 to `cloud/1.36`

Automatically created by backport workflow.

Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 16:17:35 -08:00
Comfy Org PR Bot
4957cd356b [backport cloud/1.36] Guard downgrades via billing portal (#7820)
Backport of #7813 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7820-backport-cloud-1-36-Guard-downgrades-via-billing-portal-2db6d73d3650814fb2fbd73697ed070c)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-12-31 18:27:19 -07:00
Comfy Org PR Bot
9f3bbb94fd [backport cloud/1.36] fix: restore mask editor compatibility with Impact-Pack plugin (#7801)
Backport of #7762 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7801-backport-cloud-1-36-fix-restore-mask-editor-compatibility-with-Impact-Pack-plugin-2d96d73d36508190b073e683e4f08f2b)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-30 12:54:32 -07:00
788 changed files with 12596 additions and 98956 deletions

View File

@@ -122,7 +122,7 @@ echo " pnpm build - Build for production"
echo " pnpm test:unit - Run unit tests"
echo " pnpm typecheck - Run TypeScript checks"
echo " pnpm lint - Run ESLint"
echo " pnpm format - Format code with oxfmt"
echo " pnpm format - Format code with Prettier"
echo ""
echo "Next steps:"
echo "1. Run 'pnpm dev' to start developing"

View File

@@ -0,0 +1,21 @@
---
description: Creating unit tests
globs:
alwaysApply: false
---
# Creating unit tests
- This project uses `vitest` for unit testing
- Tests are stored in the `test/` directory
- Tests should be cross-platform compatible; able to run on Windows, macOS, and linux
- e.g. the use of `path.resolve`, or `path.join` and `path.sep` to ensure that tests work the same on all platforms
- Tests should be mocked properly
- Mocks should be cleanly written and easy to understand
- Mocks should be re-usable where possible
## Unit test style
- Prefer the use of `test.extend` over loose variables
- To achieve this, import `test as baseTest` from `vitest`
- Never use `it`; `test` should be used in place of this

14
.github/AGENTS.md vendored
View File

@@ -1,14 +0,0 @@
# PR Review Context
Context for automated PR review system.
## Review Scope
This automated review performs comprehensive analysis:
- Architecture and design patterns
- Security vulnerabilities
- Performance implications
- Code quality and maintainability
- Integration concerns
For implementation details, see `.claude/commands/comprehensive-pr-review.md`.

39
.github/CLAUDE.md vendored
View File

@@ -1,3 +1,36 @@
<!-- A rose by any other name would smell as sweet,
But Claude insists on files named for its own conceit. -->
@AGENTS.md
# ComfyUI Frontend - Claude Review Context
This file provides additional context for the automated PR review system.
## Quick Reference
### PrimeVue Component Migrations
When reviewing, flag these deprecated components:
- `Dropdown` → Use `Select` from 'primevue/select'
- `OverlayPanel` → Use `Popover` from 'primevue/popover'
- `Calendar` → Use `DatePicker` from 'primevue/datepicker'
- `InputSwitch` → Use `ToggleSwitch` from 'primevue/toggleswitch'
- `Sidebar` → Use `Drawer` from 'primevue/drawer'
- `Chips` → Use `AutoComplete` with multiple enabled and typeahead disabled
- `TabMenu` → Use `Tabs` without panels
- `Steps` → Use `Stepper` without panels
- `InlineMessage` → Use `Message` component
### API Utilities Reference
- `api.apiURL()` - Backend API calls (/prompt, /queue, /view, etc.)
- `api.fileURL()` - Static file access (templates, extensions)
- `$t()` / `i18n.global.t()` - Internationalization
- `DOMPurify.sanitize()` - HTML sanitization
## Review Scope
This automated review performs comprehensive analysis including:
- Architecture and design patterns
- Security vulnerabilities
- Performance implications
- Code quality and maintainability
- Integration concerns
For implementation details, see `.claude/commands/comprehensive-pr-review.md`.

View File

@@ -42,7 +42,7 @@ jobs:
- name: Run Stylelint with auto-fix
run: pnpm stylelint:fix
- name: Run oxfmt with auto-format
- name: Run Prettier with auto-format
run: pnpm format
- name: Check for changes
@@ -60,7 +60,7 @@ jobs:
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
git commit -m "[automated] Apply ESLint and Oxfmt fixes"
git commit -m "[automated] Apply ESLint and Prettier fixes"
git push
- name: Final validation
@@ -80,7 +80,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Oxfmt formatting'
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Prettier formatting'
})
- name: Comment on PR about manual fix needed

View File

@@ -1,9 +1,9 @@
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
name: 'CI: Tests E2E (Deploy for Forks)'
name: "CI: Tests E2E (Deploy for Forks)"
on:
workflow_run:
workflows: ['CI: Tests E2E']
workflows: ["CI: Tests E2E"]
types: [requested, completed]
env:
@@ -81,7 +81,6 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# Rename merged report if exists
[ -d "reports/playwright-report-chromium-merged" ] && \

View File

@@ -1,5 +1,5 @@
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
name: 'CI: Tests E2E'
name: "CI: Tests E2E"
on:
push:
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -85,7 +85,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -144,10 +144,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
# Setup pnpm/node to run playwright merge-reports (no browsers needed)
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Download blob reports
uses: actions/download-artifact@v4
@@ -159,10 +158,10 @@ jobs:
- name: Merge into HTML Report
run: |
# Generate HTML report
pnpm dlx @playwright/test merge-reports --reporter=html ./all-blob-reports
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
# Generate JSON report separately with explicit output path
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
@@ -223,7 +222,6 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GITHUB_TOKEN: ${{ github.token }}
GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
run: |
bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \

View File

@@ -77,7 +77,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -69,7 +69,7 @@ jobs:
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
with:
repository: Comfy-Org/ComfyUI
repository: comfyanonymous/ComfyUI
sparse-checkout: |
requirements.txt
path: comfyui
@@ -184,7 +184,7 @@ jobs:
# Note: This only affects the local checkout, NOT the fork's master branch
# We only push the automation branch, leaving the fork's master untouched
echo "Fetching upstream master..."
if ! git fetch https://github.com/Comfy-Org/ComfyUI.git master; then
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
echo "Failed to fetch upstream master"
exit 1
fi
@@ -257,7 +257,7 @@ jobs:
# Extract fork owner from repository name
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
echo "Creating PR from ${COMFYUI_FORK} to Comfy-Org/ComfyUI"
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
# Configure git
git config user.name "github-actions[bot]"
@@ -288,7 +288,7 @@ jobs:
# Try to create PR, ignore error if it already exists
if ! gh pr create \
--repo Comfy-Org/ComfyUI \
--repo comfyanonymous/ComfyUI \
--head "${FORK_OWNER}:${BRANCH}" \
--base master \
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
@@ -297,7 +297,7 @@ jobs:
# Check if PR already exists
set +e
EXISTING_PR=$(gh pr list --repo Comfy-Org/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
PR_LIST_EXIT=$?
set -e
@@ -318,7 +318,7 @@ jobs:
run: |
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Draft PR created in Comfy-Org/ComfyUI" >> $GITHUB_STEP_SUMMARY
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
cat pr-body.txt >> $GITHUB_STEP_SUMMARY

View File

@@ -1,7 +1,7 @@
// This file is intentionally kept in CommonJS format (.cjs)
// to resolve compatibility issues with dependencies that require CommonJS.
// Do not convert this file to ESModule format unless all dependencies support it.
const { defineConfig } = require('@lobehub/i18n-cli')
const { defineConfig } = require('@lobehub/i18n-cli');
module.exports = defineConfig({
modelName: 'gpt-4.1',
@@ -10,19 +10,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: [
'zh',
'zh-TW',
'ru',
'ja',
'ko',
'fr',
'es',
'ar',
'tr',
'pt-BR',
'fa'
],
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
@@ -31,11 +19,5 @@ module.exports = defineConfig({
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
IMPORTANT Persian Translation Guidelines:
- For 'fa' locale: Use formal Persian (فارسی رسمی) for professional tone throughout the UI.
- Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow).
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
- Maintain consistency with terminology used in Persian software and design applications.
`
})
});

View File

@@ -1,20 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80,
"ignorePatterns": [
"packages/registry-types/src/comfyRegistryTypes.ts",
"src/types/generatedManagerTypes.ts",
"**/*.md",
"**/*.json",
"**/*.css",
"**/*.yaml",
"**/*.yml",
"**/*.html",
"**/*.svg",
"**/*.xml"
]
}

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
packages/registry-types/src/comfyRegistryTypes.ts
src/types/generatedManagerTypes.ts

11
.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none",
"printWidth": 80,
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
}

View File

@@ -1,17 +0,0 @@
# Storybook Guidelines
See `@docs/guidance/storybook.md` for story patterns (auto-loaded for `*.stories.ts`).
## Available Context
Stories have access to:
- All ComfyUI stores
- PrimeVue with ComfyUI theming
- i18n system
- CSS variables and styling
## Troubleshooting
1. **Import Errors**: Verify `@/` alias works
2. **Missing Styles**: Check CSS imports in `preview.ts`
3. **Store Errors**: Check store initialization in setup

View File

@@ -1,3 +1,197 @@
<!-- Though standards bloom in open fields so wide,
Anthropic walks a path of lonely pride. -->
@AGENTS.md
# Storybook Development Guidelines for Claude
## Quick Commands
- `pnpm storybook`: Start Storybook development server
- `pnpm build-storybook`: Build static Storybook
- `pnpm test:unit`: Run unit tests (includes Storybook components)
## Development Workflow for Storybook
1. **Creating New Stories**:
- Place `*.stories.ts` files alongside components
- Follow the naming pattern: `ComponentName.stories.ts`
- Use realistic mock data that matches ComfyUI schemas
2. **Testing Stories**:
- Verify stories render correctly in Storybook UI
- Test different component states and edge cases
- Ensure proper theming and styling
3. **Code Quality**:
- Run `pnpm typecheck` to verify TypeScript
- Run `pnpm lint` to check for linting issues
- Follow existing story patterns and conventions
## Story Creation Guidelines
### Basic Story Structure
```typescript
import type { Meta, StoryObj } from '@storybook/vue3'
import ComponentName from './ComponentName.vue'
const meta: Meta<typeof ComponentName> = {
title: 'Category/ComponentName',
component: ComponentName,
parameters: {
layout: 'centered' // or 'fullscreen', 'padded'
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
// Component props
}
}
```
### Mock Data Patterns
For ComfyUI components, use realistic mock data:
```typescript
// Node definition mock
const mockNodeDef = {
input: {
required: {
prompt: ["STRING", { multiline: true }]
}
},
output: ["CONDITIONING"],
output_is_list: [false],
category: "conditioning"
}
// Component instance mock
const mockComponent = {
id: "1",
type: "CLIPTextEncode",
// ... other properties
}
```
### Common Story Variants
Always include these story variants when applicable:
- **Default**: Basic component with minimal props
- **WithData**: Component with realistic data
- **Loading**: Component in loading state
- **Error**: Component with error state
- **LongContent**: Component with edge case content
- **Empty**: Component with no data
### Storybook-Specific Code Patterns
#### Store Access
```typescript
// In stories, access stores through the setup function
export const WithStore: Story = {
render: () => ({
setup() {
const store = useMyStore()
return { store }
},
template: '<MyComponent :data="store.data" />'
})
}
```
#### Event Testing
```typescript
export const WithEvents: Story = {
args: {
onUpdate: fn() // Use Storybook's fn() for action logging
}
}
```
## Configuration Notes
### Vue App Setup
The Storybook preview is configured with:
- Pinia stores initialized
- PrimeVue with ComfyUI theme
- i18n internationalization
- All necessary CSS imports
### Build Configuration
- Vite integration with proper alias resolution
- Manual chunking for better performance
- TypeScript support with strict checking
- CSS processing for Vue components
## Troubleshooting
### Common Issues
1. **Import Errors**: Verify `@/` alias is working correctly
2. **Missing Styles**: Ensure CSS imports are in `preview.ts`
3. **Store Errors**: Check store initialization in setup
4. **Type Errors**: Use proper TypeScript types for story args
### Debug Commands
```bash
# Check TypeScript issues
pnpm typecheck
# Lint Storybook files
pnpm lint .storybook/
# Build to check for production issues
pnpm build-storybook
```
## File Organization
```
.storybook/
├── main.ts # Core configuration
├── preview.ts # Global setup and decorators
├── README.md # User documentation
└── CLAUDE.md # This file - Claude guidelines
src/
├── components/
│ └── MyComponent/
│ ├── MyComponent.vue
│ └── MyComponent.stories.ts
```
## Integration with ComfyUI
### Available Context
Stories have access to:
- All ComfyUI stores (widgetStore, colorPaletteStore, etc.)
- PrimeVue components with ComfyUI theming
- Internationalization system
- ComfyUI CSS variables and styling
### Testing Components
When testing ComfyUI-specific components:
1. Use realistic node definitions and data structures
2. Test with different node types (sampling, conditioning, etc.)
3. Verify proper CSS theming and dark/light modes
4. Check component behavior with various input combinations
### Performance Considerations
- Use manual chunking for large dependencies
- Minimize bundle size by avoiding unnecessary imports
- Leverage Storybook's lazy loading capabilities
- Profile build times and optimize as needed
## Best Practices
1. **Keep Stories Focused**: Each story should demonstrate one specific use case
2. **Use Descriptive Names**: Story names should clearly indicate what they show
3. **Document Complex Props**: Use JSDoc comments for complex prop types
4. **Test Edge Cases**: Create stories for unusual but valid use cases
5. **Maintain Consistency**: Follow established patterns in existing stories

View File

@@ -69,42 +69,19 @@ const config: StorybookConfig = {
allowedHosts: true
},
resolve: {
alias: [
{
find: '@/composables/queue/useJobList',
replacement: process.cwd() + '/src/storybook/mocks/useJobList.ts'
},
{
find: '@/composables/queue/useJobActions',
replacement: process.cwd() + '/src/storybook/mocks/useJobActions.ts'
},
{
find: '@/utils/formatUtil',
replacement:
process.cwd() +
'/packages/shared-frontend-utils/src/formatUtil.ts'
},
{
find: '@/utils/networkUtil',
replacement:
process.cwd() +
'/packages/shared-frontend-utils/src/networkUtil.ts'
},
{
find: '@',
replacement: process.cwd() + '/src'
}
]
alias: {
'@': process.cwd() + '/src'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
output: {
keepNames: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -1,22 +1,25 @@
{
"recommendations": [
"antfu.vite",
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"donjayamanne.githistory",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"kisstkondoros.vscode-codemetrics",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"oxc.oxc-vscode",
"sonarsource.sonarlint-vscode",
"vitest.explorer",
"vue.volar",
"wix.vscode-import-cost"
"sonarsource.sonarlint-vscode",
"deque-systems.vscode-axe-linter",
"kisstkondoros.vscode-codemetrics",
"donjayamanne.githistory",
"wix.vscode-import-cost",
"prograhammer.tslint-vue",
"antfu.vite"
]
}

View File

@@ -1,7 +1,5 @@
# Repository Guidelines
See @docs/guidance/*.md for file-type-specific conventions (auto-loaded by glob).
## Project Structure & Module Organization
- Source: `src/`
@@ -27,10 +25,10 @@ See @docs/guidance/*.md for file-type-specific conventions (auto-loaded by glob)
- Build output: `dist/`
- Configs
- `vite.config.mts`
- `vitest.config.ts`
- `playwright.config.ts`
- `eslint.config.ts`
- `.oxfmtrc.json`
- `.oxlintrc.json`
- `.prettierrc`
- etc.
## Monorepo Architecture
@@ -46,23 +44,8 @@ The project uses **Nx** for build orchestration and task management
- `pnpm test:unit`: Run Vitest unit tests
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm format` / `pnpm format:check`: Prettier
- `pnpm typecheck`: Vue TSC type checking
- `pnpm storybook`: Start Storybook development server
## Development Workflow
1. Make code changes
2. Run relevant tests
3. Run `pnpm typecheck`, `pnpm lint`, `pnpm format`
4. Check if README updates are needed
5. Suggest docs.comfy.org updates for user-facing changes
## Git Conventions
- Use `prefix:` format: `feat:`, `fix:`, `test:`
- Add "Fixes #n" to PR descriptions
- Never mention Claude/AI in commits
## Coding Style & Naming Conventions
@@ -72,7 +55,7 @@ The project uses **Nx** for build orchestration and task management
- Composition API only
- Tailwind 4 styling
- Avoid `<style>` blocks
- Style: (see `.oxfmtrc.json`)
- Style: (see `.prettierrc`)
- Indent 2 spaces
- single quotes
- no trailing semicolons

View File

@@ -1 +1,30 @@
@AGENTS.md
# Claude Code specific instructions
@Agents.md
## Repository Setup
For first-time setup, use the Claude command:
```sh
/setup_repo
```
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports for dev server, storybook, etc.
## Development Workflow
1. **First-time setup**: Run `/setup_repo` Claude command
2. Make code changes
3. Run tests (see subdirectory CLAUDE.md files)
4. Run typecheck, lint, format
5. Check README updates
6. Consider docs.comfy.org updates
## Git Conventions
- Use `prefix:` format: `feat:`, `fix:`, `test:`
- Add "Fixes #n" to PR descriptions
- Never mention Claude/AI in commits

View File

@@ -37,7 +37,7 @@
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
@@ -55,7 +55,8 @@
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
/src/locales/pt-BR/ @JonatanAtila @Yorha4D @KarryCharon @shinshin86
# LLM Instructions (blank on purpose)
.claude/

View File

@@ -1 +0,0 @@
AMD and the AMD Arrow logo are trademarks of Advanced Micro Devices, Inc.

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/desktop-ui",
"version": "0.0.6",
"version": "0.0.4",
"type": "module",
"nx": {
"tags": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,84 +0,0 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type {
ElectronAPI,
TorchDeviceType
} from '@comfyorg/comfyui-electron-types'
import { ref } from 'vue'
import GpuPicker from './GpuPicker.vue'
type Platform = ReturnType<ElectronAPI['getPlatform']>
type ElectronAPIStub = Pick<ElectronAPI, 'getPlatform'>
type WindowWithElectron = Window & { electronAPI?: ElectronAPIStub }
const meta: Meta<typeof GpuPicker> = {
title: 'Desktop/Components/GpuPicker',
component: GpuPicker,
parameters: {
layout: 'padded',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
}
}
export default meta
type Story = StoryObj<typeof meta>
function createElectronDecorator(platform: Platform) {
function getPlatform() {
return platform
}
return function ElectronDecorator() {
const windowWithElectron = window as WindowWithElectron
windowWithElectron.electronAPI = { getPlatform }
return { template: '<story />' }
}
}
function renderWithDevice(device: TorchDeviceType | null) {
return function Render() {
return {
components: { GpuPicker },
setup() {
const selected = ref<TorchDeviceType | null>(device)
return { selected }
},
template: `
<div class="min-h-screen bg-neutral-950 p-8">
<GpuPicker v-model:device="selected" />
</div>
`
}
}
}
const windowsDecorator = createElectronDecorator('win32')
const macDecorator = createElectronDecorator('darwin')
export const WindowsNvidiaSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('nvidia')
}
export const WindowsAmdSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('amd')
}
export const WindowsCpuSelected: Story = {
decorators: [windowsDecorator],
render: renderWithDevice('cpu')
}
export const MacMpsSelected: Story = {
decorators: [macDecorator],
render: renderWithDevice('mps')
}

View File

@@ -11,32 +11,29 @@
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
image-path="./assets/images/apple-mps-logo.png"
:image-path="'./assets/images/apple-mps-logo.png'"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
:selected="selected === 'mps'"
:recommended="true"
@click="pickGpu('mps')"
/>
<template v-else>
<HardwareOption
image-path="./assets/images/nvidia-logo-square.jpg"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:selected="selected === 'nvidia'"
@click="pickGpu('nvidia')"
/>
<HardwareOption
image-path="./assets/images/amd-rocm-logo.png"
placeholder-text="AMD"
:subtitle="$t('install.gpuPicker.amdSubtitle')"
:selected="selected === 'amd'"
@click="pickGpu('amd')"
/>
</template>
<HardwareOption
v-else
:image-path="'./assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
:selected="selected === 'nvidia'"
:recommended="true"
@click="pickGpu('nvidia')"
/>
<!-- CPU -->
<HardwareOption
placeholder-text="CPU"
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
:value="'cpu'"
:selected="selected === 'cpu'"
@click="pickGpu('cpu')"
/>
@@ -44,6 +41,7 @@
<HardwareOption
placeholder-text="Manual Install"
:subtitle="$t('install.gpuPicker.manualSubtitle')"
:value="'unsupported'"
:selected="selected === 'unsupported'"
@click="pickGpu('unsupported')"
/>
@@ -83,15 +81,13 @@ const selected = defineModel<TorchDeviceType | null>('device', {
const electron = electronAPI()
const platform = electron.getPlatform()
const recommendedDevices: TorchDeviceType[] = ['mps', 'nvidia', 'amd']
const showRecommendedBadge = computed(() =>
selected.value ? recommendedDevices.includes(selected.value) : false
const showRecommendedBadge = computed(
() => selected.value === 'mps' || selected.value === 'nvidia'
)
const descriptionKeys = {
mps: 'appleMetal',
nvidia: 'nvidia',
amd: 'amd',
cpu: 'cpu',
unsupported: 'manual'
} as const
@@ -101,7 +97,7 @@ const descriptionText = computed(() => {
return st(`install.gpuPicker.${key}Description`, '')
})
function pickGpu(value: TorchDeviceType) {
const pickGpu = (value: TorchDeviceType) => {
selected.value = value
}
</script>

View File

@@ -29,6 +29,7 @@ export const AppleMetalSelected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: true
}
}
@@ -38,6 +39,7 @@ export const AppleMetalUnselected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: false
}
}
@@ -46,6 +48,7 @@ export const CPUOption: Story = {
args: {
placeholderText: 'CPU',
subtitle: 'Subtitle',
value: 'cpu',
selected: false
}
}
@@ -54,6 +57,7 @@ export const ManualInstall: Story = {
args: {
placeholderText: 'Manual Install',
subtitle: 'Subtitle',
value: 'unsupported',
selected: false
}
}
@@ -63,6 +67,7 @@ export const NvidiaSelected: Story = {
imagePath: '/assets/images/nvidia-logo-square.jpg',
placeholderText: 'NVIDIA',
subtitle: 'NVIDIA',
value: 'nvidia',
selected: true
}
}

View File

@@ -36,13 +36,17 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { cn } from '@/utils/tailwindUtil'
interface Props {
imagePath?: string
placeholderText: string
subtitle?: string
value: TorchDeviceType
selected?: boolean
recommended?: boolean
}
defineProps<Props>()

View File

@@ -104,8 +104,8 @@
</template>
<script setup lang="ts">
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
import { isInChina } from '@comfyorg/shared-frontend-utils/networkUtil'
import Accordion from 'primevue/accordion'
import AccordionContent from 'primevue/accordioncontent'
@@ -155,7 +155,7 @@ const activeAccordionIndex = ref<string[] | undefined>(undefined)
const electron = electronAPI()
// Mirror configuration logic
function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
case 'mps':
@@ -170,7 +170,6 @@ function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
mirror: TorchMirrorUrl.Cuda,
fallbackMirror: TorchMirrorUrl.Cuda
}
case 'amd':
case 'cpu':
default:
return {

View File

@@ -63,6 +63,7 @@ const taskStore = useMaintenanceTaskStore()
defineProps<{
displayAsList: string
filter: MaintenanceFilter
isRefreshing: boolean
}>()
const executeTask = async (task: MaintenanceTask) => {

View File

@@ -143,8 +143,6 @@ const goToPreviousStep = () => {
const electron = electronAPI()
const router = useRouter()
const install = async () => {
if (!device.value) return
const options: InstallOptions = {
installPath: installPath.value,
autoUpdate: autoUpdate.value,
@@ -154,6 +152,7 @@ const install = async () => {
pythonMirror: pythonMirror.value,
pypiMirror: pypiMirror.value,
torchMirror: torchMirror.value,
// @ts-expect-error fixme ts strict error
device: device.value
}
electron.installComfyUI(options)
@@ -167,11 +166,7 @@ onMounted(async () => {
if (!electron) return
const detectedGpu = await electron.Config.getDetectedGpu()
if (
detectedGpu === 'mps' ||
detectedGpu === 'nvidia' ||
detectedGpu === 'amd'
) {
if (detectedGpu === 'mps' || detectedGpu === 'nvidia') {
device.value = detectedGpu
}

View File

@@ -74,6 +74,7 @@
class="border-neutral-700 border-solid border-x-0 border-y"
:filter
:display-as-list
:is-refreshing
/>
<!-- Actions -->

View File

@@ -64,7 +64,7 @@ export default defineConfig(() => {
})
],
build: {
minify: SHOULD_MINIFY,
minify: SHOULD_MINIFY ? ('esbuild' as const) : false,
target: 'es2022',
sourcemap: true
}

View File

@@ -1,8 +0,0 @@
# E2E Testing Guidelines
See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded for `*.spec.ts`).
## Directory Structure
- `assets/` - Test data (JSON workflows, fixtures)
- Tests use premade JSON workflows to load desired graph state

View File

@@ -1,3 +1,17 @@
<!-- In gardens where the agents freely play,
One stubborn flower turns the other way. -->
@AGENTS.md
# E2E Testing Guidelines
## Browser Tests
- Test user workflows
- Use Playwright fixtures
- Follow naming conventions
## Best Practices
- Check assets/ for test data
- Prefer specific selectors
- Test across viewports
## Testing Process
After code changes:
1. Create browser tests as appropriate
2. Run tests until passing
3. Then run typecheck, lint, format

View File

@@ -3,7 +3,7 @@ import { test as base, expect } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
@@ -21,6 +21,7 @@ import {
import { Topbar } from './components/Topbar'
import type { Position, Size } from './types'
import { NodeReference, SubgraphSlotReference } from './utils/litegraphUtils'
import TaskHistory from './utils/taskHistory'
dotenv.config()
@@ -145,6 +146,8 @@ class ConfirmDialog {
}
export class ComfyPage {
private _history: TaskHistory | null = null
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
@@ -298,6 +301,11 @@ export class ComfyPage {
}
}
setupHistory(): TaskHistory {
this._history ??= new TaskHistory(this)
return this._history
}
async setup({
clearStorage = true,
mockReleases = true
@@ -1583,29 +1591,14 @@ export class ComfyPage {
return window['app'].graph.nodes
})
}
async waitForGraphNodes(count: number) {
await this.page.waitForFunction((count) => {
return window['app']?.canvas.graph?.nodes?.length === count
}, count)
}
async getNodeRefsByType(
type: string,
includeSubgraph: boolean = false
): Promise<NodeReference[]> {
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate(
({ type, includeSubgraph }) => {
const graph = (
includeSubgraph ? window['app'].canvas.graph : window['app'].graph
) as LGraph
const nodes = graph.nodes
return nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
},
{ type, includeSubgraph }
)
await this.page.evaluate((type) => {
return window['app'].graph.nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
}, type)
).map((id: NodeId) => this.getNodeRefById(id))
)
}

View File

@@ -159,18 +159,8 @@ export class VueNodeHelpers {
getInputNumberControls(widget: Locator) {
return {
input: widget.locator('input'),
decrementButton: widget.getByTestId('decrement'),
incrementButton: widget.getByTestId('increment')
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').nth(1)
}
}
/**
* Enter the subgraph of a node.
* @param nodeId - The ID of the node to enter the subgraph of. If not provided, the first matched subgraph will be entered.
*/
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId('subgraph-enter-button')
await editButton.click()
}
}

View File

@@ -79,15 +79,48 @@ export class SubgraphSlotReference {
const node =
type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
const slots =
type === 'input' ? currentGraph.inputs : currentGraph.outputs
if (!node) {
throw new Error(`No ${type} node found in subgraph`)
}
// Calculate position for next available slot
// const nextSlotIndex = slots?.length || 0
// const slotHeight = 20
// const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight
// Find last slot position
const lastSlot = slots.at(-1)
let slotX: number
let slotY: number
if (lastSlot) {
// If there are existing slots, position the new one below the last one
const gapHeight = 20
slotX = lastSlot.pos[0]
slotY = lastSlot.pos[1] + gapHeight
} else {
// No existing slots - use slotAnchorX if available, otherwise calculate from node position
if (currentGraph.slotAnchorX !== undefined) {
// The actual slot X position seems to be slotAnchorX - 10
slotX = currentGraph.slotAnchorX - 10
} else {
// Fallback: calculate from node edge
slotX =
type === 'input'
? node.pos[0] + node.size[0] - 10 // Right edge for input node
: node.pos[0] + 10 // Left edge for output node
}
// For Y position when no slots exist, use middle of node
slotY = node.pos[1] + node.size[1] / 2
}
// Convert from offset to canvas coordinates
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
node.emptySlot.pos[0],
node.emptySlot.pos[1]
slotX,
slotY
])
return canvasPos
},

View File

@@ -0,0 +1,164 @@
import type { Request, Route } from '@playwright/test'
import _ from 'es-toolkit/compat'
import fs from 'fs'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
import type {
HistoryTaskItem,
TaskItem,
TaskOutput
} from '../../../src/schemas/apiSchema'
import type { ComfyPage } from '../ComfyPage'
/** keyof TaskOutput[string] */
type OutputFileType = 'images' | 'audio' | 'animated'
const DEFAULT_IMAGE = 'example.webp'
const getFilenameParam = (request: Request) => {
const url = new URL(request.url())
return url.searchParams.get('filename') || DEFAULT_IMAGE
}
const getContentType = (filename: string, fileType: OutputFileType) => {
const subtype = path.extname(filename).slice(1)
switch (fileType) {
case 'images':
return `image/${subtype}`
case 'audio':
return `audio/${subtype}`
case 'animated':
return `video/${subtype}`
}
}
const setQueueIndex = (task: TaskItem) => {
task.prompt[0] = TaskHistory.queueIndex++
}
const setPromptId = (task: TaskItem) => {
task.prompt[1] = uuidv4()
}
export default class TaskHistory {
static queueIndex = 0
static readonly defaultTask: Readonly<HistoryTaskItem> = {
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
outputs: {},
status: {
status_str: 'success',
completed: true,
messages: []
},
taskType: 'History'
}
private tasks: HistoryTaskItem[] = []
private outputContentTypes: Map<string, string> = new Map()
constructor(readonly comfyPage: ComfyPage) {}
private loadAsset: (filename: string) => Buffer = _.memoize(
(filename: string) => {
const filePath = this.comfyPage.assetPath(filename)
return fs.readFileSync(filePath)
}
)
private async handleGetHistory(route: Route) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(this.tasks)
})
}
private async handleGetView(route: Route) {
const fileName = getFilenameParam(route.request())
if (!this.outputContentTypes.has(fileName)) {
return route.continue()
}
const asset = this.loadAsset(fileName)
return route.fulfill({
status: 200,
contentType: this.outputContentTypes.get(fileName),
body: asset,
headers: {
'Cache-Control': 'public, max-age=31536000',
'Content-Length': asset.byteLength.toString()
}
})
}
async setupRoutes() {
return this.comfyPage.page.route(
/.*\/api\/(view|history)(\?.*)?$/,
async (route) => {
const request = route.request()
const method = request.method()
const isViewReq = request.url().includes('view') && method === 'GET'
if (isViewReq) return this.handleGetView(route)
const isHistoryPath = request.url().includes('history')
const isGetHistoryReq = isHistoryPath && method === 'GET'
if (isGetHistoryReq) return this.handleGetHistory(route)
const isClearReq =
method === 'POST' &&
isHistoryPath &&
request.postDataJSON()?.clear === true
if (isClearReq) return this.clearTasks()
return route.continue()
}
)
}
private createOutputs(
filenames: string[],
filetype: OutputFileType
): TaskOutput {
return filenames.reduce((outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
}
const contentType = getContentType(filename, filetype)
this.outputContentTypes.set(filename, contentType)
return outputs
}, {})
}
private addTask(task: HistoryTaskItem) {
setPromptId(task)
setQueueIndex(task)
this.tasks.unshift(task) // Tasks are added to the front of the queue
}
clearTasks(): this {
this.tasks = []
return this
}
withTask(
outputFilenames: string[],
outputFiletype: OutputFileType = 'images',
overrides: Partial<HistoryTaskItem> = {}
): this {
this.addTask({
...TaskHistory.defaultTask,
outputs: this.createOutputs(outputFilenames, outputFiletype),
...overrides
})
return this
}
/** Repeats the last task in the task history a specified number of times. */
repeat(n: number): this {
for (let i = 0; i < n; i++)
this.addTask(structuredClone(this.tasks.at(0)) as HistoryTaskItem)
return this
}
}

View File

@@ -19,7 +19,6 @@ test.describe('Graph', () => {
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Validation.Workflows', true)
await comfyPage.loadWorkflow('links/bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
})

View File

@@ -133,11 +133,8 @@ test.describe('Menu', () => {
// Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
// Click in top-right corner to close menu (avoid hamburger menu at top-left)
const viewport = comfyPage.page.viewportSize()!
await comfyPage.page
.locator('body')
.click({ position: { x: viewport.width - 10, y: 10 } })
// Click outside to close menu
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
// Verify menu is now closed
await expect(menu).not.toBeVisible()

View File

@@ -22,14 +22,8 @@ test.describe('Mobile Baseline Snapshots', () => {
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
]
}
'mobile-settings-dialog.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -8,11 +8,13 @@ test.describe('Properties panel', () => {
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview')
await expect(propertiesPanel.panelTitle).toContainText(
'No node(s) selected'
)
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.panelTitle).toContainText('3 items selected')
await expect(propertiesPanel.panelTitle).toContainText('3 nodes selected')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -83,7 +83,7 @@ test.describe('Templates', () => {
await comfyPage.page
.locator(
'nav > div:nth-child(3) > div > span:has-text("Getting Started")'
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
)
.click()
await comfyPage.templates.loadTemplate('default')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -102,7 +102,7 @@ test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
// await comfyPage.setup()
await comfyPage.setup()
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
@@ -993,51 +993,4 @@ test.describe('Vue Node Link Interaction', () => {
expect(linked).toBe(true)
})
})
test('Dragging from subgraph input connects to correct slot', async ({
comfyPage,
comfyMouse
}) => {
// Setup workflow with a KSampler node
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.waitForGraphNodes(0)
await comfyPage.executeCommand('Workspace.SearchBox.Toggle')
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await comfyPage.waitForGraphNodes(1)
// Convert the KSampler node to a subgraph
let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0]
await comfyPage.vueNodes.selectNode(String(ksamplerNode.id))
await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph')
// Enter the subgraph
await comfyPage.vueNodes.enterSubgraph()
await fitToViewInstant(comfyPage)
// Get the KSampler node inside the subgraph
ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0]
const positiveInput = await ksamplerNode.getInput(1)
const negativeInput = await ksamplerNode.getInput(2)
const positiveInputPos = await getSlotCenter(
comfyPage.page,
ksamplerNode.id,
1,
true
)
const sourceSlot = await comfyPage.getSubgraphInputSlot()
const calculatedSourcePos = await sourceSlot.getOpenSlotPosition()
await comfyMouse.move(calculatedSourcePos)
await comfyMouse.drag(positiveInputPos)
await comfyMouse.drop()
// Verify connection went to the correct slot
const positiveLinks = await positiveInput.getLinkCount()
const negativeLinks = await negativeInput.getLinkCount()
expect(positiveLinks).toBe(1)
expect(negativeLinks).toBe(0)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -194,10 +194,7 @@ test.describe('Image widget', () => {
const comboEntry = comfyPage.page.getByRole('menuitem', {
name: 'image32x32.webp'
})
await comboEntry.click()
// Stabilization for the image swap
await comfyPage.nextFrame()
await comboEntry.click({ noWaitAfter: true })
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,66 +0,0 @@
# Template Ranking System
Usage-based ordering for workflow templates with position bias normalization.
Scores are pre-computed and normalized offline and shipped as static JSON (mirrors `sorted-custom-node-map.json` pattern for node search).
## Sort Modes
| Mode | Formula | Description |
| -------------- | ------------------------------------------------ | ---------------------- |
| `recommended` | `usage × 0.5 + internal × 0.3 + freshness × 0.2` | Curated recommendation |
| `popular` | `usage × 0.9 + freshness × 0.1` | Pure user-driven |
| `newest` | Date sort | Existing |
| `alphabetical` | Name sort | Existing |
Freshness computed at runtime from `template.date`: `1.0 / (1 + daysSinceAdded / 90)`, min 0.1.
## Data Files
**Usage scores** (generated from Mixpanel):
```json
// In templates/index.json, add to any template:
{
"name": "some_template",
"usage": 1000,
...
}
```
**Search rank** (set per-template in workflow_templates repo):
```json
// In templates/index.json, add to any template:
{
"name": "some_template",
"searchRank": 8, // Scale 1-10, default 5
...
}
```
| searchRank | Effect |
| ---------- | ---------------------------- |
| 1-4 | Demote (bury in results) |
| 5 | Neutral (default if not set) |
| 6-10 | Promote (boost in results) |
## Position Bias Correction
Raw usage reflects true preference AND UI position bias. We use linear interpolation:
```
correction = 1 + (position - 1) / (maxPosition - 1)
normalizedUsage = rawUsage × correction
```
| Position | Boost |
| -------- | ----- |
| 1 | 1.0× |
| 50 | 1.28× |
| 100 | 1.57× |
| 175 | 2.0× |
Templates buried at the bottom get up to 2× boost to compensate for reduced visibility.
---

View File

@@ -1,33 +0,0 @@
---
globs:
- '**/*.spec.ts'
---
# Playwright E2E Test Conventions
See `docs/testing/*.md` for detailed patterns.
## Best Practices
- Follow [Playwright Best Practices](https://playwright.dev/docs/best-practices)
- Do NOT use `waitForTimeout` - use Locator actions and retrying assertions
- Prefer specific selectors (role, label, test-id)
- Test across viewports
## Test Tags
Tags are respected by config:
- `@mobile` - Mobile viewport tests
- `@2x` - High DPI tests
## Test Data
- Check `browser_tests/assets/` for test data and fixtures
- Use realistic ComfyUI workflows for E2E tests
## Running Tests
```bash
pnpm test:browser # Run all E2E tests
pnpm test:browser -- --ui # Interactive UI mode
```

View File

@@ -1,55 +0,0 @@
---
globs:
- '**/*.stories.ts'
---
# Storybook Conventions
## File Placement
Place `*.stories.ts` files alongside their components:
```
src/components/MyComponent/
├── MyComponent.vue
└── MyComponent.stories.ts
```
## Story Structure
```typescript
import type { Meta, StoryObj } from '@storybook/vue3'
import ComponentName from './ComponentName.vue'
const meta: Meta<typeof ComponentName> = {
title: 'Category/ComponentName',
component: ComponentName,
parameters: { layout: 'centered' }
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: { /* props */ }
}
```
## Required Story Variants
Include when applicable:
- **Default** - Minimal props
- **WithData** - Realistic data
- **Loading** - Loading state
- **Error** - Error state
- **Empty** - No data
## Mock Data
Use realistic ComfyUI schemas for mocks (node definitions, components).
## Running Storybook
```bash
pnpm storybook # Development server
pnpm build-storybook # Production build
```

View File

@@ -1,37 +0,0 @@
---
globs:
- '**/*.ts'
- '**/*.tsx'
- '**/*.vue'
---
# TypeScript Conventions
## Type Safety
- Never use `any` type - use proper TypeScript types
- Never use `as any` type assertions - fix the underlying type issue
- Type assertions are a last resort; they lead to brittle code
- Avoid `@ts-expect-error` - fix the underlying issue instead
## Utility Libraries
- Use `es-toolkit` for utility functions (not lodash)
## API Utilities
When making API calls in `src/`:
```typescript
// ✅ Correct - use api helpers
const response = await api.get(api.apiURL('/prompt'))
const template = await fetch(api.fileURL('/templates/default.json'))
// ❌ Wrong - direct URL construction
const response = await fetch('/api/prompt')
```
## Security
- Sanitize HTML with `DOMPurify.sanitize()`
- Never log secrets or sensitive data

View File

@@ -1,36 +0,0 @@
---
globs:
- '**/*.test.ts'
---
# Vitest Unit Test Conventions
See `docs/testing/*.md` for detailed patterns.
## Test Quality
- Do not write change detector tests (tests that just assert defaults)
- Do not write tests dependent on non-behavioral features (styles, classes)
- Do not write tests that just test mocks - ensure real code is exercised
- Be parsimonious; avoid redundant tests
## Mocking
- Use Vitest's mocking utilities (`vi.mock`, `vi.spyOn`)
- Keep module mocks contained - no global mutable state
- Use `vi.hoisted()` for per-test mock manipulation
- Don't mock what you don't own
## Component Testing
- Use Vue Test Utils for component tests
- Follow advice about making components easy to test
- Wait for reactivity with `await nextTick()` after state changes
## Running Tests
```bash
pnpm test:unit # Run all unit tests
pnpm test:unit -- path/to/file # Run specific test
pnpm test:unit -- --watch # Watch mode
```

View File

@@ -1,46 +0,0 @@
---
globs:
- '**/*.vue'
---
# Vue Component Conventions
Applies to all `.vue` files anywhere in the codebase.
## Vue 3 Composition API
- Use `<script setup lang="ts">` for component logic
- Destructure props (Vue 3.5 style with defaults) like `const { color = 'blue' } = defineProps<...>()`
- Use `ref`/`reactive` for state
- Use `computed()` for derived state
- Use lifecycle hooks: `onMounted`, `onUpdated`, etc.
## Component Communication
- Prefer `emit/@event-name` for state changes (promotes loose coupling)
- Use `defineExpose` only for imperative operations (`form.validate()`, `modal.open()`)
- Proper props and emits definitions
## VueUse Composables
Prefer VueUse composables over manual event handling:
- `useElementHover` instead of manual mouseover/mouseout listeners
- `useIntersectionObserver` for visibility detection instead of scroll handlers
- `useFocusTrap` for modal/dialog focus management
- `useEventListener` for auto-cleanup event listeners
Prefer Vue native options when available:
- `defineModel` instead of `useVModel` for two-way binding with props
## Styling
- Use inline Tailwind CSS only (no `<style>` blocks)
- Use `cn()` from `@/utils/tailwindUtil` for conditional classes
- Refer to packages/design-system/src/css/style.css for design tokens and tailwind configuration
## Best Practices
- Extract complex conditionals to `computed`
- In unmounted hooks, implement cleanup for async operations

View File

@@ -4,7 +4,9 @@ import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import oxlint from 'eslint-plugin-oxlint'
// eslint-config-prettier disables ESLint rules that conflict with formatters (oxfmt)
// WORKAROUND: eslint-plugin-prettier causes segfault on Node.js 24 + Windows
// See: https://github.com/nodejs/node/issues/58690
// Prettier is still run separately in lint-staged, so this is safe to disable
import eslintConfigPrettier from 'eslint-config-prettier'
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
@@ -109,7 +111,7 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
// Disables ESLint rules that conflict with formatters
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
eslintConfigPrettier,
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
storybookConfigs['flat/recommended'],

View File

@@ -8,8 +8,7 @@ const config: KnipConfig = {
'src/assets/css/style.css',
'src/main.ts',
'src/scripts/ui/menu/index.ts',
'src/types/index.ts',
'src/storybook/mocks/**/*.ts'
'src/types/index.ts'
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},

View File

@@ -1,9 +1,6 @@
import path from 'node:path'
export default {
'tests-ui/**': () =>
'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
@@ -17,7 +14,7 @@ function formatAndEslint(fileNames: string[]) {
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
return [
`pnpm exec oxfmt --write ${joinedPaths}`,
`pnpm exec prettier --cache --write ${joinedPaths}`,
`pnpm exec oxlint --fix ${joinedPaths}`,
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
]

View File

@@ -3,13 +3,6 @@
"short_name": "ComfyUI",
"description": "ComfyUI: AI image generation platform",
"start_url": "/",
"icons": [
{
"src": "/assets/images/comfy-logo-single.svg",
"sizes": "any",
"type": "image/svg+xml"
}
],
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000"

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.38.8",
"version": "1.36.12",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -22,8 +22,10 @@
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
"dev": "nx serve",
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
"format:check": "oxfmt --check",
"format": "oxfmt --write",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
"json-schema": "tsx scripts/generate-json-schema.ts",
"knip:no-cache": "knip",
"knip": "knip --cache",
@@ -40,7 +42,6 @@
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"storybook": "nx storybook",
"storybook:desktop": "nx run @comfyorg/desktop-ui:storybook",
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
@@ -61,12 +62,14 @@
"@nx/vite": "catalog:",
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@prettier/plugin-oxc": "catalog:",
"@sentry/vite-plugin": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/addon-mcp": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
"@tailwindcss/vite": "catalog:",
"@trivago/prettier-plugin-sort-imports": "catalog:",
"@types/fs-extra": "catalog:",
"@types/jsdom": "catalog:",
"@types/node": "catalog:",
@@ -97,11 +100,11 @@
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",
"prettier": "catalog:",
"pretty-bytes": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"storybook": "catalog:",
@@ -184,14 +187,8 @@
"vue-i18n": "catalog:",
"vue-router": "catalog:",
"vuefire": "catalog:",
"wwobjloader2": "catalog:",
"yjs": "catalog:",
"zod": "catalog:",
"zod-validation-error": "catalog:"
},
"pnpm": {
"overrides": {
"vite": "^8.0.0-beta.8"
}
}
}

View File

@@ -9,8 +9,6 @@
@config '../../tailwind.config.ts';
@custom-variant touch (@media (hover: none));
@theme {
--text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625);
@@ -247,7 +245,6 @@
--inverted-background-hover: var(--color-charcoal-600);
--warning-background: var(--color-gold-400);
--warning-background-hover: var(--color-gold-500);
--success-background: var(--color-jade-600);
--border-default: var(--color-smoke-600);
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
@@ -282,7 +279,7 @@
--modal-card-border-highlighted: var(--secondary-background-selected);
--modal-card-button-surface: var(--color-smoke-300);
--modal-card-placeholder-background: var(--color-smoke-600);
--modal-card-tag-background: var(--color-smoke-200);
--modal-card-tag-background: var(--color-smoke-400);
--modal-card-tag-foreground: var(--base-foreground);
--modal-panel-background: var(--color-white);
}
@@ -373,7 +370,6 @@
--inverted-background-hover: var(--color-smoke-200);
--warning-background: var(--color-gold-600);
--warning-background-hover: var(--color-gold-500);
--success-background: var(--color-jade-600);
--border-default: var(--color-charcoal-200);
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
@@ -518,7 +514,6 @@
--color-inverted-background-hover: var(--inverted-background-hover);
--color-warning-background: var(--warning-background);
--color-warning-background-hover: var(--warning-background-hover);
--color-success-background: var(--success-background);
--color-border-default: var(--border-default);
--color-border-subtle: var(--border-subtle);
--color-muted-background: var(--muted-background);

1537
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ packages:
catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.6.2
'@comfyorg/comfyui-electron-types': 0.5.5
'@eslint/js': ^9.39.1
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
@@ -15,8 +15,9 @@ catalog:
'@nx/playwright': 22.2.6
'@nx/storybook': 22.2.4
'@nx/vite': 22.2.6
'@pinia/testing': ^1.0.3
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.57.0
'@prettier/plugin-oxc': ^0.1.3
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
@@ -25,21 +26,22 @@ catalog:
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^10.32.1
'@sentry/vue': ^8.48.0
'@sparkjsdev/spark': ^0.1.10
'@storybook/addon-docs': ^10.1.9
'@storybook/addon-mcp': 0.1.6
'@storybook/vue3': ^10.1.9
'@storybook/vue3-vite': ^10.1.9
'@tailwindcss/vite': ^4.1.12
'@trivago/prettier-plugin-sort-imports': ^5.2.0
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^24.1.0
'@types/semver': ^7.7.0
'@types/three': ^0.169.0
'@vitejs/plugin-vue': ^6.0.0
'@vitest/coverage-v8': ^4.0.16
'@vitest/ui': ^4.0.16
'@vitest/coverage-v8': ^3.2.4
'@vitest/ui': ^3.2.0
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
@@ -58,22 +60,22 @@ catalog:
eslint-plugin-unused-imports: ^4.3.0
eslint-plugin-vue: ^10.6.2
firebase: ^11.6.0
globals: ^16.5.0
happy-dom: ^20.0.11
globals: ^15.9.0
happy-dom: ^15.11.0
husky: ^9.1.7
jiti: 2.6.1
jsdom: ^27.4.0
jsdom: ^26.1.0
knip: ^5.75.1
lint-staged: ^16.2.7
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 22.2.6
oxfmt: ^0.26.0
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
picocolors: ^1.1.1
pinia: ^3.0.4
pinia: ^2.1.7
postcss-html: ^1.8.0
prettier: ^3.7.4
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5
@@ -91,19 +93,18 @@ catalog:
unplugin-icons: ^22.5.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^30.0.0
vite: ^8.0.0-beta.8
vite: ^7.3.0
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^8.0.0
vitest: ^4.0.16
vitest: ^3.2.4
vue: ^3.5.13
vue-component-type-helpers: ^3.2.1
vue-component-type-helpers: ^3.0.7
vue-eslint-parser: ^10.2.0
vue-i18n: ^9.14.3
vue-router: ^4.4.3
vue-tsc: ^3.2.1
vue-tsc: ^3.1.8
vuefire: ^3.2.1
wwobjloader2: ^6.2.1
yjs: ^13.6.27
zod: ^3.23.8
zod-to-json-schema: ^3.24.1

View File

@@ -10,158 +10,37 @@ interface TestStats {
finished?: number
}
interface TestResult {
status: string
duration?: number
error?: {
message?: string
stack?: string
}
attachments?: Array<{
name: string
path?: string
contentType: string
}>
}
interface TestCase {
title: string
ok: boolean
outcome: string
results: TestResult[]
}
interface Suite {
title: string
file: string
suites?: Suite[]
tests?: TestCase[]
}
interface FullReportData {
stats?: TestStats
suites?: Suite[]
}
interface ReportData {
stats?: TestStats
}
interface FailedTest {
name: string
file: string
traceUrl?: string
error?: string
}
interface TestCounts {
passed: number
failed: number
flaky: number
skipped: number
total: number
failures?: FailedTest[]
}
/**
* Extract failed test details from Playwright report
*/
function extractFailedTests(
reportData: FullReportData,
baseUrl?: string
): FailedTest[] {
const failures: FailedTest[] = []
function processTest(test: TestCase, file: string, suitePath: string[]) {
// Check if test failed or is flaky
const hasFailed = test.results.some(
(r) => r.status === 'failed' || r.status === 'timedOut'
)
const isFlaky = test.outcome === 'flaky'
if (hasFailed || isFlaky) {
const fullTestName = [...suitePath, test.title]
.filter(Boolean)
.join(' ')
const failedResult = test.results.find(
(r) => r.status === 'failed' || r.status === 'timedOut'
)
// Find trace attachment
let traceUrl: string | undefined
if (failedResult?.attachments) {
const traceAttachment = failedResult.attachments.find(
(a) => a.name === 'trace' && a.contentType === 'application/zip'
)
if (traceAttachment?.path) {
// Convert local path to URL path
const tracePath = traceAttachment.path.replace(/\\/g, '/')
const traceFile = path.basename(tracePath)
if (baseUrl) {
// Construct trace viewer URL
const traceDataUrl = `${baseUrl}/data/${traceFile}`
traceUrl = `${baseUrl}/trace/?trace=${encodeURIComponent(traceDataUrl)}`
}
}
}
failures.push({
name: fullTestName,
file: file,
traceUrl,
error: failedResult?.error?.message
})
}
}
function processSuite(suite: Suite, parentPath: string[] = []) {
const suitePath = suite.title ? [...parentPath, suite.title] : parentPath
// Process tests in this suite
if (suite.tests) {
for (const test of suite.tests) {
processTest(test, suite.file, suitePath)
}
}
// Recursively process nested suites
if (suite.suites) {
for (const childSuite of suite.suites) {
processSuite(childSuite, suitePath)
}
}
}
if (reportData.suites) {
for (const suite of reportData.suites) {
processSuite(suite)
}
}
return failures
}
/**
* Extract test counts from Playwright HTML report
* @param reportDir - Path to the playwright-report directory
* @param baseUrl - Base URL of the deployed report (for trace links)
* @returns Test counts { passed, failed, flaky, skipped, total, failures }
* @returns Test counts { passed, failed, flaky, skipped, total }
*/
function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts {
function extractTestCounts(reportDir: string): TestCounts {
const counts: TestCounts = {
passed: 0,
failed: 0,
flaky: 0,
skipped: 0,
total: 0,
failures: []
total: 0
}
try {
// First, try to find report.json which Playwright generates with JSON reporter
const jsonReportFile = path.join(reportDir, 'report.json')
if (fs.existsSync(jsonReportFile)) {
const reportJson: FullReportData = JSON.parse(
const reportJson: ReportData = JSON.parse(
fs.readFileSync(jsonReportFile, 'utf-8')
)
if (reportJson.stats) {
@@ -175,12 +54,6 @@ function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts {
counts.failed = stats.unexpected || 0
counts.flaky = stats.flaky || 0
counts.skipped = stats.skipped || 0
// Extract detailed failure information
if (counts.failed > 0 || counts.flaky > 0) {
counts.failures = extractFailedTests(reportJson, baseUrl)
}
return counts
}
}
@@ -296,18 +169,15 @@ function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts {
// Main execution
const reportDir = process.argv[2]
const baseUrl = process.argv[3] // Optional: base URL for trace links
if (!reportDir) {
console.error(
'Usage: extract-playwright-counts.ts <report-directory> [base-url]'
)
console.error('Usage: extract-playwright-counts.ts <report-directory>')
process.exit(1)
}
const counts = extractTestCounts(reportDir, baseUrl)
const counts = extractTestCounts(reportDir)
// Output as JSON for easy parsing in shell script
process.stdout.write(JSON.stringify(counts) + '\n')
console.log(JSON.stringify(counts))
export { extractTestCounts, extractFailedTests }
export { extractTestCounts }

View File

@@ -134,22 +134,23 @@ post_comment() {
# Main execution
if [ "$STATUS" = "starting" ]; then
# Post concise starting comment
# Post starting comment
comment=$(cat <<EOF
$COMMENT_MARKER
## 🎭 Playwright Tests: ⏳ Running...
## 🎭 Playwright Test Results
Tests started at $START_TIME UTC
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Tests are starting...**
<details>
<summary>📊 Browser Tests</summary>
⏰ Started at: $START_TIME UTC
- **chromium**: Running...
- **chromium-0.5x**: Running...
- **chromium-2x**: Running...
- **mobile-chrome**: Running...
### 🚀 Running Tests
- 🧪 **chromium**: Running tests...
- 🧪 **chromium-0.5x**: Running tests...
- 🧪 **chromium-2x**: Running tests...
- 🧪 **mobile-chrome**: Running tests...
</details>
---
⏱️ Please wait while tests are running...
EOF
)
post_comment "$comment"
@@ -188,8 +189,7 @@ else
if command -v tsx > /dev/null 2>&1 && [ -f "$EXTRACT_SCRIPT" ]; then
echo "Extracting counts from $REPORT_DIR using $EXTRACT_SCRIPT" >&2
# Pass the base URL so we can generate trace links
counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" "$url" 2>&1 || echo '{}')
counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" 2>&1 || echo '{}')
echo "Extracted counts for $browser: $counts" >&2
echo "$counts" > "$temp_dir/$i.counts"
else
@@ -286,74 +286,43 @@ else
# Determine overall status
if [ $total_failed -gt 0 ]; then
status_icon="❌"
status_text="Failed"
status_text="Some tests failed"
elif [ $total_flaky -gt 0 ]; then
status_icon="⚠️"
status_text="Passed with flaky tests"
status_text="Tests passed with flaky tests"
elif [ $total_tests -gt 0 ]; then
status_icon="✅"
status_text="Passed"
status_text="All tests passed!"
else
status_icon="🕵🏻"
status_text="No test results"
status_text="No test results found"
fi
# Generate concise completion comment
# Generate completion comment
comment="$COMMENT_MARKER
## 🎭 Playwright Tests: $status_icon **$status_text**"
## 🎭 Playwright Test Results
$status_icon **$status_text**
⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC"
# Add summary counts if we have test data
if [ $total_tests -gt 0 ]; then
comment="$comment
**Results:** $total_passed passed, $total_failed failed, $total_flaky flaky, $total_skipped skipped (Total: $total_tests)"
fi
# Extract and display failed tests from all browsers
if [ $total_failed -gt 0 ] || [ $total_flaky -gt 0 ]; then
comment="$comment
### ❌ Failed Tests"
# Process each browser's failures
for counts_json in "${counts_array[@]}"; do
[ -z "$counts_json" ] || [ "$counts_json" = "{}" ] && continue
if command -v jq > /dev/null 2>&1; then
# Extract failures array from JSON
failures=$(echo "$counts_json" | jq -r '.failures // [] | .[]? | "\(.name)|\(.file)|\(.traceUrl // "")"')
if [ -n "$failures" ]; then
while IFS='|' read -r test_name test_file trace_url; do
[ -z "$test_name" ] && continue
# Convert file path to GitHub URL (relative to repo root)
github_file_url="https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/$test_file"
# Build the failed test line
test_line="- [$test_name]($github_file_url)"
if [ -n "$trace_url" ] && [ "$trace_url" != "null" ]; then
test_line="$test_line: [View trace]($trace_url)"
fi
comment="$comment
$test_line"
done <<< "$failures"
fi
fi
done
### 📈 Summary
- **Total Tests:** $total_tests
- **Passed:** $total_passed
- **Failed:** $total_failed $([ $total_failed -gt 0 ] && echo '❌' || echo '')
- **Flaky:** $total_flaky $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '')
- **Skipped:** $total_skipped $([ $total_skipped -gt 0 ] && echo '⏭️' || echo '')"
fi
# Add browser reports in collapsible section
comment="$comment
<details>
<summary>📊 Browser Reports</summary>
"
### 📊 Test Reports by Browser"
# Add browser results
# Add browser results with individual counts
i=0
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
IFS=' ' read -r -a url_array <<< "$urls"
@@ -380,7 +349,7 @@ $test_line"
fi
if [ -n "$b_total" ] && [ "$b_total" != "0" ]; then
counts_str=" ($b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped)"
counts_str=" $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped"
else
counts_str=""
fi
@@ -389,10 +358,10 @@ $test_line"
fi
comment="$comment
- **${browser}**: [View Report](${url})${counts_str}"
- **${browser}**: [View Report](${url})${counts_str}"
else
comment="$comment
- **${browser}**: Deployment failed"
- **${browser}**: Deployment failed"
fi
i=$((i + 1))
done
@@ -400,7 +369,8 @@ $test_line"
comment="$comment
</details>"
---
🎉 Click on the links above to view detailed test results for each browser configuration."
post_comment "$comment"
fi

View File

@@ -12,7 +12,6 @@ declare global {
const __ALGOLIA_API_KEY__: string
const __USE_PROD_CONFIG__: boolean
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
const __IS_NIGHTLY__: boolean
}
type GlobalWithDefines = typeof globalThis & {
@@ -23,7 +22,6 @@ type GlobalWithDefines = typeof globalThis & {
__ALGOLIA_API_KEY__: string
__USE_PROD_CONFIG__: boolean
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
__IS_NIGHTLY__: boolean
window?: Record<string, unknown>
}
@@ -38,7 +36,6 @@ globalWithDefines.__ALGOLIA_APP_ID__ = ''
globalWithDefines.__ALGOLIA_API_KEY__ = ''
globalWithDefines.__USE_PROD_CONFIG__ = false
globalWithDefines.__DISTRIBUTION__ = 'localhost'
globalWithDefines.__IS_NIGHTLY__ = false
// Provide a minimal window shim for Node environment
// This is needed for code that checks window existence during imports

View File

@@ -1,26 +0,0 @@
# Source Code Guidelines
## Error Handling
- User-friendly and actionable messages
- Proper error propagation
## Security
- Sanitize HTML with DOMPurify
- Validate trusted sources
- Never log secrets
## State Management (Stores)
- Follow domain-driven design for organizing files/folders
- Clear public interfaces
- Restrict extension access
- Clean up subscriptions
## General Guidelines
- Use `es-toolkit` for utility functions
- Use TypeScript for type safety
- Avoid `@ts-expect-error` - fix the underlying issue
- Use `vue-i18n` for ALL user-facing strings (`src/locales/en/main.json`)

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