Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4879129
Show a channel's history once it is all in, already at the bottom
guidovizoso Oct 29, 2026
77a5746
Wait for a channel's voice chats before showing its history
guidovizoso Oct 29, 2026
542bc18
Open a conversation without a cascade; animate only what arrives
guidovizoso Oct 29, 2026
91292e2
Glide only for messages sent after a conversation is open
guidovizoso Oct 29, 2026
45592f9
Open group chats at the bottom and follow only from there
guidovizoso Oct 29, 2026
a2c0dbe
Show a new conversation's first message without waiting for voice chats
guidovizoso Oct 29, 2026
04e991c
Show a message typed while history loads under the placeholder
guidovizoso Oct 29, 2026
b254a9a
Bring a person's own group message into view even when scrolled up
guidovizoso Oct 29, 2026
8d0e059
Render a conversation's newest rows in with a short stagger as it opens
guidovizoso Oct 29, 2026
c8b3b24
Pass the opening stagger delay as a variable so reduced motion can dr…
guidovizoso Oct 29, 2026
52a8853
Keep a tool row and its key for its whole life as calls join and leav…
guidovizoso Oct 29, 2026
f11fa7e
Draw gallery cards once and wait for grants instead of refusing
guidovizoso Oct 29, 2026
7674549
Let a card that failed on half-sent arguments try again on the next c…
guidovizoso Oct 29, 2026
8ba8a7d
Keep gallery items in place while their labels stream, and hold a pla…
guidovizoso Oct 29, 2026
493985b
Hold back a half-arrived number, link or colour until it is complete
guidovizoso Oct 29, 2026
03e12f9
Keep a lone tool call's row when its answer turns it into a card
guidovizoso Oct 29, 2026
c9c11a7
Register decisions with one render so they are not rebuilt as grants …
guidovizoso Oct 29, 2026
c3a2ca9
Draw a streaming table's complete rows while its last row is still ar…
guidovizoso Oct 29, 2026
2189688
Draw a conversation's newest rows first and older ones as the reader …
guidovizoso Oct 29, 2026
8f5aea8
Decide whether to follow a reply from what the person does, not from …
guidovizoso Oct 29, 2026
27d39b8
Follow a streaming reply until the person scrolls up or selects text,…
guidovizoso Oct 29, 2026
182eea4
Draw one older page per frame, after the scroller has put the reader …
guidovizoso Oct 29, 2026
cd0a363
Don't stop following for keys typed into a field inside the transcript
guidovizoso Oct 29, 2026
3cdc5f4
Count an upward scroll as the person's only when they are dragging th…
guidovizoso Oct 29, 2026
c29ec29
Say which case drawing pages above does not cover
guidovizoso Oct 29, 2026
9cef2eb
Reveal a streaming reply at a steady pace and draw history as finishe…
guidovizoso Oct 29, 2026
69719c8
Keep finished rows from re-rendering while a reply streams
guidovizoso Oct 29, 2026
59e392c
Draw generative UI that arrived before its renderer once the renderer…
guidovizoso Oct 29, 2026
6ef3694
Note the conversation polish in the changelog
guidovizoso Oct 29, 2026
faa6d3d
Redraw a running tool row as its arguments stream in
guidovizoso Oct 29, 2026
f06a28c
Show a reply's last word once it stops growing, and text with no spac…
guidovizoso Oct 29, 2026
3831fcd
Let a jump draw every page down to a turn and stop following
guidovizoso Oct 29, 2026
ccc4f0e
Add a rail of turns beside a conversation, with a preview and a jump
guidovizoso Oct 29, 2026
53686a9
Mark the last turn as current at the end of a conversation, however s…
guidovizoso Oct 29, 2026
89b8f28
Hold back a card's last value only while its row can still receive ar…
guidovizoso Oct 29, 2026
43d59c5
Hold the page boundary on rows, so rows added above never cross it
guidovizoso Oct 29, 2026
6be8701
Put the scroller back to following when somebody scrolls down at the end
guidovizoso Oct 29, 2026
d155f48
Count a press on an overlay or left-hand scrollbar as the person's
guidovizoso Oct 29, 2026
d101b42
Keep following when a jump from the rail leaves the view at the end
guidovizoso Oct 29, 2026
4f7184b
Pick following up on a person's first message in a channel of firings
guidovizoso Oct 29, 2026
bc3d2be
Show a channel's history without its voice chats when they are slow o…
guidovizoso Oct 29, 2026
785c3ea
Draw nothing for a card that fails on arguments still arriving
guidovizoso Oct 29, 2026
ad67585
Keep the rail and its marks still while a reply streams, and read the…
guidovizoso Oct 29, 2026
cb5e5a6
Listen for late activity renderers once per transcript, and look agai…
guidovizoso Oct 29, 2026
66e546b
Remove a decision's renderer when it unmounts, as the SDK's own hook …
guidovizoso Oct 29, 2026
5e75089
Fix three stale comments and draw both kinds of tool row from one branch
guidovizoso Oct 29, 2026
eefed68
Keep one reveal loop across tokens, so each frame counts the time sin…
guidovizoso Oct 29, 2026
1c14a82
Show a decision's failure while the run waits on it: a call is final …
guidovizoso Oct 29, 2026
c62c88b
Put two doc comments back above what they describe
guidovizoso Oct 29, 2026
0b24122
Merge remote-tracking branch 'origin/main' into guido/chat-polish-4-s…
guidovizoso Oct 29, 2026
ec5d335
Draw a late voice chat from before the conversation opened at rest
guidovizoso Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,17 @@ for its default of thirty minutes, and `browserIdleMs: 0` (keep browsers open) w
of these settings now also reach the computers `computers.mode: sandbox` makes, which had none of
them.

