diff --git a/apps/website/content/docs/chat/a2ui/overview.mdx b/apps/website/content/docs/chat/a2ui/overview.mdx index 674518865..ee98a4fc7 100644 --- a/apps/website/content/docs/chat/a2ui/overview.mdx +++ b/apps/website/content/docs/chat/a2ui/overview.mdx @@ -1,203 +1,204 @@ +--- +description: How the flight-booking demo streams A2UI surfaces into the chat composition, and how chat detects them, renders them, and returns actions to the agent. +--- + # A2UI Overview A2UI is the structured UI path for agent-built surfaces in chat. -The important boundary is simple: the agent streams declarative messages, the client owns rendering, and Angular handlers stay inside your application. The model does not ship code. It emits a constrained surface description that `@threadplane/chat`, `@threadplane/a2ui`, and `@threadplane/render` turn into Angular UI. Threadplane implements the **A2UI v0.9.1 stable release** — every envelope carries `"version": "v0.9"`. +The boundary is simple: the agent streams declarative messages, the client owns rendering, and Angular handlers stay inside your application. The model does not ship code. It emits a constrained surface description that `@threadplane/chat`, `@threadplane/a2ui`, and `@threadplane/render` turn into Angular UI. Threadplane implements the **A2UI v0.9.1 stable release** — every envelope carries `"version": "v0.9"`. + +The running example is a flight booking flow served by LangGraph. The agent authors each screen as an A2UI surface, and this page walks the files that produce it. + +## What the demo does + +The Run tab shows the prebuilt `` composition with the A2UI catalog registered on it. Choose the welcome suggestion "Book LAX → JFK" and the agent replies with a booking form rather than with prose: origin and destination pickers already set to those two airports, a departure date, a passenger count, and a fare class. + +Fill the form and press "Search flights". The button does not post a chat message; it sends a structured A2UI action back to the agent, which searches the flight fixtures and answers with a second surface listing the matching flights. Selecting one produces a third surface, the booking confirmation, whose "Modify search" button returns to the form with the earlier values already filled in. The model authors both the results list and the confirmation surface at request time, so their exact layout and wording vary from run to run; only the form's shape is prescribed. + +The second suggestion, "Book SFO → SEA", runs the same pattern on a different route, which is worth trying because nothing about the form is hardcoded on the client. + +## How it is built + +Three files carry the feature: a LangGraph graph that authors the surfaces, an application config that registers the agent, and a component that hands the A2UI catalog to ``. Open the Code tab to read them in full. + +### The component shape the model must satisfy + +A2UI v0.9 components are flat. Each entry carries an `id`, a `component` name from the catalog, and its props at the same level of the same object. The example models that as a Pydantic class and nests it inside the structured-output schema, so the model authors the component list under a validator instead of free-typing JSON. + + + +The validator is the safety gate: an unknown `component` name renders nothing visible, so the schema rejects it and the model is re-prompted with the error. + +### The three parts of a surface + +Every surface in this demo is the same triple: an id, an initial data model, and a flat list of components. Three subclasses give each node its own schema name and description without changing the shape. + + + +The `data_model` is what path bindings such as `{"path": "/origin"}` resolve against. + +### Wrapping a surface in v0.9 envelopes + +The model authors the components; the code writes the wire format. `_wrap_envelopes` emits the sentinel prefix and then one JSON envelope per line, each stamped `"version": "v0.9"`. The protocol requires only that `createSurface` comes first and that a component with id `root` is defined before anything paints. + + + +The demo emits `createSurface`, then `updateComponents`, then `updateDataModel`. + + +`A2UI_PREFIX` is `---a2ui_JSON---`. The content classifier in `@threadplane/chat` looks for exactly that string at the start of assistant content and routes the rest of the message into the A2UI pipeline instead of the markdown renderer. + + +### The node that authors the form + +`build_form` runs on the first turn and again on a "Modify search" turn. It recovers any prior submission from the message history, or, on a true first turn, seeds the origin and destination from a phrase such as "I want to fly LAX to JFK", substitutes those values into the system prompt as the form defaults, and asks the model for a `BookingFormSpec`. + + + +The node returns an ordinary `AIMessage` whose content is the wrapped JSONL, which is why no custom event type is needed to carry a surface. -When an assistant message starts with `---a2ui_JSON---`, the chat streaming pipeline treats the rest of the content as newline-delimited A2UI JSON. + +`_emit_with_retry` re-prompts the model with the validation error up to three attempts in total, and `build_form` falls back to a hand-written sentinel form when they all fail. A surface is the whole response here, so a validation failure with no fallback is a blank turn. + -## Runtime Flow +### Routing an action message back into the graph + +When the user presses a button, the surface sends an A2UI action message back to the agent as the next user message, and its content is JSON rather than prose. The entry node reads that last message and dispatches on the action name. + + + +`_is_submit_event` and `_is_flight_select_event` each parse the content and compare `action.name` against `bookingSubmit` and `flightSelect`, so anything that is not one of those two is treated as a fresh request for the form. + +### The graph wiring + +The wiring is a fan-out from `route` into the three surface-authoring nodes, each of which ends through background title generation. + + + + +The graph is served by the LangGraph API server, which supplies persistence, so `_builder.compile()` takes no saver. A graph you serve yourself — behind AG-UI, for instance — has to compile one, because the adapter reads thread state through the graph. + + +### Registering the LangGraph agent + +`provideAgent()` from `@threadplane/langgraph` needs the API URL and the assistant id. The example passes a factory because it resolves both at runtime from the host that serves the demo; an application of your own passes `apiUrl` and `assistantId` directly. + + + +`provideChat({})` registers the chat composition defaults alongside it. + +### Giving the chat composition a catalog + +The client side is one input. `a2uiBasicCatalog()` from `@threadplane/chat` returns a view registry covering all eighteen components of the A2UI basic catalog, and passing it as `[views]` is what lets `` mount a surface — the composition renders `` only when a catalog is bound. + + + +No handler wiring appears here: `` builds the action message from the surface and submits it to the agent for you. + +## Runtime flow This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. `A2uiSurfaceComponent` turns the accumulated surface state into a render spec and delegates to `@threadplane/render`, so handlers, render events, and json-render state bindings use the same path for both the preferred `state` input and the legacy `surface` input. -## Message Envelopes +## Message envelopes The parser recognizes four envelope keys: | Envelope | Purpose | | --- | --- | | `createSurface` | Creates a surface and declares its component catalog, theme, and `sendDataModel` behavior. Must come first. | -| `updateComponents` | Adds or replaces components on a surface, merged incrementally by `id`. One component must have `id: "root"`. | +| `updateComponents` | Adds or replaces components on a surface, merged incrementally by `id`. One component must have `id: "root"`; the surface store will not commit a surface without one. The surface-to-spec conversion itself falls back to the first component, which matters only when you drive `` directly. | | `updateDataModel` | Sets (or deletes) data at a JSON-pointer `path` in the surface data model. | | `deleteSurface` | Removes a surface. | -Unknown envelope keys are ignored (future v1.0 messages included). Malformed JSONL lines are skipped. Incomplete JSON waits in the parser buffer until a newline arrives. +Unknown envelope keys are ignored (future v1.0 messages included). Malformed JSONL lines are skipped. Incomplete JSON waits in the parser buffer until a newline arrives. A line with no `version` field is treated as `v0.9`. That behavior is deliberate. Agent streams are partial. The parser should not crash the UI because one line is unfinished mid-token. -## Minimal Protocol Stream +## The wire shape -A surface needs a `createSurface`, its data, and a component tree containing `root`. On the wire, each envelope is one newline-delimited JSON object after the sentinel: +On the wire, each envelope from `_wrap_envelopes` is one newline-delimited JSON object after the sentinel: ```text ---a2ui_JSON--- {"version":"v0.9","createSurface":{...}} -{"version":"v0.9","updateDataModel":{...}} {"version":"v0.9","updateComponents":{...}} +{"version":"v0.9","updateDataModel":{...}} ``` -The same `updateComponents` envelope, expanded for readability, looks like this: +Components are flat objects discriminated by the `component` string. This is the demo's submit button, with its action context abridged to two of its six keys: ```json { - "version": "v0.9", - "updateComponents": { - "surfaceId": "contact", - "components": [ - { - "id": "root", - "component": "Column", - "children": ["title", "name", "submit"] - }, - { - "id": "title", - "component": "Text", - "text": "Contact us", - "variant": "h2" - }, - { - "id": "name", - "component": "TextField", - "label": "Name", - "value": { "path": "/name" } - }, - { - "id": "submit_label", - "component": "Text", - "text": "Send" - }, - { - "id": "submit", - "component": "Button", - "child": "submit_label", - "variant": "primary", - "action": { - "event": { - "name": "formSubmit", - "context": { "name": { "path": "/name" } } - } - } + "id": "submit", + "component": "Button", + "child": "submit_label", + "variant": "primary", + "action": { + "event": { + "name": "bookingSubmit", + "context": { + "formId": "booking", + "origin": { "path": "/origin" }, + "dest": { "path": "/dest" } } - ] - } -} -``` - -The create and data envelopes are smaller: - -```json -{ - "version": "v0.9", - "createSurface": { - "surfaceId": "contact", - "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" + } } } ``` -```json -{ - "version": "v0.9", - "updateDataModel": { - "surfaceId": "contact", - "value": { "name": "" } - } -} -``` +There is no type-keyed wrapper and no literal wrapper objects — `"Search flights"` is just a bare string, and `{ "path": "/origin" }` is a data-model binding. Rendering starts as soon as `createSurface` has arrived and the root component is defined; the rest of the tree fills in progressively as more `updateComponents` envelopes merge by id. -Components are flat objects discriminated by the `component` string: +## Data model -```json -{ - "id": "title", - "component": "Text", - "text": "Contact us", - "variant": "h2" -} -``` - -There is no type-keyed wrapper and no literal wrapper objects — `"Contact us"` is just a bare string, and `{ "path": "/name" }` is a data-model binding. Rendering starts as soon as `createSurface` has arrived and a component with id `root` is defined; the rest of the tree fills in progressively as more `updateComponents` envelopes merge by id. - -This stream demonstrates the protocol boundary. The chat path accumulates the surface state as JSONL arrives, then `A2uiSurfaceComponent` converts the current surface into a render spec: it maps flat components onto catalog views, wires children, creates render state bindings, and turns actions into handlers. - -## Data Model - -A2UI component props can point at the surface data model. +A2UI component props can point at the surface data model, and the surface-to-spec conversion turns those path references into json-render state bindings. It also collects them into a `_bindings` map that it passes to the component, so an input knows where to write back; an agent never authors `_bindings` itself. -```json -{ - "id": "name", - "component": "TextField", - "label": "Name", - "value": { "path": "/name" } -} -``` - -The surface-to-spec conversion turns path references into json-render state bindings. Catalog input components can use `emitBinding()` to write back through the render event pipeline. +Catalog input components use `emitBinding()` for that write-back. It is exported from `@threadplane/chat`, and it takes the render host, the component's `_bindings` map, the prop name, and the new value: ```ts import { emitBinding } from '@threadplane/chat'; onInput(event: Event): void { const value = (event.target as HTMLInputElement).value; - emitBinding(this.emit(), this._bindings(), 'text', value); + emitBinding(this.host, this._bindings(), 'value', value); } ``` -The write-back protocol is client-side state. If the surface's `createSurface` set `sendDataModel: true`, outgoing action messages also include the current **live** surface data model (user edits included) under `metadata.a2uiClientDataModel`. - -## Actions +The write-back protocol is client-side state. If the surface's `createSurface` set `sendDataModel: true`, outgoing action messages also include the current **live** surface data model (user edits included) under `metadata.a2uiClientDataModel`. The demo does not set it: every value the agent needs travels in the action context instead. -Buttons carry an `A2uiAction`. The agent-bound form wraps an `event` with a name and a plain-object `context`: +## Actions and the round trip -```json -{ - "action": { - "event": { - "name": "formSubmit", - "context": { "name": { "path": "/name" } } - } - } -} -``` - -The surface-to-spec conversion turns this into a render `click` binding that calls the built-in `a2ui:event` handler. `A2uiSurfaceComponent` then emits an `A2uiActionMessage` with the context values resolved against the current data model: +Buttons carry an `A2uiAction`. The agent-bound form wraps an `event` with a name and a plain-object `context`, exactly as the submit button above does. The surface-to-spec conversion turns that into a render `click` binding that calls the built-in `a2ui:event` handler. `A2uiSurfaceComponent` then evaluates the surface's check rules, resolves the context values against the live data model, and emits an `A2uiActionMessage`: ```json { "version": "v0.9", "action": { - "name": "formSubmit", - "surfaceId": "contact", + "name": "bookingSubmit", + "surfaceId": "booking", "sourceComponentId": "submit", "timestamp": "2026-04-10T14:30:00.000Z", - "context": { "name": "Alice" }, - "label": "Send" + "context": { "formId": "booking", "origin": "LAX" }, + "label": "Search flights" } } ``` -`label` is a Threadplane extension, derived from the Button's child Text; transcripts use it to label the user bubble. If the surface has `sendDataModel: true`, the emitted message also includes `metadata.a2uiClientDataModel` with the live surface data model (user edits included). - -The other action form, `{ "functionCall": { "call": ..., "args": ... } }`, executes a client-side function locally instead of round-tripping to the agent — wired to the surface component's `a2ui:localAction` handler, with `openUrl` (new tab, `noopener`) built in. - -Catalog components receive resolved props as Angular inputs from the render engine. Bind `(action)` when you want agent-bound events, and bind `(events)` when you want the lower-level render stream. - -## Theming +`label` is a Threadplane extension, derived from the Button's child Text. `` serializes that message and submits it through the agent like any other user message, then renders the resulting human bubble from that label rather than from the serialized content: the transcript shows "Search flights" instead of the raw JSON. When no label was stamped, the composition humanizes the action name (`bookingSubmit` becomes "Booking submit"). That is the whole round trip, and it is why the demo's Angular component wires no handlers. -`createSurface.theme` carries agent-supplied presentation hints. `primaryColor` flows to `` as the `--a2ui-primary` CSS custom property, which catalog components consume for accents (buttons, sliders, focus rings). `iconUrl` and `agentDisplayName` identify the agent that owns the surface: when either is set, `` renders a small identity header (a 16px round icon and the display name in muted label text) above the surface. Themeless surfaces render no header. +If a check rule fails, no action is emitted. The surface component writes each failure message into the live store under `/_a2uiChecks/` and emits a `validationError` instead. -## Local Handlers - -`A2uiSurfaceComponent` also registers an `a2ui:localAction` handler. Consumer handlers take priority, and the built-in fallback currently supports `openUrl`. +The other action form, `{ "functionCall": { "call": ..., "args": ... } }`, executes a client-side function locally instead of round-tripping to the agent — wired to the surface component's `a2ui:localAction` handler, with `openUrl` (new tab, `noopener`) built in. Consumer handlers passed to `[handlers]` take priority over the built-in fallback. Use local handlers for client-owned behavior. Use A2UI event actions for agent-bound events. +This is the standalone ``, documented in full at [the surface component page](/docs/chat/a2ui/surface-component); `state()` here is the surface state your host holds, not a value the demo above provides. + ```html ` as the `--a2ui-primary` CSS custom property, which catalog components consume for accents (buttons, sliders, focus rings). `iconUrl` and `agentDisplayName` identify the agent that owns the surface: when either is set, `` renders a small identity header (a 16px round icon and the display name in muted label text) above the surface. The demo sends no theme, so its surfaces render with the host application's colors and no header. + +## A2UI versus json-render Both paths render structured UI, but they optimize for different jobs. @@ -225,71 +230,31 @@ Both paths render structured UI, but they optimize for different jobs. | Detection | `---a2ui_JSON---` prefix | JSON object content | | Rendering | Surface state converted to a render spec | json-render spec directly | -Use A2UI when the agent needs to keep updating a surface and you are working at the protocol boundary. Use json-render when the agent needs a stable, directly rendered structured result. For production interaction that depends on component handlers, state bindings, and child projection, verify the exact A2UI rendering path you are using. - -## Setup - -Pass the built-in A2UI catalog to chat: - -```ts -import { Component } from '@angular/core'; -import { ChatComponent, a2uiBasicCatalog } from '@threadplane/chat'; -import { injectAgent, provideAgent } from '@threadplane/langgraph'; - -@Component({ - standalone: true, - imports: [ChatComponent], - providers: [provideAgent({ apiUrl: '/api/langgraph', assistantId: 'support' })], - template: ``, -}) -export class SupportChatComponent { - protected readonly chat = injectAgent(); - - protected readonly catalog = a2uiBasicCatalog(); -} -``` - -For custom component sets, build a catalog with the same view registry tools used by `@threadplane/render`. +Use A2UI when the agent needs to keep updating a surface and you are working at the protocol boundary. Use json-render when the agent needs a stable, directly rendered structured result. ## Gotchas -The A2UI parser is not a full schema validator. It recognizes envelope keys and leaves deeper validation to typed code, tests, and your runtime boundary. +The A2UI parser is not a full schema validator. It recognizes envelope keys and leaves deeper validation to typed code, tests, and your runtime boundary. That is why the demo puts a Pydantic validator on the authoring side. Schema-valid messages are not enough to make UI executable. Your catalog must contain components for the emitted types, and your handlers must exist for the actions you expect users to take. Do not use pre-v0.9 envelope names such as `surfaceUpdate`, `dataModelUpdate`, or `beginRendering` — the current parser recognizes only `createSurface`, `updateComponents`, `updateDataModel`, and `deleteSurface`, and silently ignores anything else. Likewise, the pre-v0.9 type-keyed component wrappers and `literalString`-style value wrappers are gone: components are flat, and literals are bare values. -Do not assume the progressive chat renderer and the render-spec compatibility path have identical capabilities. The compatibility path projects a surface through the surface-to-spec conversion. The progressive path tracks per-component readiness from `A2uiComponentView` state. +Progressive rendering is per component, not per surface. The surface-to-spec conversion leaves each data-model prop as a `$bindState` binding, and the render element defers mounting a component while any bound prop is still undefined, showing the catalog entry's fallback, or the library default when the entry declares none, in its place. The surface store also tracks a monotonic `ready` flag per `A2uiComponentView` for hosts that drive their own progressive renderer. A component whose binding never arrives keeps showing its fallback. ## What's Next - + Render a surface outside the full ChatComponent composition. - + Understand how messages update surfaces and data models. - + See the built-in catalog components and their props. - + Read the protocol package docs for parser, schema, and pointer helpers. diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx index a51e57189..4ced0fcb4 100644 --- a/apps/website/content/docs/chat/components/chat-debug.mdx +++ b/apps/website/content/docs/chat/components/chat-debug.mdx @@ -1,6 +1,10 @@ +--- +description: How the debug example mounts the chat-debug panel against a multi-node LangGraph agent, plus the panel inputs, outputs, sidenav integration and bundle gate. +--- + # ChatDebugComponent -`ChatDebugComponent` provides a docked development panel for inspecting an `Agent` or `AgentWithHistory`. It ships from the debug-only secondary entry point so applications can keep debug implementation code out of production bundles unless they explicitly opt in. +`ChatDebugComponent` is a docked development panel for inspecting an agent. It reads the agent's checkpoint history and current state and renders them in a floating panel, and it ships from a debug-only secondary entry point so applications can keep the implementation out of production bundles unless they opt in. The running example mounts the panel on its own, which is the clearest way to see what the panel contributes and what it does not. **Selector:** `chat-debug` @@ -10,97 +14,118 @@ import { ChatDebugComponent } from '@threadplane/chat/debug'; ``` -## Basic Usage +## What the demo does -```typescript -import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; -import { injectAgent, provideAgent } from '@threadplane/langgraph'; -import { ChatDebugComponent } from '@threadplane/chat/debug'; +The Run tab mounts ``, bound to a LangGraph agent, inside the shared example layout shell, ``, which contributes only a full-height background. The panel keeps itself out of the layout — its host renders `display: contents`, and the launcher and the panel are both fixed-position — so the page shows nothing but a small round status pill in the top-right corner. Click the pill and the docked Chat Devtools panel opens, with a Timeline tab listing the agent's checkpoints and a State tab pretty-printing the agent's current state under a Copy button. Pressing Escape or clicking outside the panel closes it. + +The demo mounts no chat composition beside the panel, so there is no composer on the page and the Timeline tab shows its empty state: "No checkpoints yet. Send a message to populate the timeline." Everything else is live. The dock buttons in the header move the panel between the left, bottom and right edges, and the choice is persisted, as is the open state and the selected tab. + +## How it is built + +Three files carry the example: the graph that produces the checkpoints, the provider that points Angular at it, and the component that mounts the panel. Open the Code tab to read them in place. + +### A graph with several nodes per turn + +The backend is deliberately multi-step, because one node per turn produces one checkpoint and very little to look at. `generate` answers with the model, `process` appends a synthetic message derived from the answer, and `summarize` asks the model for a one-sentence summary of the conversation so far. The system prompt read by `generate` frames the assistant as an aviation helper working over a mock dataset of ten United States airports and four airlines. + + + +Each node returns a partial state update, and each of those updates becomes a checkpoint on the thread. + +### Wiring the nodes into a linear pipeline + +The nodes are registered on a `StateGraph` over `MessagesState` and chained: `generate` to `process` to `summarize` to `generate_title`, then to the end. `generate_title` is a background node that summarizes the first user message into a thread title; it returns an empty update, so it changes the message list not at all while still adding a step to the run. + + + + +The last line calls `compile()` with no checkpointer. The checkpoints the Timeline tab reads come from the LangGraph API server, which persists thread state for every graph it serves. `langgraph dev` refuses to load a graph that compiles its own saver, and a deployment ignores one — see [Persistence](/docs/langgraph/guides/persistence). + + +### The agent provider + +`provideAgent()` registers the agent once for the whole application. The example resolves its connection details at runtime from the host that serves the demo, which is why its factory reads them rather than hard-coding them, and it sits alongside `provideChat({})`, which supplies the chat library's own providers. -@Component({ - selector: 'app-debug-page', - standalone: true, - imports: [ChatDebugComponent], - providers: [ - provideAgent({ - apiUrl: '/api/langgraph', - assistantId: 'chat', - threadId: signal(localStorage.getItem('threadId')), - onThreadId: (id) => localStorage.setItem('threadId', id), - }), - ], - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - - `, -}) -export class DebugPageComponent { - protected readonly replayCheckpointId = signal(null); - protected readonly forkSourceCheckpointId = signal(null); - - protected readonly chat = injectAgent(); - - replay(checkpointId: string) { - this.replayCheckpointId.set(checkpointId); - // Route this into your app's replay workflow. - } - - fork(checkpointId: string) { - this.forkSourceCheckpointId.set(checkpointId); - // Route this into your app's thread fork workflow. - } -} + + +Your own application passes the two values directly instead: + +```typescript +provideAgent({ + apiUrl: 'https://your-deployment.langgraph.app', + assistantId: 'debug', +}), +provideChat({}), ``` +### Mounting the panel + +The component wraps `` in `` and binds one field. `injectAgent()` returns the agent registered above, and it is handed straight to `[agent]`. Nothing else is bound, so the panel runs on its defaults: docked right, closed on first load, with the floating launcher visible. + + + +The agent returned by `injectAgent()` exposes a `history()` signal as well as `state()`, which is what makes the Timeline tab appear; an agent without `history()` gets the State tab alone. + ## Inputs | Input | Type | Default | Description | | --- | --- | --- | --- | -| `agent` | `Agent \| AgentWithHistory` | **Required** | Agent state. Agents with `history()` also enable the Timeline tab. | -| `dock` | `'right' \| 'bottom' \| 'left'` | `'right'` | Initial dock position. | -| `defaultOpen` | `boolean` | `false` | Initial open state when no persisted state exists. | +| `agent` | `DebugAgent \| DebugAgentWithHistory \| null` | `null` | Agent to inspect. The panel renders nothing while this is `null`; the floating launcher still appears, but opening it shows no content. An agent that also exposes `history()` enables the Timeline tab. | +| `dock` | `'right' \| 'bottom' \| 'left'` | `'right'` | Initial dock position, used when no persisted position exists. When a sibling `` is on the page and the user has not clicked a dock button this session, an auto-dock effect forces `bottom` on open. | +| `defaultOpen` | `boolean` | `false` | Initial open state, used when no persisted state exists. | | `launcher` | `'floating' \| 'none'` | `'floating'` | Shows the built-in floating launcher, or hides it when another surface opens the panel. | -| `storageKey` | `string` | `'chat-debug'` | Local storage key prefix for persisted open/dock/tab state. | +| `storageKey` | `string` | `'chat-debug'` | Local storage key prefix for the persisted open, dock and tab state. | + +`DebugAgent` is a structural type: signals for `messages`, `status`, `isLoading`, `error`, `toolCalls` and `state`. `DebugAgentWithHistory` adds `history`. The agents returned by `injectAgent()` satisfy the second. ## Outputs | Output | Type | Description | | --- | --- | --- | -| `replayRequested` | `string` | Emits a checkpoint id when the built-in Timeline tab requests replay. | -| `forkRequested` | `string` | Emits a checkpoint id when the built-in Timeline tab requests fork. | +| `replayRequested` | `string` | Emits a checkpoint id when Replay is pressed on a selected checkpoint in the Timeline tab. | +| `forkRequested` | `string` | Emits a checkpoint id when Fork is pressed on a selected checkpoint in the Timeline tab. | | `openChange` | `boolean` | Emits when the panel opens or closes. | | `dockChange` | `'right' \| 'bottom' \| 'left'` | Emits when the dock position changes. | -`replayRequested` and `forkRequested` are integration hooks. The debug panel does not mutate the agent by itself; the host app decides whether a checkpoint opens a replay view, starts a forked thread, or maps to a backend-specific time travel operation. - -## Sidenav Integration - -`ChatSidenavComponent` can own the launcher for apps that already use the sidenav footer. Pass the same agent and keep `[debug]` enabled: +`replayRequested` and `forkRequested` are integration hooks, and the example demonstrates the panel without them. The panel never mutates the agent by itself: selecting a checkpoint reveals a Replay button, a Fork button and a before-and-after diff of that step's state, and the host application decides whether an emitted id opens a replay view, starts a forked thread, or maps to a backend-specific time travel operation. ```html - ``` -The footer button is labelled in expanded and drawer modes. In collapsed mode it uses the status dot only. The dot pulses while `agent.status()` is `running`. +## Sidenav integration + +`ChatSidenavComponent` can own the launcher for applications that already use the sidenav footer. Pass the same agent and leave `[debug]` at its default of `true`: + +```html + +``` + +The footer button appears only when an agent is bound and the debug entry point is included in the build. It is labeled "Devtools" in expanded and drawer modes; in collapsed mode it is the status dot alone, and the dot pulses while `agent.status()` is `running`. Pressing it lazily imports `ChatDebugComponent`, mounts it with `launcher="none"` and the storage key `chat-sidenav-debug`, and opens it. The sidenav does not re-expose `replayRequested` or `forkRequested`, so mount `` yourself when you need those hooks. -## Production Bundles +## Production bundles -The debug implementation lives under `@threadplane/chat/debug`; the main `@threadplane/chat` entry point no longer exports it. In the canonical Angular demo, normal production builds set `THREADPLANE_CHAT_DEBUG=false` and externalize `@threadplane/chat/debug`, so the debug implementation is absent from the emitted bundle. The `production-debug` build opts back in with `THREADPLANE_CHAT_DEBUG=true`. +The implementation lives under `@threadplane/chat/debug`; the main `@threadplane/chat` entry point does not export it. Importing it yourself, as the example does, always puts it in the bundle. The sidenav's lazy import is different: it is gated on an internal flag that is true whenever `ngDevMode` is true, and otherwise only when a `THREADPLANE_CHAT_DEBUG` compile-time constant is defined as `true`. The canonical Angular demo defines that constant per build configuration — `false` for `production`, and `true` for a separate `production-debug` configuration. -Keep debug controls for your app outside the debug panel. The debug panel intentionally exposes a small fixed surface so consumers do not expect demo-specific controls to appear in their own applications. +Keep debug controls of your own outside the panel. The panel intentionally exposes a small fixed surface, so consumers do not expect application-specific controls to appear in it. -## See also +## What's Next -- [Time travel](/docs/langgraph/guides/time-travel) — what the `replayRequested` / `forkRequested` checkpoint hooks plug into, and how to wire replay and fork against the agent's history. -- [``](/docs/chat/concepts/primitives-vs-compositions#compositions) — the layout composition that can own the debug launcher (see Sidenav Integration above). + + + What the replay and fork checkpoint hooks plug into, and how to wire them against the agent's history. + + + Where the checkpoints come from, and why this graph compiles without a checkpointer. + + + The layout composition that can own the debug launcher. + + + An in-conversation view of graph execution, for surfaces your users see. + + diff --git a/apps/website/content/docs/chat/components/chat-input.mdx b/apps/website/content/docs/chat/components/chat-input.mdx index 04694fa38..a91ec21ef 100644 --- a/apps/website/content/docs/chat/components/chat-input.mdx +++ b/apps/website/content/docs/chat/components/chat-input.mdx @@ -1,75 +1,120 @@ +--- +description: How the input example wires chat-input to a streaming agent, plus the component inputs, outputs, keyboard handling, projection slots and styling hooks. +--- + # ChatInputComponent -`ChatInputComponent` is the text input primitive for sending messages to a LangGraph agent. It provides a textarea with auto-sizing, a send button, keyboard handling, and automatic disabling while the agent is loading. +`ChatInputComponent` is the text input primitive for sending messages to an agent. It renders a pill containing a textarea and one button, submits the trimmed text to the agent it is bound to, and swaps the send button for a stop button while a response streams. The running example puts it under a live conversation with a panel that prints the agent status beside it, so every state this page describes is visible while you type. -**Selector:** `chat-input` +## What the demo does -**Import:** +The Run tab shows a conversation on the left and an Input State panel on the right. The input sits in a strip under the messages with the placeholder "Try typing here...", and the agent behind it is an aviation assistant whose mock dataset covers ten United States airports and four airlines. -```typescript -import { ChatInputComponent, submitMessage } from '@threadplane/chat'; -``` +Ask it something like which airlines fly out of SFO, and watch the panel: Stream Status reads `running` and Is Loading reads `true` while the answer streams, and the send button turns into a stop button for as long as that lasts. Press Shift and Enter together to add a second line before sending, which is the other half of the keyboard contract below. -## Basic Usage +## How it is built -```html - +Three files carry the example: the graph that streams the answer, the provider that points Angular at it, and the component that mounts the input. The input itself is one element with an agent and a placeholder. + +### The streaming graph + +The backend is a two-node graph. `generate` reads the capability's prompt file, prepends it as a system message, and awaits a model constructed with `streaming=True`, which is what keeps the agent loading long enough to watch the button swap. `generate_title` runs after it and writes a short thread title back through the LangGraph SDK. + + + +The compiled graph is exported as `graph`, which is the symbol `langgraph.json` points at. + +### The application configuration + +`provideAgent()` registers the agent for the whole application. This example resolves its connection details at runtime from the host that serves the demo, which is why its factory reads them rather than hard-coding them. It sits beside `provideChat({})`, which registers the chat library's own providers. + + + +Your own application does not need the factory: pass `apiUrl` and `assistantId` directly, where `assistantId` is the graph name declared in `langgraph.json`, which is `c-input` for this example. + +### Mounting the input + +`` takes the agent and a placeholder. Nothing else is wired, because the component submits to the agent on its own. + + + + +The `submitted` output fires after the message has already been sent to the agent. Calling `submit()` again in that handler posts the user message twice. + + +### Reading the state the input reacts to + +The demo component mirrors two agent signals into computed fields so the panel can print them. `isLoading` is the one `chat-input` reads on its own, through the agent it is bound to. + + + +The panel is a definition list over those two fields. + + + +## Import + +```typescript +import { ChatInputComponent, submitMessage } from '@threadplane/chat'; ``` -## API +The selector is `chat-input`, and the component is standalone, so add it to a component's `imports` array. -### Inputs +## Inputs | Input | Type | Default | Description | |-------|------|---------|-------------| -| `agent` | `Agent` | **Required** | The agent to submit messages to | -| `submitOnEnter` | `boolean` | `true` | When `true`, pressing Enter submits the message. Shift+Enter inserts a newline. When `false`, Enter always inserts a newline. | -| `placeholder` | `string` | `''` | Placeholder text shown when the input is empty | -| `showStopButton` | `boolean` | `true` | When `true`, shows a stop button in place of the send button while the agent is streaming. | +| `agent` | `Agent` | **Required** | The agent to submit messages to. Its `isLoading()` signal drives the send and stop buttons. | +| `submitOnEnter` | `boolean` | `true` | When `true`, pressing Enter submits the message. Shift and Enter together insert a newline. When `false`, Enter always inserts a newline. | +| `placeholder` | `string` | `''` | Placeholder text shown when the textarea is empty. | +| `showStopButton` | `boolean` | `true` | When `true`, the send button is replaced by a stop button while the agent is loading. | -### Outputs +## Outputs | Output | Type | Description | |--------|------|-------------| -| `submitted` | `string` | Emits the trimmed message text after successful submission | -| `stopped` | `void` | Emits when the user clicks the stop button to halt a streaming response | +| `submitted` | `string` | Emits the trimmed message text after the message has been submitted. | +| `stopped` | `void` | Emits after the stop button is clicked, whether or not the agent aborted anything. | + +## Submit flow + +Enter and the send button both call the same method: -## Behavior +1. The textarea value is trimmed. +2. If nothing remains after trimming, the call is a no-op: no submit, no output, and the text is left alone. +3. `agent.submit({ message: trimmed })` is called. +4. The `submitted` output emits the trimmed text. +5. The textarea is cleared, both in the component signal and on the element. +6. Focus returns to the textarea on the next animation frame. -### Submit Flow +## The send button and the stop button -When the user submits (via Enter key or clicking the send button): +Only one button is rendered at a time. While `agent.isLoading()` is `false`, it is the send button, disabled unless the textarea holds at least one non-whitespace character. While `isLoading()` is `true` and `showStopButton` is left at its default, it is the stop button instead. -1. The message text is trimmed -2. If empty after trimming, nothing happens -3. `agent.submit()` is called with `{ message: trimmed }` -4. The `submitted` output emits the trimmed text -5. The input is cleared -6. Focus is returned to the textarea +Clicking stop calls `agent.stop()` and then emits `stopped`. Set `showStopButton` to `false` and the send button stays in place while the response streams, disabled for as long as the agent is loading. -### Disabled State + +Loading disables the button, not the field. A user can keep typing the next message while the current response streams; Enter and the button are simply inert until it finishes. + -The input is automatically disabled when `agent.isLoading()` returns `true`. This prevents sending messages while the agent is processing. Both the textarea and the send button reflect the disabled state. +## Auto-sizing -### Auto-Sizing +The textarea starts at one row and grows with its content. An effect measures `scrollHeight` after each change and sets an explicit height, capped at the smaller of forty percent of the viewport height and 320 pixels. Past the cap the textarea scrolls internally. -The textarea uses `field-sizing: content` CSS, which allows it to grow with its content up to a maximum height of `120px`. After that, the textarea scrolls internally. +The cap is recomputed on every change rather than on a resize listener, so a viewport that changes between keystrokes is picked up on the next one. -### Keyboard Handling +## Keyboard handling | Key | `submitOnEnter: true` | `submitOnEnter: false` | |-----|----------------------|----------------------| | Enter | Submits the message | Inserts a newline | -| Shift+Enter | Inserts a newline | Inserts a newline | +| Shift and Enter | Inserts a newline | Inserts a newline | -## submitMessage() Helper +Input method editors are handled explicitly. While a composition is in progress — Chinese, Japanese and Korean input, dead-key accents, autocorrect popups — Enter is left to the textarea so the candidate is committed instead of submitted. The component tracks this through the `compositionstart` and `compositionend` events and also checks `event.isComposing` and the legacy key code 229. -The `submitMessage()` function is exported as a standalone utility for programmatic message submission: +## submitMessage() + +The submit path is exported on its own for programmatic sends: ```typescript import { submitMessage } from '@threadplane/chat'; @@ -77,17 +122,14 @@ import type { Agent } from '@threadplane/chat'; function sendGreeting(agent: Agent) { const result = submitMessage(agent, 'Hello!'); - // result is 'Hello!' or null if the text was empty + // result is 'Hello!', or null when the trimmed text was empty } ``` **Signature:** ```typescript -function submitMessage( - agent: Agent, - text: string -): string | null +function submitMessage(agent: Agent, text: string): string | null; ``` | Parameter | Type | Description | @@ -95,26 +137,22 @@ function submitMessage( | `agent` | `Agent` | The agent to submit to | | `text` | `string` | The message text to send | -**Returns:** The trimmed message string if submitted, or `null` if the trimmed text was empty. - -The function calls `agent.submit({ message: trimmed })` under the hood. +It trims the text, returns `null` without calling the agent when nothing remains, and otherwise calls `agent.submit({ message: trimmed })` and returns the trimmed string. The component calls this same function, so a programmatic send behaves exactly like a typed one, minus the clearing and the focus. ## Slots -The component template exposes content-projection slots arranged around the input pill: +Six content-projection slots sit around the input pill: | Slot | Selector | Description | |------|----------|-------------| -| Banner | `[chatInputBanner]` | Rendered above the input pill (e.g., a notice or status banner). | -| Attachments | `[chatInputAttachments]` | Rendered above the input pill, below the banner (e.g., attachment chips). | -| Leading | `[chatInputLeading]` | Rendered inside the pill, before the textarea. | -| Model select | `[chatInputModelSelect]` | Rendered in the controls row, before the trailing slot and send button. | -| Trailing | `[chatInputTrailing]` | Rendered in the controls row, after the model-select slot and before the send button. | -| Footer | `[chatInputFooter]` | Rendered below the input pill. | +| Banner | `[chatInputBanner]` | Above the pill, first. | +| Attachments | `[chatInputAttachments]` | Above the pill, below the banner. | +| Leading | `[chatInputLeading]` | Inside the pill, before the textarea. | +| Model select | `[chatInputModelSelect]` | In the controls row, first. | +| Trailing | `[chatInputTrailing]` | In the controls row, after the model-select slot and before the button. | +| Footer | `[chatInputFooter]` | Below the pill. | -### `[chatInputModelSelect]` - -Projects content into the controls row of the input pill, between `[chatInputTrailing]` and the send button. Designed for `` (a model picker), but accepts any element. +The model-select slot is sized for ``, which takes an options array and a two-way value, but it accepts any element: ```html @@ -124,59 +162,42 @@ Projects content into the controls row of the input pill, between `[chatInputTra ## Styling -The component renders a `
` containing a `