Skip to content

ChatInputWidget

Defined in: src/widgets/controls/chat-input.ts:99

A framework-agnostic chat composer for AI-agent UIs. It owns its own edit buffer (so it works with any state system — React, a store, or none), grows with content, sends on Enter, and supports atomic chips, character-triggered completions, keybinding commands, inline ghost-text autocomplete, edit-aware history recall, and an in-border send/stop affordance. See the design note in docs/chat-input-design.md.

The host sets plain properties (busy, placeholder, triggers, …) and callbacks (onSubmit, onChange, onInterrupt, onCommand); the widget announces events and never assumes a render framework.

new ChatInputWidget(): ChatInputWidget

Defined in: src/widgets/controls/chat-input.ts:210

ChatInputWidget

Widget.constructor

acceptSuggestionKey: "right" | "tab" | "ctrl-e" = "right"

Defined in: src/widgets/controls/chat-input.ts:132

Which key accepts a ghost-text suggestion.


children: DOMNode[] = []

Defined in: src/dom/dom.ts:16

Child nodes in document order.

Widget.children


chipStyle: "fill" | "bracket" = "fill"

Defined in: src/widgets/controls/chat-input.ts:128

Chip visual style.


classes: Set<string>

Defined in: src/dom/dom.ts:10

Advisory class names (no CSS cascade today).

Widget.classes


commands: Command[] = []

Defined in: src/widgets/controls/chat-input.ts:144

Keybinding/palette commands.


defaultStyle: WidgetStyles = {}

Defined in: src/dom/widget.ts:218

The widget’s built-in look, overridden by style. Set by subclasses.

Widget.defaultStyle


disabled: boolean = false

Defined in: src/dom/widget.ts:332

When true, this widget (and its descendants) are inert: not focusable, they ignore key/mouse input, and interactive controls render in a muted style. Set via the disabled prop. Checked through isDisabled so disabling a container disables everything inside it.

Widget.disabled


focusable: boolean = false

Defined in: src/dom/widget.ts:308

Whether this widget can take keyboard focus.

Widget.focusable


focused: boolean = false

Defined in: src/dom/widget.ts:323

True while this widget holds keyboard focus.

Widget.focused


focusOnClick: boolean = false

Defined in: src/dom/widget.ts:315

When a click lands on this (non-focusable) container — its padding, border, or any non-focusable child — move focus to its first focusable descendant. Lets a Form/Panel/Box act like one click target that hands focus to its first field. Off by default.

Widget.focusOnClick


optional getHistory?: () => string[]

Defined in: src/widgets/controls/chat-input.ts:151

App-provided history (pulled lazily for Up/Down recall).

string[]


historyEdge: "row" | "bump" = "bump"

Defined in: src/widgets/controls/chat-input.ts:139

When Up/Down recall history. “bump” (default): only at the buffer’s true start (Up) / end (Down), so history never fires mid-edit; the first press on the boundary row just moves the caret to the edge. “row”: eager — anywhere on the first/last visual row.


hoverInterest: boolean = false

Defined in: src/dom/widget.ts:214

Opt-in hint: this widget visually or behaviorally cares about passive hover motion.

Widget.hoverInterest


id: string = ""

Defined in: src/dom/dom.ts:8

Optional stable identifier (from the id prop).

Widget.id


optional label?: string

Defined in: src/dom/widget.ts:361

Structural/accessible label; also the tab title inside TabContainer.

Widget.label


maxRows: number = 8

Defined in: src/widgets/controls/chat-input.ts:124

Maximum visible text rows before the content scrolls.


measuredHeight: number = 0

Defined in: src/dom/widget.ts:304

Intrinsic height from the last measure.

Widget.measuredHeight


measuredWidth: number = 0

Defined in: src/dom/widget.ts:302

Intrinsic width from the last measure, before layout distributes space.

Widget.measuredWidth


minRows: number = 1

Defined in: src/widgets/controls/chat-input.ts:122

Minimum visible text rows.


optional onClick?: (ev) => void

Defined in: src/dom/widget.ts:373

Pointer click handler.

any

void

Widget.onClick


optional onDragEnd?: (x, y, moved) => void