### A conversation opens at the bottom and stays still while a Bot writes

Opening a conversation shows its newest messages at once, already at the bottom, instead of loading
and then gliding down; a long one draws older messages as somebody scrolls up to them, without
moving what they are reading. Charts, tables, cards and decisions a Bot draws grow in place while
they stream, and no longer flash "not available" before they appear. A reply is revealed at a steady
pace, and the view follows it until the person scrolls up or selects some of it, and picks it up
again when they come back to the end or press the arrow. A rail beside a conversation has a mark
per turn: hovering one previews what was asked and answered, and choosing it goes there. Nothing
changes for a deployment.

### Generated interfaces use A2UI 1.0

With generative UI on, a Bot builds an interface from the A2UI 1.0 basic components instead of the
Expand Down
62 changes: 60 additions & 2 deletions app/src/components/channels/channel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,48 @@ function sameActivity(
);
}

/**
* Whether the transcript still waits behind the placeholder.
*
* A channel opened from the roster waits for its whole history: the stored thread (`restoring`) and
* its voice chats. A channel just started from the compose screen has no history; its first message
* is already in hand and shows at once, not after a voice-chat request with nothing in it.
*
* Exported for its tests.
*/
export function historyIsLoading({
restoring,
seeded,
voicePending,
voiceWaitedOut = false,
}: {
restoring: boolean;
seeded: boolean;
voicePending: boolean;
/** The voice chats have had their `VOICE_WAIT_MS` since the thread came in. */
voiceWaitedOut?: boolean;
}): boolean {
return restoring || (!seeded && voicePending && !voiceWaitedOut);
}

/**
* How long, once the thread is in, the conversation waits for its voice chats before showing
* without them. A slow or failing voice service must not hold up text chat; cards that arrive after
* this join the transcript where they belong.
*/
export const VOICE_WAIT_MS = 1500;

/** Whether `VOICE_WAIT_MS` has passed since `waiting` became true. Reset whenever it is false. */
function useWaitedOut(waiting: boolean, key: string): boolean {
const [waitedOut, setWaitedOut] = useState<string | null>(null);
useEffect(() => {
if (!waiting) return;
const timer = window.setTimeout(() => setWaitedOut(key), VOICE_WAIT_MS);
return () => window.clearTimeout(timer);
}, [waiting, key]);
return waiting && waitedOut === key;
}

export function channelHistoryNotice({
restoring,
messageCount,
Expand Down Expand Up @@ -549,8 +591,24 @@ export function ChannelChat({
useActiveBot(runtimeAgentId);

const skillCommands = useSkillCommands(runtimeAgentId);
const historyNotice = channelHistoryNotice({
/*
* History is the snapshot, the stored thread AND the voice archive. The transcript shows none of
* it until all three are in, so it lands once at the bottom instead of drawing the conversation
* and then splicing call cards into it; the notice waits for the same moment, so it appears in
* the commit that shows the history rather than a render later, shrinking the view under it.
*/
const voiceWaitedOut = useWaitedOut(
!restoring && seed === null && voiceArchive.pending,
channel.id,
);
const historyLoading = historyIsLoading({
restoring,
seeded: seed !== null,
voicePending: voiceArchive.pending,
Comment thread
davidmckayv marked this conversation as resolved.
voiceWaitedOut,
});
const historyNotice = channelHistoryNotice({
restoring: historyLoading,
messageCount: agent.messages.length,
lastMessageAt: channel.lastMessageAt,
historyAvailability,
Expand Down Expand Up @@ -1091,7 +1149,7 @@ export function ChannelChat({
* above.
*/
queueWhileBusy
restoring={restoring}
restoring={historyLoading}
/*
* The run, not the turn. Stop reaches a run through the core's abort controller, and that
* controller does not exist until `say` has finished waiting for the runtime agent — so
Expand Down
10 changes: 7 additions & 3 deletions app/src/components/channels/chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,7 @@ function isReadableMessage(message: unknown): message is Readonly<Message> {
* row it builds is keyed on the id". `message.id` is keyed on harder and was checked nowhere: it
* becomes `VisibleChatItem.id`, which is DECLARED `string`, and downstream that one value is the
* React key, `MessageScrollerItem`'s `messageId`, the turn-grouping key `anchorRowIds` and `turnOf`
* cut apart at the last colon, and the memo key `createFirstPaintDelays` hangs an entrance delay
* off.
* cut apart at the last colon, and the key `openingStagger` hangs a row's entrance delay off.
*
* IT SURVIVED BECAUSE IT DOES NOT THROW — the same reason `readAttachmentMetadata` gives for its
* own fields having gone unchecked, and the claim there that `metadata` was "the last unchecked
Expand Down Expand Up @@ -419,7 +418,12 @@ export function toVisibleChatItems(
kind: "tool",
// One assistant message can carry multiple tool calls.
id: toolCall.id,
toolCall,
/*
* A copy, not the agent's object: the client appends argument chunks to the call it
* already handed over, so a row comparing the call it drew with the next one would be
* comparing an object with itself and never see the arguments grow.
*/
toolCall: { ...toolCall, function: { ...toolCall.function } },
...(results.has(toolCall.id)
? { result: results.get(toolCall.id) }
: {}),
Expand Down
Loading
Loading