Skip to content

[finding] useObjectChat declares its messages as the AUTHORING ChatMessage, but in API mode it returns RUNTIME messages cast to that type #4424

Description

@yinlianghui

Found while implementing #4399 (the typed adapter at the @object-ui/types to plugin-chatbot ChatMessage seam). Out of that card's scope — #4399 owns the three renderer.tsx cast sites, and this is one layer up, in the hook that feeds them — so filing rather than fixing. Unassigned, finding-labelled, no pm:queue: nothing a user hits today, evidence below.

Fact

useObjectChat declares messages: OuiChatMessage[] on UseObjectChatReturn — the @object-ui/types authoring contract. In local mode that is true. In API mode it is not: the value is built from the runtime mapper and cast.

packages/plugin-chatbot/src/useObjectChat.ts:

const apiMessages: OuiChatMessage[] = uiMessagesToChatMessages(aiMessages, {
  isStreaming: isLoading,
}).map((m, idx) => ({
  ...m,
  metadata: (aiMessages[idx] as any)?.metadata,
})) as OuiChatMessage[];

uiMessagesToChatMessages returns the runtime ChatMessage (./ChatbotEnhanced). Those objects carry keys the authoring type does not declare, all of them load-bearing for rendering:

  • on the message: buildProgress, blueprintProgress, charts
  • on each tool invocation: pendingActionId, draftReview, proposedPlan, proposedChanges, builderHandoff

The as OuiChatMessage[] erases every one of them. So the hook's declared output type is narrower than the values flowing through it, in the direction that hides capability.

Why finding and not a defect

Nothing is broken today. The keys survive at runtime because every consumer between the hook and the components passes the objects through by reference rather than rebuilding them. That was true of the three messages as any casts before #4399, and it is true of the adapter that replaced them — PR #4416 spreads unlisted keys through specifically because of this cast, and says so in chatMessageAdapter.ts. Its pass-through: the API-mode payload survives the seam tests are the current pin on the behaviour.

Why it is still worth a card

The pass-through is a workaround for a mis-declared type, and it is load-bearing without being enforced. Anyone who writes the obvious thing — rebuild a message field-by-field from its declared type, anywhere on this path — silently deletes the HITL approval card, the "Review N changes" affordance, the proposed-plan card, the build panel and inline charts. The compiler will agree with them, because the declared type genuinely does not have those keys. That is a trap with no guard rail on it other than a comment.

Directions worth weighing (not a recommendation — needs the same measurement #4399 got):

  1. give UseObjectChatReturn.messages a type that is honest about both modes (a union, or the runtime type, given the runtime shape is a superset of what authoring declares for the shared keys);
  2. keep the authoring type and have API mode genuinely produce it, moving the runtime-only lift to the render seam;
  3. leave it and pin the pass-through harder.

Option 1 changes a public hook contract and touches onSend(content, messages), so it is a survey card, not a rider.

Secondary observation, same family

packages/app-shell/src/console/ai/AiChatPage.tsx:80 still carries a comment stating that @object-ui/plugin-chatbot "ALSO exports a minimal legacy ChatMessage from its own barrel module (id/role/content/timestamp/avatar only)". That has been false since #4383 / PR #4400: the barrel exports one contract, and ChatbotEnhancedMessage is now a deprecated alias of the same type. One stale paragraph, but it describes a hazard that no longer exists and would send a reader looking for a collision that was already retired.

Anchor

Measured on claude/issue-4399-chatmessage-seam-adapter at 5b7926107 (merge-base with main 0f2134831, 2026-08-12). Reproduce: git grep -n "as OuiChatMessage\[\]" -- packages/plugin-chatbot/src/useObjectChat.ts.

Related: #4399 (the seam below this one, fixed in PR #4416), #4383 (the in-package collision, fixed), #4040.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions