From 77fcc2ea31e074cbdedf23f31e682f2b76c188e2 Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Fri, 14 Aug 2026 23:26:40 -0700
Subject: [PATCH] feat(display): honor display.timestamps across desktop
transcript and TUI
One config key everywhere (#41531): the same display.timestamps that stamps
[HH:MM] on classic-CLI labels now gates the desktop transcript's timeline
timestamps and renders dim [HH:MM] labels on TUI user/assistant rows.
- desktop: $displayTimestamps store fed from config.yaml via
use-hermes-config; TimelineTimestamp renders nothing while the key is off
(the default). Hover tooltips with the exact time stay ungated (#70450).
- TUI: tui_gateway forwards each persisted row's timestamp in the display
projection; toTranscriptMessages threads it as Msg.createdAt; live rows
are stamped at append (the #82840 rule); MessageLine shows a dim [HH:MM]
above user/assistant rows when display.timestamps is on.
- No new config keys, no HERMES_* env vars; display-only, prompt-cache safe.
---
.../app/session/hooks/use-hermes-config.ts | 2 +
.../thread/assistant-message.test.tsx | 5 ++
.../thread/system-message.test.tsx | 5 ++
.../thread/timeline-timestamp.test.tsx | 46 +++++++++++++++++++
.../thread/timeline-timestamp.tsx | 8 +++-
.../assistant-ui/tool/tool-group.test.tsx | 4 ++
apps/desktop/src/store/display-timestamps.ts | 20 ++++++++
apps/desktop/src/types/hermes.ts | 1 +
hermes_cli/config_defaults.py | 2 +-
tui_gateway/server.py | 5 ++
ui-tui/src/__tests__/messages.test.ts | 38 ++++++++++++++-
ui-tui/src/app/interfaces.ts | 3 ++
ui-tui/src/app/uiStore.ts | 1 +
ui-tui/src/app/useConfigSync.ts | 5 +-
ui-tui/src/components/appLayout.tsx | 1 +
ui-tui/src/components/messageLine.tsx | 40 ++++++++++++++++
ui-tui/src/domain/messages.ts | 9 ++--
ui-tui/src/gatewayTypes.ts | 3 ++
ui-tui/src/lib/messages.ts | 7 ++-
ui-tui/src/types.ts | 4 ++
20 files changed, 200 insertions(+), 9 deletions(-)
create mode 100644 apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.test.tsx
create mode 100644 apps/desktop/src/store/display-timestamps.ts
diff --git a/apps/desktop/src/app/session/hooks/use-hermes-config.ts b/apps/desktop/src/app/session/hooks/use-hermes-config.ts
index 1250e4ca02..e18a53c441 100644
--- a/apps/desktop/src/app/session/hooks/use-hermes-config.ts
+++ b/apps/desktop/src/app/session/hooks/use-hermes-config.ts
@@ -4,6 +4,7 @@ import { setTerminalFontFamilyFromConfig } from '@/app/right-sidebar/terminal/te
import { getHermesConfig, getHermesConfigDefaults } from '@/hermes'
import { BUILTIN_PERSONALITIES, normalizePersonalityValue, personalityNamesFromConfig } from '@/lib/chat-runtime'
import { normalize } from '@/lib/text'
+import { setDisplayTimestampsFromConfig } from '@/store/display-timestamps'
import {
getComposerSelectionGeneration,
getCurrentModelSource,
@@ -109,6 +110,7 @@ export function useHermesConfig({ activeSessionIdRef }: HermesConfigOptions) {
setVoiceMaxRecordingSeconds(recordingLimit(config.voice?.max_recording_seconds))
setSttEnabled(config.stt?.enabled !== false)
+ setDisplayTimestampsFromConfig(config.display?.timestamps)
setTerminalFontFamilyFromConfig(config.terminal?.font_family)
applyAutoSpeakFromConfig(config)
applyVoiceStopPhraseFromConfig(config)
diff --git a/apps/desktop/src/components/assistant-ui/thread/assistant-message.test.tsx b/apps/desktop/src/components/assistant-ui/thread/assistant-message.test.tsx
index b75668f78e..986c4a4248 100644
--- a/apps/desktop/src/components/assistant-ui/thread/assistant-message.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/thread/assistant-message.test.tsx
@@ -8,10 +8,15 @@ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime }
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
+import { $displayTimestamps } from '@/store/display-timestamps'
+
import { formatTimelineRange, formatTimelineTimestamp } from './timestamp'
import { Thread } from '.'
+// Timeline timestamps render only when `display.timestamps` is enabled.
+$displayTimestamps.set(true)
+
const createdAt = new Date('2026-05-01T00:00:00.000Z')
const completedAt = createdAt.getTime() / 1000 + 1.25
diff --git a/apps/desktop/src/components/assistant-ui/thread/system-message.test.tsx b/apps/desktop/src/components/assistant-ui/thread/system-message.test.tsx
index df2b2219d3..d0249fc45a 100644
--- a/apps/desktop/src/components/assistant-ui/thread/system-message.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/thread/system-message.test.tsx
@@ -2,8 +2,13 @@ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime }
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
+import { $displayTimestamps } from '@/store/display-timestamps'
+
import { Thread } from '.'
+// Timeline timestamps render only when `display.timestamps` is enabled.
+$displayTimestamps.set(true)
+
const timestamp = new Date('2026-05-01T00:00:00.000Z')
class TestResizeObserver {
diff --git a/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.test.tsx b/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.test.tsx
new file mode 100644
index 0000000000..6263fc702e
--- /dev/null
+++ b/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.test.tsx
@@ -0,0 +1,46 @@
+import { cleanup, render } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+
+import { $displayTimestamps, setDisplayTimestampsFromConfig } from '@/store/display-timestamps'
+
+import { TimelineTimestamp } from './timeline-timestamp'
+
+afterEach(cleanup)
+
+beforeEach(() => {
+ $displayTimestamps.set(false)
+})
+
+describe('setDisplayTimestampsFromConfig', () => {
+ it('accepts boolean and string forms, defaulting off', () => {
+ setDisplayTimestampsFromConfig(true)
+ expect($displayTimestamps.get()).toBe(true)
+
+ setDisplayTimestampsFromConfig(false)
+ expect($displayTimestamps.get()).toBe(false)
+
+ setDisplayTimestampsFromConfig('true')
+ expect($displayTimestamps.get()).toBe(true)
+
+ setDisplayTimestampsFromConfig(undefined)
+ expect($displayTimestamps.get()).toBe(false)
+ })
+})
+
+describe('TimelineTimestamp display.timestamps gate', () => {
+ const timestamp = new Date('2026-05-01T00:00:00.000Z').getTime() / 1000
+
+ it('renders nothing while display.timestamps is off (the default)', () => {
+ const { container } = render()
+
+ expect(container.querySelector('[data-slot="timeline-timestamp"]')).toBeNull()
+ })
+
+ it('renders the stamp once display.timestamps is on', () => {
+ $displayTimestamps.set(true)
+
+ const { container } = render()
+
+ expect(container.querySelector('[data-slot="timeline-timestamp"]')).toBeTruthy()
+ })
+})
diff --git a/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.tsx b/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.tsx
index 1e74e7600e..8a920aeb92 100644
--- a/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.tsx
+++ b/apps/desktop/src/components/assistant-ui/thread/timeline-timestamp.tsx
@@ -1,7 +1,9 @@
import { useAuiState } from '@assistant-ui/react'
+import { useStore } from '@nanostores/react'
import type { FC } from 'react'
import { cn } from '@/lib/utils'
+import { $displayTimestamps } from '@/store/display-timestamps'
import { formatTimelineRange } from './timestamp'
@@ -33,9 +35,13 @@ export const TimelineTimestamp: FC<{
completedAt?: number
timestamp?: number
}> = ({ className, completedAt, timestamp }) => {
+ // One config key everywhere (#41531): `display.timestamps` in config.yaml
+ // gates transcript timestamps here exactly as it gates the classic CLI's
+ // [HH:MM] labels. Display-only, so toggling never touches model context.
+ const enabled = useStore($displayTimestamps)
const started = unixDate(timestamp)
- if (!started || !validUnixSeconds(timestamp)) {
+ if (!enabled || !started || !validUnixSeconds(timestamp)) {
return null
}
diff --git a/apps/desktop/src/components/assistant-ui/tool/tool-group.test.tsx b/apps/desktop/src/components/assistant-ui/tool/tool-group.test.tsx
index decc0d03b2..7402bd9052 100644
--- a/apps/desktop/src/components/assistant-ui/tool/tool-group.test.tsx
+++ b/apps/desktop/src/components/assistant-ui/tool/tool-group.test.tsx
@@ -2,6 +2,7 @@ import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime }
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { $displayTimestamps } from '@/store/display-timestamps'
import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
import { $activeSessionId } from '@/store/session'
import { clearDismissedToolRows } from '@/store/tool-dismiss'
@@ -10,6 +11,9 @@ import { $toolDisclosureStates } from '@/store/tool-view'
import { Thread } from '../thread'
import { formatTimelineRange } from '../thread/timestamp'
+// Timeline timestamps render only when `display.timestamps` is enabled.
+$displayTimestamps.set(true)
+
// A run of tool calls collapses to a one-line summary once it has settled, but
// a run with anything still pending always renders its rows. That rule is what
// keeps the "approval must never be buried" bug fixed: an inline ApprovalBar
diff --git a/apps/desktop/src/store/display-timestamps.ts b/apps/desktop/src/store/display-timestamps.ts
new file mode 100644
index 0000000000..75e30673f9
--- /dev/null
+++ b/apps/desktop/src/store/display-timestamps.ts
@@ -0,0 +1,20 @@
+/**
+ * `display.timestamps` — one config key for message timestamps everywhere.
+ *
+ * The same config.yaml key that puts [HH:MM] stamps on classic-CLI labels
+ * gates the desktop transcript's per-message / per-tool-run timeline
+ * timestamps (#41531, #65272, #68052). Off by default, matching the CLI
+ * default in hermes_cli/config_defaults.py.
+ *
+ * Display-only: reading or toggling it never mutates model context, so it is
+ * prompt-cache safe. Hover tooltips with the exact time (#70450) are NOT
+ * gated — they add no visual noise until the user asks for them.
+ */
+
+import { atom } from 'nanostores'
+
+export const $displayTimestamps = atom(false)
+
+export function setDisplayTimestampsFromConfig(value: unknown): void {
+ $displayTimestamps.set(value === true || value === 'true' || value === 1)
+}
diff --git a/apps/desktop/src/types/hermes.ts b/apps/desktop/src/types/hermes.ts
index 5d04fdde54..fe954367c9 100644
--- a/apps/desktop/src/types/hermes.ts
+++ b/apps/desktop/src/types/hermes.ts
@@ -331,6 +331,7 @@ export interface HermesConfig {
personality?: string
skin?: string
interim_assistant_messages?: boolean
+ timestamps?: boolean
}
desktop?: {
repo_scan_enabled?: boolean
diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py
index c4bc6386b0..d8c9ca62ab 100644
--- a/hermes_cli/config_defaults.py
+++ b/hermes_cli/config_defaults.py
@@ -1215,7 +1215,7 @@ DEFAULT_CONFIG = {
# "off" — no watcher messages at all
"background_process_notifications": "concise",
"streaming": False,
- "timestamps": False, # Show timestamp on user and assistant labels
+ "timestamps": False, # Show message timestamps (CLI labels, TUI rows, desktop transcript)
"timestamp_format": "%H:%M", # strftime format for timestamps (e.g. "%b-%d %H:%M")
"final_response_markdown": "strip", # render | strip | raw
# Preserve recent classic CLI output across Ctrl+L, /redraw, and
diff --git a/tui_gateway/server.py b/tui_gateway/server.py
index 9638e88794..c34e42fc64 100644
--- a/tui_gateway/server.py
+++ b/tui_gateway/server.py
@@ -7425,6 +7425,11 @@ def _history_to_messages(history: list[dict]) -> list[dict]:
if not content_text.strip() and not has_reasoning:
continue
msg = {"role": role, "text": content_text}
+ # Persisted authoring time (Unix seconds) for display.timestamps
+ # renderers (#41531). Display-only: never fed back into model context.
+ ts = m.get("timestamp")
+ if isinstance(ts, (int, float)) and ts > 0:
+ msg["timestamp"] = float(ts)
# Durable row identity, stamped by _rows_to_conversation. The renderer's
# own message ids are ephemeral (timestamp+index derived, and a
# different shape for live vs rehydrated vs optimistic rows), so
diff --git a/ui-tui/src/__tests__/messages.test.ts b/ui-tui/src/__tests__/messages.test.ts
index e83fe2d311..a9b271b154 100644
--- a/ui-tui/src/__tests__/messages.test.ts
+++ b/ui-tui/src/__tests__/messages.test.ts
@@ -4,10 +4,10 @@ import { renderSync } from '@hermes/ink'
import React from 'react'
import { describe, expect, it } from 'vitest'
-import { MessageLine } from '../components/messageLine.js'
+import { fmtMsgTimestamp, MessageLine } from '../components/messageLine.js'
import { MAX_HISTORY } from '../config/limits.js'
import { toTranscriptMessages } from '../domain/messages.js'
-import { capTranscriptHistory, upsert } from '../lib/messages.js'
+import { appendTranscriptMessage, capTranscriptHistory, upsert } from '../lib/messages.js'
import { stripAnsi } from '../lib/text.js'
import { DEFAULT_THEME } from '../theme.js'
@@ -237,3 +237,37 @@ describe('capTranscriptHistory', () => {
expect(capped.at(-1)?.text).toBe('m1004')
})
})
+
+describe('display.timestamps (#41531)', () => {
+ it('formats a Unix-seconds timestamp as [HH:MM] and rejects garbage', () => {
+ const noon = new Date()
+ noon.setHours(13, 5, 0, 0)
+
+ expect(fmtMsgTimestamp(noon.getTime() / 1000)).toBe('[13:05]')
+ expect(fmtMsgTimestamp(undefined)).toBeNull()
+ expect(fmtMsgTimestamp(0)).toBeNull()
+ expect(fmtMsgTimestamp(Number.NaN)).toBeNull()
+ })
+
+ it('threads persisted transcript timestamps onto rehydrated rows', () => {
+ const rows = [
+ { role: 'user', text: 'when was this', timestamp: 1_750_000_000 },
+ { role: 'assistant', text: 'right then', timestamp: 1_750_000_060 }
+ ]
+
+ const result = toTranscriptMessages(rows)
+ expect(result[0]?.createdAt).toBe(1_750_000_000)
+ expect(result[1]?.createdAt).toBe(1_750_000_060)
+ })
+
+ it('stamps live rows at append and preserves supplied times', () => {
+ const before = Date.now() / 1000
+ const [live] = appendTranscriptMessage([], { role: 'user', text: 'now' })
+
+ expect(live?.createdAt).toBeGreaterThanOrEqual(before - 1)
+ expect(live?.createdAt).toBeLessThanOrEqual(Date.now() / 1000 + 1)
+
+ const [kept] = appendTranscriptMessage([], { createdAt: 123, role: 'user', text: 'then' })
+ expect(kept?.createdAt).toBe(123)
+ })
+})
diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts
index e8a1a1d9ff..dffa3aeb50 100644
--- a/ui-tui/src/app/interfaces.ts
+++ b/ui-tui/src/app/interfaces.ts
@@ -345,6 +345,9 @@ export interface UiState {
statusBar: StatusBarMode
streaming: boolean
theme: Theme
+ // `display.timestamps` — dim [HH:MM] labels on user/assistant transcript
+ // rows, the same config key the classic CLI honors (#41531).
+ timestamps: boolean
usage: Usage
}
diff --git a/ui-tui/src/app/uiStore.ts b/ui-tui/src/app/uiStore.ts
index e43b38e501..581d576a48 100644
--- a/ui-tui/src/app/uiStore.ts
+++ b/ui-tui/src/app/uiStore.ts
@@ -33,6 +33,7 @@ const buildUiState = (): UiState => ({
status: 'summoning hermes…',
statusBar: 'top',
streaming: true,
+ timestamps: false,
// Last session's resolved theme paints frame one (flash-free boot, like
// the desktop's hermes-boot-* keys); DEFAULT_THEME only on first launch.
theme: bootTheme ?? DEFAULT_THEME,
diff --git a/ui-tui/src/app/useConfigSync.ts b/ui-tui/src/app/useConfigSync.ts
index 9509f9b44d..cf471c4633 100644
--- a/ui-tui/src/app/useConfigSync.ts
+++ b/ui-tui/src/app/useConfigSync.ts
@@ -289,7 +289,10 @@ export const applyDisplay = (
sections: resolveSections(d.sections),
showReasoning: !!d.show_reasoning,
statusBar: normalizeStatusBar(d.tui_statusbar),
- streaming: d.streaming !== false
+ streaming: d.streaming !== false,
+ // The SAME key that stamps [HH:MM] on classic-CLI labels (#41531) —
+ // no separate TUI knob.
+ timestamps: d.timestamps === true
})
}
diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx
index ad4726798f..569c969a11 100644
--- a/ui-tui/src/components/appLayout.tsx
+++ b/ui-tui/src/components/appLayout.tsx
@@ -232,6 +232,7 @@ const TranscriptPane = memo(function TranscriptPane({
})}
sections={ui.sections}
t={ui.theme}
+ timestamps={ui.timestamps}
/>
)}
diff --git a/ui-tui/src/components/messageLine.tsx b/ui-tui/src/components/messageLine.tsx
index ba59f6a343..1d3d7e6761 100644
--- a/ui-tui/src/components/messageLine.tsx
+++ b/ui-tui/src/components/messageLine.tsx
@@ -28,6 +28,26 @@ import { TodoPanel } from './todoPanel.js'
// Collapse threshold for long system messages (system prompt etc.)
const SYSTEM_COLLAPSE_CHARS = 400
+// `display.timestamps` label — same HH:MM shape the classic CLI's default
+// `display.timestamp_format` ("%H:%M") produces on its message labels, so
+// one config key reads identically across surfaces (#41531).
+export const fmtMsgTimestamp = (createdAt: number | undefined): null | string => {
+ if (typeof createdAt !== 'number' || !Number.isFinite(createdAt) || createdAt <= 0) {
+ return null
+ }
+
+ const date = new Date(createdAt * 1000)
+
+ if (Number.isNaN(date.getTime())) {
+ return null
+ }
+
+ const hh = String(date.getHours()).padStart(2, '0')
+ const mm = String(date.getMinutes()).padStart(2, '0')
+
+ return `[${hh}:${mm}]`
+}
+
export const MessageLine = memo(function MessageLine({
cols,
compact,
@@ -40,6 +60,7 @@ export const MessageLine = memo(function MessageLine({
reasoningActive = false,
sections,
t,
+ timestamps = false,
tools = []
}: MessageLineProps) {
// Per-section overrides win over the global mode, so resolve each section
@@ -238,6 +259,12 @@ export const MessageLine = memo(function MessageLine({
// against the prose around it.
const isDiffSegment = msg.kind === 'diff'
+ // `display.timestamps`: dim [HH:MM] beside the gutter glyph on user and
+ // assistant rows only — event/trail/system chrome stays unstamped, matching
+ // the classic CLI which stamps its user/assistant labels (#41531).
+ const stamp =
+ timestamps && (msg.role === 'user' || msg.role === 'assistant') && !msg.kind ? fmtMsgTimestamp(msg.createdAt) : null
+
return (
)}
+ {stamp && (
+
+
+
+
+
+ {stamp}
+
+
+ )}
+
@@ -315,5 +353,7 @@ interface MessageLineProps {
reasoningActive?: boolean
sections?: SectionVisibility
t: Theme
+ /** `display.timestamps` — dim [HH:MM] label on user/assistant rows. */
+ timestamps?: boolean
tools?: ActiveTool[]
}
diff --git a/ui-tui/src/domain/messages.ts b/ui-tui/src/domain/messages.ts
index 5ec93cecce..1edac1e90f 100644
--- a/ui-tui/src/domain/messages.ts
+++ b/ui-tui/src/domain/messages.ts
@@ -29,7 +29,9 @@ export const toTranscriptMessages = (rows: unknown): Msg[] => {
continue
}
- const { context, display_kind, name, role, text } = row as TranscriptRow
+ const { context, display_kind, name, role, text, timestamp } = row as TranscriptRow
+
+ const createdAt = typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0 ? timestamp : undefined
if (role === 'tool') {
pending.push(buildToolTrailLine(name ?? 'tool', context ?? ''))
@@ -84,10 +86,10 @@ export const toTranscriptMessages = (rows: unknown): Msg[] => {
}
if (role === 'assistant') {
- out.push({ role, text, ...(pending.length && { tools: pending }) })
+ out.push({ role, text, ...(createdAt !== undefined && { createdAt }), ...(pending.length && { tools: pending }) })
pending = []
} else if (role === 'user' || role === 'system') {
- out.push({ role, text })
+ out.push({ role, text, ...(createdAt !== undefined && { createdAt }) })
pending = []
}
}
@@ -111,4 +113,5 @@ interface TranscriptRow {
name?: string
role?: string
text?: string
+ timestamp?: number
}
diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts
index a701b45fd5..fcd634206f 100644
--- a/ui-tui/src/gatewayTypes.ts
+++ b/ui-tui/src/gatewayTypes.ts
@@ -90,6 +90,9 @@ export interface ConfigDisplayConfig {
show_reasoning?: boolean
streaming?: boolean
thinking_mode?: string
+ /** Show [HH:MM] timestamps on transcript rows — same key the classic CLI
+ * honors on its user/assistant labels (#41531). */
+ timestamps?: boolean
/**
* Nudge the user toward the /agents spawn-tree dashboard the first time a
* turn starts delegating, via a one-time transient activity hint. Opens
diff --git a/ui-tui/src/lib/messages.ts b/ui-tui/src/lib/messages.ts
index 19a2877aca..bbbe4d4275 100644
--- a/ui-tui/src/lib/messages.ts
+++ b/ui-tui/src/lib/messages.ts
@@ -3,7 +3,12 @@ import type { Msg, Role } from '../types.js'
import { appendToolShelfMessage } from './liveProgress.js'
-export const appendTranscriptMessage = (prev: Msg[], msg: Msg): Msg[] => appendToolShelfMessage(prev, msg)
+// Stamp live rows AT APPEND (wall clock, Unix seconds) rather than later:
+// a message's authoring time is when it entered the transcript, not when it
+// happened to be persisted or re-rendered (#82840-class rule). Rehydrated
+// rows arrive with their persisted `createdAt` and keep it.
+export const appendTranscriptMessage = (prev: Msg[], msg: Msg): Msg[] =>
+ appendToolShelfMessage(prev, msg.createdAt === undefined ? { ...msg, createdAt: Date.now() / 1000 } : msg)
export const capTranscriptHistory = (items: Msg[]): Msg[] => {
if (items.length <= MAX_HISTORY) {
diff --git a/ui-tui/src/types.ts b/ui-tui/src/types.ts
index c976913522..f90d3b9c45 100644
--- a/ui-tui/src/types.ts
+++ b/ui-tui/src/types.ts
@@ -119,6 +119,10 @@ export interface Msg {
panelData?: PanelData
role: Role
text: string
+ // Unix seconds the message was authored (persisted transcript timestamp on
+ // rehydrate, wall clock at append time for live rows). Rendered as a dim
+ // [HH:MM] label when `display.timestamps` is on (#41531).
+ createdAt?: number
thinking?: string
// MoA reference-model output stored in `thinking` (see turnController's
// recordMoaReference): unlike ordinary model reasoning, this is the