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
116 changes: 116 additions & 0 deletions .changeset/7703-chatbot-dark-keys-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
'@object-ui/types': minor
---

Retire the six `ChatbotSchema` members no `plugin-chatbot` registration reads —
`loading`, `showAvatars`, `userAvatar`, `assistantAvatar`, `markdown` and `height` — as
ADR-0049 retirement tombstones on **both** published faces (objectui#7703).

⚠️ **BREAKING for anyone authoring one of these six against `ChatbotSchema`, on either
face.** Ships as `minor` per the launch-window convention: objectui's `major` is a
cross-repo pin to `@objectstack`'s so that "same major means compatible" holds across the
two repos (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes
ship as `minor` with the break named where it lands — this entry is the channel that
carries it.

## What was retired, and why

The published type taught six knobs that did nothing. An author — or an AI author reading
the `.d.ts` — wrote `showAvatars: true` or `height: 400` on a `chatbot` node, got no error
on either face, and saw no change. Two of them advertised a `@default true` for a switch
that did not exist.

Re-measured on this branch's base (`21d7989fb`, i.e. **after** objectui#7708's fence
landed as PR #8077) rather than inherited from the card: one `schema.KEY` count per
`ComponentRegistry.register(...)` body of `packages/plugin-chatbot/src/renderer.tsx`, the
file split at the three register calls.

| key | declared type | `schema.KEY` reads (`chatbot` / `chatbot-enhanced` / `chatbot-floating`) |
| :-- | :-- | :-- |
| `loading` | `boolean` | 0 / 0 / 0 |
| `showAvatars` | `boolean` (`@default true`) | 0 / 0 / 0 |
| `userAvatar` | `string` | 0 / 0 / 0 |
| `assistantAvatar` | `string` | 0 / 0 / 0 |
| `markdown` | `boolean` (`@default true`) | 0 / 0 / 0 |
| `height` | `string \| number` | 0 / 0 / 0 |

Lit controls on the same instrument in the same pass: `placeholder` 1 / 1 / 1, `messages`
1 / 1 / 1, `userAvatarUrl` 1 / 1 / 1, `maxHeight` 1 / 1 / 0, `floatingConfig` 0 / 0 / 1,
`processVisibility` 0 / 1 / 0. The zeros are readings, not a blind grep.

⛔ `processVisibility` is **not** in this retirement — `chatbot-enhanced` reads it, and
objectui#7655 left the `ChatbotSchema` member as it was. It is pinned live as a control.

### `showAvatars` is the one key the FENCE turned dark, not a key nothing ever read

The distinction is recorded rather than smoothed over, because the two provenances are
different facts. `ChatbotEnhanced` really does have a `showAvatars` prop, and until
objectui#7708 the `chatbot-floating` registration ended its panel element with a raw
`{...props}` spread that handed an authored value straight to it — measured live through
the real host. That card was ruled **fence**, not declare, and landed as PR #8077: the
spread is filtered through `toDomProps` and moved ahead of every named prop. So the key is
dark on all three registrations **by ruling**, and this retirement records that, not an
absence. The other five were live on no channel at any time — they are not
`ChatbotEnhancedProps` members either (`markdown` exists there only as `enableMarkdown`),
so the spread had nothing to land them on.

## Enforce-or-remove, decided per key

The other arm was taken key by key and refused each time. `<Chatbot>`
(`plugin-chatbot/src/index.tsx`) — the component the `chatbot` registration renders —
declares `messages`, `placeholder`, `onSendMessage`, `disabled`, `showTimestamp`,
`userAvatarUrl`, `userAvatarFallback`, `assistantAvatarUrl`, `assistantAvatarFallback` and
`maxHeight`, and **not one of the six**.

| key | why not enforce | migration |
| :-- | :-- | :-- |
| `loading` | Chat progress is runtime state the chat runtime owns — the registration derives it from `useObjectChat` as `isLoading`. A static authored boolean would fight the runtime, not configure it | delete the key |
| `showAvatars` | No target on `<Chatbot>`. Declaring it on the two faces that DO reach `<ChatbotEnhanced>` would re-open by declaration the channel objectui#7708 closed by fence, one card earlier | delete the key — a `chatbot` node already renders an avatar beside every message |
| `userAvatar` | A second authorable spelling of an image `userAvatarUrl` already carries (AGENTS.md #0.1: one strict contract, not N dialects) | `userAvatarUrl` (+ `userAvatarFallback`) |
| `assistantAvatar` | Same | `assistantAvatarUrl` (+ `assistantAvatarFallback`) |
| `markdown` | `<Chatbot>` prints message content as text and has no markdown path; on the two nodes that render markdown, `enableMarkdown` is the live key | `type: 'chatbot-enhanced'` with `enableMarkdown` |
| `height` | `<Chatbot>` has no `height` prop, and the live `maxHeight` it forwards is a `string`, not this key's `string \| number` union | `maxHeight`, or `floatingConfig.panelHeight` on a floating node |

## FROM → TO

Each member goes to `?: never` on `packages/types/src/complex.ts` and to
`retirementTombstone(...)` on `packages/types/src/zod/complex.zod.ts` — both halves, in
lockstep, the convention `MarkdownSchema.sanitize` (objectui#6972),
`TimelineSchema.timeScale` (objectui#6355) and `ObjectViewSchema.viewTabBar`
(objectui#7779) already carry. Each refusal names the key, says why it is retired, and
points at what to write instead; one string feeds both the parse-time message and the
`.describe()` metadata, so the two cannot drift.

## Accept-set change, one line per face

- **TypeScript.** A write of any of the six against `ChatbotSchema` used to compile and
now does not — including through a widened (non-fresh) value, which is the half a
deletion would have missed on a `BaseSchema` carrier.
- **Runtime (Zod / `safeValidateSchema`).** A `chatbot` document authoring one of the six
used to parse **green** and now parses **red**, `invalid_type` at the key's own path
with the guidance as the message. This is the narrowing that carries
`needs:contract-review`.
- **`chatbot-enhanced` / `chatbot-floating`.** Unchanged in both directions. Those faces
never declared the six (objectui#7655 censused them out), their twins have no arm to
refuse one, and `BaseSchema` is `.passthrough()` — so a stored node of either type
carrying a retired key parses exactly as it did. Pinned.

## Why tombstones and not deletions

All six **have** a Zod arm, and that is what decides the route here. `BaseSchema` is
`.passthrough()` on the Zod side and carries a `[key: string]: any` index signature on the
TS side, so an UNDECLARED key is not refused — it is KEPT. Deleting the members would hand
the authored spelling exactly the silent no-op this card exists to close, on both faces at
once. The two-prong discriminator (`mobile.ts`, objectui#5941 / #7526 / #7678) leaves that
structural hazard to the carrier: where there is no mirror there is "no silent-strip hazard
for prong 2 to guard". Here there is a mirror to host the refusal, and prong 1 holds by the
letter for four of the six. The "deleted" row is pinned live as a control in
`packages/types/src/__tests__/chatbot-dark-keys-retired-7703.test.ts`, so the contrast
cannot rot into prose.

## Docs

`content/docs/plugins/plugin-chatbot.mdx` gains the restatement of the six removed keys
with their replacements (objectui#7070: a control is restated, never deleted into a
vacuum), and its three present-tense claims that the `chatbot-floating` props spread is
still unfiltered are corrected to what PR #8077 actually left behind.
47 changes: 37 additions & 10 deletions content/docs/plugins/plugin-chatbot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,16 @@ twenty shared rows below are one declaration the two newer faces pick off
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 Zod twins are `.passthrough()`). It is then dropped silently at render
time, on all three registrations alike. `chatbot-floating` used to be the
exception: its registration ended its panel element with an unfiltered props
spread, so three keys its type does not declare (`processVisibility`, `surface`,
`showAvatars`) did reach the panel. That channel was accidental, not a contract
to author against, and objectui#7708 closed it - the spread is filtered through
the DOM-safe whitelist and moved ahead of every named prop, the shape the two
sibling registrations already used. A `chatbot-floating` node's declared set is
now also its delivered set. 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
six more keys of its own - the six `floatingConfig` entries - which no row
Expand Down Expand Up @@ -189,10 +191,35 @@ to apply. They are documented in their own table after this one, under
| `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** (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 |
| `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 used to reach that panel through the registration's unfiltered props spread, an accidental channel objectui#7708 fenced - it is dark there now, so switch the node to `chatbot-enhanced` if you need the frameless chrome |
| `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 used to reach its panel through the registration's unfiltered props spread, an accidental channel objectui#7708 fenced - it is dark there now, and there is no floating-side substitute to author |
| `onError` | function | - | Error callback for streaming/API errors |

**There are no `loading`, `showAvatars`, `userAvatar`, `assistantAvatar`,
`markdown` or `height` keys.** All six were declared on `ChatbotSchema`, mirrored
on its Zod twin, and read by no registration - a `showAvatars: true` or a
`height: 400` on a `chatbot` node type-checked, parsed green, and changed
nothing. objectui#7703 retired them (ADR-0049 enforce-or-remove, one decision per
key): each is a `never` tombstone on the declaration and a named refusal on the
twin, so writing one is now a compile error **and** a parse error that says what
to write instead. Four have a live replacement, and it is the key the
registrations already read:

| retired | write instead |
|---------|---------------|
| `userAvatar` | `userAvatarUrl` (with `userAvatarFallback`) |
| `assistantAvatar` | `assistantAvatarUrl` (with `assistantAvatarFallback`) |
| `height` | `maxHeight` on `chatbot` / `chatbot-enhanced`; `floatingConfig.panelHeight` on `chatbot-floating` |
| `markdown` | `enableMarkdown`, on a `chatbot-enhanced` or `chatbot-floating` node |
| `loading` | nothing - chat progress is runtime state the chat runtime owns, never authored |
| `showAvatars` | nothing - a `chatbot` node already renders an avatar beside every message; the images are the `*AvatarUrl` keys above |

`showAvatars` is the one of the six that was not always inert: `ChatbotEnhanced`
has such a prop, and until objectui#7708 fenced it the `chatbot-floating`
registration's unfiltered props spread delivered an authored value to it. It is
dark on all three registrations now. The other five were live on no path at any
time.

### `chatbot-floating` panel and trigger keys

The six keys below are declared in the `chatbot-floating` registration's own
Expand Down
Loading
Loading