fix(voice): follow a new chat request once the log is full

The conversation is kept to its newest 60 messages, so once full it no
longer grows as a message is sent, and a reader who had scrolled up was
left behind by their own request. The view now spots a send by the newest
user message's id instead of the list's length.
This commit is contained in:
Sameh Khamis committed 2026-11-03 15:54:54 -08:00
1 parent 51b561815c
commit eed6162753
2 files changed
+39 -1

No files matched your search

+4 -1
View File
@@ -37,9 +37,12 @@ const onListScroll = () => {
const el = list.value;
if (el) stuck = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
};
// A send is a new newest user message. The log is capped, so a full one
// does not grow as it takes a message: compare ids, not lengths.
const lastSent = (list) => list?.findLast((entry) => entry.role === 'user')?.id;
watch(messages, (next, previous) => {
const sent =
next.length > (previous?.length ?? 0) && next.at(-1)?.role === 'user';
next.at(-1)?.role === 'user' && lastSent(next) !== lastSent(previous);
if (!stuck && !sent) return;
void nextTick(() => {
if (list.value) list.value.scrollTop = list.value.scrollHeight;
+35
View File
@@ -319,6 +319,41 @@ test('the conversation follows new messages only while read at the bottom', asyn
assert.equal(top, 800);
});
test('a message sent from higher up a full, capped log is still followed', async (t) => {
manualFrames(t);
const { pinia, settle } = mountVoiceView(t);
const { ui, session, send, reply } = chat(t, { pinia });
ui.emit('chat-button');
for (let turn = 1; turn <= 30; turn++) {
await send(`Request ${turn}`);
reply(`Answer ${turn}`, { itemId: `reply-${turn}`, final: true });
session.emit({ type: 'completion', status: 'completed' });
}
await settle();
// The log keeps its newest 60 messages, so sending no longer lengthens it.
assert.equal(ui.chatMessages.length, 60);
const list = $('#gev-chat-messages');
let top = 800;
Object.defineProperties(list, {
scrollHeight: { configurable: true, get: () => 1000 },
clientHeight: { configurable: true, get: () => 200 },
scrollTop: {
configurable: true,
get: () => top,
set: (value) => {
top = Math.min(value, 800);
},
},
});
top = 300;
list.dispatchEvent(new Event('scroll'));
await send('Show live flights');
await settle();
assert.equal(ui.chatMessages.length, 60);
assert.equal(ui.chatMessages.at(-1).text, 'Show live flights');
assert.equal(top, 800, 'the new request is brought into view');
});
test('the chat gear is named for the settings it opens', async (t) => {
const { pinia, settle } = mountVoiceView(t);
const { ui } = chat(t, { pinia });