From e89ca90956760c972227e4686414f491ce7eaa30 Mon Sep 17 00:00:00 2001 From: iris Date: Wed, 10 Jun 2026 13:56:30 +0200 Subject: [PATCH] feat(agent-ui): show turn start/end times + duration on the agent terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/packages/agent/src/app.js | 36 ++++++++++++++++++++++- frontend/packages/shared/src/terminal.css | 4 +++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 53feb7e5..3798351a 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -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 || ''); diff --git a/frontend/packages/shared/src/terminal.css b/frontend/packages/shared/src/terminal.css index 2cfe7dee..37413827 100644 --- a/frontend/packages/shared/src/terminal.css +++ b/frontend/packages/shared/src/terminal.css @@ -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); }