Compare commits

..

6 Commits

Author SHA1 Message Date
Austin Mroz
e377d11087 Add test 2026-07-07 22:19:52 -07:00
Austin Mroz
f09e344c0d Fix output restoration 2026-07-07 21:49:12 -07:00
CodeJuggernaut
e58b231664 fix: suppress the stray focus ring on the sidebar splitter panel (#13482)
Clicking empty space in a workspace panel focuses the whole PrimeVue
SplitterPanel (tabindex=-1 makes it click-focusable), and any following
non-chord keypress (e.g. Shift) trips the browser focus-visible
heuristic, painting the default blue ring around the entire panel.

The wrappers are not Tab-reachable (Tab lands on the controls inside,
never the panel box) and nothing focuses them programmatically, so the
ring conveys nothing. `focus-visible:outline-hidden` suppresses it while
keeping a forced-colors (High Contrast) indicator. Confirmed as noise
with design (Alex Tov).

Covers every click-focusable panel in
`LiteGraphCanvasSplitterOverlay.vue`: the sidebar panel (both
locations), the properties-side panel (both branches), and the bottom
panel. The center and graph-canvas panels are left untouched - they
inherit `pointer-events-none`, so a click can never focus them.

## Repro / QA

Ring trigger: click an empty, non-interactive spot inside the panel,
then press solo Shift. On main the browser paints a blue ring around the
whole panel; on this PR nothing appears. (Ctrl+Shift only triggers when
Shift lands first, hence the original "sometimes".)

| Panel | How to open | Fixed |
| --- | --- | --- |
| Sidebar (left, default) | Any rail icon, e.g. Assets | yes |
| Sidebar (right) | Settings > Sidebar Location > right | yes |
| Properties-side panel | Toggle properties panel / builder mode | yes |
| Bottom panel | Toggle Logs/Terminal | yes |
| Canvas / center | n/a | untouched - pointer-events-none, cannot be
click-focused |

- [ ] Each fixed panel: click empty spot, press Shift, no ring
- [ ] Same steps on main/prod show the ring (before-state)
- [ ] Tab still reaches controls inside each panel and their own focus
rings still show
- [ ] Media Assets shortcuts unchanged (Ctrl/Cmd+A, marquee modifiers) -
PR is CSS-only

- Surfaced during design review of #13323
2026-07-08 00:33:41 +00:00
Comfy Org PR Bot
545b48ee5b [chore] Update Ingest API types from cloud@421de6d (#12777)
## Automated Ingest API Type Update

This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.

- Cloud commit: 421de6d
- Generated using @hey-api/openapi-ts with Zod plugin

These types cover cloud-only endpoints (workspaces, billing, secrets,
assets, tasks, etc.).
Overlapping endpoints shared with the local ComfyUI Python backend are
excluded.

---------

Co-authored-by: mattmillerai <7741082+mattmillerai@users.noreply.github.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2026-07-08 00:29:59 +00:00
Benjamin Lu
22ea53fb56 fix(ci): remove out-of-rootDir config files from package tsconfig includes (#13485)
## Summary

Type-aware oxlint rejects `packages/ingest-types/tsconfig.json` and
`packages/object-info-parser/tsconfig.json` as invalid (TS6059): their
`include` lists a root-level config file (`openapi-ts.config.ts` /
`vitest.config.ts`) that sits outside `rootDir: "src"`. This fails the
lint-and-format job with "Invalid tsconfig" on any PR that touches those
packages' src files — currently blocking every auto-generated
ingest-types sync (e.g. #12777).

## Changes

- **What**: Drop the out-of-`rootDir` config-file entries from the two
package tsconfig `include` arrays, matching the other workspace
packages. Repro: `pnpm exec oxlint --type-aware
packages/ingest-types/src/index.ts` fails before, passes after; the
config files themselves still lint clean.

## Review Focus

Neither package emits a build, so `rootDir`/`outDir` are
editor/lint-only; excluding the config files from the project has no
runtime effect (vitest/openapi-ts load their configs directly).
2026-07-07 17:03:12 -07:00
cloud-code-bot[bot]
9fe5dd51b8 ci: bump cursor-review to github-workflows@df507e6 (#13493)
Automatic SHA bump — `cursor-review.yml` was updated in
`Comfy-Org/github-workflows` at
[`df507e6`](df507e6bae).
_Opened by the `bump-cursor-review-callers` workflow._

Co-authored-by: cloud-code-bot[bot] <234529496+cloud-code-bot[bot]@users.noreply.github.com>
2026-07-07 23:06:59 +00:00
10 changed files with 1614 additions and 105 deletions

View File

@@ -63,62 +63,3 @@ reviews:
Pass if none of these patterns are found in the diff.
When warning, reference the specific ADR by number and link to `docs/adr/` for context. Frame findings as directional guidance since ADR 0003 and 0008 are in Proposed status.
- name: App queue credential cleanup on rejection
mode: warning
instructions: |
Use only PR metadata already available in the review context: the changed-file list relative to the PR base, the PR description, and the diff content. Do not rely on shell commands.
This check applies ONLY when the PR changes `src/scripts/app.ts` and that diff assigns `api.authToken` or `api.apiKey` before awaiting `api.queuePrompt`.
When applicable, require a changed app test file such as `src/scripts/app.core.test.ts` or `src/scripts/app.test.ts` to include rejected-queue coverage that:
1. Populates both credential sources (`authToken` and API key, or the stores that feed them) with non-empty values.
2. Makes `api.queuePrompt` reject.
3. Calls `app.queuePrompt`.
4. Asserts after the rejection path that both `api.authToken` and `api.apiKey` are cleared, deleted, or `undefined`.
Warn if rejection is tested without those cleanup assertions, or if cleanup is asserted only on the successful `api.queuePrompt` path. Mention that success-path cleanup alone does not prove credentials are cleared after a failed queue request.
path_instructions:
- path: '**/*.test.ts'
instructions: |
Treat `.agents/checks/test-quality.md`, `docs/testing/README.md`, and `docs/guidance/vitest.md` as required review context for every changed Vitest test file.
Flag missing behavioral coverage for changed behavior, change-detector tests, mock-heavy tests, snapshot abuse, fragile assertions, missing edge cases, unclear setup, and unrestored global mutations.
Prefer colocated behavioral tests named after the source file.
Build partial mocks with fromPartial<T>() from @total-typescript/shoehorn; flag `as unknown as` double assertions and fromAny().
Mock only at seams (Pinia stores, settings, third-party libs); flag mocked type guards or sibling composables.
Use a real createI18n instance rather than vi.mock('vue-i18n').
Flag bare expect(fn).not.toThrow() as a sole assertion, assertions that echo stub return values, and .mock.results assertions.
For rejected promises, thrown errors, and failed async calls, require assertions for post-error state cleanup and side-effect rollback, especially auth tokens, API keys, globals, listeners, timers, subscriptions, and caches that production mutates before awaiting.
Tests for production changes must exercise the changed runtime/public entrypoint directly; flag helper-only coverage when the changed branch runs through a higher-level entrypoint.
Use @testing-library/vue for component tests, not @vue/test-utils.
For platform-owned types (Response, CustomEvent, DOM events), require real instances (new Response(), Response.json(), new CustomEvent()) instead of fromPartial or casts.
When a fixture cast hides a too-wide production signature, suggest narrowing the production type instead of casting the fixture.
Flag process-level listeners (process.on('unhandledRejection')) in tests; assert the rejected promise directly.
Tests should import the module under test from its public entrypoint, not deep internal paths.
- path: 'src/scripts/app*.test.ts'
instructions: |
For app tests that mock or spy on `api.queuePrompt`, require rejected-queue coverage whenever the code temporarily populates `api.authToken` or `api.apiKey`.
The rejected-path test must populate both values, make `api.queuePrompt` reject, and assert both fields are cleared afterward; success-path cleanup alone is insufficient.
If `api.queuePrompt` rejection is tested without asserting `api.authToken` and `api.apiKey` cleanup afterward, flag the missing assertion even when a success-path test already covers cleanup.
- path: 'src/scripts/app.ts'
instructions: |
When app queuing code temporarily assigns `api.authToken` or `api.apiKey` before awaiting `api.queuePrompt`, require app tests for both resolved and rejected `api.queuePrompt` paths.
The rejected-path test must prove both credential fields are cleared after `api.queuePrompt` rejects; a rejection assertion without cleanup checks is insufficient.
- path: 'src/lib/litegraph/src/LGraph.ts'
instructions: |
When changes touch `LGraph.configure()`, graph deserialization, link loading, or nullable serialized links, require a direct regression test that constructs an `LGraph` and calls `configure()` with the changed serialized shape.
Helper-only coverage is insufficient for these changes. For sparse legacy links, expect a v0.4 workflow with `links: [null, validLink]` to not throw and still create the valid link.
- path: 'src/lib/litegraph/**/*.test.ts'
instructions: |
Treat `.agents/checks/test-quality.md`, `docs/testing/README.md`, and `docs/guidance/vitest.md` as required review context for every changed litegraph Vitest test file.
Reuse shared factories in `src/utils/__tests__/litegraphTestUtils.ts` instead of hand-rolling litegraph mock builders.
Flag mocked litegraph classes when a real instance or shared factory would exercise behavior directly.
- path: '{browser_tests,apps/website/e2e}/**/*.spec.ts'
instructions: |
Treat `.agents/checks/test-quality.md` and `docs/testing/README.md` as required review context for every changed Playwright test file.
Flag missing behavioral coverage, change-detector tests, mock-heavy tests, snapshot abuse, fragile assertions, missing edge cases, unclear setup, and test isolation problems.
Every route.fulfill() body must be typed with generated types or schemas from packages/ingest-types, packages/registry-types, src/workbench/extensions/manager/types/generatedManagerTypes.ts, or src/schemas/; flag untyped inline JSON objects.
Never use waitForTimeout; use Locator actions and auto-retrying assertions instead.
Restrict page.evaluate() to reading internal state or fixture setup; flag any page.evaluate() that drives UI actions when a Playwright action method exists.
New shared test helpers must be Playwright fixtures via base.extend(), not properties added to ComfyPage.

View File

@@ -29,7 +29,7 @@ jobs:
# SHA-pinned per zizmor `unpinned-uses: hash-pin`. Bump this SHA to pick up
# upstream changes; keep `workflows_ref` matching so prompts/scripts load
# from the same commit as the workflow definition.
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@047ca48febe3a6647608ed2e0c4331b491cb9d6a # github-workflows#9
uses: Comfy-Org/github-workflows/.github/workflows/cursor-review.yml@df507e6bae179c567ad3849370f99dae588985dc # github-workflows main (df507e6)
with:
# Overriding diff_excludes replaces the reusable default wholesale, so
# this restates the generated/vendored defaults and adds this repo's heavy
@@ -48,7 +48,7 @@ jobs:
:!**/*-snapshots/**
:!src/workbench/extensions/manager/types/generatedManagerTypes.ts
# Load the prompts/scripts from the same ref as `uses:`.
workflows_ref: 047ca48febe3a6647608ed2e0c4331b491cb9d6a
workflows_ref: df507e6bae179c567ad3849370f99dae588985dc
secrets:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
# Optional — enables start/complete Slack DMs to the triggerer.

View File

@@ -1,7 +1,13 @@
import { mergeTests } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'
import { webSocketFixture } from '@e2e/fixtures/ws'
const wstest = mergeTests(test, webSocketFixture)
test.describe('Preview as Text node', () => {
test('does not include preview widget values in the API prompt', async ({
@@ -39,4 +45,33 @@ test.describe('Preview as Text node', () => {
expect(previewEntry!.inputs).not.toHaveProperty('preview_text')
expect(previewEntry!.inputs).not.toHaveProperty('previewMode')
})
wstest(
'restoring workflow restores state',
{ tag: '@vue-nodes' },
async ({ comfyPage, getWebSocket }) => {
const execution = new ExecutionHelper(comfyPage, await getWebSocket())
await comfyPage.menu.topbar.newWorkflowButton.click()
await comfyPage.searchBoxV2.addNode('Preview as Text')
const node = await comfyPage.vueNodes.getFixtureByTitle('Preview as Text')
const preview = node.root.locator('textarea')
await test.step('node previews execution result', async () => {
execution.executed('', '1', { text: 'massive fennec ears' })
await expect(preview).toHaveValue('massive fennec ears')
})
await test.step('swap to a different workflow and back', async () => {
await comfyPage.menu.topbar.getTab(0).click()
await expect(node.root).toBeHidden()
await comfyPage.menu.topbar.getTab(1).click()
await expect(node.root).toBeVisible()
})
await expect(preview, 'previous output is restored').toHaveValue(
'massive fennec ears'
)
}
)
})

View File

@@ -12,6 +12,11 @@ export type {
AddAssetTagsErrors,
AddAssetTagsResponse,
AddAssetTagsResponses,
AdminDeleteHubWorkflowData,
AdminDeleteHubWorkflowError,
AdminDeleteHubWorkflowErrors,
AdminDeleteHubWorkflowResponse,
AdminDeleteHubWorkflowResponses,
Asset,
AssetCreated,
AssetCreatedWritable,
@@ -42,6 +47,11 @@ export type {
CancelJobErrors,
CancelJobResponse,
CancelJobResponses,
CancelJobsData,
CancelJobsError,
CancelJobsErrors,
CancelJobsResponse,
CancelJobsResponses,
CancelSubscriptionData,
CancelSubscriptionError,
CancelSubscriptionErrors,
@@ -84,6 +94,11 @@ export type {
CreateDeletionRequestErrors,
CreateDeletionRequestResponse,
CreateDeletionRequestResponses,
CreateDesktopLoginCodeData,
CreateDesktopLoginCodeError,
CreateDesktopLoginCodeErrors,
CreateDesktopLoginCodeResponse,
CreateDesktopLoginCodeResponses,
CreateHubAssetUploadUrlData,
CreateHubAssetUploadUrlError,
CreateHubAssetUploadUrlErrors,
@@ -186,12 +201,31 @@ export type {
DeleteWorkspaceResponses,
DeletionRequest,
DeletionStatus,
DesktopLoginCodeCreateRequest,
DesktopLoginCodeCreateResponse,
DesktopLoginCodeExchangeRequest,
DesktopLoginCodeExchangeResponse,
DesktopLoginCodeRedeemRequest,
DesktopLoginCodeRedeemResponse,
DownloadExportData,
DownloadExportError,
DownloadExportErrors,
DownloadExportResponse,
DownloadExportResponses,
EnsureWorkspaceBillingLegacySnapshot,
EnsureWorkspaceBillingProvisionedData,
EnsureWorkspaceBillingProvisionedError,
EnsureWorkspaceBillingProvisionedErrors,
EnsureWorkspaceBillingProvisionedRequest,
EnsureWorkspaceBillingProvisionedResponse,
EnsureWorkspaceBillingProvisionedResponse2,
EnsureWorkspaceBillingProvisionedResponses,
ErrorResponse,
ExchangeDesktopLoginCodeData,
ExchangeDesktopLoginCodeError,
ExchangeDesktopLoginCodeErrors,
ExchangeDesktopLoginCodeResponse,
ExchangeDesktopLoginCodeResponses,
ExchangeTokenData,
ExchangeTokenError,
ExchangeTokenErrors,
@@ -230,6 +264,11 @@ export type {
GetAssetByIdErrors,
GetAssetByIdResponse,
GetAssetByIdResponses,
GetAssetContentData,
GetAssetContentError,
GetAssetContentErrors,
GetAssetContentResponse,
GetAssetContentResponses,
GetAssetSeedStatusData,
GetAssetSeedStatusResponse,
GetAssetSeedStatusResponses,
@@ -303,6 +342,11 @@ export type {
GetHistoryData,
GetHistoryError,
GetHistoryErrors,
GetHistoryEventsData,
GetHistoryEventsError,
GetHistoryEventsErrors,
GetHistoryEventsResponse,
GetHistoryEventsResponses,
GetHistoryForPromptData,
GetHistoryForPromptError,
GetHistoryForPromptErrors,
@@ -345,8 +389,6 @@ export type {
GetJwksData,
GetJwksResponse,
GetJwksResponses,
GetLegacyAssetContentData,
GetLegacyAssetContentErrors,
GetLegacyHistoryByIdData,
GetLegacyHistoryByIdErrors,
GetLegacyHistoryData,
@@ -556,6 +598,7 @@ export type {
HistoryDetailEntry,
HistoryDetailResponse,
HistoryEntry,
HistoryEventRequest,
HistoryManageRequest,
HistoryResponse,
HubAssetUploadUrlRequest,
@@ -589,6 +632,8 @@ export type {
JobCancelResponse,
JobDetailResponse,
JobEntry,
JobsCancelRequest,
JobsCancelResponse,
JobsListResponse,
JobStatusResponse,
JwkKey,
@@ -627,7 +672,19 @@ export type {
ListJobsErrors,
ListJobsResponse,
ListJobsResponses,
ListLinkedFirebaseUidsData,
ListLinkedFirebaseUidsError,
ListLinkedFirebaseUidsErrors,
ListLinkedFirebaseUidsRequest,
ListLinkedFirebaseUidsResponse,
ListLinkedFirebaseUidsResponse2,
ListLinkedFirebaseUidsResponses,
ListMembersResponse,
ListSecretProvidersData,
ListSecretProvidersError,
ListSecretProvidersErrors,
ListSecretProvidersResponse,
ListSecretProvidersResponses,
ListSecretsData,
ListSecretsError,
ListSecretsErrors,
@@ -775,6 +832,17 @@ export type {
QueueInfo,
QueueManageRequest,
QueueManageResponse,
RedeemDesktopLoginCodeData,
RedeemDesktopLoginCodeError,
RedeemDesktopLoginCodeErrors,
RedeemDesktopLoginCodeResponse,
RedeemDesktopLoginCodeResponses,
ReleaseDeletionHoldData,
ReleaseDeletionHoldError,
ReleaseDeletionHoldErrors,
ReleaseDeletionHoldResponse,
ReleaseDeletionHoldResponses,
ReleaseHoldResponse,
RemoveAssetTagsData,
RemoveAssetTagsError,
RemoveAssetTagsErrors,
@@ -785,6 +853,11 @@ export type {
RemoveWorkspaceMemberErrors,
RemoveWorkspaceMemberResponse,
RemoveWorkspaceMemberResponses,
ReportHistoryEventData,
ReportHistoryEventError,
ReportHistoryEventErrors,
ReportHistoryEventResponse,
ReportHistoryEventResponses,
ReportPartnerUsageData,
ReportPartnerUsageError,
ReportPartnerUsageErrors,
@@ -808,6 +881,8 @@ export type {
RevokeWorkspaceInviteResponse,
RevokeWorkspaceInviteResponses,
SecretListResponse,
SecretProvider,
SecretProvidersResponse,
SecretResponse,
SeedAssetsData,
SeedAssetsResponse,
@@ -819,6 +894,8 @@ export type {
SetReviewStatusResponse,
SetReviewStatusResponse2,
SetReviewStatusResponses,
ShortLinkRedirectData,
ShortLinkRedirectErrors,
SubmitFeedbackData,
SubmitFeedbackError,
SubmitFeedbackErrors,
@@ -848,6 +925,10 @@ export type {
TaskEntry,
TaskResponse,
TasksListResponse,
TeamCreditStop,
TeamCreditStopPrice,
TeamCreditStops,
TeamCreditStopSummary,
UpdateAssetData,
UpdateAssetError,
UpdateAssetErrors,
@@ -865,6 +946,7 @@ export type {
UpdateHubWorkflowRequest,
UpdateHubWorkflowResponse,
UpdateHubWorkflowResponses,
UpdateMemberRoleRequest,
UpdateMultipleSettingsData,
UpdateMultipleSettingsError,
UpdateMultipleSettingsErrors,
@@ -895,6 +977,11 @@ export type {
UpdateWorkspaceData,
UpdateWorkspaceError,
UpdateWorkspaceErrors,
UpdateWorkspaceMemberRoleData,
UpdateWorkspaceMemberRoleError,
UpdateWorkspaceMemberRoleErrors,
UpdateWorkspaceMemberRoleResponse,
UpdateWorkspaceMemberRoleResponses,
UpdateWorkspaceRequest,
UpdateWorkspaceResponse,
UpdateWorkspaceResponses,

File diff suppressed because it is too large Load Diff

View File

@@ -465,6 +465,20 @@ export const zCreateWorkflowRequest = z.object({
forked_from_workflow_version_id: z.string().optional()
})
/**
* Request body for forwarding a comfy-api audit/history event. Identify the target workspace by either user_id (cloud resolves the user's personal workspace via the converged identity, BE-1047) or an explicit workspace_id. At least one must be provided; workspace_id wins when both are set.
*/
export const zHistoryEventRequest = z.object({
user_id: z.string().optional(),
workspace_id: z.string().optional(),
event_type: z.string().min(1),
event_id: z.string().min(1),
params: z.record(z.unknown()).optional(),
auth_method: z.enum(['api_key', 'bearer_token']).optional(),
customer_ref: z.string().optional(),
timestamp: z.string().datetime().optional()
})
/**
* Response after recording partner usage data.
*/
@@ -540,11 +554,11 @@ export const zPaymentPortalRequest = z.object({
})
/**
* Response after successfully resubscribing to a billing plan.
* Response after accepting a resubscribe request.
*/
export const zResubscribeResponse = z.object({
billing_op_id: z.string(),
status: z.enum(['active']),
status: z.enum(['active', 'pending']),
message: z.string().optional()
})
@@ -585,6 +599,8 @@ export const zSubscribeResponse = z.object({
*/
export const zSubscribeRequest = z.object({
plan_slug: z.string(),
team_credit_stop_id: z.string().optional(),
billing_cycle: z.enum(['monthly', 'yearly']).optional(),
idempotency_key: z.string().optional(),
return_url: z.string().optional(),
cancel_url: z.string().optional()
@@ -626,7 +642,8 @@ export const zSubscriptionTier = z.enum([
'STANDARD',
'CREATOR',
'PRO',
'FOUNDERS_EDITION'
'FOUNDERS_EDITION',
'TEAM'
])
/**
@@ -714,6 +731,57 @@ export const zPreviewSubscribeRequest = z.object({
plan_slug: z.string()
})
/**
* Pre/post-discount price for a team credit stop, in cents.
*/
export const zTeamCreditStopPrice = z.object({
list_price_cents: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
}),
price_cents: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
})
})
/**
* A selectable preset on the team pricing slider. Echoed on subscribe via
* team_credit_stop_id; the backend owns the resolved amounts. credits is a
* RAW monthly credit count (not cents). Save% is derived by the FE as
* (list_price_cents - price_cents) / list_price_cents.
*
*/
export const zTeamCreditStop = z.object({
id: z.string(),
credits: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
}),
monthly: zTeamCreditStopPrice,
yearly: zTeamCreditStopPrice
})
/**
* Credit-stop ladder for the pricing slider (BE-1254). Returned by GET /api/billing/plans for every workspace regardless of the caller's token or workspace type (the personal/team distinction was removed); omitted only when the catalog defines no stops.
*/
export const zTeamCreditStops = z.object({
default_stop_index: z.number().int(),
stops: z.array(zTeamCreditStop)
})
/**
* Reason why a plan is unavailable
*/
@@ -773,7 +841,50 @@ export const zPlan = z.object({
*/
export const zBillingPlansResponse = z.object({
current_plan_slug: z.string().optional(),
plans: z.array(zPlan)
plans: z.array(zPlan),
team_credit_stops: zTeamCreditStops.optional()
})
/**
* The team credit stop a workspace is currently subscribed to: the
* per-workspace slider choice recorded at subscribe time
* (workspace_subscriptions.team_credit_stop_id). Amounts are owned by the
* catalog, not the subscription row. Returned on GET /api/billing/status
* for per-credit Team plans (BE-1254).
*
*/
export const zTeamCreditStopSummary = z.object({
id: z.string(),
credits_monthly: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
}),
stop_usd: z.coerce
.bigint()
.min(BigInt('-9223372036854775808'), {
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
})
.max(BigInt('9223372036854775807'), {
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
})
})
/**
* A provider the user may configure a secret for. The shape is deliberately minimal (identifier only) and reserved for future per-provider fields such as sub-keys.
*/
export const zSecretProvider = z.object({
id: z.string()
})
/**
* The providers available to the authenticated user in the current workspace.
*/
export const zSecretProvidersResponse = z.object({
data: z.array(zSecretProvider)
})
/**
@@ -813,7 +924,7 @@ export const zCreateSecretRequest = z.object({
})
/**
* A single billing event such as a charge, credit, or adjustment.
* A single history event. The cloud history-events store is the single source of truth for both billing events (charges, credits, adjustments) and user-facing usage events.
*/
export const zBillingEvent = z.object({
event_type: z.string(),
@@ -868,7 +979,8 @@ export const zBillingStatusResponse = z.object({
billing_status: zBillingStatus.optional(),
has_funds: z.boolean(),
cancel_at: z.string().datetime().optional(),
renewal_date: z.string().datetime().optional()
renewal_date: z.string().datetime().optional(),
team_credit_stop: zTeamCreditStopSummary.nullable()
})
/**
@@ -930,6 +1042,7 @@ export const zOAuthConsentChallenge = z.object({
csrf_token: z.string(),
client_display_name: z.string(),
resource_display_name: z.string(),
redirect_uri: z.string().url(),
scopes: z.array(z.string()),
workspaces: z.array(zOAuthConsentChallengeWorkspace)
})
@@ -1056,6 +1169,66 @@ export const zSyncApiKeyRequest = z.object({
customer_id: z.string().min(1)
})
/**
* The personal workspace's provisioned billing identity.
*/
export const zEnsureWorkspaceBillingProvisionedResponse = z.object({
workspace_id: z.string(),
stripe_customer_id: z.string(),
metronome_customer_id: z.string(),
metronome_contract_id: z.string()
})
/**
* The caller's already-resolved legacy (comfy-api) customer identity. When
* present and carrying provider IDs, provisioning ATTACHES this identity to
* the personal workspace (sharing the existing balance and subscription)
* instead of minting a net-new empty customer. Omit (or send with no
* provider IDs) for a free user with nothing to attach — provisioning then
* creates net-new. This closes the create-new-before-attach gap: a caller
* that already knows the legacy identity hands it over so the very first
* provisioning is an attach.
*
*/
export const zEnsureWorkspaceBillingLegacySnapshot = z.object({
stripe_customer_id: z.string().optional(),
metronome_customer_id: z.string().optional(),
metronome_contract_id: z.string().optional(),
has_funds: z.boolean().optional(),
subscription_tier: z.string().optional(),
legacy_stripe_subscription_id: z.string().optional(),
legacy_comfy_user_id: z.string().optional()
})
/**
* Request body for ensuring a user's personal workspace carries a fully
* provisioned billing identity. Sent by comfy-api's CreateCustomer (BE-1047)
* with the already canonical-resolved user identity.
*
*/
export const zEnsureWorkspaceBillingProvisionedRequest = z.object({
user_id: z.string().min(1),
email: z.string().email().min(1),
snapshot: zEnsureWorkspaceBillingLegacySnapshot.optional()
})
/**
* Firebase UIDs linked to the canonical comfy_user_id. Empty list when
* no mappings exist (not an error — callers can treat empty as "unknown
* canonical").
*
*/
export const zListLinkedFirebaseUidsResponse = z.object({
firebase_uids: z.array(z.string())
})
/**
* Request body for reverse-looking-up Firebase UIDs linked to a canonical comfy_user_id.
*/
export const zListLinkedFirebaseUidsRequest = z.object({
comfy_user_id: z.string().min(1)
})
/**
* Response confirming the validity and scope of a workspace API key.
*/
@@ -1172,7 +1345,8 @@ export const zMember = z.object({
name: z.string(),
email: z.string().email(),
role: z.enum(['owner', 'member']),
joined_at: z.string().datetime()
joined_at: z.string().datetime(),
is_original_owner: z.boolean()
})
/**
@@ -1183,6 +1357,13 @@ export const zListMembersResponse = z.object({
pagination: zPaginationInfo
})
/**
* Request body for changing a workspace member's role.
*/
export const zUpdateMemberRoleRequest = z.object({
role: z.enum(['owner', 'member'])
})
/**
* Request body for updating an existing workspace's settings.
*/
@@ -1227,6 +1408,60 @@ export const zWorkspace = z.object({
created_at: z.string().datetime()
})
/**
* Exchange poll result. Pending until the code is redeemed in the browser.
*/
export const zDesktopLoginCodeExchangeResponse = z.object({
status: z.enum(['pending', 'complete']),
custom_token: z.string().optional()
})
/**
* Request to exchange a redeemed login code for a custom token.
*/
export const zDesktopLoginCodeExchangeRequest = z.object({
code: z.string(),
code_verifier: z.string().min(43).max(128)
})
/**
* Result of redeeming a desktop login code.
*/
export const zDesktopLoginCodeRedeemResponse = z.object({
status: z.enum(['redeemed'])
})
/**
* Request to claim a desktop login code for the authenticated user.
*/
export const zDesktopLoginCodeRedeemRequest = z.object({
code: z.string()
})
/**
* A freshly minted desktop login code and its polling parameters.
*/
export const zDesktopLoginCodeCreateResponse = z.object({
code: z.string(),
expires_in: z.number().int(),
poll_interval: z.number().int()
})
/**
* Request to mint a desktop login code.
*/
export const zDesktopLoginCodeCreateRequest = z.object({
installation_id: z
.string()
.min(8)
.max(128)
.regex(/^[A-Za-z0-9._-]+$/)
.optional(),
platform: z.string().min(1).max(32),
app_version: z.string().min(1).max(64),
code_challenge: z.string().min(43).max(128)
})
/**
* Abbreviated workspace metadata used in list responses.
*/
@@ -1294,6 +1529,15 @@ export const zTasksListResponse = z.object({
pagination: zPaginationInfo
})
/**
* Result of authorizing a legal-hold release on a user's deletion.
*/
export const zReleaseHoldResponse = z.object({
firebase_id: z.string(),
released: z.boolean(),
message: z.string()
})
/**
* Current status of a user data deletion request.
*/
@@ -1363,6 +1607,20 @@ export const zJobDetailResponse = z.object({
execution_meta: z.record(z.unknown()).optional()
})
/**
* Response for POST /api/jobs/cancel.
*/
export const zJobsCancelResponse = z.object({
cancelled: z.array(z.string())
})
/**
* Request to cancel multiple jobs by ID.
*/
export const zJobsCancelRequest = z.object({
job_ids: z.array(z.string().uuid()).min(1).max(100)
})
/**
* Response for POST /api/jobs/{job_id}/cancel. Returned on both fresh cancels and idempotent no-ops.
*/
@@ -1529,6 +1787,7 @@ export const zAsset = z.object({
user_metadata: z.record(z.unknown()).optional(),
metadata: z.record(z.unknown()).readonly().optional(),
preview_url: z.string().url().optional(),
short_url: z.string().nullish(),
preview_id: z.string().uuid().nullish(),
job_id: z.string().uuid().nullish(),
created_at: z.string().datetime(),
@@ -1624,6 +1883,7 @@ export const zSystemStatsResponse = z.object({
python_version: z.string(),
embedded_python: z.boolean(),
comfyui_version: z.string(),
deploy_environment: z.string().optional(),
comfyui_frontend_version: z.string().optional(),
workflow_templates_version: z.string().optional(),
cloud_version: z.string().optional(),
@@ -1962,6 +2222,7 @@ export const zAssetWritable = z.object({
tags: z.array(z.string()).optional(),
user_metadata: z.record(z.unknown()).optional(),
preview_url: z.string().url().optional(),
short_url: z.string().nullish(),
preview_id: z.string().uuid().nullish(),
job_id: z.string().uuid().nullish(),
created_at: z.string().datetime(),
@@ -2180,7 +2441,11 @@ export const zGetJobDetailData = z.object({
path: z.object({
job_id: z.string().uuid()
}),
query: z.never().optional()
query: z
.object({
short_link: z.enum(['ephemeral_tool_chain', 'default']).optional()
})
.optional()
})
/**
@@ -2201,6 +2466,17 @@ export const zCancelJobData = z.object({
*/
export const zCancelJobResponse = zJobCancelResponse
export const zCancelJobsData = z.object({
body: zJobsCancelRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Success - cancel requests dispatched (or jobs were already terminal)
*/
export const zCancelJobsResponse = zJobsCancelResponse
export const zViewFileData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -2580,6 +2856,17 @@ export const zCreateSecretData = z.object({
*/
export const zCreateSecretResponse = zSecretResponse
export const zListSecretProvidersData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Success
*/
export const zListSecretProvidersResponse = zSecretProvidersResponse
export const zDeleteSecretData = z.object({
body: z.never().optional(),
path: z.object({
@@ -2881,6 +3168,40 @@ export const zExchangeTokenData = z.object({
*/
export const zExchangeTokenResponse2 = zExchangeTokenResponse
export const zCreateDesktopLoginCodeData = z.object({
body: zDesktopLoginCodeCreateRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Login code created
*/
export const zCreateDesktopLoginCodeResponse = zDesktopLoginCodeCreateResponse
export const zRedeemDesktopLoginCodeData = z.object({
body: zDesktopLoginCodeRedeemRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Code redeemed (or already redeemed by the same user)
*/
export const zRedeemDesktopLoginCodeResponse = zDesktopLoginCodeRedeemResponse
export const zExchangeDesktopLoginCodeData = z.object({
body: zDesktopLoginCodeExchangeRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Pending (not yet redeemed) or complete with a custom token
*/
export const zExchangeDesktopLoginCodeResponse =
zDesktopLoginCodeExchangeResponse
export const zGetJwksData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -3150,6 +3471,19 @@ export const zRemoveWorkspaceMemberData = z.object({
*/
export const zRemoveWorkspaceMemberResponse = z.void()
export const zUpdateWorkspaceMemberRoleData = z.object({
body: zUpdateMemberRoleRequest,
path: z.object({
userId: z.string()
}),
query: z.never().optional()
})
/**
* Member role updated
*/
export const zUpdateWorkspaceMemberRoleResponse = zMember
export const zListWorkspaceApiKeysData = z.object({
body: z.never().optional(),
path: z.never().optional(),
@@ -3236,6 +3570,19 @@ export const zSetReviewStatusData = z.object({
*/
export const zSetReviewStatusResponse2 = zSetReviewStatusResponse
export const zAdminDeleteHubWorkflowData = z.object({
body: z.never().optional(),
path: z.object({
share_id: z.string()
}),
query: z.never().optional()
})
/**
* Successfully deleted
*/
export const zAdminDeleteHubWorkflowResponse = z.void()
export const zUpdateHubWorkflowData = z.object({
body: zUpdateHubWorkflowRequest,
path: z.object({
@@ -3277,6 +3624,19 @@ export const zCreateDeletionRequestResponse = z.object({
user_found_in_cloud: z.boolean()
})
export const zReleaseDeletionHoldData = z.object({
body: z.object({
firebase_id: z.string()
}),
path: z.never().optional(),
query: z.never().optional()
})
/**
* Release authorized; the deletion workflow will proceed
*/
export const zReleaseDeletionHoldResponse = zReleaseHoldResponse
export const zReportPartnerUsageData = z.object({
body: zPartnerUsageRequest,
path: z.never().optional(),
@@ -3288,6 +3648,38 @@ export const zReportPartnerUsageData = z.object({
*/
export const zReportPartnerUsageResponse = zPartnerUsageResponse
export const zGetHistoryEventsData = z.object({
body: z.never().optional(),
path: z.never().optional(),
query: z
.object({
workspace_id: z.string().optional(),
user_id: z.string().optional(),
event_type: z.string().optional(),
start_date: z.string().datetime().optional(),
end_date: z.string().datetime().optional(),
page: z.number().int().optional(),
limit: z.number().int().optional()
})
.optional()
})
/**
* Paginated cloud history events for the workspace
*/
export const zGetHistoryEventsResponse = zBillingEventsResponse
export const zReportHistoryEventData = z.object({
body: zHistoryEventRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* History event recorded successfully
*/
export const zReportHistoryEventResponse = zPartnerUsageResponse
export const zUpdateSubscriptionCacheData = z.object({
body: z.object({
user_id: z.string(),
@@ -3305,6 +3697,29 @@ export const zUpdateSubscriptionCacheResponse = z.object({
status: z.string().optional()
})
export const zListLinkedFirebaseUidsData = z.object({
body: zListLinkedFirebaseUidsRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* Linked Firebase UIDs (possibly empty list)
*/
export const zListLinkedFirebaseUidsResponse2 = zListLinkedFirebaseUidsResponse
export const zEnsureWorkspaceBillingProvisionedData = z.object({
body: zEnsureWorkspaceBillingProvisionedRequest,
path: z.never().optional(),
query: z.never().optional()
})
/**
* The workspace's provisioned billing identity
*/
export const zEnsureWorkspaceBillingProvisionedResponse2 =
zEnsureWorkspaceBillingProvisionedResponse
export const zInsertDynamicConfigData = z.object({
body: z.record(z.unknown()),
path: z.never().optional(),
@@ -4010,6 +4425,14 @@ export const zGetModelPreviewData = z.object({
query: z.never().optional()
})
export const zShortLinkRedirectData = z.object({
body: z.never().optional(),
path: z.object({
id: z.string()
}),
query: z.never().optional()
})
export const zGetLegacyPromptByIdData = z.object({
body: z.never().optional(),
path: z.object({
@@ -4070,14 +4493,23 @@ export const zGetLegacyUserdataV2Data = z.object({
query: z.never().optional()
})
export const zGetLegacyAssetContentData = z.object({
export const zGetAssetContentData = z.object({
body: z.never().optional(),
path: z.object({
id: z.string()
}),
query: z.never().optional()
query: z
.object({
disposition: z.enum(['inline', 'attachment']).optional()
})
.optional()
})
/**
* Asset content stream (local runtime streams the bytes directly)
*/
export const zGetAssetContentResponse = z.string()
export const zGetLegacyViewMetadataData = z.object({
body: z.never().optional(),
path: z.object({

View File

@@ -4,5 +4,5 @@
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*", "*.config.ts"]
"include": ["src/**/*"]
}

View File

@@ -4,5 +4,5 @@
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*", "vitest.config.ts"]
"include": ["src/**/*"]
}

View File

@@ -35,10 +35,10 @@
:class="
sidebarLocation === 'left'
? cn(
'side-bar-panel pointer-events-auto bg-comfy-menu-bg',
'side-bar-panel pointer-events-auto bg-comfy-menu-bg focus-visible:outline-hidden',
sidebarPanelVisible && 'min-w-78'
)
: 'pointer-events-auto bg-comfy-menu-bg'
: 'pointer-events-auto bg-comfy-menu-bg focus-visible:outline-hidden'
"
:min-size="
sidebarLocation === 'left' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
@@ -82,7 +82,7 @@
</SplitterPanel>
<SplitterPanel
v-show="bottomPanelVisible && !focusMode"
class="bottom-panel pointer-events-auto max-w-full overflow-x-auto rounded-lg border border-(--p-panel-border-color) bg-comfy-menu-bg"
class="bottom-panel pointer-events-auto max-w-full overflow-x-auto rounded-lg border border-(--p-panel-border-color) bg-comfy-menu-bg focus-visible:outline-hidden"
>
<slot name="bottom-panel" />
</SplitterPanel>
@@ -95,10 +95,10 @@
:class="
sidebarLocation === 'right'
? cn(
'side-bar-panel pointer-events-auto bg-comfy-menu-bg',
'side-bar-panel pointer-events-auto bg-comfy-menu-bg focus-visible:outline-hidden',
sidebarPanelVisible && 'min-w-78'
)
: 'pointer-events-auto bg-comfy-menu-bg'
: 'pointer-events-auto bg-comfy-menu-bg focus-visible:outline-hidden'
"
:min-size="
sidebarLocation === 'right' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE

View File

@@ -3,12 +3,16 @@ Preview Any - original implement from
https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
*/
import { whenever } from '@vueuse/core'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import { type DOMWidget } from '@/scripts/domWidget'
import { ComfyWidgets } from '@/scripts/widgets'
import { useExtensionService } from '@/services/extensionService'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
useExtensionService().registerExtension({
name: 'Comfy.PreviewAny',
@@ -75,6 +79,25 @@ useExtensionService().registerExtension({
showAsPlaintextWidget.widget.options.serialize = false
}
const applyValue = (node: LGraphNode, text: string | string[]) => {
const previewWidgets =
node.widgets?.filter((w) => w.name.startsWith('preview_')) ?? []
const value = Array.isArray(text) ? (text?.join('\n\n') ?? '') : text
for (const previewWidget of previewWidgets) previewWidget.value = value
}
nodeType.prototype.onGraphConfigured = useChainCallback(
nodeType.prototype.onGraphConfigured,
function (this: LGraphNode) {
const outputStore = useNodeOutputStore()
whenever(
() => outputStore.nodeOutputs[this.id],
(output) => applyValue(this, output.text ?? ''),
{ once: true }
)
}
)
const onExecuted = nodeType.prototype.onExecuted
nodeType.prototype.onExecuted = function (message) {
@@ -82,15 +105,7 @@ useExtensionService().registerExtension({
? void 0
: onExecuted.apply(this, [message])
const previewWidgets =
this.widgets?.filter((w) => w.name.startsWith('preview_')) ?? []
for (const previewWidget of previewWidgets) {
const text = message.text ?? ''
previewWidget.value = Array.isArray(text)
? (text?.join('\n\n') ?? '')
: text
}
applyValue(this, message.text ?? '')
}
}
}