Defined in: src/dom/widget.ts:397

Drag released; moved is false for a tap with no movement.

number

number

boolean

void

Widget.onDragEnd


optional onDragMove?: (x, y) => void

Defined in: src/dom/widget.ts:395

Pointer moved while dragging from this widget.

number

number

void

Widget.onDragMove


optional onDragStart?: (x, y) => void

Defined in: src/dom/widget.ts:393

Drag began on this widget (pointer pressed).

number

number

void

Widget.onDragStart


optional onHintsChange?: (hints) => void

Defined in: src/widgets/controls/chat-input.ts:176

Fires whenever the active contextual hint set changes (mode transitions, not every keystroke). The host renders these as a help line below the composer.

ChatHint[]

void

Widget.onHintsChange


optional onKey?: (ev) => void

Defined in: src/dom/widget.ts:382

Key handler invoked while focused (the base handleKey forwards here).

any

void

Widget.onKey


optional onMouseDown?: (ev) => void

Defined in: src/dom/widget.ts:380

Pointer pressed on this widget, for any button — fired on press before the left-button focus/onClick path. The event carries button and x/y, so this is how to react to a right-click (e.g. open a context menu). Set ev.handled = true to suppress the default focus/click.

MouseEvent

void

Widget.onMouseDown


optional onMouseEnter?: (ev) => void

Defined in: src/dom/widget.ts:386

Called when the pointer enters this widget’s region.

any

void

Widget.onMouseEnter


optional onMouseLeave?: (ev) => void

Defined in: src/dom/widget.ts:388

Called when the pointer leaves this widget’s region.

any

void

Widget.onMouseLeave


optional onScroll?: (ev) => void

Defined in: src/dom/widget.ts:384

Wheel/scroll handler (the base handleScroll forwards here).

MouseEvent

void

Widget.onScroll


parent: DOMNode | null = null

Defined in: src/dom/dom.ts:14

Parent node, or null when detached / at the root.

Widget.parent


placeholder: string = "Message…"

Defined in: src/widgets/controls/chat-input.ts:108

Hint shown when empty.


pointerTransparent: boolean = false

Defined in: src/dom/widget.ts:321

This widget (and its subtree) never captures the pointer — hit-testing falls through to whatever is beneath. For decorative full-screen overlays like the DevTools highlight. CSS pointer-events: none. Off by default.

Widget.pointerTransparent


positionFixed: boolean = false

Defined in: src/dom/widget.ts:352

When true, an absolutely-positioned child is pinned to its parent’s viewport and is NOT shifted by the parent’s scroll offset (CSS position: fixed semantics). Used by overlay chrome like the copy button so it stays in the top-right corner instead of scrolling away with the content.

Widget.positionFixed


prevRegion: Region = Region.EMPTY

Defined in: src/dom/widget.ts:300

The region as of the last laid-out frame, for geometry-stability detection: a queueRepaintWidget may scope a frame to one widget only when a fresh layout leaves every region equal to its prevRegion (nothing moved). Updated by the App after each layout pass.

Widget.prevRegion


region: Region = Region.EMPTY

Defined in: src/dom/widget.ts:293

This widget’s rectangle in terminal cells, assigned by the layout engine.

Widget.region


scrollOffset: Offset = Offset.ORIGIN

Defined in: src/dom/widget.ts:306

Scroll position of this widget’s content (see Scrollable).

Widget.scrollOffset


selectable: boolean = true

Defined in: src/dom/widget.ts:345

Whether this widget’s selectable content participates in read-only text selection. Default true; set false on chrome leaves (e.g. Markdown list bullets / horizontal rules) so they register no content runs and are skipped.

Widget.selectable


selectionContainer: boolean = false

Defined in: src/dom/widget.ts:339

When true, a read-only text selection started on any descendant is anchored to this widget’s region instead of the leaf, so a drag can span the composed children (e.g. selecting across the paragraphs/code blocks a Markdown widget renders into separate leaves). See widgets/readonly-selection.

Widget.selectionContainer


selectionRaw: string | null = null

Defined in: src/dom/widget.ts:359

