Skip to content

Commit 8e3981f

Browse files
committed
feat(opencode): extend gutter to assistant messages, add seconds to popup
The gutter mode now renders an HH:MM column on assistant messages too, not just user messages — closes the original ask in opencode#8634 ("Add timestamp next to messages in chat (both agent and user messages)"). Assistant message body is wrapped in flexDirection="row" only when gutter mode is active; non-gutter modes keep the existing fragment layout untouched. Assistant text lands at column 9 (gutter 6 + paddingLeft 3), which is also where user text lands (gutter 6 + border 1 + paddingLeft 2), so the two sides align visually. The timestamp popup now shows HH:MM:SS instead of HH:MM, addressing opencode#20406 ("Add seconds to message timestamp display"). 24-hour formatting is used consistently for both gutter and popup to avoid opencode#28804 (Bun ignores system locale → spurious AM/PM on Linux). Three new unit tests for hourMinuteSecond bring the suite to 14/14.
1 parent c7e5a4f commit 8e3981f

4 files changed

Lines changed: 71 additions & 3 deletions

File tree

packages/opencode/src/cli/cmd/tui/routes/session/dialog-timestamp.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@ import { TextAttributes } from "@opentui/core"
22
import { useTheme } from "../../context/theme"
33
import { useDialog, type DialogContext } from "../../ui/dialog"
44
import { useBindings } from "../../keymap"
5-
import { Locale } from "@/util/locale"
5+
import { hourMinuteSecond } from "../../util/timestamps"
66

77
export type DialogTimestampProps = {
88
created: number
99
}
1010

11+
// Seconds-precision datetime for the popup. The gutter renders fixed HH:MM
12+
// because alignment matters there; the popup is where users come for precision
13+
// (debugging, auditing, distinguishing messages sent close together — see
14+
// opencode#20406). 24-hour formatting matches the gutter and side-steps
15+
// opencode#28804 (Bun ignoring system locale → spurious AM/PM on Linux).
16+
function preciseDateTime(input: number): string {
17+
const date = new Date(input)
18+
const localDate = date.toLocaleDateString()
19+
return `${hourMinuteSecond(input)} · ${localDate}`
20+
}
21+
1122
function relative(input: number, now: number): string {
1223
const delta = Math.max(0, now - input)
1324
if (delta < 60_000) return "just now"
@@ -49,7 +60,7 @@ export function DialogTimestamp(props: DialogTimestampProps) {
4960
</text>
5061
</box>
5162
<box paddingBottom={1} gap={1}>
52-
<text fg={theme.text}>{Locale.datetime(props.created)}</text>
63+
<text fg={theme.text}>{preciseDateTime(props.created)}</text>
5364
<text fg={theme.textMuted}>{relative(props.created, Date.now())}</text>
5465
</box>
5566
</box>

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,7 @@ function UserMessage(props: {
14511451
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) {
14521452
const ctx = use()
14531453
const local = useLocal()
1454+
const dialog = useDialog()
14541455
const { theme } = useTheme()
14551456
const sync = useSync()
14561457
const messages = createMemo(() => sync.data.message[props.message.sessionID] ?? [])
@@ -1469,8 +1470,11 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
14691470
})
14701471

14711472
const childShortcut = useCommandShortcut("session.child.first")
1473+
const isGutter = createMemo(() => ctx.timestampsMode() === "gutter")
14721474

1473-
return (
1475+
// Assistant message body — rendered as-is when timestamps_mode != "gutter", or
1476+
// wrapped in a horizontal row alongside a gutter cell when "gutter" is active.
1477+
const body = () => (
14741478
<>
14751479
<For each={props.parts}>
14761480
{(part, index) => {
@@ -1537,6 +1541,30 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
15371541
</Switch>
15381542
</>
15391543
)
1544+
1545+
return (
1546+
<Show when={isGutter()} fallback={body()}>
1547+
{/* In gutter mode, lay the message row out horizontally: HH:MM column on
1548+
* the left, message content on the right. The first part of an assistant
1549+
* message has marginTop=1 (TextPart/ReasoningPart/ToolPart all do), so
1550+
* the gutter uses the same paddingTop to align vertically with the first
1551+
* line of text. flexShrink=0 on the gutter keeps it from collapsing on
1552+
* narrow terminals; the body column gets flexGrow=1 to take the rest. */}
1553+
<box flexDirection="row" flexShrink={0}>
1554+
<box
1555+
width={TIMESTAMP_GUTTER_WIDTH}
1556+
paddingTop={1}
1557+
flexShrink={0}
1558+
onMouseUp={() => DialogTimestamp.show(dialog, props.message.time.created)}
1559+
>
1560+
<text fg={theme.textMuted}>{hourMinute(props.message.time.created)}</text>
1561+
</box>
1562+
<box flexDirection="column" flexGrow={1}>
1563+
{body()}
1564+
</box>
1565+
</box>
1566+
</Show>
1567+
)
15401568
}
15411569

15421570
const PART_MAPPING = {

packages/opencode/src/cli/cmd/tui/util/timestamps.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ export function hourMinute(input: number): string {
3131
const minutes = String(date.getMinutes()).padStart(2, "0")
3232
return `${hours}:${minutes}`
3333
}
34+
35+
// "HH:MM:SS" 24-hour, locale-independent. Used by the message timestamp popup
36+
// — alignment doesn't matter there, but seconds-level precision does. Closes
37+
// the spirit of opencode#20406 (users want seconds for debugging / auditing).
38+
export function hourMinuteSecond(input: number): string {
39+
const date = new Date(input)
40+
const hours = String(date.getHours()).padStart(2, "0")
41+
const minutes = String(date.getMinutes()).padStart(2, "0")
42+
const seconds = String(date.getSeconds()).padStart(2, "0")
43+
return `${hours}:${minutes}:${seconds}`
44+
}

packages/opencode/test/cli/cmd/tui/timestamps.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
22
import {
33
getTimestampsMode,
44
hourMinute,
5+
hourMinuteSecond,
56
nextTimestampsMode,
67
normalizeTimestampsMode,
78
TIMESTAMPS_MODES,
@@ -80,3 +81,20 @@ describe("hourMinute", () => {
8081
expect(hourMinute(ms)).toBe("23:45")
8182
})
8283
})
84+
85+
describe("hourMinuteSecond", () => {
86+
test("returns 8-cell HH:MM:SS, zero-padded, 24-hour", () => {
87+
const ms = new Date(2025, 0, 2, 7, 5, 9).getTime()
88+
expect(hourMinuteSecond(ms)).toBe("07:05:09")
89+
})
90+
91+
test("23:59:59 boundary", () => {
92+
const ms = new Date(2025, 0, 2, 23, 59, 59).getTime()
93+
expect(hourMinuteSecond(ms)).toBe("23:59:59")
94+
})
95+
96+
test("midnight", () => {
97+
const ms = new Date(2025, 0, 2, 0, 0, 0).getTime()
98+
expect(hourMinuteSecond(ms)).toBe("00:00:00")
99+
})
100+
})

0 commit comments

Comments
 (0)