diff --git a/src/ui/views/panels/ChatPanel.vue b/src/ui/views/panels/ChatPanel.vue index 51284d831..5fad06987 100644 --- a/src/ui/views/panels/ChatPanel.vue +++ b/src/ui/views/panels/ChatPanel.vue @@ -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; diff --git a/src/voice/chatControlView.test.mjs b/src/voice/chatControlView.test.mjs index 7fef351c4..5dee1ad97 100644 --- a/src/voice/chatControlView.test.mjs +++ b/src/voice/chatControlView.test.mjs @@ -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 });