This page is generated from the authoritative declaration file. Do not edit it by hand. Contract SHA-256:
152c7fab547f.SidebarTabBase, MountedSidebarTab, VueSidebarTab, SidebarTabDef, VueComponent, DialogBase, DialogKeyEvent, MountedDialog, VueDialog, DialogDef, DialogHandle, UiHandle, PromptDef, MenuItemDef, MenuDef, MenuHandle, WidgetValue, WidgetOptions, WidgetHandle, WidgetSerializeEvent, Unsubscribe, MountDef, MountedData, MountedValue, CanvasPointerEvent, CanvasTheme, CanvasDef, CanvasHandle, WidgetDef, WidgetCollection, ComboPreviewRegistration, ComboPreviewAssignment, WidgetsHandle, LocalizationMessage, LocalizationCatalog, LocalizationHandle, WidgetTextSelection, WidgetTextEventBase, WidgetTextInputEvent, WidgetTextWheelEvent, WidgetTextKeyEvent, WidgetTextInteractionEvent, WidgetTypeData, WidgetTypeValue, WidgetTypeContext, WidgetTypeDef.
Download the complete contract from the docs source: TypeScript declaration.
Contract
// ─── uiHandle.ts ─────────────────────────────────────────────────
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface SidebarTabBase {
/**
* Unique across every pack, so namespace it — `'mtb.assets'`, not
* `'assets'`. Registering an id twice throws rather than silently replacing
* the other pack's tab.
*/
readonly id: string
readonly title: string
/**
* An iconify class, e.g. `'icon-[lucide--activity]'`. Omit for no icon.
*/
readonly icon?: string
readonly tooltip?: string
}
/**
* A tab the pack draws into a container itself.
*
* Framework-agnostic, and the only form available to a pack that ships
* hand-written ES modules with no build step — which is most of them.
*/
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface MountedSidebarTab extends SidebarTabBase {
/**
* Fills the tab's panel. Called each time the tab becomes visible, so treat
* it as mount rather than as one-time setup, and put teardown in `destroy`.
*/
render(container: HTMLElement): void
/** Releases what `render` retained — listeners, timers, observers. */
destroy?(): void
}
/**
* A tab that is a Vue component, mounted and torn down by the host.
*
* The preferred form where a pack can build. It keeps reactivity, scoped
* styles and `onUnmounted`, and the host mounts and unmounts it.
*
* Per ADR 0005 the pack bundles its own Vue (~30KB gzipped) — there is no
* import map, so `import { defineComponent } from 'vue'` resolves at the
* pack's build time, not ours. That is a second Vue instance on the page,
* which the ADR weighed and accepted; nothing is shared across the boundary,
* so the two runtimes never touch.
*/
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface VueSidebarTab extends SidebarTabBase {
readonly component: VueComponent
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export type SidebarTabDef = MountedSidebarTab | VueSidebarTab
/** A Vue component bundled by the pack. */
/** @knipIgnoreUnusedButUsedByCustomNodes */
export type VueComponent = object
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface DialogBase {
/**
* Unique across every pack, so namespace it. The host prefixes it with
* `extension-`, which keeps packs out of the internal dialog keyspace.
*/
readonly key: string
readonly title?: string
}
/** A bounded keyboard event captured while a mounted dialog owns focus. */
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface DialogKeyEvent {
readonly key: string
readonly code: string
readonly repeat: boolean
readonly altKey: boolean
readonly ctrlKey: boolean
readonly metaKey: boolean
readonly shiftKey: boolean
/** True for an input, textarea, select, or editable content target. */
readonly editableTarget: boolean
}
/** A dialog the pack draws into a container itself. */
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface MountedDialog extends DialogBase {
render(container: HTMLElement): void
/** Receives dialog-scoped key events even before a child takes focus. */
onKeyDown?(event: DialogKeyEvent): void | Promise<void>
destroy?(): void
}
/** A dialog that is a Vue component, mounted and torn down by the host. */
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface VueDialog extends DialogBase {
readonly component: VueComponent
readonly props?: Readonly<Record<string, unknown>>
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export type DialogDef = MountedDialog | VueDialog
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface DialogHandle {
close(): void
}
export interface UiHandle {
/**
* Adds a tab to the sidebar. Returns a function that removes it again.
*/
addSidebarTab(def: SidebarTabDef): Unsubscribe
/**
* Shows a small readout in the top bar — a status, a count, a live metric.
*
* Replaces `app.menu.settingsGroup` and inserting an element next to
* `.comfy-settings-btn`. Declarative on purpose: the pack says what to show
* and the host renders it, in house style and at whatever size the viewport
* allows. Nothing here takes an element, a class or a style, which is what
* keeps the chrome ours to restyle.
*
* Returns a handle rather than an unsubscribe: for a value that changes,
* call `update({ text })`. A closure would not work — the host renders when
* reactive state changes and cannot see a plain function, so the readout
* would show its first value forever.
*/
addTopBarBadge(badge: BadgeContribution): ChromeItemHandle<BadgeContribution>
/**
* Adds a button to the action bar. `run` is called on click.
*
* For a pack that also wants a keyboard shortcut or a palette entry,
* register a command and call it from `run`, rather than duplicating the
* behaviour in both places.
*/
addActionBarButton(
button: ButtonContribution
): ChromeItemHandle<ButtonContribution>
/**
* Opens a modal dialog. Returns a handle that closes it again.
*
* Replaces `app.ui.dialog` and the `new app.ui.dialog.constructor()` idiom.
* Several conversions hand-rolled a native `<dialog>` or borrowed core's
* `.comfy-modal` class names instead — the latter couples a pack to markup
* we rename freely, so both are worth retiring.
*/
showDialog(def: DialogDef): DialogHandle
/**
* Shows a menu where the user clicked.
*
* `b.addMenuItem` is the node's own context menu — a different menu, on a
* different target, opened by the host. This is for a menu a pack raises
* itself: a lora row's Move Up / Remove, a chip that picks an output type.
* Four files hand-rolled it by constructing the renderer's menu class
* directly, which pins them to a renderer we intend to replace.
*
* Positioned from the event so the menu lands under the pointer, which is the
* only placement that reads as a context menu. Arrow keys traverse nested
* items, Enter or Tab selects one, and Escape closes the menu.
*/
showMenu(def: MenuDef): MenuHandle
/**
* Asks the user for a value. Resolves `undefined` if they cancel.
*
* Packs called `canvas.prompt(...)`, which draws a small field at the cursor
* — clicking a lora's strength to type a new one. That field belongs to the
* legacy canvas and the host itself no longer uses it; this is the prompt the
* host does use, so a pack keeps the capability and loses only the placement.
*/
prompt(def: PromptDef): Promise<string | undefined>
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface PromptDef {
/** What is being asked for — "Strength", "Label". */
readonly label: string
readonly value?: string
readonly placeholder?: string
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface MenuItemDef {
readonly label: string
/** Shown but not selectable. */
readonly disabled?: boolean
/** A nested menu. Mutually exclusive with {@link run}. */
readonly submenu?: readonly MenuItemDef[]
run?(): void
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface MenuDef {
readonly items: readonly MenuItemDef[]
/** Shown above the items. */
readonly title?: string
/** The event that asked for the menu; it decides where the menu appears. */
readonly event: MouseEvent
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface MenuHandle {
close(): void
}
// ─── widgetHandle.ts ─────────────────────────────────────────────
// `null` is included because core's own `WidgetValue` has it and
// `addWidget('button', name, null, cb)` produced exactly that. Omitting it made
// a null value inexpressible through the published API, so a converted button's
// `widgets_values` entry changed and the saved workflow differed.
export type WidgetValue = string | number | boolean | object | undefined | null
/** Options understood by core or by a widget type declared by the pack. */
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetOptions {
readonly [key: string]: unknown
readonly on?: string
readonly off?: string
readonly max?: number
readonly min?: number
readonly precision?: number
readonly read_only?: boolean
readonly step?: number
readonly step2?: number
readonly multiline?: boolean
readonly property?: string
readonly socketless?: boolean
readonly canvasOnly?: boolean
readonly hideInPanel?: boolean
readonly nodeType?: string
readonly serialize?: boolean
readonly values?: unknown
readonly iconClass?: string
readonly disabled?: boolean
readonly useGrouping?: boolean
readonly placeholder?: string
readonly showThumbnails?: boolean
readonly showItemNavigators?: boolean
readonly hidden?: boolean
}
/**
* Shapes follow `src/types/extensionV2.ts`, the agreed extension contract.
*
* Accessor methods rather than properties, so a read can be a store query and
* a write can dispatch a command.
*/
export interface WidgetHandle extends HandleCommon {
readonly name: string
readonly widgetType: string
getValue<T = WidgetValue>(): T
/**
* Commits a value exactly as a user edit does: the value is written, a
* widget bound to a node property syncs it, the widget's callback chain and
* the node's `onWidgetChanged` run, and `graph.version` advances. This
* replaces the manual pair `widget.value = x; widget.callback?.(x)` — and
* the bare write too, because a write the rest of the system cannot see was
* never a feature, it was litegraph defaulting to inconsistency.
*
* Writing the current value again is a no-op, which is also what ends a
* cycle of handlers writing to each other. `on('change')` fires once per
* commit; `on('activate')` does not fire, because activate reports a user's
* act.
*/
setValue(value: WidgetValue): void
/**
* The widgets core attached to this one — a seed's `control_after_generate`,
* a bounding box's components.
*
* `setHidden` already cascades through these, so hiding needs no call here.
* What does is reading one: a pack asks a seed's control widget whether it
* says `fixed` or `randomize` to know what the node will do next.
*/
linked(): readonly WidgetHandle[]
/**
* Replaces the controls attached to this widget.
*
* Core uses this relationship for compound inputs: hiding a seed also hides
* its `control_after_generate` picker. Packs build the same compound control
* when they add a random-seed button or an index policy, and assigning
* `linkedWidgets` directly was the only way to make conversion-to-input hide
* the whole unit.
*
* Every name must identify another widget on this node. Pass an empty array
* to clear the relationship.
*/
setLinked(names: readonly string[]): void
isHidden(): boolean
/**
* Replaces the `type = 'converted-widget'` hack. Value is retained.
*
* Cascades to the widgets core attached to this one — a seed's
* `control_after_generate`, a bounding box's components. The legacy
* `hideWidget` helper this replaces recursed through `linkedWidgets`, and
* packs that lost the cascade were left with an orphaned control widget
* floating where its owner used to be.
*/
setHidden(hidden: boolean): void
getOptions(): Readonly<WidgetOptions> | undefined
setOption(key: string, value: unknown): void
setLabel(label: string): void
isDisabled(): boolean
setDisabled(disabled: boolean): void
isSerialized(): boolean
/** The height the host most recently allocated, or undefined before layout. */
getHeight(): number | undefined
/**
* Pins the widget's height in graph units, instead of letting it share
* whatever space the node has spare.
*
* The node divides free height between every widget that does not state one,
* so a node carrying two mounted strips gave each half the node however
* small they were meant to be. `MountDef.height` does not do this — it sets
* the container's CSS height *inside* an allocation the renderer already
* chose, which is why a fixed strip still drifted.
*
* Replaces re-assigning `node.computeSize`, which is what packs did and
* which is not published. Omit it for a panel meant to fill the node: the
* growable path is the one that fills.
*/
setHeight(px: number): void
/**
* Replaces capture-and-chain on `widget.callback`, which 1,000+ sites do and
* which silently drops an earlier pack's listener whenever one forgets to
* call through. Listeners here are additive and independent.
*/
on(
event: 'change',
listener: (value: WidgetValue, oldValue: WidgetValue) => void
): Unsubscribe
on(event: 'removed', listener: () => void): Unsubscribe
/**
* The widget was activated — a button click, or a value committed.
*
* Buttons carry no value, so `change` can never fire for one and a button
* created through this API would otherwise be inert. Prefer `change` when you
* care about the value; use this when you care that the user acted — a
* programmatic `setValue` never fires it.
*/
on(event: 'activate', listener: (value: WidgetValue) => void): Unsubscribe
/**
* Contributes behavior to a host-owned multiline text editor without exposing
* its DOM. The event reports the live value and caret on each input,
* selection change, or wheel gesture; its write method preserves both the
* widget commit protocol and the requested selection.
*/
on(
event: 'textInteraction',
listener: (event: WidgetTextInteractionEvent) => void
): Unsubscribe
/**
* The value is about to be written out, and may be replaced for this
* destination only.
*
* This is what `widget.serializeValue` did, and the reason it is back: a
* static `serialize` flag can only *suppress* a value, and a whole class of
* packs needs to *supply* a different one. rgthree's Seed keeps the sentinel
* `-1` in the saved workflow and sends the rolled seed; pysssss' PresetText
* expands `@name` into the queued prompt while the user keeps seeing the
* reference; Impact Pack embeds image data the canvas never shows.
*
* `context` says which destination is being built, because those packs want
* to change one and not the other:
*
* - `'workflow'` — the file the user saves.
* - `'prompt'` — the queued API payload the backend executes.
* - `'embedded'` — the copy of the workflow that travels with that prompt
* and is written into the output image. Distinct from `'workflow'`
* because a pack may want the image to reproduce the run while the saved
* file keeps its sentinel: rgthree's Seed saves `-1` but embeds the seed
* it actually rolled, so dragging the PNG back in reproduces it.
*
* A handler that ignores `context` changes all three.
*
* Calling `setSerializedValue` replaces the value for this write only; the
* widget itself is untouched, so the user still sees what they typed. Last
* handler to call it wins.
*/
on(
event: 'beforeSerialize',
listener: (event: WidgetSerializeEvent) => void
): Unsubscribe
}
/** Where a value is being written, and the chance to change it. */
export interface WidgetSerializeEvent {
readonly context: 'workflow' | 'prompt' | 'embedded'
/** What would be written if no handler intervened. */
readonly value: WidgetValue
setSerializedValue(value: WidgetValue): void
}
export type Unsubscribe = () => void
/**
* A widget whose body the pack renders itself.
*
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export interface MountDef {
readonly name: string
/**
* Fills the mounted container. Called once, with an element already attached
* to the node.
*
* `value` holds meaningful serialized state only when `defaultValue` was
* given. A decorative mount receives the same accessor for one render shape,
* but should not use it as storage.
*/
render(container: HTMLElement, value: MountedValue): void
/** Releases anything `render` retained — listeners, timers, observers. */
destroy?(): void
/** Reserved height in graph units. Omit to size to content. */
readonly height?: number
/** Set false to keep the element rendered at low zoom. Defaults to true. */
readonly hideOnZoom?: boolean
readonly hidden?: boolean
/**
* Whether the value is written into the saved workflow.
*
* Defaults to `true` when `defaultValue` makes this a value-holding control,
* and to `false` for a decorative mount.
*/
readonly serialize?: boolean
/**
* Whether the value is sent in the API prompt. Defaults to `serialize`.
*
* These are two different flags in litegraph — `widget.serialize` gates the
* saved workflow, `options.serialize` gates the prompt — and collapsing them
* into one boolean made two states unsayable. "Saved but not sent" is the
* one packs need: it is exactly what the legacy
* `addDOMWidget(…, { serialize: false })` did, and a readout that a node
* fills in from its own execution result belongs in the workflow but has no
* business appearing as an input on the next queue.
*
* Set it apart from `serialize` only when the two genuinely differ.
*/
readonly sendToPrompt?: boolean
/**
* Makes this a value-holding widget rather than decoration.
*
* Without it a mount is a drawing: it can occupy a `widgets_values` slot but
* has nothing to put in it, so a colour picker or a text box converted onto
* `mount` kept its position and silently lost what the user typed. Supplying
* a default gives the widget a real cell, reachable through `render`'s second
* argument.
*/
readonly defaultValue?: MountedData
}
/** What a mounted control can hold. @knipIgnoreUnusedButUsedByCustomNodes */
export type MountedData = string | number | boolean | object | null
/**
* Reading and writing a mounted widget's value.
*
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export interface MountedValue {
get(): MountedData
set(value: MountedData): void
/** Notified when the value changed elsewhere — a workflow load. */
onChange(listener: (value: MountedData) => void): Unsubscribe
}
/**
* A pointer event on the widget's own canvas, in the same units `draw` uses.
*
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export interface CanvasPointerEvent {
/** Distance from the canvas's left edge, in CSS pixels. */
readonly x: number
/** Distance from its top edge, in CSS pixels. */
readonly y: number
/** The DOM event, for modifier keys, `button`, and `preventDefault()`. */
readonly event: PointerEvent
}
/**
* The colours a pack should draw its own controls in.
*
* Published because we told packs to draw. A widget that hardcodes its palette
* looks wrong the moment the user switches theme, and the alternative — reading
* `LiteGraph.WIDGET_BGCOLOR` and friends — is a renderer constant we intend to
* delete. These are the design system's own tokens, resolved from the widget's
* computed style, so they follow the theme without the pack knowing which one
* is active.
*
* Named by intent rather than by token, because the token names will churn and
* a pack should not have to follow. Re-read on every draw, so a theme switch
* needs nothing from the pack.
*
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export interface CanvasTheme {
/** A control's background. */
readonly surface: string
/** The same under the pointer. */
readonly surfaceHovered: string
/** A control's outline. */
readonly border: string
/** A label. */
readonly text: string
/** A value, a unit, anything the label outranks. */
readonly textSecondary: string
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface CanvasDef {
readonly name: string
/** Reserved height in pixels. Omit to size to the node's width. */
readonly height?: number
draw(
context: CanvasRenderingContext2D,
size: readonly [number, number],
theme: CanvasTheme,
value: MountedValue | undefined
): void
/**
* The pointer went down on this widget.
*
* Coordinates are relative to the canvas and in the same units `draw`
* receives, so a hit test written against the drawing works unchanged —
* which is the point. A pack that drew its own controls keeps both the
* drawing and the hit testing; only the surface changes, from the host's
* canvas to its own.
*
* The primary button is taken: it stops here rather than also reaching the
* node, or adjusting a slider would drag the node underneath it. Middle and
* right are left alone, so panning and the context menu still work over the
* widget.
*
* The pointer is captured for the gesture, so a drag that leaves the widget
* still reports moves and the release.
*/
onPointerDown?(event: CanvasPointerEvent): void
/** Moves during a drag, and hover when no button is down. */
onPointerMove?(event: CanvasPointerEvent): void
onPointerUp?(event: CanvasPointerEvent): void
/**
* The secondary button went down on this widget.
*
* Right-click is left alone by {@link onPointerDown} so the node's own
* context menu keeps working over a widget, which is right by default and
* wrong for a widget that has its own menu — a lora row wants Move Up, Move
* Down, Remove. Declaring this claims the gesture: the browser menu is
* suppressed and the node's does not open.
*/
onContextMenu?(event: CanvasPointerEvent): void
/**
* Makes the surface hold a value rather than only draw one.
*
* Without it a drawn control that stores something has to be two widgets — a
* hidden value widget and a surface — and two widgets cannot occupy the one
* position the original had. That is not a tidiness point: `serialize` writes
* at each widget's own index and leaves a hole where a non-serializing widget
* sits, so the pair has to be ordered value-first to keep the saved array
* intact, and a pack that gets that wrong writes a null into every workflow
* the node has ever appeared in. It moved rgthree's Power Puter chip row
* below its code box.
*
* `draw` receives the current value as its fourth argument.
*/
readonly defaultValue?: MountedData
/** Whether the value reaches the saved workflow. See {@link MountDef.serialize}. */
readonly serialize?: boolean
/** Whether the value reaches the API prompt. See {@link MountDef.sendToPrompt}. */
readonly sendToPrompt?: boolean
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface CanvasHandle {
readonly widget: WidgetHandle
/** Redraws now. Call when the data behind the drawing changed. */
redraw(): void
}
/** Everything needed to create a widget. */
export interface WidgetDef {
readonly type: string
readonly name: string
readonly value?: WidgetValue
readonly options?: WidgetOptions
/** Display-only widgets — replaces the readOnly/opacity DOM fiddling. */
readonly disabled?: boolean
readonly hidden?: boolean
/**
* Whether the value is written into the saved workflow.
*
* Replaces `widget.serializeValue = async () => {}`, the idiom packs use to
* keep a derived readout out of `widgets_values`. Orthogonal to `hidden`.
*/
readonly serialize?: boolean
}
export interface WidgetCollection {
readonly length: number
get(name: string): WidgetHandle | undefined
at(index: number): WidgetHandle | undefined
all(): readonly WidgetHandle[]
names(): readonly string[]
/**
* Replaces splice/assign reordering. `names` must be a permutation of the
* current names — a partial list throws rather than silently dropping
* widgets, which is how the array-splice idiom lost them.
*/
reorder(names: readonly string[]): void
move(name: string, toIndex: number): void
/**
* Creates a widget on this node.
*
* The counterpart to `remove` — packs that rebuild a readout widget do
* remove-then-create, and without this only half the operation has a
* destination, which makes the conversion cosmetic.
*/
add(def: WidgetDef): WidgetHandle
/**
* Mounts an element on the node and hands it to the pack to fill.
*
* The replacement for `addDOMWidget`, and the destination for hand-painted
* canvas controls. Across kjnodes' canvas editors the drawing is rectangles,
* images, straight lines and text — all DOM primitives — but a pack that
* wants to keep its existing `ctx` code can append a `<canvas>` to the
* container and carry it over unchanged.
*
* The gain is not the drawing, it is the input: these editors hand-roll
* hit-testing against bounding boxes because canvas gives them nothing to
* attach a listener to. Mounted in the DOM, pointer events land on the
* element and most of that code goes away.
*/
mount(def: MountDef): WidgetHandle
/**
* A per-node drawing surface, and the destination for `onDrawForeground`.
*
* Works under both renderers without the pack knowing which it is on: the
* canvas is a DOM element, which the legacy renderer positions over the
* graph canvas and Nodes 2.0 renders directly. That is the whole reason it
* is a mounted element rather than a hook into the graph's own context —
* drawing into the shared context is what ties a pack to the old renderer.
*
* `draw` is called on mount, on resize, and whenever `redraw()` is called.
*/
canvas(def: CanvasDef): CanvasHandle
remove(name: string): boolean
[Symbol.iterator](): Iterator<WidgetHandle>
}
export interface ComboPreviewRegistration {
/** Namespaced registration id. */
readonly id: string
/** Managed model catalogues searched in order. */
readonly modelCategories: readonly (
| 'loras'
| 'checkpoints'
| 'unet'
| 'diffusion_models'
)[]
/** Model filename suffixes that activate this policy. */
readonly extensions: readonly (
| 'safetensors'
| 'sft'
| 'pt'
| 'ckpt'
| 'gguf'
)[]
/** Host-owned adjacent-preview lookup policy. */
readonly candidatePolicy: 'adjacent-model-preview-v1'
/** Preview media types the host may display. */
readonly media: readonly (
| 'image/png'
| 'image/webp'
| 'image/jpeg'
| 'video/mp4'
| 'video/webm'
)[]
}
export interface ComboPreviewAssignment {
/** Managed model catalogue containing `modelValue`. */
readonly category: 'loras' | 'checkpoints' | 'unet' | 'diffusion_models'
/** Logical model filename from the managed combo; never a host path. */
readonly modelValue: string
/** Graph node whose host-owned output image is used as the preview. */
readonly sourceNodeId: string
/** Exact image in that node's current host-owned output list. */
readonly imageIndex: number
readonly policy: 'adjacent-model-preview-v1'
}
export interface WidgetsHandle {
/**
* Adds a declarative preview policy to host-owned combo option menus.
* The host resolves managed assets and renders the hover surface; the pack
* receives neither filesystem paths nor media URLs.
*/
registerComboPreview(definition: ComboPreviewRegistration): Unsubscribe
/**
* Re-encodes one managed graph output as an adjacent managed-model preview.
* The host resolves both resources; the pack receives no path or image bytes.
*/
assignComboPreview(assignment: ComboPreviewAssignment): Promise<void>
}
export type LocalizationMessage =
| string
| null
| { readonly [key: string]: LocalizationMessage }
export interface LocalizationCatalog {
/** Native vue-i18n-shaped messages such as main/nodeDefs/nodeCategories. */
readonly messages: Readonly<Record<string, LocalizationMessage>>
/** Exact-source fallback translations used only at host-owned render points. */
readonly phrases?: Readonly<Record<string, string>>
}
export interface LocalizationHandle {
/**
* Contributes one bounded catalog for a host-supported locale. The host
* owns merging, rendering, precedence, and cleanup; no DOM access is given.
*/
registerCatalog(locale: string, catalog: LocalizationCatalog): Unsubscribe
}
// ─── widgetTextInteraction.ts ────────────────────────────────────
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetTextSelection {
readonly start: number
readonly end: number
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetTextEventBase {
readonly value: string
readonly selection: WidgetTextSelection
/** Positions a host menu at the text editor without exposing its element. */
readonly menuEvent: MouseEvent
/** Commits through the widget protocol and optionally restores the caret. */
setValue(value: string, selection?: WidgetTextSelection): void
focus(): void
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetTextInputEvent extends WidgetTextEventBase {
readonly kind: 'input' | 'selection'
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetTextWheelEvent extends WidgetTextEventBase {
readonly kind: 'wheel'
readonly deltaY: number
readonly ctrlKey: boolean
/** Claims the wheel gesture so the canvas does not pan or zoom. */
preventDefault(): void
}
/** @knipIgnoreUnusedButUsedByCustomNodes */
export interface WidgetTextKeyEvent extends WidgetTextEventBase {
readonly kind: 'keydown'
readonly key: string
readonly ctrlKey: boolean
readonly altKey: boolean
readonly shiftKey: boolean
readonly metaKey: boolean
readonly repeat: boolean
preventDefault(): void
stopPropagation(): void
}
/**
* An interaction with a host-owned multiline text editor.
*
* This is the renderer-independent replacement for reaching through
* `widget.inputEl`: packs can inspect the live caret, offer a menu through
* `menuEvent`, replace text, and implement selection-based wheel edits without
* receiving the host's element or markup.
*/
export type WidgetTextInteractionEvent =
| WidgetTextInputEvent
| WidgetTextWheelEvent
| WidgetTextKeyEvent
// ─── widgetTypes.ts ──────────────────────────────────────────────
/** What a pack-declared widget can hold. */
export type WidgetTypeData = string | number | boolean | object | null
/**
* Reading and writing the widget's value, for the renderer to bind to.
*
* @knipIgnoreUnusedButUsedByCustomNodes
*/
export interface WidgetTypeValue {
get(): WidgetTypeData
set(value: WidgetTypeData): void
/** Notified when the value changes for any other reason — a workflow load. */
onChange(listener: (value: WidgetTypeData) => void): Unsubscribe
}
export interface WidgetTypeContext {
/** A frozen snapshot of the input declaration's current options. */
getOptions(): Readonly<Record<string, unknown>>
/**
* Runs while the widget's owning node belongs to a graph.
*
* Widget constructors run before a node has an id or graph, so a node handle
* cannot be supplied directly to `render`. The listener runs after the node
* joins a graph and tears down when it leaves.
*/
onNodeReady(listener: (node: NodeHandle) => Unsubscribe | void): Unsubscribe
}
export interface WidgetTypeDef {
/** Used when the definition supplies none. */
readonly defaultValue?: WidgetTypeData
/** Height in pixels. Omit to size to content. */
readonly height?: number
/** Smallest width the control needs, in pixels. */
readonly minWidth?: number
/** Smallest height the control needs, in pixels. */
readonly minHeight?: number
/**
* Whether the value is saved and sent. Defaults to `true`: this widget holds
* a real input value, unlike a mounted decoration.
*/
readonly serialize?: boolean
/**
* Fills the container. Return a teardown if the control owns listeners,
* timers or observers.
*
* `name` is the input being rendered — controls commonly label themselves
* with it, which a type-level renderer otherwise has no way to know.
*/
render(
container: HTMLElement,
value: WidgetTypeValue,
name: string,
context: WidgetTypeContext
): Unsubscribe | void
}