diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index b0a10ac0aaf..45d7f3eb1da 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -570,6 +570,16 @@ const SETTINGS_SCHEMA = {
{ value: 'json', label: 'JSON' },
],
},
+ showTimestamps: {
+ type: 'boolean',
+ label: 'Show Timestamps',
+ category: 'General',
+ requiresRestart: false,
+ default: false,
+ description:
+ 'Show [HH:MM:SS] timestamp before each assistant response.',
+ showInDialog: true,
+ },
},
},
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
index 987d0b2be45..a53c6e0a2fb 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
@@ -15,6 +15,7 @@ import type {
} from '@qwen-code/qwen-code-core';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { renderWithProviders } from '../../test-utils/render.js';
+import { LoadedSettings } from '../../config/settings.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
import { CompactModeProvider } from '../contexts/CompactModeContext.js';
@@ -406,4 +407,79 @@ describe('', () => {
expect(lastFrame()).not.toContain('Continuing the reasoning');
});
+
+ describe('showTimestamps', () => {
+ const timestampItem: HistoryItem = {
+ ...baseItem,
+ type: 'gemini',
+ text: 'Hello from assistant',
+ timestamp: new Date('2026-01-15T14:30:45').getTime(),
+ };
+
+ const makeTimestampSettings = () =>
+ new LoadedSettings(
+ { path: '', settings: {}, originalSettings: {} },
+ { path: '', settings: {}, originalSettings: {} },
+ {
+ path: '',
+ settings: { output: { showTimestamps: true } },
+ originalSettings: {},
+ },
+ { path: '', settings: {}, originalSettings: {} },
+ true,
+ new Set(),
+ );
+
+ it('does not render timestamp when showTimestamps is disabled', () => {
+ const { lastFrame } = renderWithProviders(
+ ,
+ );
+ expect(lastFrame()).not.toMatch(/\[\d{2}:\d{2}:\d{2}\]/);
+ });
+
+ it('renders [HH:MM:SS] timestamp when showTimestamps is enabled', () => {
+ const { lastFrame } = renderWithProviders(
+ ,
+ { settings: makeTimestampSettings() },
+ );
+ expect(lastFrame()).toMatch(/\[\d{2}:\d{2}:\d{2}\]/);
+ });
+
+ it('renders timestamp even when isPending is true (streaming)', () => {
+ const { lastFrame } = renderWithProviders(
+ ,
+ { settings: makeTimestampSettings() },
+ );
+ expect(lastFrame()).toMatch(/\[\d{2}:\d{2}:\d{2}\]/);
+ });
+
+ it('does not render timestamp when timestamp field is missing', () => {
+ const noTimestampItem: HistoryItem = {
+ id: 1,
+ type: 'gemini',
+ text: 'Hello',
+ };
+ const { lastFrame } = renderWithProviders(
+ ,
+ { settings: makeTimestampSettings() },
+ );
+ expect(lastFrame()).not.toMatch(/\[\d{2}:\d{2}:\d{2}\]/);
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index d9f49cd73ea..bf87fe163b8 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -57,6 +57,7 @@ import { MemorySavedMessage } from './messages/MemorySavedMessage.js';
import { DiffStatsDisplay } from './messages/DiffStatsDisplay.js';
import { GoalStatusMessage } from './messages/GoalStatusMessage.js';
import { useCompactMode } from '../contexts/CompactModeContext.js';
+import { useSettings } from '../contexts/SettingsContext.js';
interface HistoryItemDisplayProps {
item: HistoryItem;
@@ -137,6 +138,9 @@ const HistoryItemDisplayComponent: React.FC = ({
const marginTop = getHistoryItemMarginTop(item);
const { compactMode } = useCompactMode();
+ const settings = useSettings();
+ const showTimestamps = settings.merged.output?.showTimestamps === true;
+
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
const contentWidth = terminalWidth - 4;
const boxWidth = mainAreaWidth || contentWidth;
@@ -160,15 +164,26 @@ const HistoryItemDisplayComponent: React.FC = ({
)}
{itemForDisplay.type === 'gemini' && (
-
+ <>
+ {showTimestamps && itemForDisplay.timestamp != null && (
+
+ [
+ {new Date(itemForDisplay.timestamp).toLocaleTimeString('en-US', {
+ hour12: false,
+ })}
+ ]
+
+ )}
+
+ >
)}
{itemForDisplay.type === 'gemini_content' && (
{
@@ -83,32 +82,28 @@ function fakeResumedData(
describe('SessionPreview', () => {
it('shows loading state before data arrives', () => {
const svc = mockService(new Promise(() => {})); // never resolves
- const { lastFrame } = render(
-
-
- ,
+ const { lastFrame } = renderWithProviders(
+ ,
);
expect(lastFrame()).toContain('Loading session preview');
});
it('renders all messages after load', async () => {
const svc = mockService(fakeResumedData());
- const { lastFrame } = render(
-
-
- ,
+ const { lastFrame } = renderWithProviders(
+ ,
);
await wait(100);
const frame = lastFrame() ?? '';
@@ -127,16 +122,14 @@ describe('SessionPreview', () => {
{ text: 'FINAL-ANSWER-MARKER' },
]),
);
- const { lastFrame } = render(
-
-
- ,
+ const { lastFrame } = renderWithProviders(
+ ,
);
await wait(100);
const frame = lastFrame() ?? '';
@@ -151,19 +144,17 @@ describe('SessionPreview', () => {
it('renders footer metadata (messageCount · time · branch)', async () => {
const svc = mockService(fakeResumedData());
- const { lastFrame } = render(
-
-
- ,
+ const { lastFrame } = renderWithProviders(
+ ,
);
await wait(100);
const frame = lastFrame() ?? '';
@@ -176,16 +167,14 @@ describe('SessionPreview', () => {
// through to SessionPreview. The footer must still show a count, derived
// from the loaded ResumedSessionData using unique user/assistant UUIDs.
const svc = mockService(fakeResumedData());
- const { lastFrame } = render(
-
-
- ,
+ const { lastFrame } = renderWithProviders(
+ ,
);
await wait(100);
const frame = lastFrame() ?? '';
@@ -196,16 +185,14 @@ describe('SessionPreview', () => {
it('calls onExit when Escape is pressed', async () => {
const onExit = vi.fn();
const svc = mockService(fakeResumedData());
- const { stdin } = render(
-
-
- ,
+ const { stdin } = renderWithProviders(
+ ,
);
await wait(100);
stdin.write('\u001B'); // ESC
@@ -216,16 +203,14 @@ describe('SessionPreview', () => {
it('calls onResume(sessionId) when Enter is pressed', async () => {
const onResume = vi.fn();
const svc = mockService(fakeResumedData());
- const { stdin } = render(
-
-
- ,
+ const { stdin } = renderWithProviders(
+ ,
);
await wait(100);
stdin.write('\r'); // Enter
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
index b13b3b24a63..c01de0142f5 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
+++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx
@@ -3759,10 +3759,10 @@ describe('useGeminiStream', () => {
});
expect(mockAddItem).toHaveBeenCalledWith(
- {
+ expect.objectContaining({
type: 'gemini',
text: 'Initial',
- },
+ }),
expect.any(Number),
);
@@ -6695,10 +6695,10 @@ describe('useGeminiStream', () => {
});
expect(mockAddItem).toHaveBeenCalledWith(
- {
+ expect.objectContaining({
type: 'gemini',
text: 'First call content',
- },
+ }),
expect.any(Number),
);
expect(mainAbortSignal?.aborted).toBe(true);
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts
index a5bf15f6576..33be9fcf2f0 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.ts
+++ b/packages/cli/src/ui/hooks/useGeminiStream.ts
@@ -64,6 +64,7 @@ import type {
HistoryItemGoalStatus,
HistoryItemWithoutId,
HistoryItemToolGroup,
+ HistoryItemGemini,
SlashCommandProcessorResult,
} from '../types.js';
import { StreamingState, MessageType, ToolCallStatus } from '../types.js';
@@ -399,6 +400,20 @@ export const useGeminiStream = (
// alongside lastTurnUserItemRef.
const turnSawContentEventRef = useRef(false);
const lastPromptErroredRef = useRef(false);
+
+ // Wrapper around addItem that attaches timestamp to gemini items for display.
+ // Only 'gemini' (new assistant turn) gets a timestamp; 'gemini_content'
+ // (same turn, performance-split continuation) does not.
+ const commitItem = useCallback(
+ (item: HistoryItemWithoutId, userMessageTimestamp: number): number => {
+ if (item.type === 'gemini' && !(item as HistoryItemGemini).timestamp) {
+ (item as HistoryItemGemini).timestamp = Date.now();
+ }
+ return addItem(item, userMessageTimestamp);
+ },
+ [addItem],
+ );
+
const dualOutput = useDualOutput();
const [isResponding, setIsResponding] = useState(false);
// React state can lag by one render; this tracks the actual stream lifetime.
@@ -691,7 +706,7 @@ export const useGeminiStream = (
// events that arrived inside the throttle window
// (STREAM_UPDATE_THROTTLE_MS), making AppContainer's auto-restore
// wrongly conclude the model produced nothing — and the subsequent
- // addItem(pendingHistoryItemRef.current) below would commit content
+ // commitItem(pendingHistoryItemRef.current) below would commit content
// that auto-restore then truncates away.
for (const flushBufferedStreamEvents of flushBufferedStreamEventsRef.current) {
flushBufferedStreamEvents();
@@ -728,7 +743,7 @@ export const useGeminiStream = (
logApiCancel(config, cancellationEvent);
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, Date.now());
+ commitItem(pendingHistoryItemRef.current, Date.now());
}
addItem(
{
@@ -766,6 +781,7 @@ export const useGeminiStream = (
}, [
streamingState,
addItem,
+ commitItem,
setPendingHistoryItem,
onCancelSubmit,
pendingHistoryItemRef,
@@ -975,9 +991,13 @@ export const useGeminiStream = (
return newGeminiMessageBuffer;
}
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
}
- setPendingHistoryItem({ type: 'gemini', text: '' });
+ setPendingHistoryItem({
+ type: 'gemini',
+ text: '',
+ timestamp: Date.now(),
+ });
newGeminiMessageBuffer = stripLeadingBlankLines(newGeminiMessageBuffer);
}
// Split large messages for better rendering performance. Ideally,
@@ -1005,7 +1025,7 @@ export const useGeminiStream = (
// broken up so that there are more "statically" rendered.
const beforeText = newGeminiMessageBuffer.substring(0, safeSplitPoint);
const afterText = newGeminiMessageBuffer.substring(safeSplitPoint);
- addItem(
+ commitItem(
{
type: nextPendingType,
text: beforeText,
@@ -1016,13 +1036,21 @@ export const useGeminiStream = (
newGeminiMessageBuffer = afterText;
}
// Update the existing message with accumulated content.
- setPendingHistoryItem({
- type: nextPendingType,
- text: newGeminiMessageBuffer,
+ setPendingHistoryItem((item) => {
+ const base: HistoryItemWithoutId = {
+ type: nextPendingType,
+ text: newGeminiMessageBuffer,
+ };
+ if (item && 'timestamp' in item) {
+ (base as HistoryItemGemini).timestamp = (
+ item as HistoryItemGemini
+ ).timestamp;
+ }
+ return base;
});
return newGeminiMessageBuffer;
},
- [addItem, pendingHistoryItemRef, setPendingHistoryItem],
+ [commitItem, pendingHistoryItemRef, setPendingHistoryItem],
);
const mergeThought = useCallback(
@@ -1200,7 +1228,7 @@ export const useGeminiStream = (
};
addItem(pendingItem, userMessageTimestamp);
} else {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
}
setPendingHistoryItem(null);
}
@@ -1215,6 +1243,7 @@ export const useGeminiStream = (
[
addItem,
commitPendingThought,
+ commitItem,
pendingHistoryItemRef,
setPendingHistoryItem,
setThought,
@@ -1228,7 +1257,7 @@ export const useGeminiStream = (
// Persist any streamed reasoning (collapsed) above the error.
commitPendingThought(userMessageTimestamp);
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
// Only show Ctrl+Y hint if not already showing an auto-retry countdown
@@ -1268,8 +1297,8 @@ export const useGeminiStream = (
});
},
[
- addItem,
commitPendingThought,
+ commitItem,
pendingHistoryItemRef,
setPendingHistoryItem,
setPendingRetryErrorItem,
@@ -1286,12 +1315,18 @@ export const useGeminiStream = (
}
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
addItem({ type: MessageType.INFO, text }, userMessageTimestamp);
},
- [addItem, pendingHistoryItemRef, setPendingHistoryItem, settings],
+ [
+ addItem,
+ commitItem,
+ pendingHistoryItemRef,
+ setPendingHistoryItem,
+ settings,
+ ],
);
const handleFinishedEvent = useCallback(
@@ -1354,7 +1389,7 @@ export const useGeminiStream = (
userMessageTimestamp: number,
) => {
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
const reasonClause =
@@ -1373,7 +1408,7 @@ export const useGeminiStream = (
Date.now(),
);
},
- [addItem, config, pendingHistoryItemRef, setPendingHistoryItem],
+ [addItem, commitItem, config, pendingHistoryItemRef, setPendingHistoryItem],
);
const handleMaxSessionTurnsEvent = useCallback(
@@ -1446,7 +1481,7 @@ export const useGeminiStream = (
userMessageTimestamp: number,
) => {
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
addItem(
@@ -1458,7 +1493,7 @@ export const useGeminiStream = (
userMessageTimestamp,
);
},
- [addItem, pendingHistoryItemRef, setPendingHistoryItem],
+ [addItem, commitItem, pendingHistoryItemRef, setPendingHistoryItem],
);
const handleStopHookLoopEvent = useCallback(
@@ -1471,7 +1506,7 @@ export const useGeminiStream = (
userMessageTimestamp: number,
) => {
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
// When the active loop is driven by `/goal`, replace the generic
@@ -1502,7 +1537,7 @@ export const useGeminiStream = (
userMessageTimestamp,
);
},
- [addItem, config, pendingHistoryItemRef, setPendingHistoryItem],
+ [addItem, commitItem, config, pendingHistoryItemRef, setPendingHistoryItem],
);
const handleActiveGoalEvent = useCallback(
@@ -1712,7 +1747,7 @@ export const useGeminiStream = (
// as "t" → "te" → "tes" cumulative rendering even though each
// turn is persisted as a clean, separate assistant message.
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
geminiMessageBuffer = '';
@@ -1768,7 +1803,7 @@ export const useGeminiStream = (
// Display system message from Stop hooks with "Stop says:" prefix
// First commit any pending AI response to ensure correct ordering
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
addItem(
@@ -1887,6 +1922,7 @@ export const useGeminiStream = (
handleStopHookLoopEvent,
handleActiveGoalEvent,
addItem,
+ commitItem,
dualOutput,
],
);
@@ -2112,7 +2148,7 @@ export const useGeminiStream = (
}
if (pendingHistoryItemRef.current) {
- addItem(pendingHistoryItemRef.current, userMessageTimestamp);
+ commitItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
@@ -2204,6 +2240,7 @@ export const useGeminiStream = (
processGeminiStreamEvents,
pendingHistoryItemRef,
addItem,
+ commitItem,
setPendingHistoryItem,
setInitError,
geminiClient,
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index da81500cb32..ea58b345ae3 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -126,6 +126,7 @@ export type HistoryItemUser = HistoryItemBase & {
export type HistoryItemGemini = HistoryItemBase & {
type: 'gemini';
text: string;
+ timestamp?: number;
};
export type HistoryItemGeminiContent = HistoryItemBase & {
diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json
index 4a22690a3dd..ac682997408 100644
--- a/packages/vscode-ide-companion/schemas/settings.schema.json
+++ b/packages/vscode-ide-companion/schemas/settings.schema.json
@@ -158,6 +158,11 @@
"json"
],
"default": "text"
+ },
+ "showTimestamps": {
+ "description": "Show [HH:MM:SS] timestamp before each assistant response.",
+ "type": "boolean",
+ "default": false
}
}
},