Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
[
{new Date(itemForDisplay.timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
]
</Text>
Expand Down
74 changes: 74 additions & 0 deletions packages/cli/src/ui/hooks/useGeminiStream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8370,6 +8370,80 @@ describe('useGeminiStream', () => {
});
});
});

describe('timestamp attachment', () => {
it('attaches a numeric timestamp to gemini items via commitItem', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The test name says "via commitItem" but the timestamp is actually set by setPendingHistoryItem at useGeminiStream.ts:1133 (which creates the pending item with timestamp: Date.now()). By the time commitItem receives the item, !item.timestamp is already false, so the guard's value-assignment branch is never exercised.

The branch where commitItem itself assigns Date.now() — when a gemini item arrives without a pre-existing timestamp — is only reachable via the split path at useGeminiStream.ts:1163 (buffer exceeding STREAM_PENDING_ITEM_MAX_CHARS). No test triggers this path.

Consider adding a test that directly invokes commitItem with a gemini item lacking a timestamp, or triggers the split path, to validate the guard's assignment branch.

— qwen3.7-max via Qwen Code /review

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',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Inconsistent (call: any[]) type annotations in the new filter callbacks. All other mockAddItem.mock.calls.filter callbacks in this file (e.g., lines 5934, 5991, 7468) use untyped (call) parameters. The explicit any[] suppresses type checking and breaks the file's convention.

Suggested change
(call: any[]) => call[0]?.type === 'gemini',
const geminiCalls = mockAddItem.mock.calls.filter(
(call) => call[0]?.type === 'gemini',
);

— qwen3.7-max via Qwen Code /review

);
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', () => {
Expand Down
40 changes: 35 additions & 5 deletions packages/cli/src/ui/utils/resumeHistoryUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('resumeHistoryUtils', () => {
},
{
type: 'assistant',
timestamp: '2026-01-15T14:30:00.000Z',
Comment thread
ZijianZhang989 marked this conversation as resolved.
message: {
parts: [
{ text: 'Hi there' } as Part,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -155,6 +161,7 @@ describe('resumeHistoryUtils', () => {
messages: [
{
type: 'assistant',
timestamp: '2026-01-15T15:00:00.000Z',
message: {
parts: [
{
Expand Down Expand Up @@ -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',
Expand All @@ -213,6 +225,7 @@ describe('resumeHistoryUtils', () => {
messages: [
{
type: 'assistant',
timestamp: '2026-01-15T16:00:00.000Z',
message: {
parts: [
{
Expand All @@ -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(),
},
]);
});

Expand Down Expand Up @@ -337,6 +355,7 @@ describe('resumeHistoryUtils', () => {
},
{
type: 'assistant',
timestamp: '2026-01-15T17:00:00.000Z',
message: { parts: [{ text: 'Follow-up' } as Part] },
},
],
Expand All @@ -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(),
},
]);
});

Expand All @@ -373,6 +397,7 @@ describe('resumeHistoryUtils', () => {
},
{
type: 'assistant',
timestamp: '2026-01-15T18:00:00.000Z',
message: { parts: [{ text: 'Follow-up' } as Part] },
},
],
Expand All @@ -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(),
},
]);
});

Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/ui/utils/resumeHistoryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,11 @@ function convertToHistoryItems(
});
currentToolGroup = [];
}
items.push({ type: 'gemini', text });
items.push({
type: 'gemini',
text,
timestamp: new Date(record.timestamp).getTime(),
Comment thread
ZijianZhang989 marked this conversation as resolved.
});
}

// Track function calls for pairing with results
Expand Down
Loading