Original source text for this subtree (e.g. the raw markdown of the block a Markdown widget rendered it from). When a read-only selection fully covers the subtree’s content, copy emits this verbatim instead of the rendered text, so copied markdown round-trips its formatting.

Widget.selectionRaw


showActionGlyph: boolean = true

Defined in: src/widgets/controls/chat-input.ts:130

Show the in-border send/stop glyph (purely an affordance; Enter/Esc always work).


softWrap: boolean = true

Defined in: src/widgets/controls/chat-input.ts:126

Wrap long lines instead of scrolling horizontally.


style: WidgetStyles = {}

Defined in: src/dom/widget.ts:216

Author-set inline styles; override the widget’s defaultStyle key-by-key.

Widget.style


submitMode: "enter" | "modifier-enter" = "enter"

Defined in: src/widgets/controls/chat-input.ts:120

“enter” → Enter sends, Shift+Enter newline. “modifier-enter” → inverted.


optional suggestionProvider?: (ctx) => string | Promise<string | null> | null

Defined in: src/widgets/controls/chat-input.ts:146

App-provided inline ghost-text autocomplete.

number

string

string | Promise<string | null> | null


tagName: string = ""

Defined in: src/dom/dom.ts:12

Host element tag, lowercased (e.g. “ztui-button”).

Widget.tagName


triggers: Trigger[] = []

Defined in: src/widgets/controls/chat-input.ts:142

Character-triggered completion sources (slash, mention, …).


visible: boolean = true

Defined in: src/dom/widget.ts:325

Whether this widget renders and participates in layout.

Widget.visible

get app(): WidgetApp | null

Defined in: src/dom/widget.ts:284

The App that owns this widget, found by walking the parent chain to the tree root (the App node sets screen.parent = this). Returns null when the widget is detached (e.g. mid-construction, before it’s mounted).

Prefer this over the App.instance singleton for anything that must act on this widget’s app — style resolution, render scheduling — so multiple live apps (tests, the web backend) don’t resolve each other’s trees.

WidgetApp | null

Widget.app


get borderSize(): Spacing

Defined in: src/dom/widget.ts:556

Border thickness as a Spacing — 1 per side that has a border, else zero.

Spacing

Widget.borderSize


get busy(): boolean

Defined in: src/widgets/controls/chat-input.ts:111

boolean

set busy(v): void

Defined in: src/widgets/controls/chat-input.ts:114

boolean

void


get computedStyle(): WidgetStyles

Defined in: src/dom/widget.ts:224

Fully resolved styles for this frame ($tokens, hover/focus, defaults folded in). Read this in render, not style.

WidgetStyles

set computedStyle(val): void

Defined in: src/dom/widget.ts:228

Set by the engine each frame after resolving styles; you rarely set this yourself.

WidgetStyles

void

Widget.computedStyle


get cursorShape(): "default" | "move" | "pointer" | "text" | "vertical-text" | "wait" | "progress" | "help" | "crosshair" | "cell" | "grab" | "grabbing" | "alias" | "copy" | "no-drop" | "not-allowed" | "zoom-in" | "zoom-out" | "n-resize" | "e-resize" | "s-resize" | "w-resize" | "ne-resize" | "nw-resize" | "se-resize" | "sw-resize" | "ew-resize" | "ns-resize" | "nesw-resize" | "nwse-resize" | null

Defined in: src/dom/widget.ts:250

The mouse-pointer shape shown over this widget, or null (default arrow). An explicit cursor style wins (resolved style first, so a :hover rule applies); otherwise the role-based defaultCursor is used. A disabled interactive widget always reports "not-allowed".

"default" | "move" | "pointer" | "text" | "vertical-text" | "wait" | "progress" | "help" | "crosshair" | "cell" | "grab" | "grabbing" | "alias" | "copy" | "no-drop" | "not-allowed" | "zoom-in" | "zoom-out" | "n-resize" | "e-resize" | "s-resize" | "w-resize" | "ne-resize" | "nw-resize" | "se-resize" | "sw-resize" | "ew-resize" | "ns-resize" | "nesw-resize" | "nwse-resize" | null

Widget.cursorShape


get extraHints(): ChatHint[]

