agent ui: terminal-themed live panel; pretty tool calls; collapsed results
- tool_use renders per-tool (Read /path, Bash $ cmd, send → operator: ...) - tool_result with >120 chars collapses into <details>; short ones inline - session_init / result / rate_limit dropped from the panel - thinking content shown inline if present, fallback indicator otherwise - TurnStart carries unread count → header badge "· 3 unread" - per-tool [status] line dropped from envelope; lives in wake prompt + UI - send form moved below the live panel - live panel themed as a terminal (crust bg, inset shadow, monospace)
This commit is contained in:
parent
d8807b8e8c
commit
ace13cd785
7 changed files with 191 additions and 87 deletions
|
|
@ -113,6 +113,20 @@ pre.diff {
|
|||
word-break: break-all;
|
||||
max-height: 30em;
|
||||
}
|
||||
/* Terminal-ish look for the live panel. Crust as bg (almost-black),
|
||||
slightly inset, mauve phosphor glow. */
|
||||
.live.terminal {
|
||||
background: #11111b;
|
||||
border: 1px solid var(--purple-dim);
|
||||
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.7);
|
||||
border-radius: 4px;
|
||||
padding: 0.8em 1em;
|
||||
overflow-y: auto;
|
||||
max-height: 32em;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Source Code Pro", monospace;
|
||||
font-size: 0.92em;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
.live {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--purple-dim);
|
||||
|
|
@ -121,6 +135,43 @@ pre.diff {
|
|||
max-height: 32em;
|
||||
font-family: inherit;
|
||||
}
|
||||
.live .unread-badge {
|
||||
color: var(--amber);
|
||||
font-weight: normal;
|
||||
margin-left: 0.6em;
|
||||
font-size: 0.85em;
|
||||
text-shadow: 0 0 6px rgba(250, 179, 135, 0.55);
|
||||
}
|
||||
details.row {
|
||||
white-space: normal;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
details.row > summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
list-style: none;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
details.row > summary::before {
|
||||
content: '▸ ';
|
||||
color: var(--muted);
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
}
|
||||
details.row[open] > summary::before { content: '▾ '; }
|
||||
details.row.tool-result-block > summary { color: var(--muted); }
|
||||
details.row > pre.tool-body {
|
||||
margin: 0.3em 0 0.4em 1.2em;
|
||||
padding: 0.4em 0.6em;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-left: 2px solid var(--purple-dim);
|
||||
color: var(--fg);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 22em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.live .row {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
|
|
|||
|
|
@ -225,46 +225,101 @@
|
|||
log.scrollTop = log.scrollHeight;
|
||||
return e;
|
||||
}
|
||||
function details(cls, summary, body) {
|
||||
clearPlaceholder();
|
||||
const d = document.createElement('details');
|
||||
d.className = 'row ' + (cls || '');
|
||||
const s = document.createElement('summary');
|
||||
s.textContent = summary;
|
||||
d.appendChild(s);
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'tool-body';
|
||||
pre.textContent = body;
|
||||
d.appendChild(pre);
|
||||
log.appendChild(d);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
return d;
|
||||
}
|
||||
function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }
|
||||
// Pretty-print a tool call: per-known-tool format, fallback to JSON
|
||||
// for unknown tools.
|
||||
function fmtToolUse(c) {
|
||||
const name = c.name || '';
|
||||
const input = c.input || {};
|
||||
const short = name.startsWith('mcp__hyperhive__')
|
||||
? name.slice('mcp__hyperhive__'.length) + '*' : name;
|
||||
switch (name) {
|
||||
case 'Read': return short + ' ' + (input.file_path || '');
|
||||
case 'Write': return short + ' ' + (input.file_path || '');
|
||||
case 'Edit': return short + ' ' + (input.file_path || '');
|
||||
case 'Glob': return short + ' ' + (input.pattern || '');
|
||||
case 'Grep': return short + ' ' + (input.pattern || '');
|
||||
case 'Bash': return short + ' $ ' + (input.command || '');
|
||||
case 'TodoWrite': return short + ' (' + ((input.todos || []).length) + ' items)';
|
||||
case 'mcp__hyperhive__send': return short + ' → ' + (input.to || '?') + ': '
|
||||
+ JSON.stringify(input.body || '').slice(0, 80);
|
||||
case 'mcp__hyperhive__recv': return short + '()';
|
||||
case 'mcp__hyperhive__request_spawn': return short + ' ' + (input.name || '');
|
||||
case 'mcp__hyperhive__kill': return short + ' ' + (input.name || '');
|
||||
case 'mcp__hyperhive__request_apply_commit':
|
||||
return short + ' ' + (input.agent || '') + ' @ ' + (input.commit_ref || '').slice(0, 12);
|
||||
default: return name + ' ' + trim(JSON.stringify(input), 200);
|
||||
}
|
||||
}
|
||||
function renderToolResult(c) {
|
||||
const txt = Array.isArray(c.content)
|
||||
? c.content.map(p => p.text || '').join('')
|
||||
: (c.content || '');
|
||||
const summary = '← ' + (() => {
|
||||
const trimmed = txt.replace(/\s+/g, ' ').trim();
|
||||
if (!trimmed) return '(empty)';
|
||||
if (trimmed.length <= 120) return trimmed;
|
||||
const lines = txt.split('\n').filter(l => l.length).length;
|
||||
const headline = trimmed.slice(0, 90) + '…';
|
||||
return `${lines}L · ${headline}`;
|
||||
})();
|
||||
// For empty / short results, render as a flat row (no expand).
|
||||
if (!txt.trim() || txt.length <= 120) {
|
||||
row('tool-result', summary);
|
||||
} else {
|
||||
details('tool-result-block', summary, txt);
|
||||
}
|
||||
}
|
||||
function renderStream(v) {
|
||||
if (v.type === 'system' && v.subtype === 'init') {
|
||||
row('sys', '· session init · tools=' + (v.tools||[]).length + ' model=' + (v.model || '?'));
|
||||
return;
|
||||
}
|
||||
if (v.type === 'rate_limit_event') {
|
||||
const u = Math.round((v.rate_limit_info?.utilization || 0) * 100);
|
||||
const s = v.rate_limit_info?.status || '';
|
||||
row('sys', '· rate-limit util=' + u + '% (' + s + ')');
|
||||
return;
|
||||
}
|
||||
// Drop session init, claude's result line, rate-limit — they're
|
||||
// noise. TurnEnd communicates pass/fail; session init data isn't
|
||||
// actionable.
|
||||
if (v.type === 'system' && v.subtype === 'init') return;
|
||||
if (v.type === 'rate_limit_event') return;
|
||||
if (v.type === 'result') return;
|
||||
if (v.type === 'assistant' && v.message && v.message.content) {
|
||||
for (const c of v.message.content) {
|
||||
if (c.type === 'text' && c.text && c.text.trim()) row('text', c.text);
|
||||
else if (c.type === 'thinking') row('thinking', '· thinking …');
|
||||
else if (c.type === 'tool_use') row('tool-use', '→ ' + c.name + ' ' + trim(JSON.stringify(c.input || {}), 240));
|
||||
else if (c.type === 'thinking') {
|
||||
const txt = (c.thinking || c.text || '').trim();
|
||||
row('thinking', txt ? '· ' + txt : '· thinking …');
|
||||
}
|
||||
else if (c.type === 'tool_use') row('tool-use', '→ ' + fmtToolUse(c));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (v.type === 'user' && v.message && v.message.content) {
|
||||
for (const c of v.message.content) {
|
||||
if (c.type === 'tool_result') {
|
||||
const txt = Array.isArray(c.content)
|
||||
? c.content.map(p => p.text || '').join(' ')
|
||||
: (c.content || '');
|
||||
row('tool-result', '← ' + trim(txt, 300));
|
||||
}
|
||||
if (c.type === 'tool_result') renderToolResult(c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (v.type === 'result') {
|
||||
row('result', '✓ done · ' + (v.subtype || '') + (v.is_error ? ' [error]' : ''));
|
||||
return;
|
||||
}
|
||||
row('sys', '· ' + trim(JSON.stringify(v), 200));
|
||||
}
|
||||
function handle(ev) {
|
||||
if (ev.kind === 'turn_start') {
|
||||
const block = row('turn-start', '◆ TURN ← ' + ev.from);
|
||||
if (ev.unread > 0) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'unread-badge';
|
||||
badge.textContent = '· ' + ev.unread + ' unread';
|
||||
block.appendChild(badge);
|
||||
}
|
||||
const body = document.createElement('div');
|
||||
body.className = 'turn-body';
|
||||
body.textContent = ev.body;
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@
|
|||
<h2 id="title">◆ … ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
|
||||
<h3>live</h3>
|
||||
<div id="live" class="live terminal"><div class="meta">connecting…</div></div>
|
||||
|
||||
<div id="status">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<h3>live</h3>
|
||||
<div id="live" class="live"><div class="meta">connecting…</div></div>
|
||||
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue