feat(agent-ui): show turn start/end times + duration on the agent terminal

The per-agent terminal marks turn boundaries (◆ TURN ← / ✓ turn) but
shows no time. Append a wall-clock HH:MM:SS to both the turn-start and
turn-end rows, plus the elapsed duration on turn-end, rendered as dim
metadata so the boundary glyph stays the focus.

This is the frontend half of the feature; it reads a per-event `ts`
(unix seconds) off the turn_start / turn_end events. The read is guarded
on a numeric `ts`, so until the harness surfaces per-event timestamps
the rows render exactly as before (inert-until-capture). Once the
backend attaches `ts` to the history rows + live SSE frame, the times
light up automatically for both live and scrollback, with no further
frontend change.
This commit is contained in:
iris 2026-06-10 13:56:30 +02:00 committed by mara
commit e89ca90956
2 changed files with 39 additions and 1 deletions

View file

@ -756,6 +756,16 @@ window.marked = marked;
const h = Math.floor(m / 60);
return h + 'h ' + (m % 60) + 'm';
}
// Wall-clock HH:MM:SS (UTC, matching the inbox timestamps on this page)
// from a unix-seconds value. Used to label turn-start / turn-end rows
// when the event carries a `ts` (see the turn renderers below).
function fmtClock(sec) {
return new Date(sec * 1000).toISOString().slice(11, 19);
}
// Unix-seconds stamp of the most recent open turn-start, so the
// matching turn-end can show a duration. Turns are sequential, so a
// single slot is enough (history replays chronologically too).
let pendingTurnStartTs = null;
const STATE_TOOLTIPS = {
loading: 'harness not yet contacted',
offline: 'harness unreachable or claude not logged in',
@ -1697,6 +1707,16 @@ window.marked = marked;
if (api.fromHistory) openTurnsFromHistory += 1;
else { setBannerActive(true); setState('thinking'); }
const block = api.row('turn-start', '◆ TURN ← ' + ev.from);
// Turn start time. Guarded on a numeric `ts` (unix seconds)
// so the row degrades to its old text-only form until the
// backend surfaces per-event timestamps.
if (typeof ev.ts === 'number') {
pendingTurnStartTs = ev.ts;
const t = document.createElement('span');
t.className = 'turn-time';
t.textContent = '· ' + fmtClock(ev.ts);
block.appendChild(t);
}
if (ev.unread > 0) {
const badge = document.createElement('span');
badge.className = 'unread-badge';
@ -1717,9 +1737,23 @@ window.marked = marked;
refreshLooseEnds();
}
const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail';
api.row(cls,
const row = api.row(cls,
(ev.ok ? '✓' : '✗') + ' turn ' + (ev.ok ? 'ok' : 'fail')
+ (ev.note ? ' — ' + ev.note : ''));
// Turn end time + duration since the paired turn-start.
// Same `ts` guard as turn_start; duration only when we saw the
// matching start's stamp.
if (typeof ev.ts === 'number') {
const t = document.createElement('span');
t.className = 'turn-time';
let label = '· ' + fmtClock(ev.ts);
if (pendingTurnStartTs != null && ev.ts >= pendingTurnStartTs) {
label += ' · ' + fmtAge((ev.ts - pendingTurnStartTs) * 1000);
}
t.textContent = label;
row.appendChild(t);
}
pendingTurnStartTs = null;
},
note(ev, api) {
const t = String(ev.text || '');

View file

@ -100,6 +100,10 @@
.live .row .md, .live .row > details { text-indent: 0; }
.live .turn-end-ok { color: var(--green); border-left-color: var(--green); }
.live .turn-end-fail { color: var(--red); border-left-color: var(--red); }
/* Wall-clock time (+ duration on turn-end) appended to the turn-start /
turn-end rows. Dim + smaller so the boundary glyph stays the focus and
the timestamp reads as metadata. */
.live .turn-time { color: var(--muted); font-size: 0.85em; margin-left: 0.5em; }
.live .text { color: var(--fg); }
.live .thinking { color: var(--muted); font-style: italic; }
.live .tool-use { color: var(--cyan); }