Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,6 @@
"@primevue/themes": "catalog:",
"@sentry/vue": "catalog:",
"@sparkjsdev/spark": "catalog:",
"@tiptap/core": "catalog:",
"@tiptap/extension-link": "catalog:",
"@tiptap/extension-table": "catalog:",
"@tiptap/extension-table-cell": "catalog:",
"@tiptap/extension-table-header": "catalog:",
"@tiptap/extension-table-row": "catalog:",
"@tiptap/pm": "catalog:",
"@tiptap/starter-kit": "catalog:",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@xterm/addon-fit": "^0.10.0",
Expand Down Expand Up @@ -105,7 +97,6 @@
"reka-ui": "catalog:",
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"typegpu": "catalog:",
"vue": "catalog:",
"vue-i18n": "catalog:",
Expand Down
623 changes: 0 additions & 623 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ catalog:
'@storybook/vue3': ^10.2.10
'@storybook/vue3-vite': ^10.2.10
'@tailwindcss/vite': ^4.2.0
'@tiptap/core': ^2.27.2
'@tiptap/extension-link': ^2.27.2
'@tiptap/extension-table': ^2.27.2
'@tiptap/extension-table-cell': ^2.27.2
'@tiptap/extension-table-header': ^2.27.2
'@tiptap/extension-table-row': ^2.27.2
'@tiptap/pm': 2.27.2
'@tiptap/starter-kit': ^2.27.2
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^24.1.0
Expand Down
184 changes: 184 additions & 0 deletions src/composables/element/useDomValueBridge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { effectScope, watch } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { useDomValueBridge } from './useDomValueBridge'

describe('useDomValueBridge', () => {
let element: HTMLTextAreaElement

beforeEach(() => {
element = document.createElement('textarea')
element.value = 'initial'
})

it('reads initial element value', () => {
const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(element)
expect(ref.value).toBe('initial')
})
scope.stop()
})

it('detects programmatic element.value writes', () => {
const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(element)
const spy = vi.fn()
watch(ref, spy, { flush: 'sync' })

element.value = 'programmatic'

expect(ref.value).toBe('programmatic')
expect(spy).toHaveBeenCalledWith(
'programmatic',
'initial',
expect.anything()
)
})
scope.stop()
})

it('detects user input events', () => {
const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(element)
const spy = vi.fn()
watch(ref, spy, { flush: 'sync' })

const nativeDesc = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value'
)!
nativeDesc.set!.call(element, 'typed')
element.dispatchEvent(new Event('input'))

expect(ref.value).toBe('typed')
expect(spy).toHaveBeenCalled()
})
scope.stop()
})

it('setting ref updates element value', () => {
const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(element)
ref.value = 'from-ref'
expect(element.value).toBe('from-ref')
})
scope.stop()
})

it('chains through existing Object.defineProperty on element', () => {
const existingSetter = vi.fn()

const nativeDesc = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value'
)!
Object.defineProperty(element, 'value', {
configurable: true,
get() {
return nativeDesc.get!.call(element)
},
set(v: string) {
existingSetter(v)
nativeDesc.set!.call(element, v)
}
})

const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(element)

element.value = 'new'
expect(existingSetter).toHaveBeenCalledWith('new')
expect(ref.value).toBe('new')
})
scope.stop()
})

it('restores previous descriptor on scope dispose', () => {
const scope = effectScope()
scope.run(() => {
useDomValueBridge(element)
})

const duringDesc = Object.getOwnPropertyDescriptor(element, 'value')
expect(duringDesc).toBeDefined()

scope.stop()

const afterDesc = Object.getOwnPropertyDescriptor(element, 'value')
expect(afterDesc).toBeUndefined()
})

it('restores existing override descriptor on scope dispose', () => {
const nativeDesc = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value'
)!
const customGetter = vi.fn(() => nativeDesc.get!.call(element))

Object.defineProperty(element, 'value', {
configurable: true,
get: customGetter,
set(v: string) {
nativeDesc.set!.call(element, v)
}
})

const scope = effectScope()
scope.run(() => {
useDomValueBridge(element)
})
scope.stop()

element.value
expect(customGetter).toHaveBeenCalled()
})

