Compare commits

...

11 Commits

Author SHA1 Message Date
Benjamin Lu
128e03504b fix: drop app mode reexport shim 2026-06-17 17:24:48 -07:00
Benjamin Lu
c5e487fc38 fix: guard run button telemetry tracking 2026-06-17 17:24:48 -07:00
Benjamin Lu
b1e1f5bb3e fix: decouple run telemetry from providers 2026-06-17 17:24:48 -07:00
Benjamin Lu
4f23581891 fix: separate bridge types publish workflow 2026-06-17 16:04:28 -07:00
Benjamin Lu
fb1c641089 fix: handle desktop bridge rollout 2026-06-17 15:33:46 -07:00
Benjamin Lu
a4bcdcab89 fix: move desktop bridge types into frontend 2026-06-17 14:58:57 -07:00
Benjamin Lu
a3c4233887 fix: declare bridge types package dependency 2026-06-15 14:24:16 -07:00
Benjamin Lu
73546f02ef Update bridge types package 2026-06-15 14:08:45 -07:00
Benjamin Lu
cd3b322ba5 Use bridge remote mode for Desktop downloads 2026-06-15 13:52:00 -07:00
Benjamin Lu
7d09c17646 Fix workspace YAML comment 2026-06-15 13:08:34 -07:00
Benjamin Lu
8e28087cab Use Comfy Desktop bridge types package 2026-06-15 10:44:39 -07:00
34 changed files with 670 additions and 219 deletions

View File

