fix(types): declare avatar and avatarFallback on ChatMessage, on both faces (objectui#7295) - #7732
Conversation
…n both faces `packages/plugin-chatbot/src/index.tsx:173–178` reads `message.avatar || userAvatarUrl` and `message.avatarFallback || userAvatarFallback` (and the assistant twins), the authoring-to-runtime seam spreads every unlisted key through (`chatMessageAdapter.ts`, `...passthrough`), and the SDUI renderer feeds the authored `messages[]` straight in — so a per-message avatar override renders and is documented, and no authoring-facing type declared it. objectui#4424's `RuntimeOnlyMessageKeys` named only the three keys API mode lifts out of the stream. Measured on 446d93d before the change, through the built types dist: `ChatMessage` has no index signature (objectui#5155 — none is added), so an author annotating `ChatbotSchema.messages` got TS2353 on a value that renders (five on `plugin-chatbot.mdx`); `ChatMessageSchema` is a plain strip-mode `z.object`, so the value parsed green through `ChatMessageSchema`, `ChatbotSchema` and `safeValidateSchema` and was DROPPED from the parsed output, and `avatar: 42` was admitted-and-stripped. After: the parse keeps the value and refuses a non-string at the key. Pin: `__tests__/chat-message-avatar-keys-7295.test.ts` — read-site text and read-set derivation off disk, mirror-shape membership, acceptance with the value surviving through the mirror, `ChatbotSchema` and the published entry point, refusal at the key (and at `messages.0.KEY` through the entry point), invariant type-level pins (string, optional, not any, equal to the chatbot-level keys they override, no index signature), a control key the plugin never reads (`avatarUrl`) that stays undeclared on both faces and is still admitted-and-stripped, and the three doc blocks' annotations. Docs: the three `plugin-chatbot.mdx` example blocks PR #7294 left unannotated (`supportChat`, `salesBot`, `multiAgentChat`) are annotated `ChatbotSchema` again. No `UNGATED_DOCS` row for that page exists on main, so `scripts/` is untouched. `packages/plugin-chatbot/**` untouched; `SeamChatMessage` inherits both keys through its `ChatMessage` half. Changeset: `@object-ui/types` patch — the accept set only widens toward what already renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
…t-message-avatar-keys
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…t-message-avatar-keys
…e in the objectui#7295 pin
`@typescript-eslint/no-empty-object-type` flags the `{}` in
`IsOptional`: the intent is "an object with no keys is assignable to
`Pick<T, K>` only when `K` is optional", so the helper now says
`Record<string, never>`. Verified to still discriminate: true for
`avatar` / `avatarFallback`, false for the required `id` / `content`.
No other change; `@object-ui/types#lint` is back to 0 errors.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
|
Patch round 1 — gate table for head What moved:
Not re-run this round because their inputs did not change since the Placeholders and generics are spelled as words because the body sanitizer eats angle-bracket shapes. Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Generated by Claude Code |
Fixes #7295
What
avatar?: stringandavatarFallback?: stringare declared onChatMessage(packages/types/src/complex.ts) and on its zod mirrorChatMessageSchema(packages/types/src/zod/complex.zod.ts), each with a docblock citing the read site. This is reading 1 of the card, as thedomain:specseat's disposition (claim comment 5551795779) recorded it. No index signature is added (triage boundary 2, objectui#5155).packages/plugin-chatbot/**is untouched:RuntimeOnlyMessageKeysstays as it is, andSeamChatMessageinherits the two keys through itsChatMessagehalf (proven below by type-check, not edited). No other member ofChatMessageor ofChatbotSchemamoves.Read sites and the seam, re-verified on this tree
packages/plugin-chatbot/src/index.tsx:173-174—message.avatar || userAvatarUrl/message.avatar || assistantAvatarUrlpackages/plugin-chatbot/src/index.tsx:177-178—message.avatarFallback || userAvatarFallback/message.avatarFallback || assistantAvatarFallbackpackages/plugin-chatbot/src/chatMessageAdapter.ts:256—const { role, timestamp, toolInvocations, ...passthrough } = message;packages/plugin-chatbot/src/ChatbotEnhanced.tsx:92-93declares both keys asstring;ChatbotSchema's ownuserAvatar/userAvatarUrl/userAvatarFallback/assistantAvatarFallbackarestringon both faces, so the per-message keys are typed exactly like the chatbot-level keys they override (pinned by invariant equality).Measured "before" on
446d93d(builtpackages/types/dist) — the zod mirror's unknown-key policyChatMessageSchemais a plainz.objectwith no catchall: STRIP mode, not.passthrough().ChatbotSchemaitself is passthrough throughBaseSchema, but itsmessages[]element is the strict-strip object. Measured through the dist:ChatMessageSchema.safeParseof a message carryingavatarandavatarFallback→ success, data keysid, role, content— both keys DROPPEDChatbotSchema.safeParseof achatbotnode with that message inmessages→ success,messages[0]keysid, role, content— DROPPEDsafeValidateSchemaof the same node → success,messages[0]keysid, role, content— DROPPEDavatar: 42→ success (admitted-and-stripped, never refused)avatarUrl: 42→ success, strippedSo the runtime honours a per-message avatar only because the renderer receives the un-parsed document. Behaviour change this PR makes, stated as measured: the parse now KEEPS an authored
avatar/avatarFallbackthrough all three paths, and REFUSES a non-string value at the key (messages.0.avatarthrough the entry point). Still a restoration — the renderer already honours the key — and apatch: no document that validated before stops validating and no TypeScript value that compiled before stops compiling; the changeset states the argument.TS-face "before", reproducing the card's count: the three
plugin-chatbot.mdxexample blocks annotatedChatbotSchemaagainst the base dist →tscexit 2, exactly 5 x TS2353 onavatarFallback, nothing else. After, against the rebuilt dist → exit 0, 0 diagnostics.Docs
The three example blocks PR #7294 left unannotated (
supportChat,salesBot,multiAgentChat— measured from that PR's landed diff9bf0abfeand from the fence headers) are annotatedChatbotSchemaagain: import line, annotation, closing semicolon, no prose rewrite. NoUNGATED_DOCSrow forcontent/docs/plugins/plugin-chatbot.mdxexists onmain(PR #7294 already removed it), so nothing underscripts/is touched. The "Per-message Avatars" block still importsChatMessagefrom@object-ui/plugin-chatbot; it is not one of the three and is left as-is — noted for the reviewer, since the@object-ui/typesspelling now declares the keys too.Pin —
packages/types/src/__tests__/chat-message-avatar-keys-7295.test.tsThe form of
checkbox-wrapper-class-6938.test.ts/undeclared-but-consumed-keys-6150.test.ts, adapted for a mirror that STRIPS and a TS face with NO index signature: read-site text and themessage.KEYread set derived off disk (contains both keys, not the control); membership onChatMessageSchema.shape; acceptance with the value surviving throughChatMessageSchema,ChatbotSchemaandsafeValidateSchema; refusal at the key and atmessages.0.KEYthrough the entry point; invariant type-level pins (string, optional, notany, equal to the chatbot-leveluserAvatarUrl/userAvatarFallbacktypes,string extends keyof ChatMessageis false, the control key is not inkeyof); a@ts-expect-errorliteral for the control key; the control keyavatarUrl(the spelling an author would guess fromuserAvatarUrl; the plugin never reads it) absent from the shape and still admitted-and-stripped; and the three doc blocks' annotations with their five per-messageavatarFallbackoverrides.Gates (exit codes captured before any pipe; heavy runs under the shared verify lock with the
VERDICT command-exitline read)Final head
4b0f366c(=63ece2c8fix + merge oforigin/mainatb74a859):pnpm --filter @object-ui/types build→ exit 0 (dist completeness: 1 package(s) complete (120 emitted files verified));dist/complex.d.tscarriesavatar?: stringpnpm exec vitest run --maxWorkers=2 packages/types/→Test Files 115 passed (115),Tests 1985 passed (1985); the pin alone 25/25pnpm --filter @object-ui/types type-check→ exit 0;tsc -p tsconfig.test.json --listFileslists the pin (count 1)pnpm --filter @object-ui/plugin-chatbot type-checkagainst the rebuilt dist → exit 0tsc: invariant equality of NonNullable ofSeamChatMessageavatar withstring, same for avatarFallback, plus a literal carrying both keys) → exit 0, 0 diagnostics; the same probe against the ABLATED dist → exit 2, TS2339 onSeamChatMessageavatar — the inheritance goes through the types distpnpm exec vitest run --maxWorkers=2 packages/plugin-chatbot/→Test Files 37 passed (37),Tests 456 passed (456)(on63ece2c8; the merge touched no plugin-chatbot input)check-changeset-presence/check-changeset-fixed/check-changeset-no-major/check-control-bytes(6300 tracked text files) /check:spec-symbols/check:doc-types/check:doc-fences/check-doc-links→ all exit 0 with their own green verdict linescheck-governed-merges.mjs --teston the five paths →0 of 5 path(s) hit the register— NOT governedpnpm check:doc-snippets: NOT MEASURED locally. The gate's--build-filternames the whole plugin + app-shell closure (26 packages), beyond the foreground cap on a shared box. Narrowed measurement declared instead: the three annotated blocks, extracted from the mdx as they stand on disk, compiled withtsc --strictagainst the rebuiltpackages/types/dist→ exit 0, 0 diagnostics (before: exit 2, 5 x TS2353). CI'sDoc Snippet Type Checkis the measurement.Ablation (fix committed first; trap-guarded, absolute paths)
complex.ts+complex.zod.tschecked out from446d93d: mutation proven by blob hash (both equal the BASE blobs) and marker counts 1/1 → 0/0. Pin:12 failed | 13 passed (25)— red: membership x2, survive x6 (three paths), refusal x4 (two paths); green: read-site, read-set, seam, control-key and doc legs.tsc -p tsconfig.test.json: exit 2, 13 diagnostics all inside the pin file (TS2339 / TS2344 / TS2353), 0 elsewhere. Restore bygit checkout HEADon both paths: blob hashes equal the HEAD blobs, markers back to 1/1,git diff HEADempty,git statusempty; dist rebuilt from the restored source (marker 1).Placeholders in this body are spelled as uppercase words (KEY, BOTH) because the body sanitizer eats angle-bracket shapes.
Contract review at tier (Clause-② yes): two new declared keys on a published payload type.
needs:contract-reviewattached; the PR stays draft for the in-seat review. Dev of thedomain:specseat's dispatch, sessionsession_01BAZFhALsQsGqxui8sNqM8s.🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Generated by Claude Code