Compare commits

..

33 Commits

Author SHA1 Message Date
DrJKL
ebb995eada chore: update pinia to v4 and @pinia/testing to v2
- useCoreCommands.test: build the setting-store mock with fromPartial;
  pinia 4's Store type gained required internal fields (_p, _hotUpdate,
  _hmrPayload) that a hand-rolled satisfies mock would have to fake
- useChart: hold the Chart instance in a shallowRef; deep-unwrapping
  chart.js's generics now exceeds TS instantiation depth, and a class
  instance updated via chart.update() should not be deeply reactive
2026-07-17 02:29:28 -07:00
DrJKL
5ffdaf4b46 chore: update typescript to 6.0.3
TypeScript 7 (native/tsgo) is blocked: vue-tsc crashes on the v7
package layout (ERR_PACKAGE_PATH_NOT_EXPORTED) and typescript-eslint
caps its peer range at <6.1.0. 6.0.3 is the last JS-based major and
passes the whole toolchain.

Cast typegpu's vendored GPUDevice to @webgpu/types' GPUDevice where
they disagree on adapterInfo.subgroupMaxSize optionality.
2026-07-17 02:03:04 -07:00
DrJKL
d28394e62b test: guard te() active-locale-only contract 2026-07-17 01:10:12 -07:00
DrJKL
8f94684c64 fix: fall back to command id for unlabeled keybinding rows
vue-i18n 11 returns the default message verbatim, so t(key, '') renders
an empty command cell for commands without a label or translation (v9
fell back to the key text). Default to command.id so the row stays
visible and clickable.
2026-07-17 00:50:10 -07:00
DrJKL
6ba4801663 fix: default te() to the active locale
vue-i18n 11's te() consults the fallback locale, which broke every
raw-fallback path built on the exported te (error catalog raw API copy,
st/stRaw custom-node strings, sidebar titles). Wrap the export to
preserve the v9 active-locale semantics instead of patching call sites,
so existing @/i18n test mocks keep working.
2026-07-17 00:18:31 -07:00
DrJKL
c9b5647773 fix: scope error catalog te() checks to the active locale
vue-i18n 11's te() consults the fallback locale, which defeated the
catalog's raw-API-copy fallback for locales missing a key.
2026-07-16 23:57:24 -07:00
DrJKL
0cea758dfa chore: restore screenshot expectations to main's bytes
The regenerated goldens were pixel-identical to main's (re-encoded
only); drop the churn to match the base branch.
2026-07-16 23:34:09 -07:00
DrJKL
7fd38c9144 Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 23:33:50 -07:00
DrJKL
b19d8d968f fix: clipboard copy and industry normalization regressions
- useCopyToClipboard: use navigator.clipboard.writeText directly.
  vueuse 14.3's useClipboard copy() no longer rejects on failure (it
  swallows errors into an internal legacy path gated on an async
  permission query), which bypassed our execCommand fallback and left
  the clipboard empty in the error-dialog e2e tests.
- Pin fuse.js to 7.0.0: 7.5 enforces minMatchCharLength strictly, so
  4-char queries like 'film' stopped matching survey industry
  categories. Retuning the fuzzy config for 7.5 is follow-up work.
2026-07-16 23:33:13 -07:00
DrJKL
04c14d98c5 ci: don't fail website screenshot regen on label cleanup
The restricted pull_request token can't always remove the trigger
label; that 403 was failing otherwise-successful runs.
2026-07-16 23:33:12 -07:00
DrJKL
ba2d7c5774 ci: bump website Playwright image to v1.61.1
Match @playwright/test 1.61.1 from the catalog; the stale v1.58.1 image
broke website-e2e and made screenshot regeneration a no-op.
2026-07-16 23:33:11 -07:00
DrJKL
5e2ce2099d Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 21:39:08 -07:00
DrJKL
a0d3379b98 fix: clipboard copy and industry normalization regressions
- useCopyToClipboard: use navigator.clipboard.writeText directly.
  vueuse 14.3's useClipboard copy() no longer rejects on failure (it
  swallows errors into an internal legacy path gated on an async
  permission query), which bypassed our execCommand fallback and left
  the clipboard empty in the error-dialog e2e tests.
- Pin fuse.js to 7.0.0: 7.5 enforces minMatchCharLength strictly, so
  4-char queries like 'film' stopped matching survey industry
  categories. Retuning the fuzzy config for 7.5 is follow-up work.
