diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 0759a716dc8..a9ae17f668e 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -250,6 +250,9 @@ const HistoryItemDisplayComponent: React.FC = ({ [ {new Date(itemForDisplay.timestamp).toLocaleTimeString('en-US', { hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', })} ] diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 01bd03087da..49420853c6d 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -8370,6 +8370,80 @@ describe('useGeminiStream', () => { }); }); }); + + describe('timestamp attachment', () => { + it('attaches a numeric timestamp to gemini items via commitItem', async () => { + mockSendMessageStream.mockReturnValueOnce( + (async function* () { + yield { + type: ServerGeminiEventType.Content, + value: 'Hello world', + }; + yield { + type: ServerGeminiEventType.Finished, + value: { + reason: undefined, + usageMetadata: { totalTokenCount: 1 }, + }, + }; + })(), + ); + + const { result } = renderTestHook(); + + await act(async () => { + await result.current.submitQuery('test'); + }); + + await waitFor(() => { + expect(result.current.streamingState).toBe(StreamingState.Idle); + }); + + const geminiCalls = mockAddItem.mock.calls.filter( + (call: any[]) => call[0]?.type === 'gemini', + ); + expect(geminiCalls.length).toBeGreaterThanOrEqual(1); + const geminiItem = geminiCalls[0][0]; + expect(typeof geminiItem.timestamp).toBe('number'); + expect(geminiItem.timestamp).toBeGreaterThan(0); + }); + + it('does not attach timestamp to non-gemini items', async () => { + mockSendMessageStream.mockReturnValueOnce( + (async function* () { + yield { + type: ServerGeminiEventType.Content, + value: 'response', + }; + yield { + type: ServerGeminiEventType.Finished, + value: { + reason: undefined, + usageMetadata: { totalTokenCount: 1 }, + }, + }; + })(), + ); + + const { result } = renderTestHook(); + + await act(async () => { + await result.current.submitQuery('test'); + }); + + await waitFor(() => { + expect(result.current.streamingState).toBe(StreamingState.Idle); + }); + + const nonGeminiCalls = mockAddItem.mock.calls.filter( + (call: any[]) => call[0]?.type !== 'gemini', + ); + expect(nonGeminiCalls.length).toBeGreaterThanOrEqual(1); + for (const call of nonGeminiCalls) { + expect(call[0]).not.toHaveProperty('timestamp'); + } + }); + }); }); describe('classifyApiError', () => { diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts index ceff81dbe17..7ea9978e925 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.test.ts @@ -52,6 +52,7 @@ describe('resumeHistoryUtils', () => { }, { type: 'assistant', + timestamp: '2026-01-15T14:30:00.000Z', message: { parts: [ { text: 'Hi there' } as Part, @@ -89,7 +90,12 @@ describe('resumeHistoryUtils', () => { expect(items).toEqual([ { id: baseTimestamp + 1, type: 'user', text: 'Hello' }, - { id: baseTimestamp + 2, type: 'gemini', text: 'Hi there' }, + { + id: baseTimestamp + 2, + type: 'gemini', + text: 'Hi there', + timestamp: new Date('2026-01-15T14:30:00.000Z').getTime(), + }, { id: baseTimestamp + 3, type: 'tool_group', @@ -155,6 +161,7 @@ describe('resumeHistoryUtils', () => { messages: [ { type: 'assistant', + timestamp: '2026-01-15T15:00:00.000Z', message: { parts: [ { @@ -190,7 +197,12 @@ describe('resumeHistoryUtils', () => { const items = buildResumedHistoryItems(session, makeConfig({})); expect(items).toEqual([ - { id: expect.any(Number), type: 'gemini', text: 'visible text' }, + { + id: expect.any(Number), + type: 'gemini', + text: 'visible text', + timestamp: new Date('2026-01-15T15:00:00.000Z').getTime(), + }, { id: expect.any(Number), type: 'tool_group', @@ -213,6 +225,7 @@ describe('resumeHistoryUtils', () => { messages: [ { type: 'assistant', + timestamp: '2026-01-15T16:00:00.000Z', message: { parts: [ { @@ -238,7 +251,12 @@ describe('resumeHistoryUtils', () => { type: 'gemini_thought', text: 'preview thought', }, - { id: expect.any(Number), type: 'gemini', text: 'visible text' }, + { + id: expect.any(Number), + type: 'gemini', + text: 'visible text', + timestamp: new Date('2026-01-15T16:00:00.000Z').getTime(), + }, ]); }); @@ -337,6 +355,7 @@ describe('resumeHistoryUtils', () => { }, { type: 'assistant', + timestamp: '2026-01-15T17:00:00.000Z', message: { parts: [{ text: 'Follow-up' } as Part] }, }, ], @@ -355,7 +374,12 @@ describe('resumeHistoryUtils', () => { type: 'about', systemInfo: expect.objectContaining({ cliVersion: '1.2.3' }), }, - { id: 8, type: 'gemini', text: 'Follow-up' }, + { + id: 8, + type: 'gemini', + text: 'Follow-up', + timestamp: new Date('2026-01-15T17:00:00.000Z').getTime(), + }, ]); }); @@ -373,6 +397,7 @@ describe('resumeHistoryUtils', () => { }, { type: 'assistant', + timestamp: '2026-01-15T18:00:00.000Z', message: { parts: [{ text: 'Follow-up' } as Part] }, }, ], @@ -386,7 +411,12 @@ describe('resumeHistoryUtils', () => { expect(items).toEqual([ { id: 21, type: 'user', text: '/filecmd', sentToModel: true }, - { id: 22, type: 'gemini', text: 'Follow-up' }, + { + id: 22, + type: 'gemini', + text: 'Follow-up', + timestamp: new Date('2026-01-15T18:00:00.000Z').getTime(), + }, ]); }); diff --git a/packages/cli/src/ui/utils/resumeHistoryUtils.ts b/packages/cli/src/ui/utils/resumeHistoryUtils.ts index b9abda777b0..08c82a26ef1 100644 --- a/packages/cli/src/ui/utils/resumeHistoryUtils.ts +++ b/packages/cli/src/ui/utils/resumeHistoryUtils.ts @@ -382,7 +382,11 @@ function convertToHistoryItems( }); currentToolGroup = []; } - items.push({ type: 'gemini', text }); + items.push({ + type: 'gemini', + text, + timestamp: new Date(record.timestamp).getTime(), + }); } // Track function calls for pairing with results