@@ -0,0 +1,142 @@
name: Publish Desktop Bridge Types
on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 0.1.2)'
required: true
type: string
dist_tag:
description: 'npm dist-tag to use'
required: true
default: latest
type: string
ref:
description: 'Git ref to checkout (commit SHA, tag, or branch)'
required: false
type: string
workflow_call:
inputs:
version:
required: true
type: string
dist_tag:
required: false
type: string
default: latest
ref:
required: false
type: string
secrets:
NPM_TOKEN:
required: true
concurrency:
group: publish-desktop-bridge-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
cancel-in-progress: false
jobs:
publish_desktop_bridge_types:
name: Publish @comfyorg/comfyui-desktop-bridge-types
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate inputs
env:
VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
exit 1
fi
- name: Determine ref to checkout
id: resolve_ref
env:
REF: ${{ inputs.ref }}
DEFAULT_REF: ${{ github.ref_name }}
shell: bash
run: |
set -euo pipefail
if [ -z "$REF" ]; then
REF="$DEFAULT_REF"
fi
if ! git check-ref-format --allow-onelevel "$REF"; then
echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
exit 1
fi
echo "ref=$REF" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
registry-url: https://registry.npmjs.org
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Verify package
id: pkg
env:
INPUT_VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
PACKAGE_JSON=packages/comfyui-desktop-bridge-types/package.json
NAME=$(node -p "require('./${PACKAGE_JSON}').name")
VERSION=$(node -p "require('./${PACKAGE_JSON}').version")
if [ "$VERSION" != "$INPUT_VERSION" ]; then
echo "::error title=Version mismatch::${PACKAGE_JSON} version $VERSION does not match input $INPUT_VERSION" >&2
exit 1
fi
echo "name=$NAME" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Check if version already on npm
id: check_npm
env:
NAME: ${{ steps.pkg.outputs.name }}
VER: ${{ steps.pkg.outputs.version }}
shell: bash
run: |
set -euo pipefail
STATUS=0
OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
if [ "$STATUS" -eq 0 ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
else
if echo "$OUTPUT" | grep -q "E404"; then
echo "exists=false" >> "$GITHUB_OUTPUT"
else
echo "::error title=Registry lookup failed::$OUTPUT" >&2
exit "$STATUS"
fi
fi
- name: Publish package
if: steps.check_npm.outputs.exists == 'false'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
DIST_TAG: ${{ inputs.dist_tag }}
run: pnpm publish --access public --tag "$DIST_TAG" --no-git-checks --ignore-scripts
working-directory: packages/comfyui-desktop-bridge-types

View File

@@ -59,6 +59,7 @@
"dependencies": {
"@alloc/quick-lru": "catalog:",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-desktop-bridge-types": "workspace:*",
"@comfyorg/comfyui-electron-types": "catalog:",
"@comfyorg/design-system": "workspace:*",
"@comfyorg/fbx-exporter-three": "^1.0.1",

View File

@@ -0,0 +1,91 @@
export interface ComfyDownloadProgress {
url: string
filename: string
directory?: string
progress: number
receivedBytes?: number
totalBytes?: number
speedBytesPerSec?: number
etaSeconds?: number
status:
| 'pending'
| 'downloading'
| 'paused'
| 'completed'
| 'error'
| 'cancelled'
error?: string
isImage?: boolean
}
export interface TerminalRestore {
buffer: string[]
size: { cols: number; rows: number }
exited: boolean
}
export interface LogsRestore {
installationId: string
buffer: string[]
}
export interface LogsOutputMsg {
installationId: string
text: string
}
export type ComfyDesktop2TelemetryValue = string | number | boolean | null
export type ComfyDesktop2TelemetryProperties = Record<
string,
ComfyDesktop2TelemetryValue | ComfyDesktop2TelemetryValue[]
>
export interface ComfyDesktop2TerminalBridge {
subscribe(installationId?: string): Promise<TerminalRestore>
unsubscribe(installationId?: string): Promise<void>
write(data: string, installationId?: string): Promise<void>
resize(cols: number, rows: number, installationId?: string): Promise<void>
restart(installationId?: string): Promise<TerminalRestore>
openPopout(): Promise<void>
onOutput(callback: (data: string) => void): () => void
onExited(callback: () => void): () => void
}
export interface ComfyDesktop2LogsBridge {
subscribe(installationId?: string): Promise<LogsRestore>
unsubscribe(installationId?: string): Promise<void>
openPopout(): Promise<void>
onOutput(callback: (msg: LogsOutputMsg) => void): () => void
}
export interface ComfyDesktop2TelemetryBridge {
capture(event: string, properties?: ComfyDesktop2TelemetryProperties): void
}
export interface ComfyDesktop2Bridge {
isRemote(): boolean
downloadModel?: (
url: string,
filename: string,
directory: string
) => Promise<boolean>
downloadAsset?: (
url: string,
filename: string,
authToken?: string
) => Promise<boolean>
pauseDownload?: (url: string) => Promise<boolean>
resumeDownload?: (url: string) => Promise<boolean>
cancelDownload?: (url: string) => Promise<boolean>
onDownloadProgress?: (
callback: (data: ComfyDownloadProgress) => void
) => () => void
reportTheme?: (bg: string, text: string) => void
Terminal?: ComfyDesktop2TerminalBridge
Logs?: ComfyDesktop2LogsBridge
Telemetry?: ComfyDesktop2TelemetryBridge
}
export type ComfyDesktop2BridgeImplementation = {
[K in keyof ComfyDesktop2Bridge]-?: NonNullable<ComfyDesktop2Bridge[K]>
}

View File

@@ -0,0 +1 @@
export * from './comfyDesktopBridge.js'

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,27 @@
{
"name": "@comfyorg/comfyui-desktop-bridge-types",
"version": "0.1.2",
"description": "TypeScript definitions for the Comfy Desktop hosted frontend bridge",
"homepage": "https://comfy.org",
"license": "MIT",
"author": {
"name": "Comfy Org",
"email": "support@comfy.org",
"url": "https://www.comfy.org"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Comfy-Org/ComfyUI_frontend.git"
},
"files": [
"comfyDesktopBridge.d.ts",
"index.d.ts",
"index.js"
],
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"publishConfig": {
"access": "public"
}
}

13
pnpm-lock.yaml generated
View File

@@ -426,6 +426,9 @@ importers:
'@atlaskit/pragmatic-drag-and-drop':
specifier: ^1.3.1
version: 1.3.1
'@comfyorg/comfyui-desktop-bridge-types':
specifier: workspace:*
version: link:packages/comfyui-desktop-bridge-types
'@comfyorg/comfyui-electron-types':
specifier: 'catalog:'
version: 0.6.2
@@ -997,6 +1000,8 @@ importers:
specifier: 'catalog:'
version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/coverage-v8@4.0.16(vitest@4.1.8))(@vitest/ui@4.0.16(vitest@4.1.8))(happy-dom@20.9.0)(jsdom@27.4.0)(vite@8.0.13(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.7.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))
packages/comfyui-desktop-bridge-types: {}
packages/design-system:
dependencies:
'@iconify-json/lucide':
@@ -8640,8 +8645,8 @@ packages:
vue-component-type-helpers@3.3.2:
resolution: {integrity: sha512-l4Z2Y34m7nFMlx8vrslJaVtXxUpzgDMSESC7TakG/c5kwjYT/do+E0NcT2/vWDzaoIhsShg/2OKwX7Q4nbzC0g==}
vue-component-type-helpers@3.3.4:
resolution: {integrity: sha512-joip1uZTaQR0nD23N400gIdJ7xY+WiiiMA/BCKz842gvGBknqDQAzklUvDEhqFvvrhQY8S2ZANBMu4X70VMFGw==}
vue-component-type-helpers@3.3.5:
resolution: {integrity: sha512-Fe1jyPJoUGpJOYKOri44jduR7My4yYINOMJISuMAbmrs+L5LbIDUc8NTWZYY3EJLK0yPLuCmcd5zoCsE4k2/KA==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -11323,7 +11328,7 @@ snapshots:
storybook: 10.2.10(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
type-fest: 2.19.0
vue: 3.5.34(typescript@5.9.3)
vue-component-type-helpers: 3.3.4
vue-component-type-helpers: 3.3.5
'@swc/helpers@0.5.21':
dependencies:
@@ -17469,7 +17474,7 @@ snapshots:
vue-component-type-helpers@3.3.2: {}
vue-component-type-helpers@3.3.4: {}
vue-component-type-helpers@3.3.5: {}
vue-demi@0.14.10(vue@3.5.34(typescript@5.9.3)):
dependencies:

View File

@@ -164,7 +164,7 @@ overrides:
vite: 'catalog:'
'@tiptap/pm': 2.27.2
'@types/eslint': '-'
#Security overrides
# Security overrides
lodash: ^4.18.0
yaml: ^2.8.3
minimatch@^9.0.0: ^9.0.7

View File

@@ -2,6 +2,12 @@ import fs from 'fs'
import path from 'path'
const mainPackage = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
const desktopBridgeTypesPackage = JSON.parse(
fs.readFileSync(
'./packages/comfyui-desktop-bridge-types/package.json',
'utf8'
)
)
// Create the types-only package.json
const typesPackage = {
@@ -16,7 +22,9 @@ const typesPackage = {
homepage: mainPackage.homepage,
description: `TypeScript definitions for ${mainPackage.name}`,
license: mainPackage.license,
dependencies: {},
dependencies: {
'@comfyorg/comfyui-desktop-bridge-types': desktopBridgeTypesPackage.version
},
peerDependencies: {
vue: mainPackage.dependencies.vue,
zod: mainPackage.dependencies.zod
@@ -34,5 +42,3 @@ fs.writeFileSync(
path.join(distDir, 'package.json'),
JSON.stringify(typesPackage, null, 2)
)
console.log('Types package.json have been prepared in the dist directory')

View File

@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import type { AppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/utils/appMode'
import BuilderFooterToolbar from '@/components/builder/BuilderFooterToolbar.vue'

View File

@@ -1,28 +1,8 @@
import { computed, ref } from 'vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
export type AppMode =
| 'graph'
| 'app'
| 'builder:inputs'
| 'builder:outputs'
| 'builder:arrange'
type WorkflowModeSource = {
activeMode: AppMode | null
initialMode: AppMode | null | undefined
}
export function getWorkflowMode(
workflow: WorkflowModeSource | null | undefined
): AppMode {
return workflow?.activeMode ?? workflow?.initialMode ?? 'graph'
}
export function isAppModeValue(mode: AppMode): boolean {
return mode === 'app' || mode === 'builder:arrange'
}
import { getWorkflowMode, isAppModeValue } from '@/utils/appMode'
import type { AppMode } from '@/utils/appMode'
const enableAppBuilder = ref(true)

View File

@@ -4,6 +4,7 @@ import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteG
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
import { useExternalLink } from '@/composables/useExternalLink'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import { useRunButtonTelemetry } from '@/composables/useRunButtonTelemetry'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
@@ -85,6 +86,7 @@ export function useCoreCommands(): ComfyCommand[] {
const executionStore = useExecutionStore()
const modelStore = useModelStore()
const telemetry = useTelemetry()
const { trackRunButton } = useRunButtonTelemetry()
const { staticUrls, buildDocsUrl } = useExternalLink()
const settingStore = useSettingStore()
@@ -499,7 +501,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -522,7 +524,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -544,7 +546,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton(metadata)
trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return

View File

@@ -0,0 +1,112 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const state = vi.hoisted(() => ({
mode: { value: 'graph' },
isAppMode: { value: false },
telemetry: {
trackRunButton: vi.fn()
},
executionContext: {
is_template: false,
workflow_name: 'Desktop workflow',
custom_node_count: 2,
total_node_count: 4,
subgraph_count: 1,
has_api_nodes: true,
api_node_names: ['LoadImage'],
has_toolkit_nodes: false,
toolkit_node_names: []
},
executionContextError: null as Error | null
}))
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
mode: state.mode,
isAppMode: state.isAppMode
})
}))
vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => state.telemetry
}))
vi.mock('@/platform/telemetry/utils/getExecutionContext', () => ({
getExecutionContext: () => {
if (state.executionContextError) throw state.executionContextError
return state.executionContext
}
}))
import {
getRunButtonTelemetryProperties,
useRunButtonTelemetry
} from './useRunButtonTelemetry'
describe('useRunButtonTelemetry', () => {
beforeEach(() => {
localStorage.clear()
state.telemetry.trackRunButton.mockClear()
state.mode.value = 'graph'
state.isAppMode.value = false
state.executionContextError = null
})
it('builds run button properties from workspace state', () => {
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
expect(
getRunButtonTelemetryProperties({
subscribe_to_run: true,
trigger_source: 'button'
})
).toEqual({
subscribe_to_run: true,
workflow_type: 'custom',
workflow_name: 'Desktop workflow',
custom_node_count: 2,
total_node_count: 4,
subgraph_count: 1,
has_api_nodes: true,
api_node_names: ['LoadImage'],
has_toolkit_nodes: false,
toolkit_node_names: [],
trigger_source: 'button',
view_mode: 'graph',
is_app_mode: false,
dock_state: 'floating'
})
})
it('tracks the completed run button payload', () => {
useRunButtonTelemetry().trackRunButton({ trigger_source: 'linear' })
expect(state.telemetry.trackRunButton).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
subscribe_to_run: false,
trigger_source: 'linear',
workflow_name: 'Desktop workflow'
})
)
})
it('does not throw when run button context collection fails', () => {
const error = new Error('Context unavailable')
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
state.executionContextError = error
try {
expect(() =>
useRunButtonTelemetry().trackRunButton({ trigger_source: 'linear' })
).not.toThrow()
expect(state.telemetry.trackRunButton).not.toHaveBeenCalled()
expect(consoleError).toHaveBeenCalledExactlyOnceWith(
'[Telemetry] Run button tracking failed',
error
)
} finally {
consoleError.mockRestore()
}
})
})