Defined in: src/widgets/controls/chat-input.ts:157

Extra host-supplied hints always appended to getHints (e.g. a quit hint, “drag a file to attach”, or any custom contextual message). Setting it re-emits onHintsChange.

ChatHint[]

set extraHints(v): void

Defined in: src/widgets/controls/chat-input.ts:160

ChatHint[]

void


get margin(): Spacing

Defined in: src/dom/widget.ts:503

This widget’s margin as a normalized Spacing.

Spacing

Widget.margin


get padding(): Spacing

Defined in: src/dom/widget.ts:514

This widget’s padding as a normalized Spacing.

Spacing

Widget.padding


set serialize(fn): void

Defined in: src/widgets/controls/chat-input.ts:230

Set how chips serialize into value.

ChipSerializer

void


get theme(): string | undefined

Defined in: src/dom/widget.ts:364

Theme name applied to this subtree; descendants resolve $tokens against it.

string | undefined

set theme(val): void

Defined in: src/dom/widget.ts:368

Apply a theme name to this subtree (descendants resolve $tokens against it).

string | undefined

void

Widget.theme


get value(): string

Defined in: src/widgets/controls/chat-input.ts:219

The serialized text value.

string

set value(text): void

Defined in: src/widgets/controls/chat-input.ts:223

Replace the text (does not emit onChange — avoids controlled-prop loops).

string

void

addAttachment(item): void

Defined in: src/widgets/controls/chat-input.ts:296

Add an attachment chip (shown in the strip above the input).

Attachment

void


animate(key, target, opts?, paintOnly?): number

Defined in: src/dom/widget.ts:629

Drive a named scalar tween toward target, returning the value to show this frame. Call it from render: while the tween is still moving it books the next animation frame on this widget, so the value advances on its own without any external clock or state. This is the framework-agnostic counterpart to React’s useAnimatedValue — the engine lives on the widget, so every binding gets smooth motion for the same call.

A non-positive duration (or opts omitted with a default of 0 from the caller) snaps immediately, so animation can be turned off by passing duration: 0.

Pass paintOnly: true when the tweened value only changes appearance (e.g. a fill color/width painted within an already-sized box) and never geometry — this books a paint-only repaint instead of a full relayout tick, the same distinction animateColor already makes.

string

number

TweenOptions

boolean = false

number

Widget.animate


animateColor(key, target, opts?): string

Defined in: src/dom/widget.ts:654

Colour counterpart to animate: tweens a named CSS colour toward target, returning the rgb(...) string to paint this frame and booking the next frame while in flight.

string

string

TweenOptions

string

Widget.animateColor


appendChild(child): void

Defined in: src/dom/dom.ts:23

Append child, detaching it from any previous parent.

DOMNode

void

Widget.appendChild


appendStreaming(text): void

Defined in: src/widgets/controls/chat-input.ts:290

Append text from an external stream (e.g. dictation).

string

void


clear(): void

Defined in: src/widgets/controls/chat-input.ts:236

Clear the composer.

void


clearSelection(): void

Defined in: src/widgets/controls/chat-input.ts:279

Clear any active selection (caret stays put).

void


copySelection(): string | null

Defined in: src/widgets/controls/chat-input.ts:262

Selected text, or null when nothing is selected.

string | null


cursorShapeAt(_x, _y): "default" | "move" | "pointer" | "text" | "vertical-text" | "wait" | "progress" | "help" | "crosshair" | "cell" | "grab" | "grabbing" | "alias" | "copy" | "no-drop" | "not-allowed" | "zoom-in" | "zoom-out" | "n-resize" | "e-resize" | "s-resize" | "w-resize" | "ne-resize" | "nw-resize" | "se-resize" | "sw-resize" | "ew-resize" | "ns-resize" | "nesw-resize" | "nwse-resize" | null

Defined in: src/dom/widget.ts:266

The pointer shape at a specific cell (absolute screen coordinates), letting a widget vary the cursor across its own area — e.g. a list returns its pointer over rows but the default arrow over its scrollbar gutter. The base ignores position and returns cursorShape; override for sub-region control. Returning null defers to the ancestor / default arrow.

