Repository navigation
Open a conversation at the bottom, keep it still while a Bot writes, and add a rail of turns - #882
Conversation
…e it A lone foldable call is now a group of one under a group: key and is drawn as the same line it was. Existing grouping tests that asserted a lone call's own id or an unprefixed group id now assert the group: id.
…ce for missing numbers
…where the view is
… and pick it up again when they come back
davidmckayv
left a comment
There was a problem hiding this comment.
Requesting changes for a few correctness issues in the inline comments. The direction is good: opening at the bottom, holding still while a Bot writes, and the turn rail are all welcome.
Must fix
- Charts and tables can lose their last value for good (
gallery-tools.tsx:202). A call counts as still streaming unless its status iscomplete. A run that was stopped, or whose page closed before the handler answered, reloads asinProgresspermanently. SosettledArgskeeps dropping its last number, link or colour, andDataTablehides the final row. - Old messages replay (
transcript-pages.ts:67).splitandfromare row indices fixed when the conversation opens. A history merge or tool steps regrouping shifts rows across the boundary. They remount as live, so their entrance plays again and they type themselves out from zero. They can also make the scroller jump to an old turn. - The view stops following a streaming reply in four cases:
- a downward wheel or End press at the bottom, because the library leaves following and nothing switches it back (
follow-intent.tsx:221); - a scrollbar drag with overlay or RTL scrollbars (
follow-intent.tsx:243); - clicking the rail's mark for the current last turn (
chat-transcript.tsx:1951); - the first message sent in a transcript with no earlier message from the person (
chat-transcript.tsx:1693).
- a downward wheel or End press at the bottom, because the library leaves following and nothing switches it back (
Should fix
- The reveal's timer resets on every token (
paced-reveal.ts:111). It falls behind, then dumps the rest in the finish. - Opening waits on the voice-chat query (
channel-chat.tsx:572). A slow or failing/api/voice/sessionsholds every conversation on the skeleton, and anything sent meanwhile isn't drawn. - The red "Couldn't show" box flashes between chunks for a card that throws on partial arguments (
tool-boundary.tsx:42).
The rest are smaller and up to you:
- per-chunk work in the rail and
readActiveTurn; - the per-row late-renderer subscriptions;
- the dropped cleanup in
useDecisionTool; - stale comments;
- the duplicated
ToolRowbranch.
Details and failure scenarios are on each line.
… current turn once a frame
…n for a few frames
…ce the last frame
davidmckayv
left a comment
There was a problem hiding this comment.
Thanks, fast turnaround. 13 of the 15 are fixed, and the other two are partly fixed and fine to merge as they are. The full app suite passes at eefed68 (2083 pass), and typecheck and lint are clean. One new bug from the fixes, so one more round:
Must fix: a decision card that fails is now invisible while the run waits on it (785c3ea, tool-boundary.tsx:59-67).
- Why it happens: the boundary renders nothing when a card failed, the call isn't final, and the row can still receive arguments. For a tool row, final means a result exists (
final={result !== undefined},chat-transcript.tsx:1410), and a decision has no result until the person answers. The run stays busy for the whole wait, so the decision stays the active last row. - Example: a choice whose
optionsarrives as a string throws at(args.options ?? []).map(decisions.tsx:180). At 53686a9 that showed "Couldn't show Choice". Now nothing is drawn, and the turn hangs until someone presses Stop. - Also affected: a gallery card for as long as a slow
executinground trip lasts. - Suggested fix: treat the call as final once its arguments are complete, either by checking that
argsparses or by passing the SDK status through. Add a regression test for a decision that throws while it waits.
Worth a look (not blocking)
- Late voice cards land among the live rows (bc3d2be). The reply says they go into the history region, but
withVoiceChatsplaces each card right after its anchor (archive.ts:228-250), and only rows above the opening split are history. In a channel of 20 rows or fewer, the split is row 0. So a voice card that arrives after the 1.5s cap plays its entrance mid-conversation, and can read to the scroller as a new row. It only happens when the voice service is slow. Showing history without voice chats is the right tradeoff; the description is just off. - Two misplaced comments from the delta:
useDecisionTool's doc block now sits aboveuseForgetRenderOnUnmount(gallery-tools.tsx:235-262);late-renderers.tsx:63points toLateRenderersScope, which doesn't exist.
- Late renderers are now one watch per transcript, which is the main thing. It still polls up to 10 frames because SDK 1.83.0's
setRenderActivityMessagesnotifies nobody. Fine for now. - Before release: a manual pass on a Mac trackpad for the downward flick at the end and the overlay-scrollbar drag. Both are fixed in the code, but tested only with simulated input.
…once its arguments are complete
…tream # Conflicts: # app/src/components/channels/chat-transcript.tsx # app/src/lib/copilot/gallery-tools.tsx
|
Fixed the late voice cards in ec5d335. A voice card for a call that started before the conversation opened now draws at rest. A card for a call made while the conversation is open still animates in. Both cases are covered in One case is left, now listed under known limits: if the conversation ends on the person's own unanswered message, a late card pushes that message into the slot the scroller checks for new rows, and it can move the message to the top of the view. Fixing that needs the scroller to recognise new rows by id instead of by position. I also merged main (#871). The new |
davidmckayv
left a comment
There was a problem hiding this comment.
Approving. The blocker is fixed: toolCallFinal treats a call as final once its arguments parse. Tool arguments are always an object, which only parses once it's closed, so a decision that throws on complete arguments now shows its failure while the run waits. Half-arrived arguments still draw nothing. Both cases are pinned in tool-boundary.test.tsx. The two misplaced doc comments are back where they belong. Late voice cards from before the conversation opened now draw at rest (transcript-late-voice.test.tsx). CI is green.
One thing still worth a manual check before release, as noted last round: a real Mac trackpad for the downward flick at the end and the overlay-scrollbar drag, since both are tested with simulated input only.
What this changes
A conversation now opens already at the bottom, holds still while a Bot writes, and streams smoothly. The work was built in five stacked stages. They ship as one PR because each stage builds on the one before it.
1. Opening. A channel's history is shown once it has all loaded, already at the bottom, instead of loading and then gliding down.
2. Drawing. Charts, tables, cards and decisions a Bot draws grow in place while they stream.
3. Scrolling. We kept
@shadcn/react'sMessageScroller. Its prepend path already keeps the reader's position when rows are added above, and itsautoScrollprop is the only part we needed to control.nextFollow. Where the view happens to be no longer decides it.4. Streaming.
staticmode.argumentsin place.5. A rail of turns. A rail in the left margin has one mark per turn, like the one in T3 Code. The turn being read is drawn longer and darker.
drawTo), and the jump happens a frame later, once the scroller has put the reader back.@3xl).Nothing to do for these:
size-32.Where it runs
Only in the browser. No server code, schema or API changes.
Boundary and audit
Changelog
CHANGELOG.md→ Unreleased → "A conversation opens at the bottom and stays still while a Bot writes".Proof
Tests: new tests cover every stage:
transcript-pages,follow-intentandtranscript-followfor scrolling;paced-revealandtranscript-streamfor the reveal;transcript-rerenderfor rows that stay still;transcript-turnsandtranscript-railfor the rail;late-renderersandvoice-archivefor the rest.Full app suite: 2065 pass, 1 fail. The failure is the existing
serve.test.ts"a stalled handshake times out…", which needs the network. Typecheck,biome lint --error-on-warningsandbiome formatare clean.Measured in the browser (a Chromium script against a running app):
Paging: drawing older pages moved what the reader was looking at by at most 0.5px when scrolled by code, and 0.4px under the mouse wheel. Jumping to the top adds one page per frame, with no errors on a 100+ page conversation.
Following: every rule held mid-stream (wheel, key, select, return to the end, the arrow, send). The 72px new-turn peek is the same as stage 1.
Opening: shows the placeholder, then the bottom in one frame.
A reply with a web search and a long answer:
The rail: on a 13-turn conversation, the current mark matched what was on screen and hovering showed the right preview. Jumping to the first turn drew the 17 older rows and landed on it, 24px below the top, with following off. On five conversations the last mark was selected on open.
Known limits
chat-transcript.tsx.streamingmode after they finish, so the renderer never swaps under the reader. They becomestaticon the next opening. Footnotes and reference-style links can render slightly differently between the two.staticmode. This is rare.