it('handles non-configurable descriptor without throwing', () => {
const nativeDesc = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value'
)!
Object.defineProperty(element, 'value', {
configurable: false,
get() {
return nativeDesc.get!.call(element)
},
set(v: string) {
nativeDesc.set!.call(element, v)
}
})

const scope = effectScope()
expect(() => {
scope.run(() => {
const ref = useDomValueBridge(element)
expect(ref.value).toBe('initial')
})
}).not.toThrow()
scope.stop()

const afterDesc = Object.getOwnPropertyDescriptor(element, 'value')
expect(afterDesc).toBeDefined()
expect(afterDesc!.configurable).toBe(false)
})

it('works with HTMLInputElement', () => {
const input = document.createElement('input')
input.value = 'input-initial'

const scope = effectScope()
scope.run(() => {
const ref = useDomValueBridge(input)
expect(ref.value).toBe('input-initial')

input.value = 'input-updated'
expect(ref.value).toBe('input-updated')
})
scope.stop()
})
})
77 changes: 77 additions & 0 deletions src/composables/element/useDomValueBridge.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should come up with a clean API for extension authors so we can add a proper deprecation warning here. How to wire up your custom widget to the widgetValueStore properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that deserves its own effort. This PR keeps the existing escape hatch working but a proper public API for extension widget authors would be the right long-term fix.

@coderabbitai Make an Issue to design a clean public API for extension authors to wire custom widgets to the widgetValueStore, replacing the current value-descriptor pattern with a supported integration point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@christian-byrne I'll create that issue for you.


🏁 Script executed:

#!/bin/bash
# Create GitHub issue for designing a clean public API for extension widget integration

gh issue create \
  --title "Design clean public API for extension authors to integrate custom widgets with widgetValueStore" \
  --body "## Context

