refactor(plugin-chatbot): one name, one ChatMessage contract (#4383) - #4400
Merged
Conversation
The barrel exported two different `ChatMessage` types: a minimal one it declared itself (id/role/content/timestamp/avatar/avatarFallback) and the shape `<ChatbotEnhanced>` actually renders, re-exported under the alias `ChatbotEnhancedMessage`. The natural name resolved to the narrow one, so an importer reaching for `ChatMessage` silently got the wrong contract and the compiler could not object (objectui#4040 / PR #4379). The minimal declaration is retired; `ChatMessage` now IS the enhanced shape, and `ChatbotEnhancedMessage` survives as a deprecated alias OF THE SAME TYPE so existing importers keep compiling. Pinned at compile time in `__tests__/chat-message-contract.test.ts`, with a runtime net that fails if the barrel ever declares a message shape of its own again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
…tmessage-collision
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
ACCEPT — PM 复核 (session
Flipping ready + arming auto-merge. Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 12, 2026 02:25
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4383
@object-ui/plugin-chatbotexported two differentChatMessagetypes: a minimal one the barrel declared itself (id/role/content/timestamp/avatar/avatarFallback) and the shapeChatbotEnhancedactually renders, re-exported from the same module under the aliasChatbotEnhancedMessage. The natural name resolved to the narrow one, so an importer reaching forChatMessagesilently got the wrong contract — and the compiler could not object, because both shapes existed on purpose and every construction site spreads the extra keys conditionally (...(x ? { toolInvocations } : {})), which defeats excess-property checking. That is how app-shell'sAiChatPageended up unable to readtoolInvocationsoff its own function's return value (#4040; re-pointed in PR #4379, which deliberately left the collision itself standing).Survey
Command:
git grep -nE '\b(ChatMessage|ChatbotEnhancedMessage)\b' -- packages apps examples,distexcluded, then filtered for type references (most raw hits are function names that merely contain the string —uiMessageToChatMessage,hydratedMessagesToChatMessages,sanitizeChatMessagesForCache— plus the unrelatedChatMessageProps/ChatMessageSource/ChatMessageSchema).ChatMessageplugin-chatbot/src/index.tsx(the barrel)ChatbotProps.messages,ChatMessageProps.message— same file onlyChatMessageplugin-chatbot/src/ChatbotEnhanced.tsxmapMessages.ts,useHitlInChat.ts, 3 test suites (all via../ChatbotEnhanced)ChatbotEnhancedMessageindex.tsxpackages/app-shell/src/console/ai/AiChatPage.tsx:88onlyChatMessage@object-ui/typescomplex.tsplugin-chatbot/renderer.tsx,useObjectChat.ts(aliasedOuiChatMessage)The decisive number is the first row: nothing imported the legacy shape. It was declared, exported under the most attractive name in the package, and read by no one but the file that declared it.
Shape diff — the enhanced type is a strict superset: every legacy field is present with the identical type, and it adds only optional keys (
streaming,toolInvocations,reasoning,sources,traceId,buildProgress,blueprintProgress,charts).Convergence
Enhanced shape under the natural name; legacy declaration deleted, not renamed:
packages/plugin-chatbot/src/index.tsxno longer declares a message type. It imports the one from./ChatbotEnhancedforChatbotProps/ChatMessagePropsand re-exports it asChatMessage, under a doc comment recording the retirement (modelled onRoleDefinition's note from PR chore(sweep): retire four zero-consumer declared surfaces — dead-surface deletions batch 3 (#4328) #4366).ChatbotEnhancedMessageis kept as a@deprecatedalias of that same type. This is not a third name and not a second contract — the two spellings now denote one type, pinned below. It is kept for the one piece of concrete compat evidence the survey turned up:AiChatPage.tsx:88imports it, and app-shell is held by app-shell: publishing a draft view does not invalidate the adapter's view caches — the console's real create-view flow bypasses ObjectStackAdapter entirely #4373 withAiChatPageexplicitly read-only. Deleting the alias would have forced an edit there; keeping it means zero importer migration in this PR, in any package, held or free.Pin, and its pre-fix red
packages/plugin-chatbot/src/__tests__/chat-message-contract.test.ts(#4118 convention, matching this package's existingspec-symbol-batch6.test.ts). Compile-time half — a violation is atscerror undertsconfig.test.json, which is the only lane that can see it, since vitest erases the assertions:plus probe hygiene (
IsAny/IsUnknown),HasIndexSignaturefalse (the objectstack#4075 mechanism — with an index signature every comparison above answers "identical"), one named pin per enhanced-only key, and a widening pin (RetiredMinimalChatMessage extends BarrelChatMessage) that fails the day one of the added keys becomes required. Runtime half: readsindex.tsxand fails if the barrel ever declares a message shape of its own again — the one edittscreports only in the type-check job.Reverse verification — direction predicted red, and red is what happened.
git checkout HEAD~1 -- packages/plugin-chatbot/src/index.tsx(commit-then-revert; nogit stash, per AGENTS.md) restored the legacy barrel declaration:and the runtime half:
Tests 3 failed | 1 passed (4), withdeclares no local ChatMessage typereportingpackages/plugin-chatbot/src/index.tsx declares a ChatMessage of its own again. Line 84 (_AliasIsEnhanced) stayed green under the reversal, correctly — the alias pointed at the enhanced type in the old barrel too; it is_AliasIsBarrelthat carries the collapse. Restored withgit checkout HEAD -- …, tree clean.Verification
pnpm --filter '@object-ui/plugin-chatbot^...' buildpnpm exec vitest run packages/plugin-chatbot/Test Files 18 passed (18)·Tests 286 passed (286)tsc --noEmit(src) +tsc -p tsconfig.test.json(tests)tsc --listFilesshowschat-message-contract.test.tsandsrc/index.tsxturbo run type-check --concurrency=2Tasks: 78 successful, 78 total· exit 0pnpm --filter '...@object-ui/plugin-chatbot', prefix = dependents)app-shell,console,site,example-console-starter,example-byo-backend-console— all covered by the repo-wide run, all greenpnpm --filter @object-ui/plugin-chatbot lint_-prefixed type aliases draw the sameno-unused-varswarnings asspec-symbol-batch6.test.ts)node scripts/check-control-bytes.mjspresence/fixed/no-major)All heavy steps ran under
flock /tmp/os-heavy-verify.lockwith--max-old-space-size=4096.Changeset
minor—.changeset/chatmessage-one-contract-4383.md, one entry,@object-ui/plugin-chatbotonly. The published export surface changes (ChatMessagenow denotes a different interface;ChatbotEnhancedMessagebecomes deprecated), which per the ruling on #4383 isminorwith the breaking semantics spelled out in the body. Nevermajor— AGENTS.md §版本号策略 forbids it outside an@objectstackmajor sync, andscripts/check-changeset-no-major.mjsenforces it. In practice the change is a widening, so existing values and call sites keep compiling; the body names the case that does not (code relying on the name meaning exactly the six-key shape).Scope notes
packages/plugin-chatbot/src/index.tsx, the new pin test,packages/plugin-chatbot/README.md(docs-driven, AGENTS.md Add automated testing infrastructure and CI/CD workflows #2), and the changeset. No held package was edited: app-shell (app-shell: publishing a draft view does not invalidate the adapter's view caches — the console's real create-view flow bypasses ObjectStackAdapter entirely #4373), components (action-param-dialogrenders a hardcoded EnglishSelect...placeholder — untranslated in zh, and ASCII after #3878 moved the packs to U+2026 #4386),content/docs/**(docs: content/docs 正文反引号里 113 条仓内路径实测 10 条不存在 —— #3735 门禁扩面的「先量红」读数 #3867), core / plugin-dashboard / plugin-report (Analytics label net: the fetch-and-memo glue is written twice (DatasetWidget + plugin-report's useDatasetDimensionLabels) — its home is @object-ui/core #4389) are all untouched, and none needed migrating.content/docs/plugins/plugin-chatbot.mdxdocuments the@object-ui/typesschemaChatMessage(itsroleincludes'tool',timestampmay be aDate), not the runtime type this PR converges — so it is correct as-is, which is fortunate since that path is held.@object-ui/typesalso exports aChatMessage, andplugin-chatbot/src/renderer.tsxbridges the two with threemessages as anycasts. That is a cross-package seam rather than the barrel collision this card owns, and it is dormant (the runtime seam coercesDatetimestamps and foldsrole: 'tool'into the assistant branch), so it is filed as [finding] plugin-chatbot's SDUI renderer castsmessages as anyacross the @object-ui/types ↔ plugin-chatbotChatMessageboundary #4399 with thefindinglabel and left alone here.Generated by Claude Code