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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .changeset/7655-chatbot-registration-authoring-faces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
'@object-ui/types': minor
'@object-ui/plugin-chatbot': patch
---

One named, importable authoring-face type per `plugin-chatbot` registration:
`ChatbotEnhancedSchema` and `ChatbotFloatingSchema` join `ChatbotSchema`
(objectui#7655, under the objectui#6169 / #6172 family ruling — every component
node has exactly one named, importable authoring-face type).

`packages/plugin-chatbot` registers three components — `chatbot`,
`chatbot-enhanced`, `chatbot-floating` — and `@object-ui/types` published ONE
face for the family with `type` pinned to `'chatbot'`. An author annotating a
`chatbot-enhanced` or `chatbot-floating` node either dropped to untyped JSON or
annotated with `ChatbotSchema` and lied about `type`; the docs' floating example
had to be a `json` fence because no `tsx` fence could compile. The two
registrations' real key sets lived in anonymous `ChatbotSchema & { ... }`
intersections local to the renderer, referenceable by nothing outside that file.

## The shape, and why not the smaller diff

One interface per registration, not `ChatbotSchema['type']` widened to the union
of the three keys. The union would give three nodes ONE type and re-open what
#6169 closed — a single interface declaring keys only some of its own `type`
values read — and this card exists because the family's declarations had already
drifted from its reads. Each face declares what ITS registration reads, censused
per key on the PR's base (one `schema.KEY` read per registration body in
`renderer.tsx`, lit by keys that are NOT shared: `processVisibility` 0 / 1 / 0,
`floatingConfig` 0 / 0 / 1), and the twenty keys all three read are picked off
`ChatbotSchema` by name (`ChatbotSharedKey`) so they stay one declaration:

- **`ChatbotEnhancedSchema`** (`type: 'chatbot-enhanced'`): the shared twenty,
plus `maxHeight` and `processVisibility` (read here, not by the floating
panel), plus `enableMarkdown`, `enableFileUpload`, `surface` (`'card' |
'plain'`, objectui#6687) and the `onClear` runtime slot — four keys
`ChatbotSchema` never declared.
- **`ChatbotFloatingSchema`** (`type: 'chatbot-floating'`): the shared twenty,
plus `enableMarkdown`, `enableFileUpload`, `onClear`, and the two keys it
declares alongside `ChatbotSchema` — `floatingConfig` (`FloatingChatbotConfig`)
and `displayMode`. No `maxHeight`, `processVisibility` or `surface`: the
floating registration has no named read for any of them. (Its trailing raw
props spread does carry authored keys into the panel today — `processVisibility`,
`surface` and `showAvatars` are live there, measured through the real host;
that accidental channel is tracked as objectui#7708, and this face neither
declares nor promises it.)
- Neither face declares `ChatbotSchema`'s six legacy members (`loading`,
`showAvatars`, `userAvatar`, `assistantAvatar`, `markdown`, `height`) — no
registration reads them by name — and neither redeclares `disabled`, which
stays `BaseSchema`'s `boolean | string` (objectui#7087).

**`ChatbotSchema` is unchanged.** It keeps `displayMode` and `floatingConfig`
(declarations verbatim), and the floating face declares the same two, so
`ChatbotSchema['displayMode']` and `ChatbotSchema['floatingConfig']` stay the
typed members they were — the objectui#7669 `triggerIcon` tombstone keeps its
reach on `chatbot` nodes, now pinned on the node. `floatingConfig`'s doc comment
is rewritten on both faces: the old text said it was "only used when
`displayMode` is `'floating'`", which was false — it is read by `chatbot-floating`
alone and forwarded to the panel. `displayMode` is RULED RETIRED — objectui#7654,
maintainer ruling B (2026-09-05): `?: never` tombstone, designer control and
`defaultProps` seed removed, in that card's own change. This change carries the
key untouched on both faces (still unmirrored, still read by nothing) so that PR
finds the member exactly as ruled, and a tripwire test pins that any value still
parses green until that PR flips it.

**New published symbol:** `ChatbotSharedKey`, the string-literal union of the
twenty keys all three registrations read. It is exported from `complex.ts`
because an exported interface may not extend a `Pick` over a private name
(TS4022), so it is emitted into `dist/complex.d.ts` and is reachable through the
published `@object-ui/types/complex` subpath (it is not re-exported from the
package entry). It is a census, not an authoring face.

## Zod twins, in lockstep

`@object-ui/types/zod` gains `ChatbotEnhancedSchema` and `ChatbotFloatingSchema`
(and `ComplexSchema` routes the two new discriminants). Every declared key is an
arm except: the three runtime slots (`onError`, `onSend`, `onClear`), refused by
name per objectui#6124; and, on the floating twin only, `floatingConfig` (no
`FloatingChatbotConfig` mirror exists — minting one is objectui#6152's axis) and
`displayMode` (unmirrored on `ChatbotSchema`'s twin too; retired by ruling on
objectui#7654 and executed there). The twins mirror the API body params under the
key the renderer reads, `requestBody`, and inherit `body` as the children slot —
they do not copy `ChatbotSchema`'s `body` naming collision.

**Accept-set change, stated plainly:** a `chatbot-enhanced` or `chatbot-floating`
document parsed through the family's only twin used to fail on `type`; through
its own twin it now parses, and the keys the twin declares are VALIDATED where
they rode through `.passthrough()` unexamined before (`surface: 'frameless'`,
`enableMarkdown: 'yes'` and `requestBody: 'x'` are refused). A `chatbot` node's
parse outcome is unchanged: `ChatbotSchema`'s twin did not move.

## `@object-ui/plugin-chatbot`

The `chatbot-enhanced` and `chatbot-floating` registrations type `schema` as the
published faces and drop the anonymous intersections. One consequence:
`chatbot-floating` used to write `disabled={schema.disabled}` and then spread
`{...props}` AFTER it — and `SchemaRenderer` always includes `disabled: verdict
|| undefined` in those props, so the raw read was overridden on every render.
With `disabled` honestly typed as `boolean | string` the raw union cannot be
forwarded into the panel's `boolean` prop, so the registration now names the
host verdict (`disabled: hostDisabled`) the way its two siblings have since
objectui#4431. No render outcome moves; the pin renders through the real host
both ways.

This ships as `minor` for `@object-ui/types` because it widens the published
surface with two new node types, two new Zod twins and one new type alias;
`ChatbotSchema`'s own accept set does not move: objectui's major is pinned to `@objectstack`'s
(`scripts/check-changeset-no-major.mjs`), and objectui's own contract changes
ship as `minor` with the semantics spelled out — as above.
98 changes: 71 additions & 27 deletions content/docs/plugins/plugin-chatbot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,25 @@ This page documents **three** registrations - `chatbot`, `chatbot-enhanced` and
`chatbot-floating` - and they do not all read the same keys. **A row whose
description carries no bolded scope note is read by all three.** The rows only
some of them read say so in bold at the start of the description, and name what
to author instead on the registrations that ignore the key. Every key below is
declared on `ChatbotSchema`, so a key a registration ignores still type-checks
and still parses - it is dropped silently at render time, which is why the scope
is spelled out here rather than left to the type to express.
to author instead on the registrations that ignore the key.

Each registration has its own importable authoring-face type in
`@object-ui/types` (objectui#7655): `ChatbotSchema` for `chatbot`,
`ChatbotEnhancedSchema` for `chatbot-enhanced` and `ChatbotFloatingSchema` for
`chatbot-floating`. Each declares exactly the keys its registration reads - the
twenty shared rows below are one declaration the two newer faces pick off
`ChatbotSchema` by name, and a scoped row is declared only on the face(s) whose
registration reads it. A key a registration ignores is therefore not a declared
member of its type. It still type-checks (`BaseSchema` ends in an index
signature, so an unlisted key is `any` rather than an error) and still parses
(the Zod twins are `.passthrough()`). On `chatbot` and `chatbot-enhanced` it is
then dropped silently at render time. `chatbot-floating` is different today: its
registration forwards the whole authored node to the panel through an
unfiltered props spread, so some keys its type does not declare
(`processVisibility`, `surface`, `showAvatars`) do reach the panel - an
accidental channel, measured and tracked as objectui#7708, not a contract to
author against. That is why the scope is spelled out here as well as in the
types.

The table below is that shared chat surface. `chatbot-floating` declares
seven more keys of its own - `displayMode` and six `floatingConfig` entries -
Expand All @@ -160,7 +175,7 @@ one, under **`chatbot-floating` panel and trigger keys**.
| `userAvatarFallback` | string | `'You'` | Fallback text for user avatar |
| `assistantAvatarUrl` | string | - | URL for assistant avatar image |
| `assistantAvatarFallback` | string | `'AI'` | Fallback text for assistant avatar |
| `maxHeight` | string | `'500px'` | **`chatbot` and `chatbot-enhanced` only.** Maximum height of the chat message container, as a CSS length. `chatbot-floating` does not read it: its panel is sized by `floatingConfig.panelHeight` (a **number** of pixels, default `520`), and the panel pins its inner chat to `maxHeight: '100%'` so it fills that panel - a `maxHeight` authored on a floating node would be overridden even if it were forwarded. Size a floating chatbot with `floatingConfig.panelHeight` instead |
| `maxHeight` | string | `'500px'` | **`chatbot` and `chatbot-enhanced` only** (declared on `ChatbotSchema` and `ChatbotEnhancedSchema`; `ChatbotFloatingSchema` does not declare it). Maximum height of the chat message container, as a CSS length. `chatbot-floating` does not read it: its panel is sized by `floatingConfig.panelHeight` (a **number** of pixels, default `520`), and the panel pins its inner chat to `maxHeight: '100%'` so it fills that panel - a `maxHeight` authored on a floating node would be overridden even if it were forwarded. Size a floating chatbot with `floatingConfig.panelHeight` instead |
| `autoResponse` | boolean | `false` | Enable auto-response (demo mode, ignored when `api` is set) |
| `autoResponseText` | string | - | Text for auto-response |
| `autoResponseDelay` | number | `1000` | Delay before auto-response (ms) |
Expand All @@ -174,8 +189,8 @@ one, under **`chatbot-floating` panel and trigger keys**.
| `headers` | object | - | Additional headers for API requests |
| `requestBody` | object | - | Additional body parameters sent with each API request. Authored on the node as `requestBody`; the renderer forwards it to the chat runtime under its own `body` option. Writing `body` on the node instead sets the base schema's children container and never reaches the API |
| `maxToolRoundtrips` | number | - | **Deprecated - has no effect.** Nothing reads this value, so it never capped anything. Cap tool-calling loops on the agent instead (`planning.maxIterations`). Still accepted so existing documents keep parsing; slated for removal in a future major |
| `surface` | `'card' \| 'plain'` | `'card'` | **`chatbot-enhanced` only.** Controls whether the chat renders as a bordered panel (`'card'`) or a frameless full-page workspace (`'plain'`). The `chatbot` and `chatbot-floating` registrations render different components, which have no such chrome to switch, and do not read this key |
| `processVisibility` | `'hidden' \| 'summary' \| 'debug'` | `'summary'` | **`chatbot-enhanced` only.** Controls how much agent reasoning and tool detail is shown. `chatbot` renders the plain chat component, which has no agent-process display to configure at all - switch the node to `chatbot-enhanced` if you need one. `chatbot-floating` does not forward the key either, so its panel always renders at the `'summary'` default; there is no floating-side substitute to author |
| `surface` | `'card' \| 'plain'` | `'card'` | **`chatbot-enhanced` only** (declared on `ChatbotEnhancedSchema`). Controls whether the chat renders as a bordered panel (`'card'`) or a frameless full-page workspace (`'plain'`). `chatbot` renders the plain chat component, which has no such chrome to switch, and does not read this key. `chatbot-floating` has no named read for it and `ChatbotFloatingSchema` does not declare it; its panel is a `ChatbotEnhanced`, and an authored value currently reaches that panel only through the registration's unfiltered props spread (objectui#7708) - not a contract to author against |
| `processVisibility` | `'hidden' \| 'summary' \| 'debug'` | `'summary'` | **`chatbot-enhanced` only** (declared on `ChatbotEnhancedSchema`; `ChatbotSchema` still declares it too, though the `chatbot` registration has no read for it). Controls how much agent reasoning and tool detail is shown. `chatbot` renders the plain chat component, which has no agent-process display to configure at all - switch the node to `chatbot-enhanced` if you need one. `chatbot-floating` has no named read for it and `ChatbotFloatingSchema` does not declare it; an authored value currently reaches its panel only through the registration's unfiltered props spread (objectui#7708), which is not a contract - there is no floating-side substitute to author |
| `onError` | function | - | Error callback for streaming/API errors |

### `chatbot-floating` panel and trigger keys
Expand All @@ -184,13 +199,15 @@ The seven keys below are declared in the `chatbot-floating` registration's own
`inputs` (`packages/plugin-chatbot/src/renderer.tsx`). They configure the
floating action button and the panel it opens; the `chatbot` and
`chatbot-enhanced` registrations render neither and ignore them. `displayMode`
and `floatingConfig` are declared on `ChatbotSchema` like every key above, so
authoring them on an inline node still type-checks and still parses - it is
dropped at render time.
and `floatingConfig` are declared on `ChatbotSchema` and on
`ChatbotFloatingSchema` alike (objectui#7655 declared the floating face with the
same two members; `ChatbotSchema` kept its own), so authoring them on an inline
node type-checks and parses - and is dropped at render time, because the
`chatbot` node never read either.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `displayMode` | `'inline' \| 'floating'` | `'floating'` | **Declared and offered in the designer, but read by nothing.** The node's own `type` selects the presentation: a `chatbot-floating` node renders the trigger and panel unconditionally, and authoring `'inline'` here does not make it inline - author a `chatbot` node for that. The registration declares it with `defaultValue: 'floating'` and writes the same value into its `defaultProps`, so nodes created in the designer carry it |
| `displayMode` | `'inline' \| 'floating'` | `'floating'` | **Declared and offered in the designer, but read by nothing.** The node's own `type` selects the presentation: a `chatbot-floating` node renders the trigger and panel unconditionally, and authoring `'inline'` here does not make it inline - author a `chatbot` node for that. The registration declares it with `defaultValue: 'floating'` and writes the same value into its `defaultProps`, so nodes created in the designer carry it. objectui#7654 ruled it retired (maintainer, 2026-09-05): the declaration becomes a `never` tombstone and the designer control and default are removed in that card's own change; until that lands the key is carried exactly as described here |
| `floatingConfig.position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Corner the trigger sits in; the panel is anchored to the same side |
| `floatingConfig.defaultOpen` | boolean | `false` | Whether the panel is already open when the node mounts |
| `floatingConfig.panelWidth` | number | `400` | Panel width in pixels, applied from the `sm` breakpoint up - below it the panel is full-bleed. Snapped to a step, see below |
Expand All @@ -211,25 +228,31 @@ On small screens `panelHeight` is additionally capped to the viewport
(`min(step, 100svh - 6rem - safe-area-inset-bottom)`), and while the panel is
fullscreen it ignores both size keys and fills the screen.

Authored on the node:
Authored on the node, with the node's own type - `ChatbotFloatingSchema` pins
`type` to `'chatbot-floating'` and declares `floatingConfig`, so this fence
compiles against the published types. (Until objectui#7655 no type could
annotate a floating node - `ChatbotSchema` pins `type` to `'chatbot'` - and
this example had to be untyped JSON.)

```json
{
"type": "chatbot-floating",
"floatingConfig": {
"position": "bottom-left",
"defaultOpen": false,
"panelWidth": 400,
"panelHeight": 520,
"title": "Support",
"triggerSize": 56
```tsx
import type { ChatbotFloatingSchema } from '@object-ui/types';

const supportChat: ChatbotFloatingSchema = {
type: 'chatbot-floating',
messages: [], // seed with your own ChatMessage values
floatingConfig: {
position: 'bottom-left',
defaultOpen: false,
panelWidth: 400,
panelHeight: 520,
title: 'Support',
triggerSize: 56,
},
"placeholder": "Ask us anything..."
}
placeholder: 'Ask us anything...',
};
```

`ChatbotSchema` pins `type` to `'chatbot'`, so it cannot annotate a floating
node, but the config object has its own exported type:
The config object also has its own exported type:

```tsx
import type { FloatingChatbotConfig } from '@object-ui/types';
Expand Down Expand Up @@ -336,6 +359,22 @@ Use `surface="plain"` for full-page chat workspaces where the surrounding app
already provides navigation chrome. The default `surface="card"` remains a
better fit for embedded dashboards, side panels, and floating chat windows.

Both keys are authorable as metadata on a `chatbot-enhanced` node, typed with
that node's own face (objectui#7655):

```tsx
import type { ChatbotEnhancedSchema } from '@object-ui/types';

const workspace: ChatbotEnhancedSchema = {
type: 'chatbot-enhanced',
messages: [], // seed with your own ChatMessage values
api: '/api/v1/ai/chat',
surface: 'plain',
processVisibility: 'debug',
enableFileUpload: true,
};
```

Console chat surfaces also keep a sanitized browser-side display cache for the
current conversation. When a conversation can be reopened but the server returns
no message rows, the UI restores user/assistant text and grouped tool names plus
Expand Down Expand Up @@ -573,8 +612,13 @@ const schema: ChatbotSchema = {

## TypeScript Support

Each of the three registrations has its own authoring-face type:
`ChatbotSchema` (`chatbot`), `ChatbotEnhancedSchema` (`chatbot-enhanced`) and
`ChatbotFloatingSchema` (`chatbot-floating`) - see the typed examples under
**Tool Messages** and **`chatbot-floating` panel and trigger keys** above.

```plaintext
import type { ChatbotSchema, ChatMessage, ChatToolInvocation } from '@object-ui/types'
import type { ChatbotSchema, ChatbotEnhancedSchema, ChatbotFloatingSchema, ChatMessage, ChatToolInvocation } from '@object-ui/types'
import { useObjectChat } from '@object-ui/plugin-chatbot'

// Basic messages
Expand Down
Loading
Loading