View File

@@ -0,0 +1,52 @@
import { useAppMode } from '@/composables/useAppMode'
import { useTelemetry } from '@/platform/telemetry'
import type {
ExecutionTriggerSource,
RunButtonProperties
} from '@/platform/telemetry/types'
import { getActionbarDockState } from '@/platform/telemetry/utils/getActionbarDockState'
import { getExecutionContext } from '@/platform/telemetry/utils/getExecutionContext'
export type RunButtonTelemetryOptions = {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}
export function getRunButtonTelemetryProperties(
options?: RunButtonTelemetryOptions
): RunButtonProperties {
const executionContext = getExecutionContext()
const { mode, isAppMode } = useAppMode()
return {
subscribe_to_run: options?.subscribe_to_run ?? false,
workflow_type: executionContext.is_template ? 'template' : 'custom',
workflow_name: executionContext.workflow_name ?? 'untitled',
custom_node_count: executionContext.custom_node_count,
total_node_count: executionContext.total_node_count,
subgraph_count: executionContext.subgraph_count,
has_api_nodes: executionContext.has_api_nodes,
api_node_names: executionContext.api_node_names,
has_toolkit_nodes: executionContext.has_toolkit_nodes,
toolkit_node_names: executionContext.toolkit_node_names,
trigger_source: options?.trigger_source,
view_mode: mode.value,
is_app_mode: isAppMode.value,
dock_state: getActionbarDockState()
}
}
export function useRunButtonTelemetry() {
function trackRunButton(options?: RunButtonTelemetryOptions): void {
const telemetry = useTelemetry()
if (!telemetry) return
try {
telemetry.trackRunButton(getRunButtonTelemetryProperties(options))
} catch (error) {
console.error('[Telemetry] Run button tracking failed', error)
}
}
return { trackRunButton }
}

