Replace the Wind message placeholder with a list on apollo-wind's
MessageScroller: one item per message group with a stable messageId,
user turns as scroll anchors, the user's message in a Bubble and the
assistant's as plain prose. Bodies dispatch through
resolveMessageRenderer: injected component renderers inline, injected
render() renderers in a DOM slot, and a minimal streaming text renderer
as the Wind default until the markdown renderer lands. MessageActions
carries copy, feedback (aria-pressed; the rating can be changed, each
change publishing Feedback again) and the message's own actions, shown
on the latest reply and on hover elsewhere. A live region announces each
settled reply; the log itself stays quiet so streamed chunks are not
read out. ThinkingIndicator shows inside the last group while waiting,
LoadingDots and the empty state are overlays, and a skeleton covers the
initial load: the scroller's rows are message groups only, because it
tells a new turn from a replaced row by counting rows, and transient
rows made it scroll back to an earlier turn. Spacing, gutters and the
user-message inset follow Material's, and the viewport cannot move
sideways.
Hoisted into chat/headless and adopted by Material where it had them:
- useChatMessages, groupChatMessages, withoutFakeStream,
getMessageCopyText: the transcript and the feedback and copy effects
- useDefaultMessageActions, useIsLastAssistantMessage,
useMessageActionHandler: built-in actions, latest-reply visibility,
pre-hooked publishing
- useInjectedMessageRenderer: the DOM slot lifecycle
- getAttachmentFileType: the friendly file type
- useMessageInset: the user message's leading inset
New, Wind-only consumers for now:
- useChatScrollAdapter and useChatAutoScroll: service scroll events onto
any scroll engine's commands, and ConversationLoadMore once per
arrival at the top when paginated
- useLoadingMessages and useSettledReplies
Adds five chat strings to en.json (the l10n sync fills the rest) and an
autoScroll control to the playground; Wind's scenario message actions
use lucide names.
MST-16563
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Second PR of the Wind chat renderer stack (MST-16563): the message list, on apollo-wind's
MessageScroller.MessageScrollerItemper message group with the group's first message id asmessageId. User turns arescrollAnchors. Grouping follows Material: consecutive messages sharing agroupId.Bubble, honouring Material's--custom-autopilot-chat-user-message-bg-color. The assistant's is plain prose. There are no avatars. Attachments render asAttachmentcards with the file name and a type icon, as Material shows them (no type text). A message of files alone shows its attachment row by itself, with no empty bubble and no placeholder text.resolveMessageRenderer:componentrenderers render inline;render(container)renderers get a DOM slot, cleaned up before each re-render and on unmount;MessageActions):actions(dispatched through the shared pre-hook then publish handler);showInOverflowones go in the "More" menu;MessageActionCopywrites the group's copy text (built from the rendered group, no per-bar lookup), and the hoisteduseChatMessagesCopy handler writes the same text for Material parity, so Wind does two identical writes by decision;mt-1 -mb-7), so its space is reserved and hover does not shift layout;aria-pressed; the rating can be switched after it is given (not cleared), each change publishing the sameFeedbackpayload with the new value. A deviation from Material, by decision.useChatScrollAdapter, described below, runs in a small leaf component so scroll-state changes do not re-render the transcript;ChatMessageis memoised. The jump button reuses the "Scroll to bottom" string. Older pages are prepended with the scroll position preserved, through the viewport'spreserveScrollOnPrepend; the adapter skips the end jump for the conversation replacement thatprependOlderMessagespublishes.role="log"witharia-live="off", and aLiveRegionannounces "New response" once per settled reply, so streamed chunks are not read out.messageSpacingapart with Material's trailing margins (14px after a request, 30px after a response, -16px for a lonedisableActionsmessage), messages inside a group touch, andmessageGroupGapseparates a message's own parts; the action bar sits in flow under the message (mt-1 -mb-7). Compact mode is 32px between turns against Material's 21px, since the in-flow bar needs room.isLatestReplydown, so no per-bar lookup. Material'suseIsLastAssistantMessagenow recomputes fromgetConversation()on mount and on every Response, so rating an older reply no longer demotes the latest one, and a message without agroupIdno longer counts as part of the latest reply.messageSpacingandmessageGroupGap, and the user-message inset (useMessageInset). The viewport isoverflow-x: hiddenwithoverscroll-behavior-x: none.ThinkingIndicatorrotates the service's loading messages (setDefaultLoadingMessages, overridden bysetLoadingMessage);LoadingDotsat the top while older messages load;Skeletonduring the initial load.chat-scroll-provider.tsxfor both renderers: its service subscriptions (Request, NewChat, OpenConversation, SetConversation, ModeChange, ToggleAutoScroll and the load-more flags) moved from the container effect into a mount effect, so a toggle sent before the lazily loaded renderer mounts, or across a layout remount, holds. Wind readsuseChatScroll().autoScroll. Under Wind the container effect still returns early, since nothing sets those refs, so it adds no listeners or observers.hasMoreMessagesadded, fromShouldShowLoadingMoreMessages), so a remount does not ask again, and edge changes are ignored until the viewport's first settled layout, so FullScreen does not ask for older messages at mount.Hoisted into
src/chat/headlesshooks/use-chat-messages.ts:useChatMessagessetHasMessageschat-message.tsxuses it, keeping its own suggestions subscriptionmessages/message-groups.ts:groupChatMessages,withoutFakeStream,getMessageCopyTextuseChatMessageshooks/use-message-actions.ts:useDefaultMessageActions,useIsLastAssistantMessage,useMessageActionHandler{ group, message, action }chat-actions.tsx,chat-actions-list.tsxhooks/use-injected-message-renderer.tschat-message-content.tsxWidgetContainerutils/file-type.ts:getAttachmentFileTypeutils/file-to-icon.tshooks/use-message-inset.ts:useMessageInsetchat-message-content.tsxMessageBoxhooks/use-chat-scroll-adapter.ts:useChatScrollAdapterscrollToEnd(instant while streaming, else smooth). SetConversation (except afterprependOlderMessages) and ModeChange call it instantly after render. Arriving at the top, after the layout has settled, publishesConversationLoadMoreonce per arrival when paginated, nothing is loading and the host has more. Material-agnostic, so C-4 can adopt itproviders/chat-scroll-provider.tsxproviders/loading-provider.tsxhasMoreMessageshooks/use-loading-messages.ts,hooks/use-settled-replies.tsgetConversation()on mount and on SetConversation so restored replies are never announcedStorybook
pnpm storybook:design, then Apollo React / Chat / Playground. Compare renderers puts Wind and Material side by side. Try thesescenariovalues:showInOverflow), and disabled actions.Copy and thumbs publish
copyandfeedbackto the Actions panel; the thumb then shows pressed and disabled. The newautoScrollcontrol appliestoggleAutoScroll, at mount and on change; a new turn switches following back on, as in Material.Tests
vitest --run src/chat src/material/components/ap-chat: 31 files, 185 tests pass. New tests cover:showInOverflowactions kept out of the bar (opening the menu itself is checked in Storybook: under apollo-react's tests apollo-wind's built overflow trigger does not open);autoScrollof false;disableActions;aria-pressed, the "Copied" tick, one Copy event and two identical clipboard writes;tsc --noEmit -p packages/apollo-react: clean.biome checkon touched files: clean apart from fouruseExhaustiveDependencieswarnings already present in untouched parts ofchat-scroll-provider.tsx.ApChat.registration.test.tshas a 30s timeout: its cold import of both renderers takes about 4.7s locally, close to the 5s default.en.jsononly (the l10n sync fills the other catalogs, as for earlier chat features):message.list-label,message.response-received,message.actions.copied,message.actions.feedback,message.loading-older.Stack
Jira: MST-16563
🤖 Generated with Claude Code