2026-07-16 21:23:21 -07:00
DrJKL
31fa8fe952 ci: don't fail website screenshot regen on label cleanup
The restricted pull_request token can't always remove the trigger
label; that 403 was failing otherwise-successful runs.
2026-07-16 20:59:59 -07:00
DrJKL
62ffad0c3c Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 20:58:06 -07:00
DrJKL
a0be6ac430 ci: bump website Playwright image to v1.61.1
Match @playwright/test 1.61.1 from the catalog; the stale v1.58.1 image
broke website-e2e and made screenshot regeneration a no-op.
2026-07-16 20:52:09 -07:00
DrJKL
d15ee851f0 [automated] Update test expectations 2026-07-16 20:45:22 -07:00
DrJKL
99faedfa81 fix: pin primevue family to 4.2.5
The lockfile regeneration floated primevue 4.2.5 -> 4.5.5, whose
@primeuix/styled 0.7.4 production bundle emits the dark-theme rule as
unmatchable nested CSS ('.dark-theme ... { & :root ... }'). Built apps
lost :root color-scheme: dark, flipping native form controls (e.g. the
legacy menu's Extra options checkbox) to light rendering. Dev builds use
the package's development bundle, which emits the flat selector - which
is why pnpm dev looked fine while pnpm preview did not.

Pin the family to main's resolved 4.2.5 until the upstream production
CSS generation is fixed.
2026-07-16 20:45:18 -07:00
DrJKL
5e7629560b Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 18:03:01 -07:00
DrJKL
2182166da0 Merge remote-tracking branch 'origin/main' into drjkl/update-deps
# Conflicts:
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
2026-07-16 18:02:26 -07:00
DrJKL
66e5d8c5f0 Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 17:58:27 -07:00
DrJKL
ecfd1f3816 fix: restore fallow tooling dropped in merge resolution
The branch merge of main accidentally reverted #13271 (fallow dep,
scripts, config, gitignore entry). Restore it from main.
2026-07-16 17:57:22 -07:00
Alexander Brown
f79f0e6066 Merge branch 'main' into drjkl/update-deps 2026-07-16 17:33:50 -07:00
Alexander Brown
c924d62f0c Update container target to v0.0.21 2026-07-17 00:33:20 +00:00
DrJKL
d833ffcc89 Merge branch 'drjkl/update-deps' into drjkl/update-vue-i18n 2026-07-16 17:13:33 -07:00
DrJKL
797fd0c78c chore: hold dompurify at 3.4.7 for happy-dom compat
DOMPurify >=3.4.8 changed its node iterator ('better template
scrubbing') in a way that silently strips legitimate tags (h1, etc.)
under happy-dom - real browsers and jsdom are unaffected. Pin to 3.4.7
(still ahead of main's 3.4.5, keeps the 3.4.6/3.4.7 DOM-clobbering
fixes) until the upstream incompatibility is resolved.
2026-07-16 17:09:44 -07:00
DrJKL
f6811a5f57 chore: update vue-i18n to v11
Two-major jump 9.14 -> 11.4.6. App is pure composition API
(legacy: false, no $tc/v-t), so no code changes required.
2026-07-16 16:33:41 -07:00
Alexander Brown
5e1f0ddd03 Merge branch 'main' into drjkl/update-deps 2026-07-16 15:25:23 -07:00
DrJKL
4adf9bdda5 ci: update e2e container to comfyui-ci-container 0.0.19
Matches the Playwright 1.61 bump in this PR.
2026-07-16 15:22:15 -07:00
DrJKL
0e360677cf chore: respect minimumReleaseAge instead of exempting fresh releases
Drop the auto-added minimumReleaseAgeExclude block and lower range floors
so resolution picks the newest age-compliant versions: vue 3.5.39,
vite 8.1.4, tailwindcss 4.3.2, storybook 10.5.0. The <24h releases flow
in automatically once they age past the gate.
2026-07-16 15:10:06 -07:00
DrJKL
3f656030f7 pnpm dedupe 2026-07-16 14:49:04 -07:00
DrJKL
08c34f2680 chore: reformat with oxfmt 0.59 2026-07-16 14:39:58 -07:00
DrJKL
935142428d chore: update core dev dependencies
- vue 3.5.40, vite 8.1.5, oxlint 1.74, oxfmt 0.59, eslint 10.7,
  vitest 4.1.10, vue-tsc 3.3.7, tailwindcss 4.3.3, playwright 1.61.1,
  storybook 10.5.2, knip 6.27, typescript-eslint 8.64 + ecosystem minors
- pnpm 11.3.0 -> 11.13.1 (packageManager)
- happy-dom held at 20.9.0: 20.10.x throws NotSupportedError on external
  script append, breaking GTM/Typeform loader tests
- fix defineModel default typing for vue 3.5.40 (default factory param
  is now Record<string, unknown>)
- fix useClipboard mock for vueuse 14.3 (ComputedRef fields + copyPending)
- drop knip ignores obsoleted by knip 6.27
- rename invalid CSS ident --color-dark-elevation-1.5 (dot) to -1-5;
  oxfmt 0.59 hard-errors on it
2026-07-16 14:37:52 -07:00
57 changed files with 5240 additions and 5381 deletions

View File

@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.21
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.21
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -121,7 +121,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.17
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.21
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -28,7 +28,7 @@ jobs:
if: ${{ needs.changes.outputs.app-website-changes == 'true' || needs.changes.outputs.packages-changes == 'true' }}
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
image: mcr.microsoft.com/playwright:v1.61.1-noble
timeout-minutes: 15
permissions:
contents: read

View File

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

View File

@@ -14,7 +14,7 @@ jobs:
update-screenshots:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.58.1-noble
image: mcr.microsoft.com/playwright:v1.61.1-noble
timeout-minutes: 15
permissions:
contents: write
@@ -128,6 +128,9 @@ jobs:
- name: Remove label
if: always() && github.event_name == 'pull_request'
# Label cleanup is best-effort; the restricted pull_request token may
# lack permission to remove labels and must not fail the run.
continue-on-error: true
uses: actions/github-script@v8
env:
PR_NUMBER: ${{ steps.pr-info.outputs.pr-number }}

View File

@@ -8,13 +8,11 @@ export class ComfyActionbar {
public readonly root: Locator
public readonly queueButton: ComfyQueueButton
public readonly propertiesButton: Locator
public readonly dragHandle: Locator
constructor(public readonly page: Page) {
this.root = page.locator('.actionbar-container')
this.queueButton = new ComfyQueueButton(this)
this.propertiesButton = this.root.getByLabel('Toggle properties panel')
this.dragHandle = this.root.locator('.drag-handle')
}
async isDocked() {

View File

@@ -1,21 +0,0 @@
import type { Locator } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
export class FreeTierQuota {
readonly root: Locator
constructor(comfyPage: ComfyPage) {
this.root = comfyPage.page.getByTestId(TestIds.topbar.freeTierQuota)
}
async getMax() {
const text = await this.root.textContent()
return text?.match(/(\d+) \/ (\d+)/)?.[2]
}
async getAvailable() {
const text = await this.root.textContent()
return text?.match(/(\d+) \/ (\d+)/)?.[1]
}
}

View File

@@ -17,9 +17,7 @@ import { mockSystemStats } from '../fixtures/data/systemStats'
// Extend the base set with test-specific nodes
const nodeDefs = createMockNodeDefinitions({
MyCustomNode: {
/* ... */
}
MyCustomNode: {/* ... */}
})
await page.route('**/api/object_info', (route) =>

View File

@@ -103,8 +103,7 @@ export const TestIds = {
loginButtonPopoverLearnMore: 'login-button-popover-learn-more',
workflowTabs: 'topbar-workflow-tabs',
integratedTabBarActions: 'integrated-tab-bar-actions',
actionBarButtons: 'action-bar-buttons',
freeTierQuota: 'free-tier-quota'
actionBarButtons: 'action-bar-buttons'
},
nodeLibrary: {
bookmarksSection: 'node-library-bookmarks-section'

View File

@@ -1,63 +0,0 @@
import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { jsonRoute } from '@e2e/fixtures/utils/jsonRoute'
import { FreeTierQuota } from '@e2e/fixtures/components/FreeTierQuota'
import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper'
import { webSocketFixture } from '@e2e/fixtures/ws'
const wstest = mergeTests(test, webSocketFixture)
test.describe('Free Tier Quota', { tag: ['@cloud', '@vue-nodes'] }, () => {
test.beforeEach(async ({ page }) => {
const features = {
free_tier_job_allowance_enabled: true,
free_tier_balance: { allowance: 5, remaining: 3, used: 0 }
}
await page.route('**/api/features', (r) => r.fulfill(jsonRoute(features)))
})
wstest('Free Tier Quota', async ({ comfyPage, comfyMouse, getWebSocket }) => {
const execution = new ExecutionHelper(comfyPage, await getWebSocket())
const freeTierQuota = new FreeTierQuota(comfyPage)
await test.step('Populates initial state from config', async () => {
await expect.poll(() => freeTierQuota.getAvailable()).toBe('3')
expect(await freeTierQuota.getMax()).toBe('5')
})
await test.step('available decrements on run', async () => {
await execution.run()
await expect.poll(() => freeTierQuota.getAvailable()).toBe('2')
})
await test.step('connects to detached run button', async () => {
const handle = comfyPage.actionbar.dragHandle
await comfyMouse.dragElementBy(handle, { x: -100, y: 100 })
await expect.poll(() => comfyPage.actionbar.isDocked()).toBe(false)
expect(await freeTierQuota.getAvailable()).toBe('2')
await comfyMouse.dragElementBy(handle, { x: 100, y: -100 })
await expect.poll(() => comfyPage.actionbar.isDocked()).toBe(true)
})
await test.step('Detects workflows with Partner nodes', async () => {
await comfyPage.searchBoxV2.addNode('Node With Price Badge')
const node = await comfyPage.vueNodes.getFixtureByTitle('Price Badge')
await expect.poll(() => freeTierQuota.getAvailable()).toBe(undefined)
await node.delete()
await expect.poll(() => freeTierQuota.getAvailable()).toBe('2')
})
await test.step('Does not decrease past 0', async () => {
await execution.run()
await expect.poll(() => freeTierQuota.getAvailable()).toBe('1')
await execution.run()
await expect.poll(() => freeTierQuota.getAvailable()).toBe(undefined)
await execution.run()
await execution.run()
await execution.run()
await comfyPage.nextFrame()
expect(await freeTierQuota.getAvailable()).toBe(undefined)
})
})
})

View File

@@ -31,9 +31,7 @@ export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
/* props */
}
args: {/* props */}
}
```

View File

@@ -37,10 +37,8 @@ const config: KnipConfig = {
entry: ['src/scripts/**/*.ts']
}
},
ignoreBinaries: ['python3'],
ignoreDependencies: [
// Weird importmap things
'@iconify-json/lucide',
'@iconify/json',
'@primeuix/forms',
'@primeuix/styled',

View File

@@ -97,7 +97,7 @@
"@vee-validate/zod": "catalog:",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@vueuse/router": "^14.2.0",
"@vueuse/router": "^14.3.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",
@@ -111,7 +111,7 @@
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"fuse.js": "7.0.0",
"glob": "catalog:",
"jsonata": "catalog:",
"loglevel": "^1.9.2",
@@ -219,5 +219,5 @@
"node": ">=25 <26",
"pnpm": ">=11.3"
},
"packageManager": "pnpm@11.3.0"
"packageManager": "pnpm@11.13.1"
}

View File

@@ -78,9 +78,9 @@
--color-interface-panel-job-progress-border: var(--base-foreground);
--color-blue-selection: rgb(from var(--color-azure-600) r g b / 0.3);
--color-node-hover-100: rgb(from var(--color-charcoal-800) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-smoke-400) r g b/ 0.4);
--color-node-hover-100: rgb(from var(--color-charcoal-800) r g b / 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b / 0.1);
--color-modal-tag: rgb(from var(--color-smoke-400) r g b / 0.4);
--color-alpha-charcoal-600-30: color-mix(
in srgb,
var(--color-charcoal-600) 30%,
@@ -107,7 +107,7 @@
--color-highlight: var(--p-primary-color);
/* Special Colors (temporary) */
--color-dark-elevation-1.5: rgba(from white r g b/ 0.015);
--color-dark-elevation-1-5: rgba(from white r g b / 0.015);
--color-dark-elevation-2: rgba(from white r g b / 0.03);
}
@@ -1873,9 +1873,9 @@ audio.comfy-audio.empty-audio-widget {
/* Shadow Cover TOP */
linear-gradient(--value(--color-*) 30%, transparent) center top,
/* Shadow Cover BOTTOM */
linear-gradient(transparent, --value(--color-*) 70%) center bottom,
linear-gradient(transparent, --value(--color-*) 70%) center bottom,
/* Shadow TOP */
radial-gradient(
radial-gradient(
farthest-side at 50% 0,
color-mix(in oklab, --value(--color-*), #777777 35%),
60%,
@@ -1883,7 +1883,7 @@ audio.comfy-audio.empty-audio-widget {
)
center top,
/* Shadow BOTTOM */
radial-gradient(
radial-gradient(
farthest-side at 50% 100%,
color-mix(in oklab, --value(--color-*), #777777 35%),
60%,

9653
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,26 +24,26 @@ catalog:
'@iconify/tailwind4': ^1.2.3
'@iconify/tools': ^5.0.3
'@iconify/utils': ^3.1.0
'@intlify/eslint-plugin-vue-i18n': ^4.5.0
'@intlify/eslint-plugin-vue-i18n': ^4.5.1
'@lobehub/i18n-cli': ^1.26.1
'@lucide/vue': ^1.17.0
'@pinia/testing': ^1.0.3
'@playwright/test': ^1.58.1
'@pinia/testing': ^2.0.1
'@playwright/test': ^1.61.1
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
'@primevue/core': ^4.2.5
'@primevue/forms': ^4.2.5
'@primevue/core': 4.2.5
'@primevue/forms': 4.2.5
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
'@primevue/themes': 4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^10.32.1
'@sparkjsdev/spark': ^2.1.0
'@storybook/addon-docs': ^10.2.10
'@storybook/addon-docs': ^10.5.0
'@storybook/addon-mcp': 0.1.6
'@storybook/vue3': ^10.2.10
'@storybook/vue3-vite': ^10.2.10
'@tailwindcss/vite': ^4.3.0
'@storybook/vue3': ^10.5.0
'@storybook/vue3-vite': ^10.5.0
'@tailwindcss/vite': ^4.3.2
'@tanstack/vue-virtual': ^3.13.12
'@testing-library/jest-dom': ^6.9.1
'@testing-library/user-event': ^14.6.1
@@ -64,11 +64,11 @@ catalog:
'@types/three': ^0.184.1
'@vee-validate/zod': ^4.15.1
'@vercel/analytics': ^2.0.1
'@vitejs/plugin-vue': ^6.0.0
'@vitest/coverage-v8': ^4.0.16
'@vitest/ui': ^4.0.16
'@vueuse/core': ^14.2.0
'@vueuse/integrations': ^14.2.0
'@vitejs/plugin-vue': ^6.0.8
'@vitest/coverage-v8': ^4.1.10
'@vitest/ui': ^4.1.10
'@vueuse/core': ^14.3.0
'@vueuse/integrations': ^14.3.0
'@webgpu/types': ^0.1.66
algoliasearch: ^5.21.0
astro: ^6.4.2
@@ -76,75 +76,75 @@ catalog:
class-variance-authority: ^0.7.1
cross-env: ^10.1.0
cva: 1.0.0-beta.4
dompurify: ^3.4.5
dompurify: 3.4.7
dotenv: ^16.4.5
eslint: ^10.4.0
eslint: ^10.7.0
eslint-config-prettier: ^10.1.8
eslint-import-resolver-typescript: ^4.4.4
eslint-plugin-better-tailwindcss: ^4.3.1
eslint-plugin-import-x: ^4.16.2
eslint-plugin-oxlint: 1.69.0
eslint-plugin-playwright: ^2.10.1
eslint-plugin-storybook: ^10.2.10
eslint-plugin-testing-library: ^7.16.1
eslint-import-resolver-typescript: ^4.4.5
eslint-plugin-better-tailwindcss: ^4.6.1
eslint-plugin-import-x: ^4.17.1
eslint-plugin-oxlint: 1.73.0
eslint-plugin-playwright: ^2.10.5
eslint-plugin-storybook: ^10.5.0
eslint-plugin-testing-library: ^7.16.2
eslint-plugin-unused-imports: ^4.4.1
eslint-plugin-vue: ^10.9.1
eslint-plugin-vue: ^10.9.2
fallow: ^2.102.0
fast-check: ^4.5.3
firebase: ^11.6.0
glob: ^13.0.6
globals: ^16.5.0
gsap: ^3.14.2
happy-dom: ^20.8.9
happy-dom: 20.9.0
husky: ^9.1.7
jiti: 2.6.1
jiti: 2.7.0
jsdom: ^27.4.0
jsonata: ^2.1.0
knip: ^6.15.0
knip: ^6.27.0
lenis: ^1.3.21
lint-staged: ^16.2.7
markdown-table: ^3.0.4
minisearch: ^7.2.0
mixpanel-browser: ^2.71.0
monocart-coverage-reports: ^2.12.9
oxfmt: ^0.54.0
oxlint: ^1.69.0
oxlint-tsgolint: ^0.23.0
oxfmt: ^0.59.0
oxlint: ^1.74.0
oxlint-tsgolint: ^0.24.0
picocolors: ^1.1.1
pinia: ^3.0.4
pinia: ^4.0.2
postcss-html: ^1.8.0
posthog-js: ^1.358.1
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5
primevue: 4.2.5
reka-ui: 2.5.0
rollup-plugin-visualizer: ^6.0.4
storybook: ^10.2.10
storybook: ^10.5.0
stylelint: ^16.26.1
tailwindcss: ^4.3.0
tailwindcss: ^4.3.2
tailwindcss-primeui: ^0.6.1
three: ^0.184.0
tsx: ^4.15.6
tw-animate-css: ^1.3.8
typegpu: ^0.8.2
typescript: ^5.9.3
typescript-eslint: ^8.60.0
typescript: ^6.0.3
typescript-eslint: ^8.64.0
unplugin-icons: ^22.5.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^30.0.0
uuid: ^11.1.1
vee-validate: ^4.15.1
vite: ^8.0.13
vite: ^8.1.4
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^8.0.0
vitest: ^4.1.0
vue: ^3.5.34
vue-component-type-helpers: ^3.2.1
vue-eslint-parser: ^10.4.0
vue-i18n: ^9.14.5
vite-plugin-vue-devtools: ^8.1.5
vitest: ^4.1.10
vue: ^3.5.39
vue-component-type-helpers: ^3.3.7
vue-eslint-parser: ^10.4.1
vue-i18n: ^11.4.6
vue-router: ^4.4.3
vue-tsc: ^3.2.5
vue-tsc: ^3.3.7
vuefire: ^3.2.1
wwobjloader2: ^6.2.1
yjs: ^13.6.27

View File

@@ -32,11 +32,11 @@ let _runWhenIdle: (
* Falls back to setTimeout-based scheduling in browsers without native support.
*/
export let runWhenGlobalIdle: (
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
// Self-invoking function to set up the idle callback implementation
// Self-invoking function to set up the idle callback implementation
;(function () {
const safeGlobal: GlobalWindow = globalThis as GlobalWindow

View File

@@ -629,7 +629,7 @@ describe('TopMenuSection', () => {
await nextTick()
expect(querySpy).toHaveBeenCalledTimes(1)
expect(actionbarContainer!.classList).not.toContain('w-0')
expect(actionbarContainer!.classList).toContain('px-2')
} finally {
unmount()
vi.unstubAllGlobals()

View File

@@ -11,7 +11,7 @@
</div>
<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-start gap-2">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value || isCloud"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
@@ -34,75 +34,61 @@
</Button>
</div>
<div
class="pointer-events-auto z-1 flex flex-col rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 py-1.75 shadow-interface"
>
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="actionbarContainerRef"
:class="
cn(
'actionbar-container relative flex items-center gap-2',
isActionbarContainerEmpty &&
'-ml-2 w-0 min-w-0 border-transparent shadow-none has-[.border-dashed]:ml-0 has-[.border-dashed]:w-auto has-[.border-dashed]:min-w-auto has-[.border-dashed]:border-interface-stroke has-[.border-dashed]:pl-2 has-[.border-dashed]:shadow-interface'
)
"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
data-testid="legacy-topbar-container"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
ref="legacyCommandsContainerRef"
data-testid="legacy-topbar-container"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
@update:progress-target="updateProgressTarget"
/>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<ComfyActionbar
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
@update:progress-target="updateProgressTarget"
/>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.bottom="shareTooltipConfig"
variant="secondary"
:aria-label="t('actionbar.shareTooltip')"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
>
<i class="icon-[comfy--send] size-4" />
<span class="not-md:hidden">
{{ t('actionbar.share') }}
</span>
</Button>
<div v-if="!isRightSidePanelOpen" class="relative">
<Button
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.bottom="shareTooltipConfig"
v-tooltip.bottom="rightSidePanelTooltipConfig"
:class="
cn(
showErrorIndicatorOnPanelButton &&
'outline-1 outline-destructive-background'
)
"
variant="secondary"
:aria-label="t('actionbar.shareTooltip')"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="openRightSidePanel"
>
<i class="icon-[comfy--send] size-4" />
<span class="not-md:hidden">
{{ t('actionbar.share') }}
</span>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<div v-if="!isRightSidePanelOpen" class="relative">
<Button
v-tooltip.bottom="rightSidePanelTooltipConfig"
:class="
cn(
showErrorIndicatorOnPanelButton &&
'outline-1 outline-destructive-background'
)
"
variant="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="openRightSidePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<StatusBadge
v-if="showErrorIndicatorOnPanelButton"
variant="dot"
severity="danger"
class="absolute -top-1 -right-1"
/>
</div>
<StatusBadge
v-if="showErrorIndicatorOnPanelButton"
variant="dot"
severity="danger"
class="absolute -top-1 -right-1"
/>
</div>
<FreeTierQuota v-if="!isActionbarFloating" />
</div>
</div>
<ErrorOverlay />
@@ -161,7 +147,6 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
@@ -224,6 +209,21 @@ const hasDockedButtons = computed(() => {
const isActionbarContainerEmpty = computed(
() => isActionbarFloating.value && !hasDockedButtons.value
)
const actionbarContainerClass = computed(() => {
const base =
'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg shadow-interface'
if (isActionbarContainerEmpty.value) {
return cn(
base,
'-ml-2 w-0 min-w-0 border-transparent shadow-none',
'has-[.border-dashed]:ml-0 has-[.border-dashed]:w-auto has-[.border-dashed]:min-w-auto',
'has-[.border-dashed]:border-interface-stroke has-[.border-dashed]:pl-2 has-[.border-dashed]:shadow-interface'
)
}
return cn(base, 'px-2', 'border-interface-stroke')
})
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
)

View File

@@ -75,7 +75,6 @@
</Button>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
</div>
<FreeTierQuota v-if="!isDocked" />
</Panel>
<Teleport v-if="inlineProgressTarget" :to="inlineProgressTarget">
@@ -110,7 +109,6 @@ import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'

View File

@@ -4,11 +4,11 @@ import { nextTick, ref } from 'vue'
import CloudRunButtonWrapper from './CloudRunButtonWrapper.vue'
const mockCanRunWorkflows = ref(true)
const mockIsActiveSubscription = ref(true)
vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
canRunWorkflows: mockCanRunWorkflows
isActiveSubscription: mockIsActiveSubscription
})
}))
@@ -32,7 +32,7 @@ function renderWrapper() {
describe('CloudRunButtonWrapper', () => {
beforeEach(() => {
mockCanRunWorkflows.value = true
mockIsActiveSubscription.value = true
})
it('renders the runnable queue button when the subscription is active', () => {
@@ -45,7 +45,7 @@ describe('CloudRunButtonWrapper', () => {
})
it('locks the run button when the subscription is inactive', () => {
mockCanRunWorkflows.value = false
mockIsActiveSubscription.value = false
renderWrapper()
expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument()
@@ -53,12 +53,12 @@ describe('CloudRunButtonWrapper', () => {
})
it('unlocks the run button once the subscription becomes active again', async () => {
mockCanRunWorkflows.value = false
mockIsActiveSubscription.value = false
renderWrapper()
expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument()
mockCanRunWorkflows.value = true
mockIsActiveSubscription.value = true
await nextTick()
expect(screen.getByTestId('queue-button')).toBeInTheDocument()

View File

@@ -1,7 +1,7 @@
<template>
<component
:is="currentButton"
:key="canRunWorkflows ? 'queue' : 'subscribe'"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
</template>
<script setup lang="ts">
@@ -11,9 +11,9 @@ import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueBu
import { useBillingContext } from '@/composables/billing/useBillingContext'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
const { canRunWorkflows } = useBillingContext()
const { isActiveSubscription } = useBillingContext()
const currentButton = computed(() =>
canRunWorkflows.value ? ComfyQueueButton : SubscribeToRunButton
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>

View File

@@ -429,7 +429,7 @@ const commandsData = computed<ICommandData[]>(() => {
id: command.id,
label: t(
`commands.${normalizeI18nKey(command.id)}.label`,
command.label ?? ''
command.label ?? command.id
),
keybindings: keybindingStore.getKeybindingsByCommandId(command.id),
source: command.source,

View File

@@ -12,7 +12,7 @@ import {
PointElement,
Tooltip
} from 'chart.js'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { Ref } from 'vue'
@@ -129,7 +129,7 @@ export function useChart(
data: Ref<ChartData>,
options?: Ref<ChartOptions | undefined>
) {
const chartInstance = ref<Chart | null>(null)
const chartInstance = shallowRef<Chart | null>(null)
function createChart() {
if (!canvasRef.value) return

View File

@@ -120,5 +120,4 @@ export interface BillingContext extends BillingState, BillingActions {
*/
isTeamPlan: ComputedRef<boolean>
getMaxSeats: (tierKey: TierKey) => number
canRunWorkflows: ComputedRef<boolean>
}

View File

@@ -6,7 +6,6 @@ import {
getTierFeatures
} from '@/platform/cloud/subscription/constants/tierPricing'
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import { useFreeTierQuota } from '@/platform/cloud/subscription/composables/useFreeTierQuota'
import type { SubscriptionDialogOptions } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import type {
PreviewSubscribeOptions,
@@ -130,16 +129,6 @@ function useBillingContextInternal(): BillingContext {
const isFreeTier = computed(() => subscription.value?.tier === 'FREE')
const freeTierQuota = useFreeTierQuota()
const canRunWorkflows = computed(
() =>
isActiveSubscription.value &&
(!isFreeTier.value ||
!freeTierQuota.quotaEnabled.value ||
freeTierQuota.freeTierExecutionPermitted.value)
)
const isLegacyTeamPlan = computed(
() =>
type.value === 'workspace' &&
@@ -323,7 +312,6 @@ function useBillingContextInternal(): BillingContext {
isLoading,
error,
isActiveSubscription,
canRunWorkflows,
isFreeTier,
isLegacyTeamPlan,
isTeamPlan,

View File

@@ -207,7 +207,8 @@ export function useGPUResources() {
async function initTypeGPU(): Promise<void> {
if (store.tgpuRoot) {
/* c8 ignore start */
device = store.tgpuRoot.device
// typegpu vendors its own WebGPU types; align with @webgpu/types
device = store.tgpuRoot.device as GPUDevice
return
/* c8 ignore stop */
}
@@ -215,7 +216,7 @@ export function useGPUResources() {
/* c8 ignore start — requires functional WebGPU hardware */
const root = await tgpu.init()
store.tgpuRoot = root
device = root.device
device = root.device as GPUDevice
console.warn('✅ TypeGPU initialized! Root:', root)
console.warn('Device info:', root.device.limits)
/* c8 ignore stop */

View File

@@ -1,19 +1,12 @@
import { createSharedComposable } from '@vueuse/core'
import { computed, toValue } from 'vue'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useNodePricing } from '@/composables/node/useNodePricing'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import { trackNodePrice } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges'
import { app } from '@/scripts/app'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { mapAllNodes } from '@/utils/graphTraversalUtil'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
type LinkedWidgetInput = INodeInputSlot & {
_subgraphSlot?: SubgraphInput
@@ -157,20 +150,3 @@ export const usePriceBadge = () => {
updateSubgraphCredits
}
}
export const useCreditsBadgesInGraph = createSharedComposable(() => {
const { isCreditsBadge } = usePriceBadge()
const vueNodeLifecycle = useVueNodeLifecycle()
return computed(() => {
void vueNodeLifecycle.nodeManager.value?.vueNodeData.size
if (!app.graph) return []
return mapAllNodes(app.graph, (node) => {
if (node.isSubgraphNode()) return
const priceBadge = node.badges.find(isCreditsBadge)
if (!priceBadge) return
trackNodePrice(node)
return [node.title, toValue(priceBadge).text, node.id] as const
})
})
})

View File

@@ -1,17 +1,8 @@
import { computed, ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockCopy = vi.fn()
const mockWriteText = vi.fn()
const mockToastAdd = vi.fn()
vi.mock('@vueuse/core', () => ({
useClipboard: vi.fn(() => ({
copy: mockCopy,
copied: ref(false),
isSupported: computed(() => true)
}))
}))
vi.mock('primevue/usetoast', () => ({
useToast: vi.fn(() => ({
add: mockToastAdd
@@ -22,34 +13,31 @@ vi.mock('@/i18n', () => ({
t: (key: string) => key
}))
import { useClipboard } from '@vueuse/core'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
describe('useCopyToClipboard', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.mocked(useClipboard).mockReturnValue({
copy: mockCopy,
copied: ref(false),
isSupported: computed(() => true),
text: ref('')
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText: mockWriteText }
})
})
it('shows success toast when modern clipboard succeeds', async () => {
mockCopy.mockResolvedValue(undefined)
mockWriteText.mockResolvedValue(undefined)
const { copyToClipboard } = useCopyToClipboard()
await copyToClipboard('hello')
expect(mockCopy).toHaveBeenCalledWith('hello')
expect(mockWriteText).toHaveBeenCalledWith('hello')
expect(mockToastAdd).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'success' })
)
})
it('falls back to legacy when modern clipboard fails', async () => {
mockCopy.mockRejectedValue(new Error('Not allowed'))
mockWriteText.mockRejectedValue(new Error('Not allowed'))
document.execCommand = vi.fn(() => true)
const { copyToClipboard } = useCopyToClipboard()
@@ -62,7 +50,7 @@ describe('useCopyToClipboard', () => {
})
it('shows error toast when both modern and legacy fail', async () => {
mockCopy.mockRejectedValue(new Error('Not allowed'))
mockWriteText.mockRejectedValue(new Error('Not allowed'))
document.execCommand = vi.fn(() => false)
const { copyToClipboard } = useCopyToClipboard()
@@ -73,19 +61,17 @@ describe('useCopyToClipboard', () => {
)
})
it('falls through to legacy when isSupported is false', async () => {
vi.mocked(useClipboard).mockReturnValue({
copy: mockCopy,
copied: ref(false),
isSupported: computed(() => false),
text: ref('')
it('falls through to legacy when clipboard API is unavailable', async () => {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: undefined
})
document.execCommand = vi.fn(() => true)
const { copyToClipboard } = useCopyToClipboard()
await copyToClipboard('hello')
expect(mockCopy).not.toHaveBeenCalled()
expect(mockWriteText).not.toHaveBeenCalled()
expect(document.execCommand).toHaveBeenCalledWith('copy')
expect(mockToastAdd).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'success' })

View File

@@ -1,4 +1,3 @@
import { useClipboard } from '@vueuse/core'
import { useToast } from 'primevue/usetoast'
import { t } from '@/i18n'
@@ -20,15 +19,14 @@ function legacyCopy(text: string): boolean {
}
export function useCopyToClipboard() {
const { copy, isSupported } = useClipboard()
const toast = useToast()
async function copyToClipboard(text: string) {
let success = false
try {
if (isSupported.value) {
await copy(text)
if (navigator.clipboard) {
await navigator.clipboard.writeText(text)
success = true
}
} catch {

View File

@@ -260,7 +260,7 @@ describe('useCoreCommands', () => {
function createMockSettingStore(
getReturnValue: boolean
): ReturnType<typeof useSettingStore> {
return {
return fromPartial<ReturnType<typeof useSettingStore>>({
get: vi.fn().mockReturnValue(getReturnValue),
addSetting: vi.fn(),
load: vi.fn(),
@@ -287,7 +287,7 @@ describe('useCoreCommands', () => {
$onAction: vi.fn(),
$dispose: vi.fn(),
_customProperties: new Set()
} satisfies ReturnType<typeof useSettingStore>
})
}
beforeEach(() => {

View File

@@ -33,7 +33,6 @@ export enum ServerFeatureFlag {
SHOW_SIGNIN_BUTTON = 'show_signin_button',
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth',
BILLING_CONTROL_ENABLED = 'billing_control_enabled',
FREE_TIER_JOB_ALLOWANCE_ENABLED = 'free_tier_job_allowance_enabled',
SIGNUP_TURNSTILE = 'signup_turnstile'
}
@@ -203,16 +202,6 @@ export function useFeatureFlags() {
cachedBillingControlEnabled
)
},
get freeTierJobAllowanceEnabled() {
const config = remoteConfig.value as typeof remoteConfig.value & {
free_tier_job_allowance_enabled?: boolean
}
return resolveFlag(
ServerFeatureFlag.FREE_TIER_JOB_ALLOWANCE_ENABLED,
config.free_tier_job_allowance_enabled,
false
)
},
get signupTurnstileMode() {
return resolveFlag(
ServerFeatureFlag.SIGNUP_TURNSTILE,

View File

@@ -7,6 +7,7 @@ let loadLocale: typeof I18nModule.loadLocale
let mergeCustomNodesI18n: typeof I18nModule.mergeCustomNodesI18n
let resolveSupportedLocale: typeof I18nModule.resolveSupportedLocale
let setActiveLocale: typeof I18nModule.setActiveLocale
let te: typeof I18nModule.te
async function importI18nModule() {
const i18nModule = await import('./i18n')
@@ -15,10 +16,13 @@ async function importI18nModule() {
mergeCustomNodesI18n = i18nModule.mergeCustomNodesI18n
resolveSupportedLocale = i18nModule.resolveSupportedLocale
setActiveLocale = i18nModule.setActiveLocale
te = i18nModule.te
}
// Mock the JSON imports before importing i18n module
vi.mock('./locales/en/main.json', () => ({ default: { welcome: 'Welcome' } }))
vi.mock('./locales/en/main.json', () => ({
default: { welcome: 'Welcome', enOnly: 'English only' }
}))
vi.mock('./locales/en/nodeDefs.json', () => ({
default: { testNode: 'Test Node' }
}))
@@ -228,6 +232,16 @@ describe('i18n', () => {
})
})
describe('te', () => {
it('checks only the active locale, never the fallback locale', async () => {
expect(te('enOnly')).toBe(true)
await setActiveLocale('zh')
expect(te('welcome')).toBe(true)
expect(te('enOnly')).toBe(false)
})
})
describe('resolveSupportedLocale', () => {
it('returns the canonical tag when the input is shipped', () => {
expect(resolveSupportedLocale('en')).toBe('en')

View File

@@ -155,7 +155,10 @@ export const i18n = createI18n({
/** Convenience shorthand: i18n.global */
export const t: (typeof i18n.global)['t'] = i18n.global.t
export const te: (typeof i18n.global)['te'] = i18n.global.te
// vue-i18n 11's te() consults the fallback locale; default to the active
// locale to preserve the v9 behavior our fallback paths rely on.
export const te: (typeof i18n.global)['te'] = (key, locale) =>
i18n.global.te(key, locale ?? i18n.global.locale.value)
export const d: (typeof i18n.global)['d'] = i18n.global.d
const tm = i18n.global.tm

View File

@@ -3587,9 +3587,6 @@
"dockToTop": "Dock to top",
"feedback": "Feedback",
"feedbackTooltip": "Feedback",
"freeTierRuns": "{available} / {MAX_AVAILABLE} runs left",
"freeTierRunsExhausted": "No runs left",
"freeTierPartner": "Partner nodes need a paid plan",
"share": "Share",
"shareTooltip": "Share workflow"
},

View File

@@ -19,7 +19,9 @@ export const hasNonEmptyValue = (
return true
}
export const isOtherValue = (current: string | string[] | undefined): boolean =>
export const isOtherValue = (
current: string | string[] | undefined
): boolean =>
Array.isArray(current) ? current.includes('other') : current === 'other'
const conditionMatches = (

View File

@@ -1,65 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { cn } from '@comfyorg/tailwind-utils'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useFreeTierQuota } from '@/platform/cloud/subscription/composables/useFreeTierQuota'
const DOT_COLORS = [
'bg-destructive-background',
'bg-warning-background',
'bg-success-background'
]
const { showSubscriptionDialog } = useBillingContext()
const { t } = useI18n()
const { available, hasInvalidNodes, maxAvailable, quotaEnabled } =
useFreeTierQuota()
const dotColor = computed(() => {
const ratio = maxAvailable.value ? available.value / maxAvailable.value : 0
return DOT_COLORS[
Math.min(Math.floor(ratio * DOT_COLORS.length), DOT_COLORS.length - 1)
]
})
const label = computed(() =>
available.value === 0
? t('actionbar.freeTierRunsExhausted')
: t('actionbar.freeTierRuns', {
available: available.value,
MAX_AVAILABLE: maxAvailable.value
})
)
</script>
<template>
<div
v-if="quotaEnabled"
class="mt-2 w-full cursor-pointer border-t border-border-subtle bg-comfy-menu-bg px-4 pt-2 select-none"
data-testid="free-tier-quota"
@click="showSubscriptionDialog({ reason: 'free_tier_quota' })"
>
<div
v-if="hasInvalidNodes"
class="flex w-full items-center justify-center gap-2"
>
<i class="icon-[comfy--credits] bg-amber-400" />
{{ t('actionbar.freeTierPartner') }}
</div>
<div v-else class="flex w-full items-center justify-between">
<div class="flex gap-2" :aria-label="label" role="img">
<div
v-for="index in maxAvailable"
:key="index"
:class="
cn(
'size-1.5 rounded-full',
index > available ? 'bg-secondary-background-selected' : dotColor
)
"
/>
</div>
<div v-text="label" />
</div>
</div>
</template>

View File

@@ -1,45 +0,0 @@
import { createSharedComposable } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { useCreditsBadgesInGraph } from '@/composables/node/usePriceBadge'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
export const useFreeTierQuota = createSharedComposable(function () {
const { flags } = useFeatureFlags()
const creditsBadges = useCreditsBadgesInGraph()
const available = ref(0)
const maxAvailable = ref(0)
watch(
() => remoteConfig.value.free_tier_balance?.remaining,
(val) => (available.value = val ?? 0),
{ immediate: true }
)
watch(
() => remoteConfig.value.free_tier_balance?.allowance,
(val) => (maxAvailable.value = val ?? 0),
{ immediate: true }
)
const quotaEnabled = computed(
() => flags.freeTierJobAllowanceEnabled && maxAvailable.value > 0
)
const hasInvalidNodes = computed(() => creditsBadges.value.length > 0)
const freeTierExecutionPermitted = computed(
() => !hasInvalidNodes.value && quotaEnabled.value && available.value > 0
)
function trackRun() {
if (available.value > 0) available.value--
}
return {
available,
freeTierExecutionPermitted,
hasInvalidNodes,
maxAvailable,
quotaEnabled,
trackRun
}
})

View File

@@ -110,11 +110,6 @@ export type RemoteConfig = {
user_secrets_enabled?: boolean
node_library_essentials_enabled?: boolean
free_tier_credits?: number
free_tier_balance?: {
allowance: number
used: number
remaining: number
}
new_free_tier_subscriptions?: boolean
workflow_sharing_enabled?: boolean
comfyhub_upload_enabled?: boolean

View File

@@ -10,7 +10,6 @@ import type {
ShareFlowMetadata,
ShareLinkOpenedMetadata,
ExecutionErrorMetadata,
ExecutionStartMetadata,
ExecutionSuccessMetadata,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
@@ -269,10 +268,6 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackWorkflowExecution?.())
}
trackExecutionStarted(metadata: ExecutionStartMetadata): void {
this.dispatch((provider) => provider.trackExecutionStarted?.(metadata))
}
trackExecutionError(metadata: ExecutionErrorMetadata): void {
this.dispatch((provider) => provider.trackExecutionError?.(metadata))
}

View File

@@ -28,8 +28,7 @@ export async function initTelemetry(): Promise<void> {
{ PostHogTelemetryProvider },
{ ClickHouseTelemetryProvider },
{ SyftTelemetryProvider },
{ CustomerIoTelemetryProvider },
{ DatadogRumTelemetryProvider }
{ CustomerIoTelemetryProvider }
] = await Promise.all([
import('./TelemetryRegistry'),
import('./providers/cloud/MixpanelTelemetryProvider'),
@@ -38,8 +37,7 @@ export async function initTelemetry(): Promise<void> {
import('./providers/cloud/PostHogTelemetryProvider'),
import('./providers/cloud/ClickHouseTelemetryProvider'),
import('./providers/cloud/SyftTelemetryProvider'),
import('./providers/cloud/CustomerIoTelemetryProvider'),
import('./providers/cloud/DatadogRumTelemetryProvider')
import('./providers/cloud/CustomerIoTelemetryProvider')
])
const registry = new TelemetryRegistry()
@@ -50,7 +48,6 @@ export async function initTelemetry(): Promise<void> {
registry.registerProvider(new ClickHouseTelemetryProvider())
registry.registerProvider(new SyftTelemetryProvider())
registry.registerProvider(new CustomerIoTelemetryProvider())
registry.registerProvider(new DatadogRumTelemetryProvider())
setTelemetryRegistry(registry)
})()

View File

@@ -1,118 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { DatadogRumTelemetryProvider } from './DatadogRumTelemetryProvider'
const addAction = vi.fn()
const getInternalContext = vi.fn()
const setViewName = vi.fn()
function installDatadogRum(): void {
Object.defineProperty(window, 'DD_RUM', {
configurable: true,
value: { addAction, getInternalContext, setViewName }
})
}
afterEach(() => {
addAction.mockReset()
getInternalContext.mockReset()
setViewName.mockReset()
Reflect.deleteProperty(window, 'DD_RUM')
})
describe('DatadogRumTelemetryProvider', () => {
it.for([
{ expected: 'workspace', path: 'https://cloud.comfy.org/' },
{ expected: 'account_access', path: 'https://cloud.comfy.org/cloud/' },
{ expected: 'account_access', path: 'https://cloud.comfy.org/cloud/login' },
{
expected: 'account_access',
path: 'https://cloud.comfy.org/cloud/subscribe?plan=creator'
},
{
expected: 'oauth_consent',
path: 'https://cloud.comfy.org/cloud/oauth/consent?oauth_request_id=redacted'
},
{
expected: 'support_recovery',
path: 'https://cloud.comfy.org/cloud/forgot-password'
},
{
expected: 'support_recovery',
path: 'https://cloud.comfy.org/cloud/sorry-contact-support'
},
{
expected: 'support_recovery',
path: 'https://cloud.comfy.org/cloud/auth-timeout'
}
] as const)('names the current view $expected', ({ expected, path }) => {
installDatadogRum()
new DatadogRumTelemetryProvider().trackPageView('ignored', { path })
expect(setViewName).toHaveBeenCalledWith(expected)
})
it('tracks workflow execution starts with the originating view', () => {
installDatadogRum()
getInternalContext.mockReturnValue({ view: { id: 'view-a' } })
new DatadogRumTelemetryProvider().trackExecutionStarted({
jobId: 'job-a',
startTime: 42
})
expect(getInternalContext).toHaveBeenCalledWith(42)
expect(addAction).toHaveBeenCalledWith('workflow_execution_started', {
product: 'cloud_generation',
origin_view_id: 'view-a'
})
})
it('preserves each origin view across workflow switches', () => {
installDatadogRum()
getInternalContext
.mockReturnValueOnce({ view: { id: 'view-a' } })
.mockReturnValueOnce({ view: { id: 'view-b' } })
const provider = new DatadogRumTelemetryProvider()
provider.trackExecutionStarted({ jobId: 'job-a', startTime: 1 })
provider.trackExecutionStarted({ jobId: 'job-b', startTime: 2 })
provider.trackExecutionSuccess({ jobId: 'job-a' })
provider.trackExecutionError({ jobId: 'job-b' })
expect(addAction).toHaveBeenNthCalledWith(
3,
'workflow_execution_completed',
{
outcome: 'success',
origin_view_id: 'view-a',
product: 'cloud_generation'
}
)
expect(addAction).toHaveBeenNthCalledWith(
4,
'workflow_execution_completed',
{
outcome: 'failure',
origin_view_id: 'view-b',
product: 'cloud_generation'
}
)
})
it('does nothing when Datadog RUM is unavailable', () => {
const provider = new DatadogRumTelemetryProvider()
expect(() => provider.trackPageView('ignored')).not.toThrow()
expect(() =>
provider.trackExecutionStarted({ jobId: 'job-a', startTime: 1 })
).not.toThrow()
expect(() =>
provider.trackExecutionSuccess({ jobId: 'job-a' })
).not.toThrow()
expect(() => provider.trackExecutionError({ jobId: 'job-a' })).not.toThrow()
expect(addAction).not.toHaveBeenCalled()
expect(setViewName).not.toHaveBeenCalled()
})
})

View File

@@ -1,95 +0,0 @@
import type {
ExecutionErrorMetadata,
ExecutionStartMetadata,
ExecutionSuccessMetadata,
PageViewMetadata,
TelemetryProvider
} from '../../types'
interface DatadogRumInternalContext {
view?: { id?: string }
}
interface DatadogRumClient {
addAction(name: string, context?: Record<string, unknown>): void
getInternalContext(startTime?: number): DatadogRumInternalContext | undefined
setViewName(name: string): void
}
interface WindowWithDatadogRum extends Window {
DD_RUM?: DatadogRumClient
}
type ViewName =
| 'account_access'
| 'oauth_consent'
| 'support_recovery'
| 'workspace'
const SUPPORT_RECOVERY_PATHS = new Set([
'/cloud/auth-timeout',
'/cloud/forgot-password',
'/cloud/sorry-contact-support'
])
const WORKFLOW_CONTEXT = {
product: 'cloud_generation'
} as const
function getDatadogRum(): DatadogRumClient | undefined {
return (window as WindowWithDatadogRum).DD_RUM
}
function getViewName(path = window.location.href): ViewName {
const pathname = new URL(path, window.location.origin).pathname.replace(
/\/$/,
''
)
if (pathname === '/cloud/oauth/consent') return 'oauth_consent'
if (SUPPORT_RECOVERY_PATHS.has(pathname)) return 'support_recovery'
if (pathname === '/cloud' || pathname.startsWith('/cloud/'))
return 'account_access'
return 'workspace'
}
export class DatadogRumTelemetryProvider implements TelemetryProvider {
private readonly originViewIdsByJobId = new Map<string, string>()
trackPageView(_pageName: string, properties?: PageViewMetadata): void {
getDatadogRum()?.setViewName(getViewName(properties?.path))
}
trackExecutionStarted(metadata: ExecutionStartMetadata): void {
const rum = getDatadogRum()
const originViewId = rum?.getInternalContext(metadata.startTime)?.view?.id
if (originViewId)
this.originViewIdsByJobId.set(metadata.jobId, originViewId)
rum?.addAction('workflow_execution_started', {
...WORKFLOW_CONTEXT,
...(originViewId && { origin_view_id: originViewId })
})
}
trackExecutionSuccess(metadata: ExecutionSuccessMetadata): void {
this.trackTerminalOutcome(metadata.jobId, 'success')
}
trackExecutionError(metadata: ExecutionErrorMetadata): void {
this.trackTerminalOutcome(metadata.jobId, 'failure')
}
private trackTerminalOutcome(
jobId: string,
outcome: 'success' | 'failure'
): void {
const originViewId = this.originViewIdsByJobId.get(jobId)
this.originViewIdsByJobId.delete(jobId)
getDatadogRum()?.addAction('workflow_execution_completed', {
...WORKFLOW_CONTEXT,
outcome,
...(originViewId && { origin_view_id: originViewId })
})
}
}

View File

@@ -33,7 +33,6 @@ export type PaymentIntentSource =
| 'invite_member_upsell'
| 'upload_model_upgrade'
| 'team_upgrade_resume'
| 'free_tier_quota'
export type SubscriptionCheckoutType = 'new' | 'change'
export type SubscriptionCheckoutTier = TierKey | 'team'
@@ -157,11 +156,6 @@ export interface ExecutionErrorMetadata {
error?: string
}
export interface ExecutionStartMetadata {
jobId: string
startTime: number
}
/**
* Execution success metadata
*/
@@ -638,7 +632,6 @@ export interface TelemetryProvider {
// Workflow execution events
trackWorkflowExecution?(): void
trackExecutionStarted?(metadata: ExecutionStartMetadata): void
trackExecutionError?(metadata: ExecutionErrorMetadata): void
trackExecutionSuccess?(metadata: ExecutionSuccessMetadata): void
trackSharedWorkflowRun?(metadata: SharedWorkflowRunMetadata): void

View File

@@ -12,7 +12,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { toNodeId } from '@/types/nodeId'
const billingMock = vi.hoisted(() => ({
canRunWorkflows: true
isActiveSubscription: true
}))
const overlayMock = vi.hoisted(() => ({
@@ -22,7 +22,7 @@ const overlayMock = vi.hoisted(() => ({
vi.mock('@/composables/billing/useBillingContext', () => ({
useBillingContext: () => ({
canRunWorkflows: billingMock.canRunWorkflows
isActiveSubscription: billingMock.isActiveSubscription
})
}))
@@ -77,14 +77,14 @@ const nodeErrors: Record<string, NodeError> = {
function renderControls({
hasError = false,
canRunWorkflows = true,
isActiveSubscription = true,
mobile = false
}: {
hasError?: boolean
canRunWorkflows?: boolean
isActiveSubscription?: boolean
mobile?: boolean
} = {}) {
billingMock.canRunWorkflows = canRunWorkflows
billingMock.isActiveSubscription = isActiveSubscription
const pinia = createTestingPinia({
createSpy: vi.fn,
@@ -120,7 +120,7 @@ function renderControls({
describe('LinearControls', () => {
beforeEach(() => {
vi.clearAllMocks()
billingMock.canRunWorkflows = true
billingMock.isActiveSubscription = true
overlayMock.overlayMessage = 'KSampler is missing a required input: model'
overlayMock.overlayTitle = 'Required input missing'
})
@@ -187,7 +187,7 @@ describe('LinearControls', () => {
({ mobile }) => {
renderControls({
hasError: true,
canRunWorkflows: false,
isActiveSubscription: false,
mobile
})

View File

@@ -11,7 +11,6 @@ import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import FreeTierQuota from '@/platform/cloud/subscription/components/FreeTierQuota.vue'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
@@ -29,7 +28,7 @@ const { t } = useI18n()
const commandStore = useCommandStore()
const { batchCount } = storeToRefs(useQueueSettingsStore())
const settingStore = useSettingStore()
const { canRunWorkflows } = useBillingContext()
const { isActiveSubscription } = useBillingContext()
const workflowStore = useWorkflowStore()
const { isBuilderMode } = useAppMode()
const appModeStore = useAppModeStore()
@@ -55,7 +54,7 @@ const linearRunButtonTestId = 'linear-run-button'
const showRunErrorWarning = computed(
() =>
hasAnyError.value &&
toValue(canRunWorkflows) &&
toValue(isActiveSubscription) &&
toValue(overlayMessage).trim().length > 0
)
@@ -153,7 +152,10 @@ function handleDragDrop() {
class="border-t border-node-component-border p-4 pb-6"
>
<LinearRunErrorWarning v-if="showRunErrorWarning" />
<SubscribeToRunButton v-if="!canRunWorkflows" class="mt-4 w-full" />
<SubscribeToRunButton
v-if="!isActiveSubscription"
class="mt-4 w-full"
/>
<div v-else class="mt-4 flex">
<PartnerNodesList mobile />
<Popover side="top" @open-auto-focus.prevent>
@@ -208,7 +210,10 @@ function handleDragDrop() {
:max="settingStore.get('Comfy.QueueButton.BatchCountLimit')"
class="h-7 min-w-40"
/>
<SubscribeToRunButton v-if="!canRunWorkflows" class="mt-4 w-full" />
<SubscribeToRunButton
v-if="!isActiveSubscription"
class="mt-4 w-full"
/>
<Button
v-else
variant="primary"
@@ -224,7 +229,6 @@ function handleDragDrop() {
<i aria-hidden="true" class="icon-[lucide--play]" />
{{ t('menu.run') }}
</Button>
<FreeTierQuota />
</section>
</div>
</div>

View File

@@ -4,17 +4,33 @@ import {
CollapsibleRoot,
CollapsibleTrigger
} from 'reka-ui'
import { computed, toValue } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import Popover from '@/components/ui/Popover.vue'
import { useCreditsBadgesInGraph } from '@/composables/node/usePriceBadge'
import { usePriceBadge } from '@/composables/node/usePriceBadge'
import PartnerNodeItem from '@/renderer/extensions/linearMode/PartnerNodeItem.vue'
import { trackNodePrice } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges'
import { app } from '@/scripts/app'
import { mapAllNodes } from '@/utils/graphTraversalUtil'
defineProps<{ mobile?: boolean }>()
const creditsBadges = useCreditsBadgesInGraph()
const { isCreditsBadge } = usePriceBadge()
const { t } = useI18n()
const creditsBadges = computed(() =>
mapAllNodes(app.graph, (node) => {
if (node.isSubgraphNode()) return
const priceBadge = node.badges.find(isCreditsBadge)
if (!priceBadge) return
trackNodePrice(node)
return [node.title, toValue(priceBadge).text, node.id] as const
})
)
</script>
<template>
<Popover v-if="mobile && creditsBadges.length" side="top">

View File

@@ -224,9 +224,10 @@ function resolveValues(values: unknown): string[] {
}
const modelValue = defineModel<string | undefined>({
default(modelProps: Props) {
default(modelProps: Record<string, unknown>) {
try {
const values = modelProps.widget.options?.values
const modelWidget = modelProps.widget as Props['widget'] | undefined
const values = modelWidget?.options?.values
const resolved = typeof values === 'function' ? values() : values
const firstValue = Array.isArray(resolved)
? resolved.find((value) => value !== null && value !== undefined)

View File

@@ -41,8 +41,9 @@ provide(
)
const modelValue = defineModel<string | undefined>({
default(props: Props) {
const values = props.widget.options?.values
default(modelProps: Record<string, unknown>) {
const modelWidget = modelProps.widget as Props['widget'] | undefined
const values = modelWidget?.options?.values
return (Array.isArray(values) ? values[0] : undefined) ?? ''
}
})

View File

@@ -24,7 +24,6 @@ import { snapPoint } from '@/lib/litegraph/src/measure'
import type { Vector2 } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import { useFreeTierQuota } from '@/platform/cloud/subscription/composables/useFreeTierQuota'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
@@ -1814,7 +1813,6 @@ export class ComfyApp {
isPartialExecution
})
}
useFreeTierQuota().trackRun()
this.canvas.draw(true, true)
await this.ui.queue.update()
}

View File

@@ -124,7 +124,7 @@ const findElementsInTag = (
end: number,
id: Uint8Array
): EbmlElementRange | null => {
for (let pos = start; pos < end - 1; ) {
for (let pos = start; pos < end - 1;) {
if (matchesId(data, pos, id)) {
const size = readVint(data, pos + 2)
if (size && size.value > 0) {
@@ -307,7 +307,7 @@ const ebmlToString = (
const parseMetadata = (data: Uint8Array): ComfyMetadata => {
const meta: ComfyMetadata = {}
for (let pos = 0; pos < data.length - 2; ) {
for (let pos = 0; pos < data.length - 2;) {
const tagInfo = findNextTag(data, pos)
if (!tagInfo) {
pos++

View File

@@ -306,13 +306,9 @@ export function useNodeFactory() {
// Create node based on type and configuration
switch (type) {
case 'basic':
return {
/* basic node implementation */
}
return {/* basic node implementation */}
case 'complex':
return {
/* complex node implementation */
}
return {/* complex node implementation */}
default:
throw new Error(`Unknown node type: ${type}`)
}
@@ -347,9 +343,7 @@ Services in ComfyUI can be tested effectively using different approaches dependi
```typescript
// Manual instantiation required
const mockData = [
/* test data */
]
const mockData = [/* test data */]
const service = new NodeSearchService(mockData)
```

View File

@@ -147,9 +147,7 @@ describe('useAuthStore', () => {
let authStateCallback: (user: User | null) => void
let idTokenCallback: (user: User | null) => void
const mockAuth: MockAuth = {
/* mock Auth object */
}
const mockAuth: MockAuth = {/* mock Auth object */}
const mockUser: MockUser = {
uid: 'test-user-id',

View File

@@ -20,7 +20,6 @@ const {
mockActiveWorkflow,
mockOpenWorkflows,
mockShowTextPreview,
mockTrackExecutionStarted,
mockTrackExecutionError,
mockTrackExecutionSuccess,
mockTrackSharedWorkflowRun
@@ -33,7 +32,6 @@ const {
mockActiveWorkflow: shallowRef<{ path?: string } | null>(null),
mockOpenWorkflows: shallowRef<{ path: string }[]>([]),
mockShowTextPreview: vi.fn(),
mockTrackExecutionStarted: vi.fn(),
mockTrackExecutionError: vi.fn(),
mockTrackExecutionSuccess: vi.fn(),
mockTrackSharedWorkflowRun: vi.fn()
@@ -93,7 +91,6 @@ vi.mock('@/platform/distribution/types', async () => ({
vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => ({
trackExecutionStarted: mockTrackExecutionStarted,
trackExecutionError: mockTrackExecutionError,
trackExecutionSuccess: mockTrackExecutionSuccess,
trackSharedWorkflowRun: mockTrackSharedWorkflowRun
@@ -634,10 +631,6 @@ describe('useExecutionStore - workflowStatus', () => {
callStoreJob('job-1', workflowA)
fireExecutionStart('job-1')
expect(mockTrackExecutionStarted).toHaveBeenCalledWith({
jobId: 'job-1',
startTime: expect.any(Number)
})
expect(store.getWorkflowStatus(workflowA)).toBe('running')
})

View File

@@ -748,10 +748,6 @@ export const useExecutionStore = defineStore('execution', () => {
if (workflow?.path) {
ensureSessionWorkflowPath(id, workflow.path)
}
useTelemetry()?.trackExecutionStarted({
jobId: id,
startTime: performance.now()
})
flushPendingWorkflowStatus(String(id), workflow)
}

View File

@@ -55,7 +55,6 @@ export function useBillingContext(): BillingContext {
isLoading: ref(false),
error: ref<string | null>(null),
isActiveSubscription: computed(() => state.value.isActiveSubscription),
canRunWorkflows: computed(() => state.value.isActiveSubscription),
isFreeTier: computed(() => false),
isLegacyTeamPlan: computed(() => false),
isTeamPlan: computed(() => state.value.isTeamPlan),

View File

@@ -132,9 +132,9 @@ export function fixBadLinks(
// We can set to null (delete), so undefined means we haven't set it at all.
if (patchedNode['inputs']![slot] !== undefined) {
logger.log(
` > Already set ${node.id}.inputs[${slot}] to ${patchedNode[
'inputs'
]![slot]!} Skipping.`
` > Already set ${node.id}.inputs[${slot}] to ${
patchedNode['inputs']![slot]!
} Skipping.`
)
return false
}