agent page: dashboard back-link + last-turn timing chip
title bar grows a '↑ DASHB04RD' link next to the rebuild button — opens the host dashboard in a new tab so the operator can pivot between agents without losing the live tail. uses the dashboardPort already plumbed via /api/state. state row picks up a 'last turn 12.3s' chip that fills in when state transitions away from thinking. format: ms / s.s / m s. hidden until the first turn completes.
This commit is contained in:
parent
ee5b85716d
commit
bd7d2d4860
4 changed files with 55 additions and 4 deletions
8
TODO.md
8
TODO.md
|
|
@ -27,8 +27,12 @@ Pick anything from here when relevant. Cross-cutting design notes live in
|
|||
|
||||
## UI / UX
|
||||
|
||||
- **Per-agent UI substance.** Show last N inbox messages, last turn timing,
|
||||
link back to dashboard.
|
||||
- **Per-agent inbox view.** Show the last N messages addressed to
|
||||
this agent on its page (the per-agent equivalent of the
|
||||
dashboard's operator inbox). Needs a new wire request from agent
|
||||
→ host (host has the broker; agent doesn't); reuse the broker's
|
||||
`recent_for` query. Last-turn timing + dashboard back-link
|
||||
already shipped.
|
||||
- **State badge: compacting + napping states.** Idle/thinking already
|
||||
ship (driven from SSE turn_start/turn_end). Add `compacting 📦` and
|
||||
`napping 😴` once the `/compact` trigger and `nap` tool exist —
|
||||
|
|
|
|||
|
|
@ -130,6 +130,26 @@ pre.diff {
|
|||
align-items: center;
|
||||
gap: 0.6em;
|
||||
}
|
||||
.last-turn {
|
||||
color: var(--muted);
|
||||
font-size: 0.8em;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.btn-dashlink {
|
||||
color: var(--cyan);
|
||||
border: 1px solid var(--cyan);
|
||||
padding: 0.15em 0.6em;
|
||||
font-size: 0.55em;
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.1em;
|
||||
margin-left: 0.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.btn-dashlink:hover {
|
||||
background: rgba(137, 220, 235, 0.1);
|
||||
box-shadow: 0 0 10px -2px currentColor;
|
||||
}
|
||||
.btn-cancel-turn {
|
||||
font-family: inherit;
|
||||
font-size: 0.8em;
|
||||
|
|
|
|||
|
|
@ -65,16 +65,25 @@
|
|||
`░▒▓█▓▒░ ${label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░`;
|
||||
const title = $('title');
|
||||
title.textContent = `◆ ${label} ◆ `;
|
||||
// ↑ DASHB04RD — back-link to the host dashboard. Opens in a new
|
||||
// tab to keep the agent page anchored where the operator is.
|
||||
const dashUrl = `${location.protocol}//${location.hostname}:${dashboardPort}/`;
|
||||
title.append(
|
||||
el('a', {
|
||||
href: dashUrl, target: '_blank', rel: 'noopener',
|
||||
class: 'btn-dashlink', title: 'host dashboard',
|
||||
}, '↑ DASHB04RD'),
|
||||
' ',
|
||||
);
|
||||
const btn = el('a', {
|
||||
href: '#', class: 'btn-rebuild', id: 'rebuild-btn',
|
||||
}, '↻ R3BU1LD');
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (!confirm(`rebuild ${label}? container will hot-reload.`)) return;
|
||||
const url = `${location.protocol}//${location.hostname}:${dashboardPort}/rebuild/${label}`;
|
||||
const f = document.createElement('form');
|
||||
f.method = 'POST';
|
||||
f.action = url;
|
||||
f.action = `${dashUrl}rebuild/${label}`;
|
||||
document.body.appendChild(f);
|
||||
f.submit();
|
||||
});
|
||||
|
|
@ -310,6 +319,13 @@
|
|||
}
|
||||
function setState(next) {
|
||||
if (next === stateName) return;
|
||||
// Capture the just-ending state's duration when leaving 'thinking'
|
||||
// so the operator can eyeball turn length without scrolling the
|
||||
// terminal back.
|
||||
if (stateName === 'thinking' && next !== 'thinking') {
|
||||
const elapsedMs = Date.now() - stateSince;
|
||||
renderLastTurn(elapsedMs);
|
||||
}
|
||||
stateName = next;
|
||||
stateSince = Date.now();
|
||||
const badge = $('state-badge');
|
||||
|
|
@ -321,6 +337,16 @@
|
|||
}
|
||||
renderStateBadge();
|
||||
}
|
||||
function renderLastTurn(ms) {
|
||||
const el_ = $('last-turn');
|
||||
if (!el_) return;
|
||||
let s = '';
|
||||
if (ms < 1000) s = ms + 'ms';
|
||||
else if (ms < 60_000) s = (ms / 1000).toFixed(1) + 's';
|
||||
else s = Math.floor(ms / 60_000) + 'm ' + Math.floor((ms / 1000) % 60) + 's';
|
||||
el_.textContent = '· last turn ' + s;
|
||||
el_.hidden = false;
|
||||
}
|
||||
function startStateTicker() {
|
||||
if (stateTickTimer) return;
|
||||
stateTickTimer = setInterval(renderStateBadge, 1000);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
<div id="state-row">
|
||||
<span id="state-badge" class="state-badge state-loading">… booting</span>
|
||||
<span id="last-turn" class="last-turn" hidden></span>
|
||||
<button type="button" id="cancel-btn" class="btn-cancel-turn" hidden>■ cancel turn</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue