feat: render per-turn thinking tiles in chat pane

Replace the single aggregate thinking tile with per-turn thinking groups.
Instead of collecting all thinking messages into one flat array and
rendering a single ThinkingProcess before the last assistant message,
process session.messages in chronological order to produce interleaved
display items that naturally group consecutive thinking messages by turn.

Each turn now shows its own collapsible thinking tile placed inline
before its assistant response, with correct per-turn isActive state
and turnStartedAt from the matching run.

Also fixes a latent index-mismatch bug in getAssistantMessagePhase where
the visible-messages index was compared against the full session.messages
index, potentially hiding the Final badge when thinking messages existed.
Switched to ID-based comparison.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-31 16:53:28 +01:00
co-authored by Copilot
parent fb3e80ec47
commit 78949c5efd
3 changed files with 75 additions and 54 deletions
+10 -10
View File
@@ -33,7 +33,7 @@ describe('assistant message phase', () => {
},
], 'running');
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('thinking');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('thinking');
});
test('marks the last completed assistant message as final when the session is idle', () => {
@@ -54,8 +54,8 @@ describe('assistant message phase', () => {
},
]);
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1], 1)).toBe('final');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1])).toBe('final');
});
test('does not mark completed assistant messages as final while the session is still running', () => {
@@ -69,7 +69,7 @@ describe('assistant message phase', () => {
},
], 'running');
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('default');
});
test('ignores non-assistant messages', () => {
@@ -83,7 +83,7 @@ describe('assistant message phase', () => {
},
]);
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('default');
});
test('returns default for thinking-kind messages regardless of other state', () => {
@@ -105,8 +105,8 @@ describe('assistant message phase', () => {
},
]);
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1], 1)).toBe('final');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1])).toBe('final');
});
test('skips thinking messages when determining the last completed assistant', () => {
@@ -136,8 +136,8 @@ describe('assistant message phase', () => {
},
]);
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1], 1)).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[2], 2)).toBe('final');
expect(getAssistantMessagePhase(session, session.messages[0])).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[1])).toBe('default');
expect(getAssistantMessagePhase(session, session.messages[2])).toBe('final');
});
});