diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index 6b060c76..6ad9da54 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -67,6 +67,10 @@ parent's negative pull. | `.tool-result-block.error` `
` | `Nl · headline` | red, body is text | long error `tool_result` (`is_error: true`); wrapper stripped | stream-json | | `.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` | `· ⚙ plugin install · loading…` or `✓ done` | muted | `system/plugin_install` (`status` = started/completed) | stream-json | +| `.note` | `· ⚙ commands changed · N available` (expandable list of `/name` entries) | muted | `system/commands_changed` (slash-command set updated, usually post-plugin_install) | stream-json | +| `.note` | `· ⚙ compact · ·
 tokens · ` | muted | `system/compact_boundary` (compaction completed; metadata includes pre/post token counts, duration, trigger) | stream-json |
+| `.note` | `· ⚙ ` | muted | other `system` subtypes (context_window_exceeded, etc.) | stream-json catch-all |
 | `.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:`) |
@@ -95,6 +99,19 @@ suffix, so the terminal degrades cleanly against older event shapes.
    while it's still the last one rendered (`nextElementSibling ==
    null`); any other event after it makes the next tick start a
    fresh row. Avoids a note-per-tick scrollback flood.
+1b. `system/plugin_install` → muted note `⚙ plugin install · loading…`
+   (on `started`) or `⚙ plugin install · ✓ done` (on `completed`).
+   Emitted in pairs: started fires before the plugin loads, completed
+   fires when it's ready. The `uuid` links the pair.
+1c. `system/commands_changed` → collapsible `.note` details row showing
+   the new slash-command count (`⚙ commands changed · N available`).
+   Expanding reveals each `/name` and its aliases. Fires after
+   `plugin_install` when a plugin registers new commands.
+1d. `system/compact_boundary` → muted note showing compaction summary:
+   `⚙ compact ·  · 
 tokens · `. Fields are
+   guarded individually — a missing field is silently omitted. Trigger
+   is `"manual"` (operator `/compact`) or `"auto"`.
+1e. Other `system/` subtypes → muted note `⚙ `.
 2. `subtype == "task_started" | "task_notification"` →
    `renderTaskEvent` (subagent activity gets the `⌁` glyph).
 3. `type == "assistant"` → walk `message.content[]`:
diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js
index abde53ea..013fe80b 100644
--- a/frontend/packages/agent/src/app.js
+++ b/frontend/packages/agent/src/app.js
@@ -1809,6 +1809,55 @@ window.marked = marked;
           }
           return;
         }
+        // plugin_install: claude is loading/finishing a plugin (MCP server or
+        // slash-command provider). Show the status so the operator knows when
+        // a fresh session is loading its toolset.
+        if (v.subtype === 'plugin_install') {
+          const status = v.status === 'completed' ? '✓ done'
+                       : v.status === 'started'   ? 'loading…'
+                       :                            (v.status || '?');
+          api.row('note', '⚙ plugin install · ' + status);
+          return;
+        }
+        // commands_changed: the set of available slash commands changed (usually
+        // right after plugin_install). Show the count in the summary; expand to
+        // see the full list.
+        if (v.subtype === 'commands_changed') {
+          const cmds = Array.isArray(v.commands) ? v.commands : [];
+          if (!cmds.length) {
+            api.row('note', '⚙ commands changed · (empty)');
+            return;
+          }
+          const summary = '⚙ commands changed · ' + cmds.length + ' available';
+          const body = cmds.map((c) => {
+            const aliases = c.aliases && c.aliases.length
+              ? '  [/' + c.aliases.join(', /') + ']' : '';
+            return '/' + c.name + aliases;
+          }).join('\n');
+          api.details('note', summary, body);
+          return;
+        }
+        // compact_boundary: claude completed a compaction pass. The metadata
+        // carries pre/post token counts, duration, and the trigger (manual vs
+        // auto). Show a single summary line so the operator can gauge how much
+        // context was shed.
+        if (v.subtype === 'compact_boundary') {
+          const m = v.compact_metadata || {};
+          const parts = ['⚙ compact'];
+          if (m.trigger) parts.push(m.trigger);
+          if (m.pre_tokens != null && m.post_tokens != null) {
+            const fmtTok = (n) => n >= 1_000_000 ? (n / 1_000_000).toFixed(1) + 'M'
+                                : n >= 1_000      ? Math.round(n / 1000) + 'k'
+                                :                   String(n);
+            parts.push(fmtTok(m.pre_tokens) + '→' + fmtTok(m.post_tokens) + ' tokens');
+          }
+          if (m.duration_ms != null) {
+            const ms = m.duration_ms;
+            parts.push(ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's');
+          }
+          api.row('note', parts.join(' · '));
+          return;
+        }
         // Other system subtypes (context_window_exceeded, etc.) — render a
         // muted note with the subtype label; reserve the loud orange `sys`
         // catch-all for truly unrecognised top-level types.