PR `#9230` introduces \`useDomValueBridge\` to synchronize DOM widget values with the \`widgetValueStore\` by intercepting \`Object.defineProperty\` on element.value and listening for input events. While this approach works as a bridge for extension-created DOM widgets, it relies on internal value-descriptor patterns that are not part of a supported public API.

## Problem

Extension authors currently lack a clean, documented integration point to wire custom widgets to the \`widgetValueStore\`. The current value-descriptor pattern is an implementation detail rather than a stable extension API.

## Proposed Solution

Design and implement a proper public API that:
- Provides a clear, documented way for extension authors to integrate custom widgets with the store
- Replaces the current value-descriptor pattern with a supported integration mechanism
- Maintains backward compatibility where feasible
- Includes documentation and migration guidance

## References

- PR: https://github.com/Comfy-Org/ComfyUI_frontend/pull/9230
- Discussion: https://github.com/Comfy-Org/ComfyUI_frontend/pull/9230#discussion_r2857107802
- Requested by: `@christian-byrne`

## Acceptance Criteria

- [ ] Public API design documented
- [ ] Extension author documentation created
- [ ] Deprecation path defined for current pattern (if applicable)
- [ ] Implementation plan outlined" \
  --assignee christian-byrne

Length of output: 57


@christian-byrne Issue created!

I've opened #9296 to track the design of a clean public API for extension authors to integrate custom widgets with the widgetValueStore. The issue captures the current value-descriptor pattern as a temporary workaround and outlines the need for a proper supported integration point with documentation and migration guidance.


🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.vue : Utilize ref and reactive for reactive state

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8753
File: src/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue:17-28
Timestamp: 2026-02-09T03:24:50.064Z
Learning: In this codebase: When destructuring reactive properties from Pinia stores, use `storeToRefs()` to maintain reactivity. Example: `const canvasStore = useCanvasStore(); const { canvas } = storeToRefs(canvasStore)`. Then access with `.value` (e.g., `canvas.value`).

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T21:34:45.361Z
Learning: Applies to src/**/*.ts : Use `ref` for reactive state, `computed()` for derived values, and `watch`/`watchEffect` for side effects

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8594
File: src/lib/litegraph/src/widgets/BaseWidget.ts:146-155
Timestamp: 2026-02-06T09:27:31.576Z
Learning: In the ComfyUI frontend codebase: `LGraphNode.removeWidget()` (and the `widget.onRemove?.()` hook) is called during subgraph conversion operations where widgets are being moved or restructured, not just when they're being permanently deleted. Therefore, widget unregistration from `widgetValueStore` should not be tied to `onRemove`, as this would cause premature state loss during legitimate graph restructuring operations.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-23T21:34:45.361Z
Learning: Applies to src/**/*.ts : Minimize addition of new refs or other state; if possible to accomplish with just a prop, don't add a `ref`; if possible to use ref/prop directly, don't add a `computed`; if possible to use `computed` for derived values, don't use a `watch`

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.{vue,ts} : Leverage VueUse functions for performance-enhancing styles

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: JemiloII
Repo: Comfy-Org/ComfyUI_frontend PR: 8282
File: src/composables/usePaste.test.ts:372-383
Timestamp: 2026-01-23T21:06:15.240Z
Learning: In paste/clipboard handling code, do not rely on ephemeral DataTransfer data across async boundaries. If an operation awaits async tasks (such as node creation) during a paste, clone the DataTransfer (e.g., using cloneDataTransfer) to persist files and string data before the paste event completes. This ensures clipboard contents remain available for subsequent processing.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Ref } from 'vue'
import { customRef, onScopeDispose } from 'vue'

import { useEventListener } from '@vueuse/core'

type ValueElement = HTMLInputElement | HTMLTextAreaElement

export function useDomValueBridge(element: ValueElement): Ref<string> {
const proto = Object.getPrototypeOf(element)
const nativeDescriptor = Object.getOwnPropertyDescriptor(proto, 'value')
const existingDescriptor = Object.getOwnPropertyDescriptor(element, 'value')

const prevGet = existingDescriptor?.get ?? nativeDescriptor?.get
const prevSet = existingDescriptor?.set ?? nativeDescriptor?.set

if (!prevGet || !prevSet) {
return customRef((track, trigger) => ({
get() {
track()
return element.value
},
set(v: string) {
element.value = v
trigger()
}
}))
}

let notifyChange: (() => void) | undefined

const ref = customRef<string>((track, trigger) => {
notifyChange = trigger
return {
get() {
track()
return prevGet.call(element)
},
set(v: string) {
prevSet.call(element, v)
trigger()
}
}
})

const canOverride =
existingDescriptor == null || existingDescriptor.configurable !== false

if (canOverride) {
Object.defineProperty(element, 'value', {
configurable: true,
enumerable: true,
get() {
return prevGet.call(element)
},
set(v: string) {
prevSet.call(element, v)
notifyChange?.()
}
})
}

useEventListener(element, 'input', () => {
notifyChange?.()
})

onScopeDispose(() => {
if (!canOverride) return

if (existingDescriptor) {
Object.defineProperty(element, 'value', existingDescriptor)
} else {
delete (element as unknown as Record<string, unknown>).value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the cast is unfortunate but necessary — delete only works on index-signature types, and HTMLTextAreaElement does not have one. This restores the element to its prototype descriptor.

}
})

return ref
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'

Expand Down Expand Up @@ -26,6 +27,9 @@ function mountComponent(
placeholder?: string
) {
return mount(WidgetTextarea, {
global: {
plugins: [createTestingPinia()]
},
props: {
widget,
modelValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
:class="
cn(
WidgetInputBaseClass,
'size-full text-xs resize-none',
'comfy-multiline-input size-full text-xs resize-none',
!hideLayoutField && 'pt-5'
)
"
:placeholder
:readonly="isReadOnly"
:spellcheck
data-testid="dom-widget-textarea"
data-capture-wheel="true"
@pointerdown.capture.stop
@pointermove.capture.stop
Expand All @@ -40,6 +42,7 @@
import { computed, useId } from 'vue'

import Textarea from '@/components/ui/textarea/Textarea.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { useHideLayoutField } from '@/types/widgetTypes'
import { cn } from '@/utils/tailwindUtil'
Expand Down Expand Up @@ -69,4 +72,9 @@ const id = useId()
const isReadOnly = computed(
() => widget.options?.read_only ?? widget.options?.disabled ?? false
)

const settingStore = useSettingStore()
const spellcheck = computed(() =>
settingStore.get('Comfy.TextareaWidget.Spellcheck')
)
</script>
Loading
Loading