87 lines
8.6 KiB
Markdown
87 lines
8.6 KiB
Markdown
# Chat composition: reference comparison and implementation
|
||
|
||
The implementation follows the user's latest REUI ai-chat-9 reference: headless MessageScroller with bottom following and no turn anchors. Persona fonts, tokens, bubbles, metrics, decorations and receipts remain StyleX-owned; no Tailwind or additional AI runtime is introduced.
|
||
|
||
| Reference inspected | Relevant composition | Decision |
|
||
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| [shadcn MessageScroller](https://ui.shadcn.com/docs/components/base/message-scroller) and [TanStack helper](https://ui.shadcn.com/docs/helpers/tanstack-ai) | Default Provider, user-turn anchors, constrained transcript viewport, sibling composer; native spacer makes room for the answer | Keep the headless parts and sibling footer; use the subsequently requested REUI ai-chat-9 scroll policy below. |
|
||
| [AI Elements Conversation](https://elements.ai-sdk.dev/components/conversation) and [Suggestions](https://elements.ai-sdk.dev/components/suggestion) | Separate conversation/input composition; Suggestions exposes ScrollArea props | Reuse the horizontal ScrollArea + button composition with installed Base UI primitives and persona StyleX. Do not add a second conversation/transport stack. |
|
||
| REUI `ai-chat-12` authenticated registry source | RetrievalReceipt uses Base UI disclosure; AskThread enables auto-scroll and disables its row anchors | Keep the receipt composition and adopt its bottom-following policy, also used by ai-chat-9. |
|
||
| [Neon AgentChat](https://ui.neon.com/agent/agent-chat), including its registry source | MessageScroller plus separate ChatInput; anchors the latest user while enabling auto-scroll; typed to AI SDK messages | Adopt its clear transcript/footer separation, retain native TanStack messages; use REUI ai-chat-9 for scroll policy. |
|
||
| [TanStack React Chat UI](https://tanstack.com/ai/latest/docs/ui/react) and installed types | Typed layout/message/part renderers and streaming context | Keep existing `createChatUI`, `ui.Message`, `ui.Part` and structured-output rendering. Scroll and disclosure belong to their headless UI packages. |
|
||
|
||
## `components/narrative-chat.tsx`
|
||
|
||
```diff
|
||
- <MessageScroller.Provider>
|
||
+ <MessageScroller.Provider autoScroll>
|
||
<MessageScroller.Root>
|
||
- <MessageScroller.Viewport onScroll={customHeaderDirectionHandler}>
|
||
+ <MessageScroller.Viewport>
|
||
<MessageScroller.Content aria-busy={chat.isLoading}>
|
||
<Messages />
|
||
</MessageScroller.Content>
|
||
</MessageScroller.Viewport>
|
||
<MessageScroller.Button direction="end" />
|
||
</MessageScroller.Root>
|
||
</MessageScroller.Provider>
|
||
<persona.components.Composer />
|
||
|
||
<MessageScroller.Item
|
||
messageId={message.id}
|
||
- scrollAnchor={message.role === "user"}
|
||
+ scrollAnchor={false}
|
||
>
|
||
```
|
||
|
||
The provider follows the live edge with `autoScroll`; every row has `scrollAnchor={false}`, matching REUI ai-chat-9 and ai-chat-12. Manual wheel/touch/keyboard navigation releases following. The native jump control returns to the latest content. No user-turn spacer is requested. Rows use `contentVisibility: visible` so the primitive measures actual content. REUI ai-chat-9's fork-slot IDs and imperative `ForkPin` concern version switching; this app does not copy that unrelated branch runtime.
|
||
|
||
Viewport styles mirror the reference frame (`min-height: 0`, contained overflow, hidden while native pending-scroll positioning runs). Content is `height: max-content; min-height: 100%`; message rows do not shrink. The browser's independent overflow anchoring is disabled so the package owns turn positioning.
|
||
|
||
## `components/narrative-composer.tsx`
|
||
|
||
```tsx
|
||
<form>
|
||
<ScrollArea.Root>
|
||
<ScrollArea.Viewport role="region" aria-label={t("suggestionActions")}>
|
||
<ScrollArea.Content>{/* actual persona suggestion buttons */}</ScrollArea.Content>
|
||
</ScrollArea.Viewport>
|
||
<ScrollArea.Scrollbar orientation="horizontal">
|
||
<ScrollArea.Thumb />
|
||
</ScrollArea.Scrollbar>
|
||
</ScrollArea.Root>
|
||
<div role="group" data-slot="input-group">
|
||
<textarea readOnly={isLoading} aria-busy={isLoading} />
|
||
<Button type="submit">{/* persona indicator */}</Button>
|
||
</div>
|
||
</form>
|
||
```
|
||
|
||
The installed `@shadcn/react` package does not export InputGroup. Its registry counterpart is a styled composition, so this implementation uses the native textarea and Base UI button without importing Tailwind wrappers. ScrollArea handles overflow, keyboard focus and thumb measurement; the existing persona card effect observes its real viewport.
|
||
|
||
All persona composers now participate in normal footer flow. Custom composer ResizeObserver/inset variables, absolute/sticky positioning and compensating 120–160px transcript padding are removed. The header stays visible; its classical title is one truncating line with the complete accessible text and native title hint. It cannot change the viewport by wrapping from two lines to one as a response starts. No fixed-height empty title row is added.
|
||
|
||
The local MessageScroller patch and its marker-specific test are removed. Scroll behavior uses unmodified `@shadcn/react@0.3.1`; the unrelated Gemini wire-schema patch remains. A separate minimal TanStack core patch corrects successful structured-output event ordering; see below.
|
||
|
||
## Offline acceptance
|
||
|
||
- `public-turn-anchor.spec.ts`: growing prose, late suggestions, completion, manual reading and subsequent submission; multi-frame screen positions, not just normalized content coordinates.
|
||
- `public-status-scroll.spec.ts`: static header geometry during wheel scrolling, normal and reduced motion.
|
||
- `public-suggestions-scroll.spec.ts`: six long options, keyboard horizontal scrolling, offscreen focus/selection, input focus, contained overflow.
|
||
- Existing streaming, receipt, lifecycle and saved-history regressions remain relevant.
|
||
|
||
Validation results are recorded after running the final implementation. No live model requests, deployment or push are part of this work.
|
||
|
||
## Completion interruption corrected at the source
|
||
|
||
The initial scroll diagnosis was incomplete: a two-turn manual-reading test exposed a transient native error during completion. The MutationObserver saw `streaming → interrupted → complete` while `aria-busy` remained true. On Nelly the transcript shrank from 5917px to 3177px and restored, clamping the scroll position in between.
|
||
|
||
The latest published `@tanstack/ai` version was checked as 0.53.0. In its native combined path, `RUN_FINISHED` preceded `structured-output.complete`; the client correctly treated the still-unfinished part as an error. The bounded source+dist patch defers that existing successful terminal until after validated structured completion. It does not suppress real errors, modify tool waits, or introduce an app-level transport/event buffer.
|
||
|
||
```diff
|
||
- flushDeferredModelRunFinishedChunks() // before structured harvest
|
||
harvestCombinedStructuredOutput()
|
||
+ flushDeferredModelRunFinishedChunks() // successful, noncancelled harvest only
|
||
```
|
||
|
||
Native regression tests observe every update, not just final data. Successful completion never enters error; invalid output still errors without a successful terminal; server cancellation never becomes completed data. Client-side cancellation guards remain because client `stop()` does not always finalize its structured part. Browser tests also observe every completion mutation and reject the transient interrupted state.
|