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
123 changes: 123 additions & 0 deletions .changeset/7654-chatbot-display-mode-tombstone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
'@object-ui/types': minor
'@object-ui/plugin-chatbot': patch
---

Retire `ChatbotSchema.displayMode` — and its copy on `ChatbotFloatingSchema` — as an
ADR-0049 retirement tombstone, and remove the `chatbot-floating` registration's
"Display Mode" designer control and its `defaultProps.displayMode: 'floating'` seed
(objectui#7654, maintainer ruling B of 2026-09-05, director decision batch #44).

⚠️ **BREAKING for anyone authoring `displayMode` against a chatbot face in TypeScript.**
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 node `type` — `chatbot-floating` versus `chatbot` / `chatbot-enhanced` — is the one
selector of presentation. `displayMode` (`'inline' | 'floating'`) was a second spelling
of that same choice, and no renderer has ever read it: `chatbot-floating` renders the
trigger and panel unconditionally, and `chatbot` never looked at the key, so
`displayMode: 'floating'` on a `chatbot` node produced no trigger and `'inline'` on a
`chatbot-floating` node changed nothing. It was nevertheless declared on both faces,
painted as a **Display Mode** control in the designer's property panel, and written as
`'floating'` into every node the designer created — two surfaces teaching a switch that
did not exist.

Re-measured on this branch's base rather than inherited from the card: a whole-repo
`git grep` census over tracked files, build output excluded, returned the declarations,
the doc comments and parity-ledger entries beside them, one historical CHANGELOG line and
two unrelated `displayMode` props on `GridField` / `MasterDetailForm` — no read. The same
pass over `floatingConfig`, a key that IS read, returned 79 lines, so the instrument was
not blind.

FROM → TO:

- `ChatbotSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**, an
ADR-0049 retirement tombstone whose comment points at `type` as the replacement.
- `ChatbotFloatingSchema.displayMode?: 'inline' | 'floating'` → **`displayMode?: never`**,
the same tombstone. objectui#7655 declared the key on the floating face with
`ChatbotSchema`'s own lines precisely so this retirement would find it on both faces;
leaving the copy typed would have kept the published face teaching the switch.
- `chatbot-floating` `inputs`: the **Display Mode** control is removed.
- `chatbot-floating` `defaultProps`: `displayMode: 'floating'` is no longer written into
designer-created nodes.

A control is restated, never deleted into a vacuum (objectui#7070): the restatement of
the removed control is the tombstone's guidance plus this note.

**Migration.** Delete `displayMode` from any TypeScript literal typed as `ChatbotSchema`
or `ChatbotFloatingSchema`; the presentation you wanted is already chosen by `type` —
`'chatbot-floating'` for the trigger-and-panel, `'chatbot'` / `'chatbot-enhanced'` for
inline. **No JSON document needs editing** — see the next section.

## Stored documents: runtime validation of this key is unchanged — zero before, zero after

`displayMode` has never had a Zod arm — it sits in the `UnmirroredDeclared` ledger for
both `complex.zod.ts#ChatbotSchema` and `#ChatbotFloatingSchema`, and `BaseSchema` is
`.passthrough()` — so a stored document carrying `displayMode: 'floating'` (every node
the designer ever created) parses green before this change and parses green after it,
and the value is dropped at render time exactly as it always was.

That is deliberate, and it is why this tombstone has **no `retirementTombstone()`
half**: minting a mirror arm to refuse the key would be the declared-but-unmirrored axis
(objectui#6152), a different defect, and a parse outcome the ruling did not ask for.
`packages/types/src/__tests__/chatbot-display-mode-retired.test.ts` pins both twins'
shapes as a **tripwire** — the same shape objectui#7669 gave `triggerIcon` — so that if
objectui#6152 ever mints an arm for `displayMode`, the pin goes red and whoever lands the
mirror adds the `retirementTombstone()` half at that time, flipping the control rather
than deleting it.

## Why a tombstone and not a deletion — measured on this carrier

`ChatbotSchema` extends `BaseSchema`, which carries a `[key: string]: any` index
signature, and on such a carrier deleting an optional member is **silent in every value
shape**: the index signature defeats both excess-property checking and the weak-type
check. Measured on this member with `tsc -p tsconfig.test.json`, a no-index-signature
control carrier (`FloatingChatbotConfig`) lit in the same run:

| route | fresh `'floating'` | fresh `'bogus'` | widened `'floating'` |
|---|---|---|---|
| declared (before) | clean | `TS2322` | clean |
| deleted | clean | **clean** | clean |
| tombstoned (after) | `TS2322` | `TS2322` | `TS2322` |

Deleted, the member reads as `any` and even a wrong-typed value goes quiet. Tombstoned,
**presence with any value** is a compile error — a channel deletion cannot produce on
this carrier at all. On a `BaseSchema` carrier the two routes are loud-vs-silent, not
louder-vs-quieter (the discriminator's carrier branch as corrected on objectui#7678).
Prong 2 of that discriminator licenses the tombstone: the key was advertised in the
3.3.0 release record (`CHANGELOG.md:578`) and its published comment taught it as the
presentation switch. The deleted row is pinned in the test file as a live control — an
undeclared key that rides both shapes with no directive — so the contrast cannot rot.

## Accept-set change, one line per face

- **TypeScript.** A write of `displayMode` against either chatbot face used to compile
and now does not.
- **Runtime (Zod / `safeValidateSchema`).** Nothing changes at all — a stored document
carrying the key parses green before and after, and keeps the value.
- **Designer.** The **Display Mode** control disappears from the `chatbot-floating`
property panel, and newly created nodes no longer carry the key.
- **Manifest, author-time validator, and generated JSX props.** The `chatbot-floating`
registration's `inputs` go from 20 entries to 19 and its `defaultProps` from 9 keys to
8, so the manifest projected from them no longer lists the prop. Measured on both sides
of this change: `validateTree` on a stored `chatbot-floating` node carrying
`displayMode` goes from **0 diagnostics to exactly 1** — code `unknown-prop`, severity
**`warning`**, message `` `<chatbot-floating> has no prop "displayMode"` `` — which is
what the JSX/HTML authoring tier reports through `compile()`. In the same pair of runs
the props interface `generateDts` derives from those same `inputs` drops from 20 members
to 19, losing its `displayMode?: string` line, so a `.tsx` page written against those
generated intrinsics no longer type-checks the attribute.

**This is author-time only: no stored document stops parsing and nothing at render
moves.** The value survives compilation — `compile()` returns a tree still carrying
`displayMode: 'floating'`, byte-for-byte the same keys before and after — and a
`warning` never blocks a page, because the page renderer filters the diagnostics to
`severity === 'error'` before deciding whether to fail. Two neighbouring instruments are
untouched and worth naming so the scope is not read wider than it is: `os validate` runs
`safeValidateSchema`, the Zod path, and is silent on this key before and after; and the
build-time `sdui-intrinsics.d.ts` artifact is generated from the PUBLIC tier, which does
not contain `chatbot-floating` on either side of this change.
38 changes: 26 additions & 12 deletions content/docs/plugins/plugin-chatbot.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ 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 -
which no row below carries and which the other two registrations have no
trigger or panel to apply. They are documented in their own table after this
one, under **`chatbot-floating` panel and trigger keys**.
six more keys of its own - the six `floatingConfig` entries - which no row
below carries and which the other two registrations have no trigger or panel
to apply. They are documented in their own table after this one, under
**`chatbot-floating` panel and trigger keys**.

| Property | Type | Default | Description |
|----------|------|---------|-------------|
Expand Down Expand Up @@ -195,19 +195,33 @@ one, under **`chatbot-floating` panel and trigger keys**.

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

The seven keys below are declared in the `chatbot-floating` registration's own
The six 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` 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.
`chatbot-enhanced` registrations render neither and ignore them. `floatingConfig`
is declared on `ChatbotSchema` and on `ChatbotFloatingSchema` alike
(objectui#7655 declared the floating face with the same member; `ChatbotSchema`
kept its own), so authoring it on an inline node type-checks and parses - and
is dropped at render time, because the `chatbot` node never read it.

**There is no `displayMode` key.** The presentation is selected by the node's
own `type`: author a `chatbot-floating` node for the trigger-and-panel
presentation, and a `chatbot` or `chatbot-enhanced` node for an inline one.
`displayMode` (`'inline' | 'floating'`) used to be declared on both faces,
offered as a **Display Mode** control in the designer and seeded as
`'floating'` into every node the designer created - and read by nothing: it was
a second spelling of the choice `type` already makes, so `'inline'` on a
`chatbot-floating` node changed nothing and `'floating'` on a `chatbot` node
produced no trigger. objectui#7654 retired it (maintainer ruling, 2026-09-05):
the declaration is a `never` tombstone on `ChatbotSchema` and
`ChatbotFloatingSchema`, so writing the key against either face is now a
compile error, and the designer control and default are gone. Stored documents
that still carry the key parse exactly as they did - it never had a Zod arm and
the twins are passthrough - and the value is ignored at render time, as it
always was.

| 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. 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 Down
17 changes: 14 additions & 3 deletions packages/plugin-chatbot/src/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,9 @@ ComponentRegistry.register('chatbot-enhanced',
// anonymous `ChatbotSchema & { ... }` intersection local to this file;
// every key that intersection carried was read-site-censused before being
// declared on `ChatbotEnhancedSchema`, and the two `ChatbotSchema` keys this
// registration never read (`displayMode`, `floatingConfig`) are not on it.
// registration never read (`floatingConfig`, and `displayMode` — since
// retired as a `?: never` tombstone on both faces, objectui#7654) are not on
// it.
// `surface` (objectui#6687, maintainer ruling 2026-08-29) is declared there
// too; the plugin's own `ChatbotSurface` alias is pinned equal to it in
// `__tests__`, so the union has one contract, not two dialects
Expand Down Expand Up @@ -494,8 +496,18 @@ ComponentRegistry.register('chatbot-floating',
{
namespace: 'plugin-chatbot',
label: 'Chatbot (Floating)',
// `displayMode` is NOT offered here and NOT seeded below (objectui#7654,
// maintainer ruling B, 2026-09-05). The control painted a "Display Mode"
// switch this registration never read — the node `type` is the one
// selector of presentation, and `<FloatingChatbot>` below renders
// unconditionally — while `defaultProps` wrote `'floating'` into every
// designer-created node. The control is restated, not deleted into a
// vacuum (objectui#7070): the restatement is the `?: never` tombstone on
// `ChatbotSchema` / `ChatbotFloatingSchema` in `@object-ui/types` and the
// release note. Stored documents carrying the key are unaffected — it has
// no Zod arm and `BaseSchema` is `.passthrough()`, so they parse exactly
// as before, and nothing here ever read the value.
inputs: [
{ name: 'displayMode', type: 'string', label: 'Display Mode', defaultValue: 'floating', description: 'Set to "floating" for FAB widget' },
{ name: 'floatingConfig.position', type: 'string', label: 'FAB Position', defaultValue: 'bottom-right', description: 'bottom-right or bottom-left' },
{ name: 'floatingConfig.defaultOpen', type: 'boolean', label: 'Default Open', defaultValue: false },
{ name: 'floatingConfig.panelWidth', type: 'number', label: 'Panel Width', defaultValue: 400 },
Expand All @@ -517,7 +529,6 @@ ComponentRegistry.register('chatbot-floating',
{ name: 'className', type: 'string', label: 'CSS Class' },
],
defaultProps: {
displayMode: 'floating',
floatingConfig: {
position: 'bottom-right',
defaultOpen: false,
Expand Down
Loading
Loading