number

number

"default" | "move" | "pointer" | "text" | "vertical-text" | "wait" | "progress" | "help" | "crosshair" | "cell" | "grab" | "grabbing" | "alias" | "copy" | "no-drop" | "not-allowed" | "zoom-in" | "zoom-out" | "n-resize" | "e-resize" | "s-resize" | "w-resize" | "ne-resize" | "nw-resize" | "se-resize" | "sw-resize" | "ew-resize" | "ns-resize" | "nesw-resize" | "nwse-resize" | null

Widget.cursorShapeAt


cutSelection(): string | null

Defined in: src/widgets/controls/chat-input.ts:270

Copy the selection, then delete it. No-op (null) when nothing is selected.

string | null


describe(): string

Defined in: src/dom/dom.ts:59

Human/LLM-readable identity for logs and diagnostics, e.g. button#submit.primary @ (2,1 10x1). Includes the laid-out region when one is present (widgets), and the text preview for text nodes.

string

Widget.describe


findResolvedBackground(): string

Defined in: src/dom/widget.ts:671

This widget’s effective background, walking up to ancestors when unset — used to composite translucent fills.

string

Widget.findResolvedBackground


getAccessibleNode(): AccessibleNode | null

Defined in: src/dom/widget.ts:711

A semantic snapshot of this widget for accessibility / non-visual output (see Screen.toAccessibleText). Returns null to be skipped — the default for pure layout containers that carry no inline text — so the accessible tree shows meaningful nodes (controls, labels, headings) without the structural scaffolding around them.

The base infers a reasonable node from the tag, inline text, focus/disabled state, and a structurally-read checked/value/label. Interactive widgets that know more (a slider’s range, a select’s options) should override to add value/state, calling super.getAccessibleNode() for the common fields.

AccessibleNode | null

Widget.getAccessibleNode


getClientRect(): Region

Defined in: src/dom/widget.ts:566

The region inside the margin (the widget’s visible box, border included).

Region

Widget.getClientRect


getContentRect(): Region

Defined in: src/dom/widget.ts:577

The drawable region inside margin + border + padding — paint custom content here.

Region

Widget.getContentRect


getHints(): ChatHint[]

Defined in: src/widgets/controls/chat-input.ts:359

The contextual help entries for the current editing mode — the most specific mode wins, then the host’s extraHints are appended. Hosts render these as a help line below the composer; onHintsChange fires when the set changes so a line can repaint only on real transitions.

ChatHint[]


getTextContent(): string

Defined in: src/dom/widget.ts:689

Concatenates the text of all direct TextNode children.

This is the canonical way widgets read their inline JSX text content. Subclasses may override to add behavior (e.g. trimming, or falling back to a label field), typically by post-processing super.getTextContent().

string

Widget.getTextContent


handleKey(ev): void

Defined in: src/dom/widget.ts:448

Handle a key event while this widget is focused. Override to implement keyboard interaction; mark ev.handled = true for keys you consume so they don’t fall through to global hotkeys. The base forwards to onKey. Requires focusable to be true to receive focus.

KeyEvent

void

Widget.handleKey


handleMouse(ev): void

Defined in: src/widgets/controls/chat-input.ts:933

Handle a mouse event hit-tested to this widget (press/release/drag/move). Override for click/drag interaction; call super.handleMouse(ev) to keep the built-in drag-source lifecycle (onDragStart/onDragMove/onDragEnd). Set ev.handled = true to consume the event.

MouseEvent

void

Widget.handleMouse


handleScroll(ev): void

Defined in: src/dom/widget.ts:435

Handle a wheel/scroll event. Override to scroll your own content; set ev.handled = true to stop it bubbling. The base forwards to onScroll.

MouseEvent

void

Widget.handleScroll


hasCursorStyle(): boolean

Defined in: src/dom/widget.ts:271

Whether the pointer takes on any shape over this widget (style or role).

boolean

Widget.hasCursorStyle


hasSelection(): boolean

Defined in: src/widgets/controls/chat-input.ts:257

True when a non-empty selection exists.

boolean


insertBefore(child, before): void

Defined in: src/dom/dom.ts:41

