diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index b5f82bcbff6..2b704f357dd 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -626,7 +626,8 @@ describe('useGeminiStream', () => { queuedPrompt, ); const queuedPromptAddItemIndex = mockAddItem.mock.calls.findIndex( - ([item]) => item.type === MessageType.USER && item.text === queuedPrompt, + ([item]) => + item.type === MessageType.NOTIFICATION && item.text === queuedPrompt, ); expect(queuedPromptAddItemIndex).toBeGreaterThanOrEqual(0); expect(recordMidTurnUserMessage.mock.invocationCallOrder[0]).toBeLessThan( @@ -636,7 +637,7 @@ describe('useGeminiStream', () => { mockSendMessageStream.mock.invocationCallOrder[0], ); expect(mockAddItem).toHaveBeenCalledWith( - { type: MessageType.USER, text: queuedPrompt }, + { type: MessageType.NOTIFICATION, text: queuedPrompt }, expect.any(Number), ); expect(mockSendMessageStream).toHaveBeenCalledWith( @@ -731,7 +732,7 @@ describe('useGeminiStream', () => { }); expect(mockAddItem).toHaveBeenCalledWith( - { type: MessageType.USER, text: queuedPrompt }, + { type: MessageType.NOTIFICATION, text: queuedPrompt }, expect.any(Number), ); expect(mockSendMessageStream).toHaveBeenCalledWith( diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index d6f937ee8a7..8f8ca1db9c7 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -2347,8 +2347,7 @@ export const useGeminiStream = ( config .getChatRecordingService() ?.recordMidTurnUserMessage(midTurnUserMessage, msg); - // Record in UI history so the transcript stays complete. - addItem({ type: MessageType.USER, text: msg }, Date.now()); + addItem({ type: MessageType.NOTIFICATION, text: msg }, Date.now()); } } diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index d6433524f8f..95b1e8ed244 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -631,6 +631,7 @@ export enum MessageType { ARENA_SESSION_COMPLETE = 'arena_session_complete', INSIGHT_PROGRESS = 'insight_progress', BTW = 'btw', + NOTIFICATION = 'notification', DIFF_STATS = 'diff_stats', GOAL_STATUS = 'goal_status', } diff --git a/packages/cli/src/ui/utils/historyMapping.test.ts b/packages/cli/src/ui/utils/historyMapping.test.ts index 8f6426a6d95..dff57ece75d 100644 --- a/packages/cli/src/ui/utils/historyMapping.test.ts +++ b/packages/cli/src/ui/utils/historyMapping.test.ts @@ -189,6 +189,45 @@ describe('computeApiTruncationIndex', () => { }); }); + describe('mid-turn user messages (notification type)', () => { + it('skips notification items so btw merged into functionResponse does not cause mismatch', () => { + // Mid-turn messages are type 'notification' in UI (not counted by + // isRealUserTurn) and merged into tool_result in API (skipped by + // isUserTextContent). Both sides agree → correct truncation index. + const ui: HistoryItem[] = [ + userItem(1, 'first prompt'), + geminiItem(2), + { + type: 'notification', + id: 3, + text: 'btw side question', + } as HistoryItem, + userItem(5, 'next prompt'), + geminiItem(6), + ]; + const btwMergedIntoToolResult: Content = { + role: 'user', + parts: [ + { + functionResponse: { name: 'tool', response: { result: 'ok' } }, + } as unknown as Part, + { text: 'btw side question' } as Part, + ], + }; + const api: Content[] = [ + userContent('first prompt'), + modelContent('response with tool call'), + btwMergedIntoToolResult, + modelContent('response after btw'), + userContent('next prompt'), + modelContent('response 5'), + ]; + // notification is not counted → uiUserTurnCount=1 before 'next prompt' + // API has 2 user text entries (idx 0 and 4) → finds idx 4 correctly + expect(computeApiTruncationIndex(ui, 5, api)).toBe(4); + }); + }); + describe('with slash-command items in UI history', () => { it('ignores slash-command items when counting user turns', () => { const ui: HistoryItem[] = [ diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts index 7fae821e4fa..7d35afa549b 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts @@ -143,7 +143,11 @@ describe('resumeHistoryUtils', () => { 20, ); - expect(items).toContainEqual({ id: 21, type: 'user', text: 'save logs' }); + expect(items).toContainEqual({ + id: 21, + type: 'notification', + text: 'save logs', + }); }); it('marks tool results as error, captures thought text, and falls back when tool is missing', () => { diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.ts index edb0ac6a487..3809a00eaed 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.ts @@ -287,7 +287,7 @@ function convertToHistoryItems( payload?.displayText || extractTextFromParts(record.message?.parts as Part[]); if (text) { - items.push({ type: 'user', text }); + items.push({ type: 'notification', text }); } break; }