View File

@@ -22,8 +22,8 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useRunButtonTelemetry } from '@/composables/useRunButtonTelemetry'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
const { t } = useI18n()
const breakpoints = useBreakpoints(breakpointsTailwind)
@@ -36,10 +36,11 @@ const buttonLabel = computed(() =>
)
const { showSubscriptionDialog } = useBillingContext()
const { trackRunButton } = useRunButtonTelemetry()
const handleSubscribeToRun = () => {
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: true })
trackRunButton({ subscribe_to_run: true })
}
showSubscriptionDialog()

View File

@@ -42,6 +42,18 @@ beforeEach(() => {
delete window.__comfyDesktop2Remote
})
function setLegacyDesktop2Bridge(
downloadModel: NonNullable<
NonNullable<typeof window.__comfyDesktop2>['downloadModel']
>
): void {
Object.defineProperty(window, '__comfyDesktop2', {
configurable: true,
writable: true,
value: { downloadModel }
})
}
describe('fetchModelMetadata', () => {
beforeEach(() => {
mockIsDesktop.value = false
@@ -258,7 +270,10 @@ describe('downloadModel', () => {
(url: string, filename: string, directory: string) => Promise<boolean>
>()
.mockResolvedValue(true)
window.__comfyDesktop2 = { downloadModel: desktopDownloadModel }
window.__comfyDesktop2 = {
isRemote: () => false,
downloadModel: desktopDownloadModel
}
downloadModel(
{
@@ -289,7 +304,10 @@ describe('downloadModel', () => {
(url: string, filename: string, directory: string) => Promise<boolean>
>()
.mockRejectedValue(bridgeError)
window.__comfyDesktop2 = { downloadModel: desktopDownloadModel }
window.__comfyDesktop2 = {
isRemote: () => false,
downloadModel: desktopDownloadModel
}
downloadModel(
{
@@ -323,7 +341,10 @@ describe('downloadModel', () => {
.mockImplementation(() => {
throw bridgeError
})
window.__comfyDesktop2 = { downloadModel: desktopDownloadModel }
window.__comfyDesktop2 = {
isRemote: () => false,
downloadModel: desktopDownloadModel
}
downloadModel(
{
@@ -353,7 +374,62 @@ describe('downloadModel', () => {
(url: string, filename: string, directory: string) => Promise<boolean>
>()
.mockResolvedValue(true)
window.__comfyDesktop2 = { downloadModel: desktopDownloadModel }
window.__comfyDesktop2 = {
isRemote: () => true,
downloadModel: desktopDownloadModel
}
downloadModel(
{
name: 'model.safetensors',
url: 'https://huggingface.co/org/model/resolve/main/model.safetensors',
directory: 'checkpoints'
},
{ checkpoints: ['/models/checkpoints'] }
)
expect(desktopDownloadModel).not.toHaveBeenCalled()
expect(anchorClick).toHaveBeenCalledTimes(1)
})
it('uses the Desktop2 bridge when the new remote check is not available', () => {
const anchorClick = vi
.spyOn(HTMLAnchorElement.prototype, 'click')
.mockImplementation(() => {})
const desktopDownloadModel = vi
.fn<
(url: string, filename: string, directory: string) => Promise<boolean>
>()
.mockResolvedValue(true)
setLegacyDesktop2Bridge(desktopDownloadModel)
downloadModel(
{
name: 'model.safetensors',
url: 'https://huggingface.co/org/model/resolve/main/model.safetensors',
directory: 'checkpoints'
},
{ checkpoints: ['/models/checkpoints'] }
)
expect(desktopDownloadModel).toHaveBeenCalledWith(
'https://huggingface.co/org/model/resolve/main/model.safetensors',
'model.safetensors',
'checkpoints'
)
expect(anchorClick).not.toHaveBeenCalled()
})
it('honors the legacy Desktop2 remote marker when the new remote check is not available', () => {
const anchorClick = vi
.spyOn(HTMLAnchorElement.prototype, 'click')
.mockImplementation(() => {})
const desktopDownloadModel = vi
.fn<
(url: string, filename: string, directory: string) => Promise<boolean>
>()
.mockResolvedValue(true)
setLegacyDesktop2Bridge(desktopDownloadModel)
window.__comfyDesktop2Remote = true
downloadModel(

View File

@@ -2,20 +2,10 @@ import { downloadUrlToHfRepoUrl, isCivitaiModelUrl } from '@/utils/formatUtil'
import { isDesktop } from '@/platform/distribution/types'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import type { ComfyDesktop2Bridge } from '@/types'
interface ComfyDesktop2Bridge {
downloadModel: (
url: string,
filename: string,
directory: string
) => Promise<boolean>
}
declare global {
interface Window {
__comfyDesktop2?: ComfyDesktop2Bridge
__comfyDesktop2Remote?: boolean
}
type Desktop2BridgeWithLegacyRemote = Omit<ComfyDesktop2Bridge, 'isRemote'> & {
isRemote?: ComfyDesktop2Bridge['isRemote']
}
const ALLOWED_SOURCES = [
@@ -51,16 +41,22 @@ export interface ModelWithUrl {
}
async function startDesktop2ModelDownload(
bridge: ComfyDesktop2Bridge,
bridge: Desktop2BridgeWithLegacyRemote,
model: ModelWithUrl
): Promise<void> {
try {
await bridge.downloadModel(model.url, model.name, model.directory)
await bridge.downloadModel?.(model.url, model.name, model.directory)
} catch (error: unknown) {
console.error('Failed to start Desktop2 model download:', error)
}
}
function isRemoteDesktop2Bridge(
bridge: Desktop2BridgeWithLegacyRemote
): boolean {
return bridge.isRemote?.() ?? window.__comfyDesktop2Remote ?? false
}
/**
* Converts a model download URL to a browsable page URL.
* - HuggingFace: `/resolve/` → `/blob/` (file page with model info)
@@ -90,7 +86,10 @@ export function downloadModel(
paths: Record<string, string[]>
): void {
const desktop2Bridge = window.__comfyDesktop2
if (desktop2Bridge?.downloadModel && !window.__comfyDesktop2Remote) {
if (
desktop2Bridge?.downloadModel &&
!isRemoteDesktop2Bridge(desktop2Bridge)
) {
void startDesktop2ModelDownload(desktop2Bridge, model)
return
}

View File

@@ -9,7 +9,6 @@ import type {
ShareLinkOpenedMetadata,
ExecutionErrorMetadata,
ExecutionSuccessMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
@@ -19,6 +18,7 @@ import type {
SearchQueryMetadata,
PageViewMetadata,
PageVisibilityMetadata,
RunButtonProperties,
SettingChangedMetadata,
SharedWorkflowRunMetadata,
ShellLayoutMetadata,
@@ -112,11 +112,8 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackApiCreditTopupSucceeded?.())
}
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
this.dispatch((provider) => provider.trackRunButton?.(options))
trackRunButton(properties: RunButtonProperties): void {
this.dispatch((provider) => provider.trackRunButton?.(properties))
}
startTopupTracking(): void {

View File

@@ -1,11 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
mode: { value: 'app' },
isAppMode: { value: true }
})
}))
import { beforeEach, describe, expect, it } from 'vitest'
import { GtmTelemetryProvider } from './GtmTelemetryProvider'
@@ -192,8 +185,22 @@ describe('GtmTelemetryProvider', () => {
it('pushes run_workflow with trigger_source', () => {
const provider = createInitializedProvider()
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
provider.trackRunButton({ trigger_source: 'button' })
provider.trackRunButton({
subscribe_to_run: false,
workflow_type: 'custom',
workflow_name: 'untitled',
custom_node_count: 0,
total_node_count: 0,
subgraph_count: 0,
has_api_nodes: false,
api_node_names: [],
has_toolkit_nodes: false,
toolkit_node_names: [],
trigger_source: 'button',
view_mode: 'app',
is_app_mode: true,
dock_state: 'floating'
})
expect(lastDataLayerEntry()).toMatchObject({
event: 'run_workflow',
trigger_source: 'button',

View File

@@ -5,7 +5,6 @@ import type {
EnterLinearMetadata,
ExecutionErrorMetadata,
ExecutionSuccessMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
@@ -13,6 +12,7 @@ import type {
NodeSearchResultMetadata,
PageViewMetadata,
PageVisibilityMetadata,
RunButtonProperties,
SettingChangedMetadata,
ShareFlowMetadata,
SubscriptionMetadata,
@@ -29,8 +29,6 @@ import type {
WorkflowImportMetadata,
WorkflowSavedMetadata
} from '../../types'
import { useAppMode } from '@/composables/useAppMode'
import { getActionbarDockState } from '../../utils/getActionbarDockState'
/**
* Google Tag Manager telemetry provider.
@@ -183,18 +181,13 @@ export class GtmTelemetryProvider implements TelemetryProvider {
)
}
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
const { mode, isAppMode } = useAppMode()
trackRunButton(properties: RunButtonProperties): void {
this.pushEvent('run_workflow', {
subscribe_to_run: options?.subscribe_to_run ?? false,
trigger_source: options?.trigger_source ?? 'unknown',
view_mode: mode.value,
is_app_mode: isAppMode.value,
dock_state: getActionbarDockState()
subscribe_to_run: properties.subscribe_to_run,
trigger_source: properties.trigger_source ?? 'unknown',
view_mode: properties.view_mode,
is_app_mode: properties.is_app_mode,
dock_state: properties.dock_state
})
}

View File

@@ -17,13 +17,6 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
useCurrentUser: () => ({ onUserResolved: mockOnUserResolved })
}))
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({
mode: { value: 'graph' },
isAppMode: { value: false }
})
}))
const topupMocks = vi.hoisted(() => ({
startTopupTracking: vi.fn(),
clearTopupTracking: vi.fn(),
@@ -31,20 +24,6 @@ const topupMocks = vi.hoisted(() => ({
}))
vi.mock('@/platform/telemetry/topupTracker', () => topupMocks)
vi.mock('@/platform/telemetry/utils/getExecutionContext', () => ({
getExecutionContext: () => ({
is_template: false,
workflow_name: 'untitled',
custom_node_count: 0,
total_node_count: 0,
subgraph_count: 0,
has_api_nodes: false,
api_node_names: [],
has_toolkit_nodes: false,
toolkit_node_names: []
})
}))
const mockNormalizeSurveyResponses = vi.hoisted(() => vi.fn())
vi.mock('@/platform/telemetry/utils/surveyNormalization', () => ({
normalizeSurveyResponses: mockNormalizeSurveyResponses
@@ -59,6 +38,7 @@ import type {
AuthMetadata,
DefaultViewSetMetadata,
EnterLinearMetadata,
RunButtonProperties,
ShareFlowMetadata,
ShellLayoutMetadata,
SurveyResponses,
@@ -450,27 +430,33 @@ describe('MixpanelTelemetryProvider — direct event tracking methods', () => {
)
})
it('trackRunButton populates RunButtonProperties from the execution context', async () => {
it('trackRunButton forwards RunButtonProperties', async () => {
const provider = new MixpanelTelemetryProvider()
await waitForMixpanelInit()
mockMixpanel.track.mockClear()
localStorage.setItem('Comfy.MenuPosition.Docked', 'false')
provider.trackRunButton({
const properties: RunButtonProperties = {
subscribe_to_run: true,
trigger_source: 'button'
})
workflow_type: 'custom',
workflow_name: 'untitled',
custom_node_count: 0,
total_node_count: 0,
subgraph_count: 0,
has_api_nodes: false,
api_node_names: [],
has_toolkit_nodes: false,
toolkit_node_names: [],
trigger_source: 'button',
view_mode: 'graph',
is_app_mode: false,
dock_state: 'floating'
}
provider.trackRunButton(properties)
expect(mockMixpanel.track).toHaveBeenCalledWith(
TelemetryEvents.RUN_BUTTON_CLICKED,
expect.objectContaining({
subscribe_to_run: true,
workflow_type: 'custom',
trigger_source: 'button',
view_mode: 'graph',
is_app_mode: false,
dock_state: 'floating'
})
properties
)
})

View File

@@ -2,7 +2,6 @@ import type { OverridedMixpanel } from 'mixpanel-browser'
import { omit } from 'es-toolkit'
import { watch } from 'vue'
import { useAppMode } from '@/composables/useAppMode'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import {
checkForCompletedTopup as checkTopupUtil,
@@ -11,14 +10,11 @@ import {
} from '@/platform/telemetry/topupTracker'
import type { AuditLog } from '@/services/customerEventsService'
import { getExecutionContext } from '../../utils/getExecutionContext'
import type {
AuthMetadata,
CreditTopupMetadata,
DefaultViewSetMetadata,
EnterLinearMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
@@ -48,7 +44,6 @@ import type {
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
import type { RemoteConfig } from '@/platform/remoteConfig/types'
import { TelemetryEvents } from '../../types'
import { getActionbarDockState } from '../../utils/getActionbarDockState'
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
const DEFAULT_DISABLED_EVENTS = [
@@ -276,31 +271,8 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
clearTopupUtil()
}
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
const executionContext = getExecutionContext()
const { mode, isAppMode } = useAppMode()
const runButtonProperties: RunButtonProperties = {
subscribe_to_run: options?.subscribe_to_run || false,
workflow_type: executionContext.is_template ? 'template' : 'custom',
workflow_name: executionContext.workflow_name ?? 'untitled',
custom_node_count: executionContext.custom_node_count,
total_node_count: executionContext.total_node_count,
subgraph_count: executionContext.subgraph_count,
has_api_nodes: executionContext.has_api_nodes,
api_node_names: executionContext.api_node_names,
has_toolkit_nodes: executionContext.has_toolkit_nodes,
toolkit_node_names: executionContext.toolkit_node_names,
trigger_source: options?.trigger_source,
view_mode: mode.value,
is_app_mode: isAppMode.value,
dock_state: getActionbarDockState()
}
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
trackRunButton(properties: RunButtonProperties): void {
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, properties)
}
trackSurvey(

View File

@@ -3,7 +3,6 @@ import { watch } from 'vue'
import { createPostHogBeforeSend } from '@comfyorg/shared-frontend-utils/piiUtil'
import { useAppMode } from '@/composables/useAppMode'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
@@ -15,7 +14,6 @@ import type {
EnterLinearMetadata,
ShareFlowMetadata,
ShareLinkOpenedMetadata,
ExecutionTriggerSource,
HelpCenterClosedMetadata,
HelpCenterOpenedMetadata,
HelpResourceClickedMetadata,
@@ -46,8 +44,6 @@ import type {
WorkflowSavedMetadata
} from '../../types'
import { TelemetryEvents } from '../../types'
import { getActionbarDockState } from '../../utils/getActionbarDockState'
import { getExecutionContext } from '../../utils/getExecutionContext'
import { normalizeSurveyResponses } from '../../utils/surveyNormalization'
const DEFAULT_DISABLED_EVENTS = [
@@ -374,31 +370,8 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
this.trackEvent(TelemetryEvents.API_CREDIT_TOPUP_SUCCEEDED)
}
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void {
const executionContext = getExecutionContext()
const { mode, isAppMode } = useAppMode()
const runButtonProperties: RunButtonProperties = {
subscribe_to_run: options?.subscribe_to_run || false,
workflow_type: executionContext.is_template ? 'template' : 'custom',
workflow_name: executionContext.workflow_name ?? 'untitled',
custom_node_count: executionContext.custom_node_count,
total_node_count: executionContext.total_node_count,
subgraph_count: executionContext.subgraph_count,
has_api_nodes: executionContext.has_api_nodes,
api_node_names: executionContext.api_node_names,
has_toolkit_nodes: executionContext.has_toolkit_nodes,
toolkit_node_names: executionContext.toolkit_node_names,
trigger_source: options?.trigger_source,
view_mode: mode.value,
is_app_mode: isAppMode.value,
dock_state: getActionbarDockState()
}
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, runButtonProperties)
trackRunButton(properties: RunButtonProperties): void {
this.trackEvent(TelemetryEvents.RUN_BUTTON_CLICKED, properties)
}
trackSurvey(

View File

@@ -12,11 +12,11 @@
* 3. Check dist/assets/*.js files contain no tracking code
*/
import type { AppMode } from '@/composables/useAppMode'
import type { SubscriptionDialogReason } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
import type { AuditLog } from '@/services/customerEventsService'
import type { AppMode } from '@/utils/appMode'
/**
* Authentication metadata for sign-up tracking
@@ -486,10 +486,7 @@ export interface TelemetryProvider {
trackAddApiCreditButtonClicked?(): void
trackApiCreditTopupButtonPurchaseClicked?(amount: number): void
trackApiCreditTopupSucceeded?(): void
trackRunButton?(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}): void
trackRunButton?(properties: RunButtonProperties): void
// Credit top-up tracking (composition with internal utilities)
startTopupTracking?(): void

View File

@@ -5,13 +5,7 @@ const state = vi.hoisted(() => ({
activeSidebarTabId: null as string | null,
rightSidePanelOpen: false,
bottomPanelVisible: false,
openWorkflows: [] as unknown[],
mode: { value: 'graph' },
isAppMode: { value: false }
}))
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({ mode: state.mode, isAppMode: state.isAppMode })
openWorkflows: [] as unknown[]
}))
vi.mock('@/platform/settings/settingStore', () => ({
@@ -48,12 +42,12 @@ describe('getShellLayoutSnapshot', () => {
state.rightSidePanelOpen = false
state.bottomPanelVisible = false
state.openWorkflows = []
state.mode.value = 'graph'
state.isAppMode.value = false
})
it('captures the default layout', () => {
expect(getShellLayoutSnapshot()).toEqual({
expect(
getShellLayoutSnapshot({ view_mode: 'graph', is_app_mode: false })
).toEqual({
view_mode: 'graph',
is_app_mode: false,
dock_state: 'docked',
@@ -71,10 +65,10 @@ describe('getShellLayoutSnapshot', () => {
state.rightSidePanelOpen = true
state.bottomPanelVisible = true
state.openWorkflows = [{}, {}, {}]
state.mode.value = 'app'
state.isAppMode.value = true
expect(getShellLayoutSnapshot()).toEqual({
expect(
getShellLayoutSnapshot({ view_mode: 'app', is_app_mode: true })
).toEqual({
view_mode: 'app',
is_app_mode: true,
dock_state: 'floating',

View File

@@ -1,4 +1,3 @@
import { useAppMode } from '@/composables/useAppMode'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -8,11 +7,15 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import type { ShellLayoutMetadata } from '../types'
import { getActionbarDockState } from './getActionbarDockState'
export function getShellLayoutSnapshot(): ShellLayoutMetadata {
const { mode, isAppMode } = useAppMode()
type ShellLayoutMode = Pick<ShellLayoutMetadata, 'view_mode' | 'is_app_mode'>
export function getShellLayoutSnapshot({
view_mode,
is_app_mode
}: ShellLayoutMode): ShellLayoutMetadata {
return {
view_mode: mode.value,
is_app_mode: isAppMode.value,
view_mode,
is_app_mode,
dock_state: getActionbarDockState(),
actionbar_position: useSettingStore().get('Comfy.UseNewMenu'),
active_sidebar_tab: useSidebarTabStore().activeSidebarTabId,

View File

@@ -18,9 +18,9 @@ import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
import { app } from '@/scripts/app'
import { useAppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/composables/useAppMode'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
import type { AppMode } from '@/utils/appMode'
import { t } from '@/i18n'
function createModeTestWorkflow(

View File

@@ -23,7 +23,6 @@ import { app } from '@/scripts/app'
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/composables/useAppMode'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
@@ -37,6 +36,7 @@ import {
appendWorkflowJsonExt,
generateUUID
} from '@/utils/formatUtil'
import type { AppMode } from '@/utils/appMode'
function linearModeToAppMode(linearMode: unknown): AppMode | null {
if (typeof linearMode !== 'boolean') return null

View File

@@ -2,13 +2,13 @@ import { markRaw } from 'vue'
import { t } from '@/i18n'
import type { ChangeTracker } from '@/scripts/changeTracker'
import type { AppMode } from '@/composables/useAppMode'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import { UserFile } from '@/stores/userFileStore'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { MissingModelCandidate } from '@/platform/missingModel/types'
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
import type { MissingNodeType } from '@/types/comfy'
import type { AppMode } from '@/utils/appMode'
export interface InputWidgetConfig {
height?: number

View File

@@ -1,11 +1,12 @@
import { useSettingStore } from '@/platform/settings/settingStore'
import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
import { type StatusWsMessageStatus } from '@/schemas/apiSchema'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
import { useRunButtonTelemetry } from '@/composables/useRunButtonTelemetry'
import { isCloud } from '@/platform/distribution/types'
import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs'
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { WORKFLOW_ACCEPT_STRING } from '@/platform/workflow/core/types/formats'
import { type StatusWsMessageStatus } from '@/schemas/apiSchema'
import { useLitegraphService } from '@/services/litegraphService'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -488,7 +489,9 @@ export class ComfyUI {
textContent: 'Queue Prompt',
onclick: () => {
if (isCloud) {
useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
useRunButtonTelemetry().trackRunButton({
trigger_source: 'legacy_ui'
})
useTelemetry()?.trackWorkflowExecution()
}
app.queuePrompt(0, this.batchCount)
@@ -596,7 +599,9 @@ export class ComfyUI {
textContent: 'Queue Front',
onclick: () => {
if (isCloud) {
useTelemetry()?.trackRunButton({ trigger_source: 'legacy_ui' })
useRunButtonTelemetry().trackRunButton({
trigger_source: 'legacy_ui'
})
useTelemetry()?.trackWorkflowExecution()
}
app.queuePrompt(-1, this.batchCount)

View File

@@ -2,12 +2,7 @@ import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
import type { AppMode } from '@/composables/useAppMode'
import {
getWorkflowMode,
isAppModeValue,
useAppMode
} from '@/composables/useAppMode'
import { useAppMode } from '@/composables/useAppMode'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
@@ -40,6 +35,8 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import { classifyCloudValidationError } from '@/utils/executionErrorUtil'
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
import type { AppMode } from '@/utils/appMode'
import { getWorkflowMode, isAppModeValue } from '@/utils/appMode'
interface ExecutionNodeInfo {
title?: string | null

View File

@@ -1,3 +1,4 @@
import type { ComfyDesktop2Bridge } from '@comfyorg/comfyui-desktop-bridge-types'
import type {
DeviceStats,
EmbeddingsResponse,
@@ -25,6 +26,7 @@ import type {
} from './extensionTypes'
export type { ComfyExtension } from './comfy'
export type { ComfyDesktop2Bridge } from '@comfyorg/comfyui-desktop-bridge-types'
export type { ComfyApi } from '@/scripts/api'
export type { ComfyApp } from '@/scripts/app'
export type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
@@ -88,5 +90,8 @@ declare global {
/** For use in tests to track app initialization state */
__appReadiness?: AppReadiness
__comfyDesktop2?: ComfyDesktop2Bridge
__comfyDesktop2Remote?: boolean
}
}

21
src/utils/appMode.ts Normal file
View File

@@ -0,0 +1,21 @@
export type AppMode =
| 'graph'
| 'app'
| 'builder:inputs'
| 'builder:outputs'
| 'builder:arrange'
type WorkflowModeSource = {
activeMode: AppMode | null
initialMode: AppMode | null | undefined
}
export function getWorkflowMode(
workflow: WorkflowModeSource | null | undefined
): AppMode {
return workflow?.activeMode ?? workflow?.initialMode ?? 'graph'
}
export function isAppModeValue(mode: AppMode): boolean {
return mode === 'app' || mode === 'builder:arrange'
}

View File

@@ -111,7 +111,7 @@ const queueStore = useQueueStore()
const assetsStore = useAssetsStore()
const versionCompatibilityStore = useVersionCompatibilityStore()
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
const { isBuilderMode } = useAppMode()
const { isBuilderMode, mode, isAppMode } = useAppMode()
const { linearMode } = storeToRefs(useCanvasStore())
watch(linearMode, (isLinear) => {
@@ -354,7 +354,12 @@ const onGraphReady = () => {
// Shell layout snapshot, once per session (cloud only)
if (isCloud && telemetry) {
telemetry.trackShellLayout(getShellLayoutSnapshot())
telemetry.trackShellLayout(
getShellLayoutSnapshot({
view_mode: mode.value,
is_app_mode: isAppMode.value
})
)
}
// Setting values now available after comfyApp.setup.