Insert child before the before sibling (appends if before isn’t found).

DOMNode

DOMNode

void

Widget.insertBefore


insertText(text): void

Defined in: src/widgets/controls/chat-input.ts:246

Insert text at the caret (replacing any selection); used by Ctrl+V + bracketed paste.

string

void


isDisabled(): boolean

Defined in: src/dom/widget.ts:593

True when this widget or any ancestor is disabled, so a disabled container (e.g. a <Form disabled>) propagates to every control inside it.

boolean

Widget.isDisabled


matchesSelector(selector): boolean

Defined in: src/dom/dom.ts:109

Match a compound selector against this node — tags, IDs, and classes (e.g. tag#id.a.b).

string

boolean

Widget.matchesSelector


measure(maxW, maxH): void

Defined in: src/widgets/controls/chat-input.ts:508

Compute this widget’s intrinsic size into measuredWidth / measuredHeight, given the space the parent offers (maxW/maxH). Runs bottom-up (children first). Override point for content-sized custom widgets (e.g. measure your text); call super.measure(maxW, maxH) first if you also have children. Clamp to the offered space so you never overflow.

number

number

void

Widget.measure


onMount(): void

Defined in: src/dom/widget.ts:996

Called once when the widget enters the live tree. Override to start timers, subscribe to stores, or kick off async loads; pair cleanup in onUnmount.

void

Widget.onMount


onUnmount(): void

Defined in: src/widgets/controls/chat-input.ts:330

Called once when the widget leaves the tree. Override to release whatever onMount acquired (timers, subscriptions) so nothing leaks.

void

Widget.onUnmount


redo(): void

Defined in: src/widgets/controls/chat-input.ts:313

void


removeAttachment(id): void

Defined in: src/widgets/controls/chat-input.ts:302

Remove an attachment by id.

string

void


removeChild(child): void

Defined in: src/dom/dom.ts:32

Remove child if it’s a child of this node.

DOMNode

void

Widget.removeChild


render(buffer): void

Defined in: src/widgets/controls/chat-input.ts:1034

Paint this widget into the cell buffer. The base draws the background, border, and children. Custom-widget override point: call super.render(buffer) to keep background/border, then paint your content within getContentRect using buffer.setCell(...). Stay inside your region — the parent clips children to its content box by default. Keep render pure and fast: it runs every frame and must not mutate the tree.

ScreenBuffer

void

Widget.render


renderChildren(buffer): void

Defined in: src/dom/widget.ts:938

Paint child widgets (z-index ordered, clipped to the content box). The base render already calls this; override only for unusual child handling (e.g. a custom scroll transform). Most custom widgets don’t need to.

ScreenBuffer

void

Widget.renderChildren


selectAll(): void

Defined in: src/widgets/controls/chat-input.ts:285

Select the entire draft.

void


undo(): void

Defined in: src/widgets/controls/chat-input.ts:310

Undo / redo the buffer.

void


walk(callback): void

Defined in: src/dom/dom.ts:83

Depth-first visit of this node and its descendants, in z-index (paint) order — mirrors what’s actually drawn on top, for callers like toAccessibleText(). z-index is a paint-only concept, so callers that need tab/focus order (nothing to do with stacking) should use walkDocumentOrder instead — otherwise a widget with a nonzero z-index purely for painting (e.g. an overlapping decorative sibling) jumps to a different position in the Tab sequence than where it structurally sits.

(node) => void

void

Widget.walk


walkDocumentOrder(callback): void

Defined in: src/dom/dom.ts:101

Depth-first visit of this node and its descendants in plain document order, ignoring z-index. Use for tab/focus order (e.g. Screen.getFocusableWidgets) — see walk’s doc comment for why z-index order is wrong there.

(node) => void

void

Widget.walkDocumentOrder


wantsTab(ev): boolean

Defined in: src/widgets/controls/chat-input.ts:323

Claim Tab only when there’s something to accept in-widget: an open completion popup, or an inline suggestion when Tab is the accept key. In every other state Tab falls through to focus traversal, so a second Tab (after accepting) moves to the next widget.

KeyEvent

boolean

Widget.wantsTab