From fb58f0e5b9dc8720b335a75433ece578ee495ef0 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 21 Jun 2026 23:04:29 +0200 Subject: [PATCH] agent-ui: per-tool emoji icons + thinking/turn-end glyphs in terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the 🧠 thinking-tokens indicator (operator liked it), give terminal rows evocative glyphs: - πŸ’­ thinking prose (pairs with the 🧠 token counter) - βœ…/❌ turn-end ok/fail (was βœ“/βœ—) - per-tool icons via toolIcon(name): πŸ“€ send Β· πŸ“₯ recv Β· ❓ ask Β· ✍️ answer Β· ⏰ remind Β· 🏷️ set_status Β· πŸͺ’ loose-ends Β· ↻/⏹️/▢️/πŸ”„ lifecycle Β· πŸ“¦ request_* Β· ⏱️ schedule Β· πŸ–₯️ bash Β· πŸ’¬ matrix Β· πŸ“– Read Β· πŸ’Ύ Write Β· ✏️ Edit Β· πŸ” Glob/Grep Β· πŸ”§ default β€” on both flat rows and the rich send/ask/answer/Write/Edit summaries. Dropped the original 🐝 task-row glyphs: claude's nested-agent Task tool is intentionally omitted from agents' allow-list (mcp.rs), so those rows never render for agents. Doc table updated. Closes the emojify-terminal request. --- docs/terminal-rendering.md | 16 +++++----- frontend/packages/agent/src/app.js | 49 +++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index 58584e54..f3b14111 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -36,20 +36,20 @@ parent's negative pull. |---|---|---|---|---| | `.turn-start` | `β—† TURN ← ` | amber, left rule | `LiveEvent::TurnStart` | harness wake | | `.turn-body` | (child div under turn-start) | fg | same | the wake-prompt body | -| `.turn-end-ok` | `βœ“ turn ok` | green, left rule | `LiveEvent::TurnEnd { ok: true }` | harness | -| `.turn-end-fail` | `βœ— turn fail β€” note` | red, left rule | `LiveEvent::TurnEnd { ok: false }` | harness | +| `.turn-end-ok` | `βœ… turn ok` | green, left rule | `LiveEvent::TurnEnd { ok: true }` | harness | +| `.turn-end-fail` | `❌ turn fail β€” note` | red, left rule | `LiveEvent::TurnEnd { ok: false }` | harness | | `.turn-time` | `Β· HH:MM:SS` on turn-start; `Β· HH:MM:SS Β· ` on turn-end (child span) | muted, smaller | per-event `ts` (unix seconds) on the live frame + history row | harness | | `.text` | (no prefix; markdown body) | fg | claude `assistant.content[].text` | stream-json | -| `.thinking` | `Β· thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json | -| `.tool-use` (flat) | `β†’ Name args…` | cyan | tool_use w/o rich renderer | stream-json | -| `.tool-use` `
` | `Write/Edit Β· +N` (no `β†’`) | cyan, body is +/- diff | `renderRichToolUse` Write/Edit | stream-json | -| `.tool-use` `
` | `send β†’ to Β· NL`, `ask β†’ to`, `answer #id` | cyan, body is markdown | rich renderer for send / ask / answer | stream-json | +| `.thinking` | `πŸ’­ thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json | +| `.tool-use` (flat) | ` Name args…` | cyan | tool_use w/o rich renderer; `` from `toolIcon(name)` (πŸ“€ send Β· πŸ“₯ recv Β· ❓ ask Β· ⏰ remind Β· 🏷️ set_status Β· πŸͺ’ loose-ends Β· πŸ–₯️ bash Β· πŸ’¬ matrix Β· πŸ“¦ request_* Β· ⏱️ schedule Β· πŸ”§ default) | stream-json | +| `.tool-use` `
` | `πŸ’Ύ/✏️ Write/Edit Β· +N` (no `β†’`) | cyan, body is +/- diff | `renderRichToolUse` Write/Edit | stream-json | +| `.tool-use` `
` | `πŸ“€ send β†’ to Β· NL`, `❓ ask β†’ to`, `✍️ answer #id` | cyan, body is markdown | rich renderer for send / ask / answer | stream-json | | `.tool-use .ask-answer-inline-slot` | (sub-block under `ask β†’ operator`) | inherits row | inline answer form bound by `reconcileAskBinds` to the loose-end | rich renderer | | `.tool-result` (flat) | `← ` | muted | short `tool_result` (≀120c, non-recv) | stream-json | | `.tool-result-block` `
` | `Nl Β· headline` | muted, body is text | long generic `tool_result` | stream-json | | `.tool-result-block` `
` | `recv ← ` | muted, body is markdown | `tool_result` correlated to a prior `recv` tool_use via id | stream-json | -| `.tool-use` | `⌁ task started Β· [type]` | cyan | claude Task-tool subagent start | `renderTaskEvent` | -| `.turn-end-ok` / `.turn-end-fail` / `.tool-result` | `⌁ task βœ“/βœ—/β—Œ Β· Β· β†’ ` | green / red / muted | claude Task-tool result | `renderTaskEvent` | +| `.tool-use` | `⌁ task started Β· [type]` | cyan | claude Task-tool subagent start (dead path β€” `Task` omitted from agent allow-list) | `renderTaskEvent` | +| `.turn-end-ok` / `.turn-end-fail` / `.tool-result` | `⌁ task βœ“/βœ—/β—Œ Β· Β· β†’ ` | green / red / muted | claude Task-tool result (dead path for agents) | `renderTaskEvent` | | `.note` | `Β· ` | muted | harness chatter | `LiveEvent::Note` | | `.note.stderr` | `! stderr: ` | amber/orange | stderr lines off claude | `LiveEvent::Note` (`text` starts `stderr:`) | | `.note.op` | `Β· operator: ` | mauve italic | operator-initiated notes (/cancel, /compact, /model, new-session) | `LiveEvent::Note` (`text` starts `operator:`) | diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index cffc2085..9fa3baed 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -1486,6 +1486,39 @@ window.marked = marked; const tail = keys.length > 4 ? ' …+' + (keys.length - 4) : ''; return name + ' ' + pretty.join(' Β· ') + tail; } + // Per-tool glyph for the tool-use row prefix β€” gives each hive / MCP / + // built-in tool a distinctive icon instead of a generic wrench, so the + // scrollback is scannable at a glance. Exact tool names first, then + // MCP-server family fallbacks, then a generic wrench default. + const TOOL_ICONS = { + 'mcp__hyperhive__send': 'πŸ“€', + 'mcp__hyperhive__recv': 'πŸ“₯', + 'mcp__hyperhive__ask': '❓', + 'mcp__hyperhive__answer': '✍️', + 'mcp__hyperhive__remind': '⏰', + 'mcp__hyperhive__set_status': '🏷️', + 'mcp__hyperhive__get_loose_ends': 'πŸͺ’', + 'mcp__hyperhive__cancel_loose_end': 'βœ‚οΈ', + 'mcp__hyperhive__get_agent_meta': 'ℹ️', + 'mcp__hyperhive__request_next_turn': '⏩', + 'mcp__hyperhive__restart': '↻', + 'mcp__hyperhive__kill': '⏹️', + 'mcp__hyperhive__start': '▢️', + 'mcp__hyperhive__update': 'πŸ”„', + 'mcp__hyperhive__list_containers': 'πŸ“‹', + Read: 'πŸ“–', Write: 'πŸ’Ύ', Edit: '✏️', Glob: 'πŸ”', Grep: 'πŸ”', + }; + function toolIcon(name) { + if (TOOL_ICONS[name]) return TOOL_ICONS[name]; + if (typeof name === 'string') { + if (name.startsWith('mcp__matrix__')) return 'πŸ’¬'; + if (name.startsWith('mcp__bash__')) return 'πŸ–₯️'; + if (name.includes('schedule')) return '⏱️'; + // request_apply_commit / request_init_config / request_update_meta_inputs + if (name.startsWith('mcp__hyperhive__request_')) return 'πŸ“¦'; + } + return 'πŸ”§'; + } // Pretty-print a tool call: per-known-tool format, fallback to JSON // for unknown tools. function fmtToolUse(c) { @@ -1563,7 +1596,7 @@ window.marked = marked; // (`β†’`) β€” the disclosure marker (`β–Έ/β–Ύ`) from CSS sits in the // prefix column for every row kind, and the row's cyan colour // already signals "outbound tool". - const summary = name + ' ' + path + ' Β· ' + const summary = toolIcon(name) + ' ' + name + ' ' + path + ' Β· ' + (minus ? '-' + minus + ' ' : '') + '+' + plus; return api.detailsDiff('tool-use', summary, body); } @@ -1575,7 +1608,7 @@ window.marked = marked; const body = String(input.body || ''); const lines = body.split('\n').length; return detailsOpenMd(api, 'tool-use', - 'send β†’ ' + to + (lines > 1 ? ` Β· ${lines}L` : ''), + toolIcon(name) + ' send β†’ ' + to + (lines > 1 ? ` Β· ${lines}L` : ''), body); } if (name === 'mcp__hyperhive__ask') { @@ -1583,7 +1616,7 @@ window.marked = marked; const q = String(input.question || ''); const lines = q.split('\n').length; const d = detailsOpenMd(api, 'tool-use', - 'ask β†’ ' + to + (lines > 1 ? ` Β· ${lines}L` : ''), + toolIcon(name) + ' ask β†’ ' + to + (lines > 1 ? ` Β· ${lines}L` : ''), q); // When the ask targets the operator, mount an inline answer // slot in the live terminal β€” see docs/web-ui.md::Per-agent @@ -1613,7 +1646,7 @@ window.marked = marked; const a = String(input.answer || ''); const lines = a.split('\n').length; return detailsOpenMd(api, 'tool-use', - 'answer #' + id + (lines > 1 ? ` Β· ${lines}L` : ''), + toolIcon(name) + ' answer #' + id + (lines > 1 ? ` Β· ${lines}L` : ''), a); } // Bash task runner β€” show full command in an expandable pre block so @@ -1622,7 +1655,7 @@ window.marked = marked; if (name === 'mcp__bash__run') { const cmd = String(input.cmd || ''); const firstLine = cmd.split('\n')[0]; - const summary = 'run* $ ' + trim(firstLine.trim(), 72); + const summary = toolIcon(name) + ' run* $ ' + trim(firstLine.trim(), 72); return api.details('tool-use', summary, '$ ' + cmd); } return null; @@ -1770,12 +1803,12 @@ window.marked = marked; } else if (c.type === 'thinking') { const txt = (c.thinking || c.text || '').trim(); - api.row('thinking', txt ? 'Β· ' + txt : 'Β· thinking …'); + api.row('thinking', txt ? 'πŸ’­ ' + txt : 'πŸ’­ thinking …'); } else if (c.type === 'tool_use') { if (c.id && c.name) toolNameById.set(c.id, c.name); if (!renderRichToolUse(c, api)) { - api.row('tool-use', 'β†’ ' + fmtToolUse(c)); + api.row('tool-use', toolIcon(c.name) + ' ' + fmtToolUse(c)); } } } @@ -1846,7 +1879,7 @@ window.marked = marked; } const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail'; const row = api.row(cls, - (ev.ok ? 'βœ“' : 'βœ—') + ' turn ' + (ev.ok ? 'ok' : 'fail') + (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