See the messages your teammates, named subagents, and other Claude Code sessions send you, grouped by peer with unread counts: a pane, a band above the prompt…

<img src="https://img.shields.io/badge/Claude_Code-2.1.288%2B-D97757" alt="Claude Code 2.1.288+"> <img src="https://img.shields.io/badge/type-mod-555555" alt="mod"> <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-555555" alt="License: MIT"></a> <img src="https://img.shields.io/badge/version-1.1.1-555555" alt="Version: 1.1.1">
The band above the prompt and the status line under it: 5 unread from scout, tester, and helper.
When your session works with other agents, their messages arrive in the transcript one at a time, between your own turns. It is hard to see who wrote what, what you have not read yet, and what each of your sends reported. Users report teammate text that looked like their own turns (#40166, #28627) and ask for a message inbox (#100724). High Command groups these messages by peer, says how far Claude Code vouches for each sender, and shows the reported result of each plain-text send.
/highcommand.high-command: 5 unread (retained), is the mod's own line, so another mod's band cannot hide it. It clears at zero. Both count the main inbox. The agentLanesInBand option adds the unread messages of agent lanes.suspicious mark, the time of the latest message, the peer's last known state, and the start of that message. Open a peer to read its thread, newest first: its messages to you and Claude's sends to it, each send with its reported result. Messages that were unread when you opened the thread, and messages that arrive while it is open, stay marked new until you go back to the roster.verified sender: a teammate whose message the team harness wrote under that name.local sender: another session or agent whose message Claude Code delivered. Claude Code does not report the sender's host, so a remote session's message can have this label too.unverified sender: everything else, such as a message that a mod in the sender's session wrote outside a verified team mailbox, or a delivery that Claude Code could not place. Teammates in their own split panes (tmux or iTerm2) write through a file that Claude Code cannot check, so their messages always get this label.The message text can lower a label but never raise one: a message that names a different teammate than the one that sent it is unverified sender. A peer's row and /highcommand show the weakest label among its unread messages, so a newer message cannot hide a warning.
suspicious in its thread, and on its peer's row while it is unread. The list covers some wordings that tell an agent to ignore its earlier instructions, to change the permission mode, to run a download in a shell, or to read a credential file, and long base64 blocks. The check ignores these invisible characters: Unicode format characters (category Cf, such as a zero-width space or a soft hyphen), the combining grapheme joiner U+034F, and the variation selectors U+FE00 to U+FE0F and U+E0100 to U+E01EF. It tests the text once with each of them read as a space and once with each removed, and marks the message when either test matches. Other invisible characters, look-alike letters from another alphabet, and one wording with these characters both inside a word and between two words can still avoid the mark. Other wording with the same aim gets no mark, so a message without the mark can still try to steer an agent. The mark is for you alone: it hides no text and changes nothing that Claude reads.The roster: three peers with unread counts and sender labels.
The helper thread, newest first, shows its message and finished notice.
flowchart LR
IN["Message to your session<br>session.receive"] --> CC["Other mods and Claude Code<br>handle it as usual"]
SUB["Named subagent's message<br>to your session<br>prompt.submit"] --> CC
OUT["Plain-text SendMessage<br>by Claude<br>session.send"] --> CC
CC -->|"queued text or reported result"| MEM[("Session memory: peer, sender label,<br>suspicious mark, send result")]
LIST["ListAgents results and<br>the local agent list"] -->|"states"| MEM
MEM --> BAND["Band and status line"]
MEM --> PANE["Pane: roster and threads"]
MEM --> CMD["/highcommand summary"]
session.receive. Claude Code submits it as a prompt from a peer, so High Command reads it in prompt.submit, both while the session is idle and during a turn. Task notifications, scheduled prompts, and your own Remote Control prompts are not recorded. A message that another mod consumes, or that you decline in an approval dialog, never reaches Claude, so High Command does not record it.Works with Claude Code 2.1.288 and later. Tested with Claude Code 2.1.296.
Agent teams are optional. Turn them on with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1.
Inside Claude Code:
/plugin marketplace add dot-agi/high-command
/plugin install high-command@high-command
From a shell:
claude plugin marketplace add dot-agi/high-command
claude plugin install high-command@high-command
For one session from a clone:
claude --plugin-dir ./high-command
Install from a shell with Claude Code 2.1.292 or later. The second part turns on auto-update. It runs only when the install succeeds and the marketplace entry exists:
claude plugin install high-command --marketplace dot-agi/high-command --scope user --json \
&& f="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" \
&& jq -e --arg name high-command '.extraKnownMarketplaces[$name].source' "$f" > /dev/null \
&& tmp=$(mktemp "$f.XXXXXX") \
&& { jq --arg name high-command '.extraKnownMarketplaces[$name].autoUpdate = true' "$f" > "$tmp" && mv "$tmp" "$f" || { rm -f "$tmp"; false; }; }
Claude Code 2.1.288 to 2.1.291 has no --marketplace option. Add the marketplace first, then run the auto-update part above, from f= on:
claude plugin marketplace add dot-agi/high-command --scope user --json \
&& claude plugin install high-command@high-command --scope user --json
Auto-update runs only in interactive sessions. A headless job updates at its start, before the session that uses the mod:
claude plugin update high-command@high-command --json
/reload-plugins.install again does not update. Use update.CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 in the environment of every claude command and session.settings.json with a new file with mode 600. If settings.json is a symbolic link, edit its target instead.| Command | What it does |
|---|---|
/highcommand | Prints the summary: one row per peer with its unread count, the time of its latest message, its state, and the kind of its latest message. Runs at once, even during a turn. |
/highcommand open | Opens the pane on the first page of the main inbox roster. In claude -p, it explains that no pane can draw. |
| Key | Where | Action |
|---|---|---|
| Ctrl+X, then Tab | Anywhere | Move the keyboard to the band or the pane. This is Claude Code's own focus chord. |
o | Band | Open the pane |
r | Band or pane roster | Mark every message read |
h | Band | Hide the band until a newer message arrives |
1-9 | Pane roster | Open the thread of that peer |
b | Pane thread | Back to the roster |
r | Pane thread | Mark that peer read, only while Mark read shows |
| Esc | Band or pane | Give the keyboard back to the prompt |
Release Ctrl before you press Tab, because many terminals read Ctrl+Tab as next tab. In Emacs terminals, Ctrl+X goes to Emacs: click the band, run /highcommand open, or rebind abovePrompt:focus with /keybindings.
Letter and digit keys work only while the band or the pane has the keyboard. Open in the band opens the pane but leaves the keyboard on the band, and focus passes each band button before it reaches the pane. Press Ctrl+X, then Tab, until the pane border shows focus, or click the pane, before you press a digit. Each letter and digit key also has a button you can click. The full guide is docs/usage.md.
Set options in /config, or under pluginConfigs["high-command@high-command"].options in ~/.claude/settings.json. For a --plugin-dir load, the key is high-command@inline.
| Option | Default | Effect |
|---|---|---|
agentLanesInBand | false | Adds the unread count of all agent lanes to the band and the status line, as in high-command: 3 unread (retained) · 2 unread in agent lanes. Read all in the band then also marks every agent lane read. Hide then also ends when a newer message arrives in an agent lane. Open still shows the main inbox. |
A change takes effect at once: Claude Code reloads the mod with the new values.
Claude Code and other mods also show peer messages. Each row says what the tool does and what High Command adds.
| Tool or feature | What it does | What High Command adds |
|---|---|---|
Built-in Message from rows in the transcript (cross-session messaging) | One dim preview line per message with the sender name. Ctrl+O expands it. | Messages grouped by peer, unread counts, sender labels, and the reported result of each plain-text send. |
Built-in /list-agents (/peers) (cross-session messaging) | Lists subagents, teammates, and local and remote sessions, with offline status. | The messages of each peer. When Claude runs ListAgents, the reported state shows on that peer's roster row. |
| Built-in agent panel (agent teams) | Lists teammates and subagents. Enter opens one teammate's transcript. | One inbox across teammates, named subagents, and other sessions. |
| session-orchestra | A read-only mod with session cards (working or idle state, model, effort, context bar), subagent cards, and a message list with a full-message view and a copy button. It reads transcripts and runs in a terminal. | Unread counts, sender labels, suspicious marks, and the reported result of each plain-text send. High Command reads no files, and its pane also draws in the desktop app, the VS Code panel, and the mobile app. |
| claude-crosstalk | A mod with a per-peer inbox for cross-session messages, unread counts, a status line, and a reply field that sends through SendMessage. It keeps history across restarts and --resume, rebuilt from the session journal. | Teammates and named subagents in the same inbox, sender labels, suspicious marks, and the reported result of each plain-text send. High Command sends nothing and keeps no history past the conversation. |
/clear, /resume, or /branch.shutdown_request, and sends by other mods or by agents that a mod started.suspicious mark covers a short list of wordings, and other wordings get no mark.The list below holds the limits you can hit in daily use. docs/usage.md has every limit, also the rare ones: size caps, unusual names and frames, changes by other mods, and display gaps.
/highcommand lists the counts apart from the main inbox. By default, the band, the status line, and Hide cover the main inbox only, so an unread message in an agent lane does not show there. The agentLanesInBand option adds the unread count of all agent lanes to the band and the status line. A message between your main conversation and a local agent can show twice, once in the main inbox and once in that agent's lane. The lane copy of a message from your main conversation never counts as unread. High Command does not see messages between teammates that run in their own split panes./highcommand, names its source. A roster snapshot status comes from Claude Code's local agent list. High Command reads that list when you open the pane or run /highcommand, when an agent starts, and when an agent ends a turn. Between reads, the status can be old. When two reads overlap, the older one can bring back an agent that the newer one removed. Open the pane or run /highcommand again after agent activity settles. A lifecycle observation says how an agent's last turn ended, such as turn answered or turn error. When the list does not hold an agent that ended a turn, an open pane shows that observation as the agent's status until the next read of the list. It describes a finished turn, so it does not prove that the agent still runs. A teammate in its own split pane can keep its last status after it crashes. /highcommand lists up to 100 local agents. The pane pages through all of them, 10 at a time.offline, stays on that peer's row. When the result holds only the printed listing, that name gets the state of the last such row that has one.shutdown_request, shutdown_response, or plan_approval_response object does not reach it. Neither its attempt nor its result appears in the main inbox, an agent lane, or /highcommand. Check the transcript for these sends./clear, /resume, and /branch empty it again, so a resumed conversation shows none of its earlier messages. The pane and /highcommand say when the capture window began. After these commands, they also say that earlier history was not restored.suspicious patterns in hooks/text.ts name credential files, such as .aws/credentials, only to mark a message that asks an agent to read one./highcommand open can say when no pane can draw. It also reads the clock and its own session state./clear, /resume, /branch, and the end of the session drop them./highcommand output reaches Claude as command output. It holds counts, times, states, and peer and agent names, never message text. Names show as code, so a name cannot add a link or formatting.The first eight hooks below pass their event on with next(e) and return the result unchanged. The last two answer /highcommand and draw High Command's own views.
| Event | What the hook does |
|---|---|
session.start | Registers /highcommand, starts the capture window, and sets the status line. |
classic.SessionStart, on /clear, /resume, and /branch | Starts a new capture window and sets the status line again. It adds no context, instructions, or settings. |
session.receive | For a message from a peer, records the text that Claude Code queues for Claude. |
prompt.submit, for peer prompts | Records a named subagent's <agent-message> prompt, and reads the local agent list for its sender label. It submits no prompt. |
session.send, for Claude's sends | Records each plain-text SendMessage attempt and its reported result. |
tool.call, for ListAgents | Reads the peer states in the result. |
agent.spawn | Reads the local agent list after the agent starts. |
turn.complete | When a local agent's turn ends, reads the local agent list and notes how the turn ended. |
command.run, for /highcommand | Prints the summary, or opens the pane. |
ui.render, for its own pane and the area above the prompt | Draws the pane, and the band above other mods' content. During a survey, or when the band does not fit, it leaves that area as it was. |
High Command has no hook on a permission check, so it never changes a permission decision. Its agent.spawn hook only reads the local agent list after the spawn. It never changes an agent's prompt, type, model, or permission mode, and it never refuses a spawn.
claude plugin test in an empty folder. no hooks module to load means mods can load on your machine. Then check that the mods active line in /plugin names high-command.claude plugin test says hooks modules are turned off: mods cannot load. turned off here means a setting or your organization's policy blocks them. turned off in this process means installed mods are off: start claude once with network access, then run the check again. If the message stays, installed mods are turned off remotely, and no setting on your machine turns them back on.hooks/register.tsx 562 lines1import { atom, read, update } from 'claude-code'
2import type { AgentInfo, CommandRunResult, EngineInterface, Register, SessionReceiveOrigin, SessionSendResult, TraceEntry } from 'claude-code'
3
4import type { HighCommandEntry, HighCommandLane, HighCommandThread } from '../types'
5import { agentSenderOf, isAgentMessage, parseDelivery, spawnerOf, UNNAMED, type NewEntry, type PeerReport } from './envelope'
6import {
7 appendEntries,
8 coreEvidence,
9 applyReports,
10 fitLanes,
11 hideLanes,
12 isNewerView,
13 keepsThread,
14 lanesSeen,
15 lanesUnread,
16 markOf,
17 markRead,
18 newestInboundSeq,
19 newestSeq,
20 newestUnreadableSeq,
21 outboundEntry,
22 recipientOf,
23 rosterOf,
24 threadOf,
25 unreadTotal,
26 type RosterRow,
27} from './inbox'
28import { parseListing, parseStructuredListing } from './listing'
29import { summaryText } from './summary'
30import { count, sanitize, peerKey, peerLabels, unreadText } from './text'
31import { bandTree, compactBandTree, fitsBounds, paneTree, type BandActions, type PaneActions } from './views'
32
33const DESCRIPTION = 'High Command: messages from teammates and other sessions (/highcommand open shows the pane)'
34const PANE = { id: 'high-command', title: 'High Command', focus: true } as const
35const USAGE = 'Usage: /highcommand shows the inbox summary. /highcommand open opens the High Command pane.'
36const NO_PANES =
37 'command.run: this session draws no panes (claude -p or an SDK host). Use /highcommand for the summary.'
38
39/** The origin kinds whose deliveries can hold messages from peers; `unclassified` counts only with a frame. */
40const MESSAGE_ORIGINS: ReadonlySet<string> = new Set(['peer', 'coordinator', 'peer-send-message', 'unclassified'])
41
42const ENTRIES = { plugin: 'high-command', key: 'entries' } as const
43const READ_THROUGH = { plugin: 'high-command', key: 'readThrough' } as const
44const STATUS_FROM = { plugin: 'high-command', key: 'statusFrom' } as const
45const LANES = { plugin: 'high-command', key: 'lanes' } as const
46
47const entries = atom(ENTRIES, [])
48const peers = atom({ plugin: 'high-command', key: 'peers' } as const, {})
49const readThrough = atom(READ_THROUGH, {})
50const openThread = atom({ plugin: 'high-command', key: 'openThread' } as const, null)
51const hiddenThrough = atom({ plugin: 'high-command', key: 'hiddenThrough' } as const, 0)
52const inboundThrough = atom({ plugin: 'high-command', key: 'inboundThrough' } as const, 0)
53
54const lanes = atom(LANES, {})
55const hiddenLanes = atom({ plugin: 'high-command', key: 'hiddenLanes' } as const, {})
56const selectedLane = atom({ plugin: 'high-command', key: 'selectedLane' } as const, null)
57const lanePage = atom({ plugin: 'high-command', key: 'lanePage' } as const, 0)
58const rosterPage = atom({ plugin: 'high-command', key: 'rosterPage' } as const, 0)
59const agents = atom({ plugin: 'high-command', key: 'agents' } as const, {})
60const capture = atom({ plugin: 'high-command', key: 'capture' } as const, null)
61
62function emptyLane(): HighCommandLane {
63 return { entries: [], peers: {}, readThrough: {}, openThread: null }
64}
65
66async function readLane($: EngineInterface, id: string): Promise<HighCommandLane> {
67 const all = await read($, lanes)
68 return Object.hasOwn(all, id) ? all[id]! : emptyLane()
69}
70
71/** Changes one agent lane; fitLanes keeps all lanes within the space of one saved value. */
72async function changeLane($: EngineInterface, id: string, change: (lane: HighCommandLane) => HighCommandLane): Promise<void> {
73 await update($, lanes, known => {
74 const next = new Map(Object.entries(known))
75 next.set(id, change(next.get(id) ?? emptyLane()))
76 return fitLanes(Object.fromEntries(next), id)
77 })
78}
79
80/** Changes every agent lane at once. */
81async function changeLanes($: EngineInterface, change: (lane: HighCommandLane) => HighCommandLane): Promise<void> {
82 await update($, lanes, known => fitLanes(Object.fromEntries(Object.entries(known).map(([id, lane]) => [id, change(lane)]))))
83}
84
85async function changeThread($: EngineInterface, id: string | undefined, change: (thread: HighCommandThread | null) => HighCommandThread | null): Promise<void> {
86 if (id === undefined) await update($, openThread, change)
87 else await changeLane($, id, lane => ({ ...lane, openThread: change(lane.openThread) }))
88}
89
90async function ensureCapture($: EngineInterface, at: number, reset = false): Promise<void> {
91 if (await read($, capture) !== null) return
92 await update($, capture, known => known ?? { at, reset })
93}
94
95/** Reads status without treating a repeated split-pane roster word as liveness evidence. */
96async function refreshAgents($: EngineInterface, event: string): Promise<void> {
97 try {
98 const at = await $.clock.now()
99 const list = await $.agent.list()
100 await update($, agents, known => {
101 const next = new Map(Object.entries(known).filter(([, value]) => value.at > at))
102 for (const agent of list) if (!next.has(agent.id)) next.set(agent.id, {
103 id: agent.id, name: agent.name === undefined ? undefined : peerKey(agent.name),
104 status: agent.status, at, source: 'roster snapshot',
105 lastEvent: Object.hasOwn(known, agent.id) ? known[agent.id]?.lastEvent : undefined,
106 })
107 return Object.fromEntries(next)
108 })
109 } catch (error) {
110 diagnose($, `high-command: ${event}: could not refresh local agents: ${causeOf(error)}`)
111 }
112}
113
114/** Transcript diagnostics already written since this module loaded. */
115const logged = new Set<string>()
116
117function causeOf(error: unknown): string {
118 return error instanceof Error ? error.message : String(error)
119}
120
121/** Writes a diagnostic to the transcript once per module load; a repeat goes to the debug log alone. */
122function diagnose($: EngineInterface, line: string): void {
123 if (logged.has(line)) {
124 $.ui.log(line.replace(/^high-command: /, ''), { to: 'debug' })
125 return
126 }
127 logged.add(line)
128 $.ui.log(line.replace(/^high-command: /, ''))
129}
130
131/**
132 * Pins the unread count as this plugin's status line, which no other mod's band can hide, or clears the
133 * line at zero. It runs after every change of the count; a failure is diagnosed under `event`. `withLanes`
134 * (agentLanesInBand) adds the unread count of all agent lanes, and the version of `lanes` to the view.
135 *
136 * Refreshes overlap, and a dispatch reads state as it stood at its own last write, so a refresh pins its
137 * count only for a view newer than the one behind the line (STATUS_FROM, written with ifVersion). The refresh
138 * after the last write of either value reads both final values; a refresh with an older view loses the
139 * ifVersion race, and its next reads show what stands. A pin can still show after a newer one when its
140 * answer comes late, so each refresh then writes its pin again with ifVersion: a miss means a newer pin
141 * landed, and the refresh shows that one. `isForced` pins the first view whatever the line shows: a reload
142 * may drop the line.
143 */
144async function syncStatus($: EngineInterface, event: string, isForced = false, withLanes = false): Promise<void> {
145 try {
146 for (let isFirst = true; ; isFirst = false) {
147 const list = await $.state.get(ENTRIES)
148 const marks = await $.state.get(READ_THROUGH)
149 const all = withLanes ? await $.state.get(LANES) : undefined
150 const pinned = await $.state.get(STATUS_FROM)
151 const view = { entries: list.version, marks: marks.version, ...(all === undefined ? {} : { lanes: all.version }) }
152 if (!isNewerView(view, isForced && isFirst ? undefined : pinned.value)) return
153 let pin: { entries: number; marks: number; unread: number; lanes?: number; laneUnread?: number } = {
154 ...view, unread: unreadTotal(rosterOf(list.value ?? [], {}, marks.value ?? {})),
155 ...(all === undefined ? {} : { laneUnread: lanesUnread(all.value ?? {}) }),
156 }
157 const written = await $.state.set(STATUS_FROM, pin, { ifVersion: pinned.version })
158 if (!written.isSet) continue
159 for (let version = written.version; ; ) {
160 $.ui.status(unreadText(pin.unread, pin.laneUnread) || undefined)
161 if ((await $.state.set(STATUS_FROM, pin, { ifVersion: version })).isSet) return
162 const newer = await $.state.get(STATUS_FROM)
163 if (newer.value === undefined) return
164 pin = newer.value
165 version = newer.version
166 }
167 }
168 } catch (error) {
169 diagnose($, `high-command: ${event}: could not update the status line: ${causeOf(error)}`)
170 }
171}
172
173/** The stored entries and the roster built from them, as `/highcommand` and the pane read them. */
174async function readRoster($: EngineInterface, laneId?: string): Promise<{ list: HighCommandEntry[]; rows: RosterRow[] }> {
175 if (laneId !== undefined) {
176 const lane = await readLane($, laneId)
177 return { list: lane.entries, rows: rosterOf(lane.entries, lane.peers, lane.readThrough) }
178 }
179 const list = await read($, entries)
180 return { list, rows: rosterOf(list, await read($, peers), await read($, readThrough)) }
181}
182
183/**
184 * Appends fresh entries. The open thread closes when this append drops its peer from the window, so the pane
185 * shows the roster and stays there when that peer writes again. The check runs inside update, so it judges
186 * the thread open now, and keepsThread keeps a thread opened after this append. `inboundThrough`, and a lane's
187 * own `inboundThrough`, keep the newest inbound seq after its entry leaves the window, so a Hide that it outdates
188 * stays ended.
189 */
190async function append($: EngineInterface, fresh: readonly NewEntry[], at: number, laneId?: string): Promise<void> {
191 if (laneId !== undefined) {
192 await changeLane($, laneId, lane => {
193 const list = appendEntries(lane.entries, fresh, at)
194 return {
195 ...lane, entries: list, inboundThrough: Math.max(lane.inboundThrough ?? 0, newestUnreadableSeq(list)),
196 openThread: lane.openThread === null || keepsThread(lane.openThread, lane.entries, list) ? lane.openThread : null,
197 }
198 })
199 return
200 }
201 let before: readonly HighCommandEntry[] = []
202 const list = await update($, entries, known => {
203 before = known
204 return appendEntries(known, fresh, at)
205 })
206 await update($, openThread, open => (open === null || keepsThread(open, before, list) ? open : null))
207 await update($, inboundThrough, seq => Math.max(seq, newestInboundSeq(list)))
208}
209
210async function saveReports($: EngineInterface, reports: readonly PeerReport[], at: number, laneId?: string): Promise<void> {
211 if (laneId === undefined) await update($, peers, known => applyReports(known, reports, at))
212 else await changeLane($, laneId, lane => ({ ...lane, peers: applyReports(lane.peers, reports, at) }))
213}
214
215/**
216 * Records the entries and peer states of one queued delivery, and says in the debug log when it names no
217 * sender. `event` is the hook that saw it: session.receive, or prompt.submit for a local agent's message.
218 * `sender`, when given, files all of the delivery under that name, whatever names its text claims.
219 */
220async function recordDelivery($: EngineInterface, event: string, text: string, origin: SessionReceiveOrigin, at: number, laneId?: string, sender?: string): Promise<void> {
221 const parsed = parseDelivery(text, origin, sender)
222 if (parsed.incomplete) diagnose($, `high-command: ${event}: incomplete frame parsing; consult the transcript for original framing`)
223 if ([...parsed.entries, ...parsed.reports].some(item => item.peer === UNNAMED)) {
224 $.ui.log(`${event}: a ${origin.kind} delivery named no sender; it shows as "${UNNAMED}"`, { to: 'debug' })
225 }
226 if (parsed.entries.length === 0 && parsed.reports.length === 0) return
227 // In an agent lane, a coordinator delivery is the main conversation's own message to that agent: never unread.
228 const fresh = laneId !== undefined && origin.kind === 'coordinator' ? parsed.entries.map(entry => ({ ...entry, fromMain: true as const })) : parsed.entries
229 if (fresh.length > 0) await append($, fresh, at, laneId)
230 if (parsed.reports.length > 0) await saveReports($, parsed.reports, at, laneId)
231}
232
233/** The local agents for a subagent message's sender label; a failure keeps the label and goes to the debug log. */
234async function agentsForLabel($: EngineInterface): Promise<readonly AgentInfo[]> {
235 try {
236 return await $.agent.list()
237 } catch (error) {
238 $.ui.log(`prompt.submit: could not read local agents: ${causeOf(error)}; the message keeps the local sender label`, { to: 'debug' })
239 return []
240 }
241}
242
243/** Records the observed send attempt and its result; an attempt to an agent with no name is left out. */
244async function recordSend($: EngineInterface, to: string, text: string, sent: SessionSendResult, trace: readonly TraceEntry<'session.send'>[], at: number, laneId?: string): Promise<void> {
245 const lane = laneId === undefined ? undefined : await readLane($, laneId)
246 const peer = recipientOf(to, lane?.peers ?? await read($, peers), await $.agent.list(), lane?.entries ?? await read($, entries))
247 if (peer === undefined) return
248 await append($, [{ ...outboundEntry(peer, text, sent), core: coreEvidence(trace) }], at, laneId)
249}
250
251/** Records the peer states a ListAgents listing names, or says in the debug log that no row gave one. */
252async function recordListing($: EngineInterface, { reports, rowCount }: ReturnType<typeof parseListing>, laneId?: string): Promise<void> {
253 reports.forEach(report => { report.source ??= 'formatted listing' })
254 if (reports.length > 0) {
255 const at = await $.clock.now()
256 await saveReports($, reports, at, laneId)
257 } else if (rowCount > 0) {
258 $.ui.log(
259 `tool.call ListAgents: no listing row gave a peer state (${count(rowCount, 'row')} read); peer states are unchanged`,
260 { to: 'debug' },
261 )
262 }
263}
264
265/** Opens the main inbox roster; answers why the pane is not drawn, or undefined when it is. */
266async function openPane($: EngineInterface): Promise<string | undefined> {
267 await refreshAgents($, 'ui.open')
268 try { await ensureCapture($, await $.clock.now()) } catch (error) { diagnose($, `high-command: ui.open: could not set capture window: ${causeOf(error)}`) }
269 await update($, selectedLane, () => '')
270 await update($, rosterPage, () => 0)
271 await update($, openThread, () => null)
272 const opened = await $.ui.open(PANE)
273 return opened.isPlaced ? undefined : `the pane waits to be drawn: ${opened.reason}`
274}
275
276/** `/highcommand open`: the pane, or the reason there is none. */
277async function openFromCommand($: EngineInterface): Promise<CommandRunResult> {
278 try {
279 if ((await $.session.surfaces()).length === 0) return { text: NO_PANES }
280 const problem = await openPane($)
281 return problem === undefined ? {} : { text: `command.run: ${problem}` }
282 } catch (error) {
283 return { text: `command.run: the pane did not open: ${causeOf(error)}` }
284 }
285}
286
287/**
288 * Runs a button's work; a failure becomes a toast that names the action. The press counts as the person's
289 * own input only while its onPress work runs, so each handler returns this promise: a `$.ui.open` after
290 * awaits is then asked, and the pane is placed at any width.
291 */
292function press($: EngineInterface, action: string, work: () => Promise<unknown>): Promise<void> {
293 return work().then(() => undefined, (error: unknown) => $.ui.toast(`ui.press: ${action}: ${causeOf(error)}`))
294}
295
296/**
297 * Marks `peer`, or every peer when none is given, read through its newest inbound entry, and refreshes the status
298 * line: for an agent lane only when the line counts agent lanes (`withLanes`).
299 */
300async function markPeersRead($: EngineInterface, peer?: string, laneId?: string, withLanes = false): Promise<void> {
301 if (laneId !== undefined) {
302 await changeLane($, laneId, lane => ({ ...lane, readThrough: markRead(lane.readThrough, lane.entries, peer) }))
303 if (withLanes) await syncStatus($, 'ui.press', false, true)
304 return
305 }
306 const list = await read($, entries)
307 await update($, readThrough, marks => markRead(marks, list, peer))
308 await syncStatus($, 'ui.press', false, withLanes)
309}
310
311/** `Read all` in the band; with agentLanesInBand (`withLanes`), it also marks every agent lane read. */
312function readAll($: EngineInterface, withLanes: boolean): Promise<void> {
313 return press($, 'read all', async () => {
314 if (withLanes) await changeLanes($, lane => ({ ...lane, readThrough: markRead(lane.readThrough, lane.entries) }))
315 await markPeersRead($, undefined, undefined, withLanes)
316 })
317}
318
319function paneActions($: EngineInterface, withLanes: boolean, laneId?: string): PaneActions {
320 /** Any change of a lane can evict lane messages (fitLanes); with agentLanesInBand, the status line then counts again. */
321 const thenSync = (work: () => Promise<unknown>) => async (): Promise<void> => {
322 await work()
323 if (laneId !== undefined && withLanes) await syncStatus($, 'ui.press', false, true)
324 }
325 return {
326 openThread: peer => press($, 'open thread', async () => {
327 const lane = laneId === undefined ? undefined : await readLane($, laneId)
328 const marks = lane?.readThrough ?? await read($, readThrough)
329 const from = newestSeq(lane?.entries ?? await read($, entries))
330 await changeThread($, laneId, () => ({ peer, since: markOf(marks, peer), from }))
331 await markPeersRead($, peer, laneId, withLanes)
332 }),
333 back: () => press($, 'back', thenSync(() => changeThread($, laneId, () => null))),
334 markRead: peer => press($, 'mark read', () => markPeersRead($, peer, laneId, withLanes)),
335 readAll: () => press($, 'read all', () => markPeersRead($, undefined, laneId, withLanes)),
336 page: page => press($, 'page', thenSync(() => changeThread($, laneId, open => open === null ? null : { ...open, page }))),
337 identity: identityPage => press($, 'identity', thenSync(() => changeThread($, laneId, open => open === null ? null : { ...open, identityPage }))),
338 reason: reasonSeq => press($, 'send details', thenSync(() => changeThread($, laneId, open => open === null ? null : { ...open, reasonSeq: open.reasonSeq === reasonSeq ? undefined : reasonSeq }))),
339 lane: id => press($, 'select lane', () => update($, selectedLane, () => id)),
340 rosterPage: page => press($, 'peer page', thenSync(async () => {
341 if (laneId === undefined) await update($, rosterPage, () => page)
342 else await changeLane($, laneId, lane => ({ ...lane, rosterPage: page }))
343 })),
344 lanePage: page => press($, 'agent page', () => update($, lanePage, () => page)),
345 }
346}
347
348/** The band's buttons; with agentLanesInBand (`withLanes`), Read all and Hide also cover agent lanes. */
349function bandActions($: EngineInterface, withLanes: boolean): BandActions {
350 return {
351 open: () =>
352 press($, 'the pane did not open', async () => {
353 const problem = await openPane($)
354 if (problem !== undefined) $.ui.toast(`ui.press: ${problem}`)
355 }),
356 readAll: () => readAll($, withLanes),
357 hide: () =>
358 press($, 'hide', async () => {
359 const through = newestSeq(await read($, entries))
360 if (withLanes) {
361 const all = await read($, lanes)
362 await update($, hiddenLanes, hidden => hideLanes(hidden, all))
363 }
364 await update($, hiddenThrough, seq => Math.max(seq, through))
365 }),
366 }
367}
368
369export const register: Register = (on, options) => {
370 /** The `agentLanesInBand` option: the band and the status line add the unread count of all agent lanes. */
371 const withLanes = options.agentLanesInBand === true
372
373 on('session.start', async ($, e, next) => {
374 try {
375 await $.command.register({ name: 'highcommand', description: DESCRIPTION, argumentHint: '[open]', immediate: true })
376 } catch (error) {
377 diagnose($, `high-command: session.start: could not register /highcommand: ${causeOf(error)}`)
378 }
379 try { await ensureCapture($, await $.clock.now()) } catch (error) { diagnose($, `high-command: session.start: could not set capture window: ${causeOf(error)}`) }
380 await syncStatus($, 'session.start', true, withLanes)
381 return next(e)
382 })
383
384 // /clear, /resume, and /branch reset $.state but leave the status line pinned.
385 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
386 try { await ensureCapture($, await $.clock.now(), true) } catch (error) { diagnose($, `high-command: classic.SessionStart: could not set capture window: ${causeOf(error)}`) }
387 await syncStatus($, 'classic.SessionStart', false, withLanes)
388 return next(e)
389 })
390
391 on('session.receive', async ($, e, next) => {
392 if (!MESSAGE_ORIGINS.has(e.origin.kind)) return next(e)
393 let at: number | undefined
394 try { at = await $.clock.now(); await ensureCapture($, at) } catch (error) { diagnose($, `high-command: session.receive: could not observe delivery time: ${causeOf(error)}`) }
395 const queued = await next(e)
396 if (queued.consumed !== undefined) return queued
397 try {
398 if (at !== undefined) {
399 await recordDelivery($, 'session.receive', queued.text, e.origin, at, e.agentId)
400 }
401 } catch (error) {
402 diagnose($, `high-command: session.receive: could not record a delivery (${e.origin.kind}): ${causeOf(error)}`)
403 }
404 if (e.agentId === undefined || withLanes) await syncStatus($, 'session.receive', false, withLanes)
405 return queued
406 })
407
408 // In Claude Code 2.1.296 a named subagent's SendMessage to the main conversation skips session.receive:
409 // Claude Code submits its <agent-message> as a peer prompt, while the session is idle or into a running
410 // turn. A message from another session passed session.receive first, so only an agent message counts here.
411 // A mod beneath can rewrite it without the frame; the text that entered is then filed under the one sender
412 // that the frames named, or under `unnamed peer`, and a name that the rewritten text claims does not count.
413 // The prompt's origin does not say whether a mod started the sender, so the local agent list can lower its label;
414 // the list is read before the mods beneath run, while an agent that ends meanwhile is still listed.
415 on('prompt.submit', { origin: { kind: 'peer' } }, async ($, e, next) => {
416 if (!isAgentMessage(e.text)) return next(e)
417 let at: number | undefined
418 try { at = await $.clock.now(); await ensureCapture($, at) } catch (error) { diagnose($, `high-command: prompt.submit: could not observe delivery time: ${causeOf(error)}`) }
419 const named = agentSenderOf(e.text, e.origin)
420 const listed = named === undefined ? undefined : agentsForLabel($)
421 const entered = await next(e)
422 if (entered.drop !== undefined) return entered
423 try {
424 if (at !== undefined) {
425 let sender: string | undefined
426 if (!isAgentMessage(entered.text)) {
427 sender = named
428 const filing = sender === undefined ? `its frames named no single sender, so it is filed under "${UNNAMED}"` : 'it is filed under the sender that the frame named'
429 $.ui.log(`prompt.submit: a mod rewrote a subagent message without its <agent-message> frame; ${filing}`, { to: 'debug' })
430 sender ??= UNNAMED
431 }
432 const plugin = named === undefined || listed === undefined ? undefined : spawnerOf(await listed, named)
433 const origin = plugin === undefined ? e.origin : { kind: 'peer' as const, plugin }
434 await recordDelivery($, 'prompt.submit', entered.text, origin, at, undefined, sender)
435 }
436 } catch (error) {
437 diagnose($, `high-command: prompt.submit: could not record a delivery (${e.origin.kind}): ${causeOf(error)}`)
438 }
439 await syncStatus($, 'prompt.submit', false, withLanes)
440 return entered
441 })
442
443 on('session.send', async ($, e, next) => {
444 if (e.origin.kind !== 'model') return next(e)
445 let at: number | undefined
446 try { at = await $.clock.now(); await ensureCapture($, at) } catch (error) { diagnose($, `high-command: session.send: could not observe send time: ${causeOf(error)}`) }
447 const sent = await next(e)
448 try {
449 if (at !== undefined) {
450 await recordSend($, e.to, e.text, sent, next.trace, at, e.agentId)
451 }
452 } catch (error) {
453 diagnose($, `high-command: session.send: could not record a message to ${sanitize(e.to)}: ${causeOf(error)}`)
454 }
455 if (e.agentId === undefined || withLanes) await syncStatus($, 'session.send', false, withLanes)
456 return sent
457 })
458
459 on('tool.call', { tool: 'ListAgents' }, async ($, e, next) => {
460 const out = await next(e)
461 const result: unknown = out.isError === true ? undefined : out.result
462 const structured = parseStructuredListing(result)
463 const listing = typeof result === 'object' && result !== null && 'listing' in result ? result.listing : undefined
464 if (structured === undefined && typeof listing !== 'string') return out
465 try {
466 if (structured !== undefined) {
467 await recordListing($, structured, e.agentId)
468 } else if (typeof listing === 'string') await recordListing($, parseListing(listing), e.agentId)
469 } catch (error) {
470 diagnose($, `high-command: tool.call ListAgents: could not record peer states: ${causeOf(error)}`)
471 }
472 // Peer states make a lane longer, so fitLanes can evict lane messages.
473 if (e.agentId !== undefined && withLanes) await syncStatus($, 'tool.call ListAgents', false, true)
474 return out
475 })
476
477 on('agent.spawn', async ($, e, next) => {
478 const result = await next(e)
479 await refreshAgents($, 'agent.spawn')
480 return result
481 })
482
483 on('turn.complete', async ($, e, next) => {
484 const result = await next(e)
485 if (e.agentId !== undefined) {
486 await refreshAgents($, 'turn.complete')
487 try {
488 const at = await $.clock.now()
489 const id = e.agentId
490 await update($, agents, known => {
491 const observed = new Map(Object.entries(known))
492 const agent = observed.get(id)
493 const word = e.reason === 'answer' ? 'turn answered' : `turn ${e.reason}`
494 if ((agent?.lastEvent?.at ?? -Infinity) <= at) observed.set(id, {
495 ...agent, id, status: agent?.status ?? word, at: agent?.at ?? at,
496 source: agent?.source ?? 'lifecycle observation', lastEvent: { word, at },
497 })
498 return Object.fromEntries(observed)
499 })
500 } catch (error) { diagnose($, `high-command: turn.complete: could not record lifecycle observation: ${causeOf(error)}`) }
501 }
502 return result
503 })
504
505 on('command.run', { command: 'highcommand' }, async ($, e) => {
506 const argument = e.args.trim()
507 if (argument === 'open') return openFromCommand($)
508 if (argument !== '') return { text: USAGE }
509 try {
510 await refreshAgents($, 'command.run')
511 const now = await $.clock.now()
512 await ensureCapture($, now)
513 const { list, rows } = await readRoster($)
514 return { text: summaryText(rows, list.length, now, {
515 evicted: list.at(-1)?.evictedTotal, capture: await read($, capture), agents: Object.values(await read($, agents)), lanes: await read($, lanes),
516 }) }
517 } catch (error) {
518 return { text: `command.run: could not build the summary: ${causeOf(error)}` }
519 }
520 })
521
522 on('ui.render', { component: 'Pane', requestId: 'high-command' }, async ($, e) => {
523 const ui = $.ui.resolve(e)
524 try {
525 const chosen = await read($, selectedLane)
526 const laneId = chosen === '' ? undefined : chosen ?? e.props.view.agentId
527 const { list, rows } = await readRoster($, laneId)
528 const thread = threadOf(list, rows, laneId === undefined ? await read($, openThread) : (await readLane($, laneId)).openThread)
529 const now = await $.clock.now()
530 const all = await read($, lanes)
531 return paneTree(ui, { rows, now, thread, laneId, rosterPage: laneId === undefined ? await read($, rosterPage) : (await readLane($, laneId)).rosterPage, lanePage: await read($, lanePage),
532 laneIds: Object.keys(all), agents: Object.values(await read($, agents)),
533 laneUnread: new Map(Object.entries(all).map(([id, lane]) => [id, unreadTotal(rosterOf(lane.entries, {}, lane.readThrough))])),
534 capture: await read($, capture), evicted: list.at(-1)?.evictedTotal ?? 0,
535 }, paneActions($, withLanes, laneId))
536 } catch (error) {
537 const { Text } = ui
538 return <Text color="red">{`high-command: ui.render (Pane): ${causeOf(error)}`}</Text>
539 }
540 })
541
542 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
543 if (e.props.hasSurvey) return next(e)
544 const below = await next(e)
545 const list = await read($, entries)
546 // A Hide holds while the person had seen every inbound entry: those in the window and those recorded before.
547 const isHidden = (await read($, hiddenThrough)) >= Math.max(await read($, inboundThrough), newestInboundSeq(list))
548 // With agentLanesInBand, it holds only while the person had also seen each agent lane's newest unread entry.
549 const all = withLanes ? await read($, lanes) : {}
550 if (isHidden && (!withLanes || lanesSeen(all, await read($, hiddenLanes)))) return below
551 const rows = rosterOf(list, {}, await read($, readThrough))
552 const waiting = rows.filter(row => row.unread > 0)
553 const laneUnread = withLanes ? lanesUnread(all) : 0
554 if (waiting.length === 0 && laneUnread === 0) return below
555 // Other mods draw `below`; when the band would push it past the engine's bounds, it yields.
556 const band = bandTree($.ui.resolve(e), waiting, bandActions($, withLanes), below, peerLabels(rows.map(row => row.peer)), laneUnread)
557 if (fitsBounds(band)) return band
558 const compact = compactBandTree($.ui.resolve(e), waiting, bandActions($, withLanes), below, laneUnread)
559 return fitsBounds(compact) ? compact : below
560 })
561}
562hooks/envelope.ts 263 lines1import type { AgentInfo, SessionReceiveOrigin } from 'claude-code'
2
3import type { HighCommandEntry, HighCommandTrust } from '../types'
4import { capBody, isSuspicious, peerKey, sanitize, summaryOf } from './text'
5
6/** An entry before the inbox numbers and stamps it. */
7export type NewEntry = Omit<HighCommandEntry, 'seq' | 'at'>
8
9/** What a frame or a listing row says about a peer. */
10export type PeerReport = { peer: string; state?: string; address?: string; source?: string }
11
12/** The peer a delivery that names no sender is filed under. */
13export const UNNAMED = 'unnamed peer'
14
15/** One frame of a delivery; `name` is the tag, absent for text kept from beside the frames. */
16type Frame = { name?: string; attributes: ReadonlyMap<string, string>; body: string }
17type Protocol = { type: string } & Record<string, unknown>
18
19const FRAMING = /^\s*Another Claude session sent a message:\s*/
20/** The start of the notice that the engine writes on a line of its own after a delivery's text. */
21const NOTICE = '\nThis came from another Claude session'
22/** Decodes the XML entities attribute values use; `&` last, so `&lt;` stays `<`. */
23function decode(value: string): string {
24 return value
25 .replaceAll('"', '"')
26 .replaceAll(''', "'")
27 .replaceAll('<', '<')
28 .replaceAll('>', '>')
29 .replaceAll('&', '&')
30}
31
32/** Tokenizes once from left to right; malformed names never trigger suffix searches. */
33function attributesOf(text: string): { attributes: Map<string, string>; malformed: boolean } {
34 const attributes = new Map<string, string>()
35 let at = 0
36 let malformed = false
37 while (at < text.length) {
38 if (/\s/.test(text[at]!)) { at++; continue }
39 const start = at
40 while (at < text.length && /[\w-]/.test(text[at]!)) at++
41 const name = text.slice(start, at)
42 if (name === '' || text[at] !== '=' || text[at + 1] !== '"') {
43 malformed = true
44 if (at === start) at++
45 continue
46 }
47 at += 2
48 const valueAt = at
49 while (at < text.length && text[at] !== '"') at++
50 if (at === text.length) { malformed = true; break }
51 const value = text.slice(valueAt, at++)
52 if (value.trim() !== '') attributes.set(name, decode(value))
53 }
54 return { attributes, malformed }
55}
56
57/** Finds a frame's closing tag, ignoring literals inside a JSON object's strings. */
58function closingOf(text: string, start: number, closing: string): number {
59 let at = start
60 while (at < text.length && /\s/.test(text[at]!)) at += 1
61 if (text[at] !== '{') return text.indexOf(closing, at)
62 let depth = 0
63 let quoted = false
64 let escaped = false
65 for (; at < text.length; at += 1) {
66 const char = text[at]
67 if (quoted) {
68 // A raw line feed or tab cannot occur in a JSON string; keep malformed text as a message.
69 if (char === '\n' || char === '\t') return text.indexOf(closing, at)
70 if (escaped) escaped = false
71 else if (char === '\\') escaped = true
72 else if (char === '"') quoted = false
73 } else if (text.startsWith(closing, at)) {
74 return at
75 } else if (char === '"') {
76 quoted = true
77 } else if (char === '{') {
78 depth += 1
79 } else if (char === '}') {
80 depth -= 1
81 if (depth === 0) return text.indexOf(closing, at + 1)
82 }
83 }
84 return -1
85}
86
87/**
88 * Finds complete frames in one forward pass, skipping closing-tag literals in JSON strings.
89 * A name with no closing tag left is not searched for again, keeping the scan linear.
90 */
91function framesOf(text: string): { frames: Frame[]; incomplete: boolean } {
92 const names = ['teammate-message', 'agent-message', 'cross-session-message']
93 const unclosed = new Set<string>()
94 const frames: Frame[] = []
95 let at = 0
96 let span = 0
97 let incomplete = false
98 const preserve = (end: number, tail = false): void => {
99 const raw = text.slice(span, end)
100 const body = (tail ? unframed(raw) : raw).trim()
101 if (body !== '') {
102 frames.push({ attributes: new Map(), body })
103 incomplete = true
104 }
105 }
106 while (at < text.length) {
107 const opening = text.indexOf('<', at)
108 if (opening < 0) break
109 at = opening + 1
110 const name = names.find(value => text.startsWith(value, at) && /[\s>]/.test(text[at + value.length] ?? ''))
111 if (name === undefined || unclosed.has(name)) continue
112 const attributesAt = at + name.length
113 at = attributesAt
114 while (at < text.length && text[at] !== '>' && text[at] !== '<') at++
115 if (text[at] !== '>') { incomplete = true; continue }
116 const start = at + 1
117 const closing = `</${name}>`
118 const end = closingOf(text, start, closing)
119 if (end === -1) { incomplete = true; unclosed.add(name); continue }
120 preserve(opening)
121 const parsed = attributesOf(text.slice(attributesAt, at))
122 incomplete ||= parsed.malformed
123 frames.push({ name, attributes: parsed.attributes, body: text.slice(start, end).trim() })
124 at = end + closing.length
125 span = at
126 }
127 if (frames.length > 0) preserve(text.length, true)
128 return { frames, incomplete }
129}
130
131/** The text of a delivery that holds no frame, cut before the engine's last notice; the peer wrote only the text before it. */
132function unframed(text: string): string {
133 const at = `\n${text}`.lastIndexOf(NOTICE)
134 return (at === -1 ? text : text.slice(0, at)).trim()
135}
136
137/** The JSON object a protocol body holds, or undefined for any other body. */
138function protocolOf(body: string): Protocol | undefined {
139 if (!body.startsWith('{')) return undefined
140 try {
141 // JSON text that starts with "{" parses to an object or throws.
142 const value = JSON.parse(body) as Record<string, unknown>
143 return typeof value.type === 'string' ? (value as Protocol) : undefined
144 } catch {
145 // A body that starts with "{" and is not JSON is a plain message.
146 return undefined
147 }
148}
149
150/** A sanitized string field with a non-space character, else undefined. */
151function textField(value: unknown): string | undefined {
152 const text = typeof value === 'string' ? sanitize(value) : ''
153 return text.trim() === '' ? undefined : text
154}
155
156/**
157 * The first claim that is a name after peerKey: the frame, the team origin's name (`stamped`), the protocol
158 * body; else the origin's default. Every other claim is text of this frame, so the work stays linear in the
159 * delivery.
160 */
161function senderOf(frame: Frame, protocol: Protocol | undefined, origin: SessionReceiveOrigin, stamped: string): string {
162 const from = protocol?.from
163 const claims = [
164 peerKey(frame.attributes.get('teammate_id') ?? ''),
165 peerKey(frame.attributes.get('from-name') ?? ''),
166 peerKey(frame.attributes.get('from') ?? ''),
167 stamped,
168 typeof from === 'string' ? peerKey(from) : '',
169 ]
170 return claims.find(name => name !== '') ?? (origin.kind === 'coordinator' ? 'coordinator' : UNNAMED)
171}
172
173/**
174 * The trust level of one sender name, from origin fields alone. A team mailbox entry is verified only
175 * when the harness checked it and the frame names that same member; another session's or agent's
176 * message that Claude Code delivered is local unless a plugin composed it; the rest is unverified.
177 */
178function trustOf(origin: SessionReceiveOrigin, peer: string, stamped: string): HighCommandTrust {
179 if ('teammate' in origin) return origin.isVerified && peer === stamped ? 'verified' : 'unverified'
180 const isPluginClaim = 'plugin' in origin && origin.plugin !== undefined
181 return isPluginClaim || origin.kind === 'unclassified' ? 'unverified' : 'local'
182}
183
184/** The state an idle notification reports. */
185function idleState(protocol: Protocol): string {
186 if (textField(protocol.failureReason) !== undefined || protocol.idleReason === 'failed') return 'failed'
187 return protocol.idleReason === 'interrupted' ? 'interrupted' : 'idle'
188}
189
190/**
191 * Whether a peer prompt is a local agent's SendMessage to the main conversation: it holds an
192 * `<agent-message>` frame and no frame of another kind. Claude Code 2.1.296 submits such a message as a
193 * prompt and skips session.receive; a message from another session passes session.receive first.
194 */
195export function isAgentMessage(text: string): boolean {
196 const { frames } = framesOf(sanitize(text).replace(FRAMING, ''))
197 const names = new Set(frames.flatMap(frame => (frame.name === undefined ? [] : [frame.name])))
198 return names.size === 1 && names.has('agent-message')
199}
200
201/**
202 * The one sender that the `<agent-message>` frames of a prompt name, or undefined when they name none or
203 * several. A mod beneath can rewrite such a prompt without its frame; the text that enters keeps this name.
204 */
205export function agentSenderOf(text: string, origin: SessionReceiveOrigin): string | undefined {
206 const { frames } = framesOf(sanitize(text).replace(FRAMING, ''))
207 const names = new Set(frames.flatMap(frame =>
208 frame.name === 'agent-message' ? [senderOf(frame, protocolOf(frame.body), origin, '')] : []))
209 const [name] = names
210 return names.size === 1 && name !== UNNAMED ? name : undefined
211}
212
213/**
214 * The mod that started the local agents whose id or name is `sender`, when a mod started every one of them;
215 * undefined when none is listed or the model or the person started one. Claude Code does not pass a mod on with a
216 * subagent's peer prompt, so this can only lower the label of a message from that sender.
217 */
218export function spawnerOf(agents: readonly AgentInfo[], sender: string): string | undefined {
219 const matches = agents.filter(agent => agent.id === sender || peerKey(agent.name ?? '') === sender)
220 return matches.length > 0 && matches.every(agent => agent.spawnedBy !== undefined) ? matches[0]!.spawnedBy : undefined
221}
222
223/**
224 * Splits one queued delivery into entries and peer reports. When `sender` is given, every frame and text of
225 * the delivery is filed under it, and the names that they claim are ignored.
226 */
227export function parseDelivery(text: string, origin: SessionReceiveOrigin, sender?: string): { entries: NewEntry[]; reports: PeerReport[]; incomplete?: boolean } {
228 const clean = sanitize(text).replace(FRAMING, '')
229 const { frames, incomplete } = framesOf(clean)
230 if (frames.length === 0 && origin.kind === 'unclassified') return { entries: [], reports: [], ...(incomplete ? { incomplete: true } : {}) }
231 const entries: NewEntry[] = []
232 const reports: PeerReport[] = []
233 // The origin's name is the same for every frame, so it is keyed once.
234 const stamped = 'teammate' in origin ? peerKey(origin.teammate) : ''
235 for (const frame of frames.length > 0 ? frames : [{ attributes: new Map<string, string>(), body: unframed(clean) }]) {
236 const protocol = protocolOf(frame.body)
237 const peer = sender ?? senderOf(frame, protocol, origin, stamped)
238 const trust = trustOf(origin, peer, stamped)
239 const add = (kind: NewEntry['kind'], preferred: string | undefined, body: string): void => {
240 const mark = isSuspicious(`${preferred ?? ''}\n${body}`) ? { isSuspicious: true as const } : {}
241 entries.push({ peer, direction: 'in', kind, summary: summaryOf(preferred, body), ...capBody(body), trust, ...mark })
242 }
243 const address = frame.attributes.has('from-name') ? frame.attributes.get('from') : undefined
244 if (address !== undefined) reports.push({ peer, address })
245 if (protocol === undefined) {
246 add('message', frame.attributes.get('summary'), frame.body)
247 } else if (protocol.type === 'idle_notification') {
248 const failure = textField(protocol.failureReason)
249 const result = textField(protocol.result)
250 reports.push({ peer, state: idleState(protocol) })
251 if (failure !== undefined) add('failure', undefined, result === undefined ? failure : `${failure}\n\n${result}`)
252 else if (result !== undefined) add('result', textField(protocol.summary), result)
253 } else if (protocol.type === 'teammate_terminated') {
254 reports.push({ peer, state: 'ended' })
255 } else if (protocol.type === 'shutdown_approved') {
256 reports.push({ peer, state: 'shut down' })
257 } else {
258 add('message', `[${sanitize(protocol.type)}]`, frame.body)
259 }
260 }
261 return { entries, reports, ...(incomplete ? { incomplete: true } : {}) }
262}
263hooks/inbox.ts 373 lines1import type { AgentInfo, SessionSendResult, TraceEntry } from 'claude-code'
2
3import type { HighCommandEntry, HighCommandLane, HighCommandPeer, HighCommandState, HighCommandThread, HighCommandTrust } from '../types'
4import type { NewEntry, PeerReport } from './envelope'
5import { LIMITS } from './limits'
6import { capBody, lineOf, peerKey, sanitize, summaryOf } from './text'
7
8type Peers = Record<string, HighCommandPeer>
9
10/** Per peer, the highest inbound seq marked read. */
11type ReadMarks = Record<string, number>
12
13/** The trust levels, the weakest first. */
14const LEVELS: readonly HighCommandTrust[] = ['unverified', 'local', 'verified']
15
16/** The six hexadecimal characters appended to a recipient name in ListAgents output. */
17const RECIPIENT_REF = / \[[0-9a-f]{6}\]$/i
18
19/**
20 * A peer with entries: its unread count, its newest entry, its last reported state, and the trust level and
21 * suspicious mark its row shows (no trust level when it only received sends).
22 */
23export type RosterRow = {
24 peer: string
25 unread: number
26 last: HighCommandEntry
27 state?: HighCommandState
28 trust?: HighCommandTrust
29 isSuspicious: boolean
30}
31
32/** The open thread as the pane draws it: the newest entries first, the count of older ones, and the count of new ones. */
33export type Thread = { row: RosterRow; since: number; shown: HighCommandEntry[]; hidden: number; fresh: number; page: number; pages: number; identityPage?: number; reasonSeq?: number }
34
35/** The seq of the newest entry, or 0 for an empty list. The window never drops its newest entry. */
36export function newestSeq(list: readonly HighCommandEntry[]): number {
37 return list.at(-1)?.seq ?? 0
38}
39
40/** The seq of the newest inbound entry, or 0 without one. */
41export function newestInboundSeq(list: readonly HighCommandEntry[]): number {
42 return list.findLast(entry => entry.direction === 'in')?.seq ?? 0
43}
44
45/**
46 * Numbers and stamps fresh entries; keeps each peer's newest LIMITS.peerEntries entries and the newest
47 * LIMITS.entries in all, so a busy peer drops its own oldest entries first.
48 */
49export function appendEntries(list: readonly HighCommandEntry[], fresh: readonly NewEntry[], at: number): HighCommandEntry[] {
50 const newest = newestSeq(list)
51 const all = [...list, ...fresh.map((entry, index) => ({ ...entry, seq: newest + index + 1, at }))]
52 const held = new Map<string, number>()
53 const kept = all.reverse().filter(entry => {
54 const count = held.get(entry.peer) ?? 0
55 held.set(entry.peer, count + 1)
56 return count < LIMITS.peerEntries
57 })
58 const result = kept.slice(0, LIMITS.entries).reverse()
59 const last = result.at(-1)
60 if (last !== undefined) result[result.length - 1] = {
61 ...last, evictedTotal: (list.at(-1)?.evictedTotal ?? 0) + all.length - result.length,
62 }
63 return result
64}
65
66/**
67 * A record's own value under a peer name. Names come from senders, so `constructor` must not read
68 * Object.prototype; records are written through Map and Object.fromEntries, so `__proto__` is a plain key.
69 */
70function own<T>(record: Readonly<Record<string, T>>, peer: string): T | undefined {
71 return Object.hasOwn(record, peer) ? record[peer] : undefined
72}
73
74/** The peer's read mark: the highest inbound seq marked read, or 0 before any. */
75export function markOf(marks: Readonly<ReadMarks>, peer: string): number {
76 return own(marks, peer) ?? 0
77}
78
79/** String addresses can survive a hot reload; seenAt is their only saved timestamp. */
80function addressOf(peer: HighCommandPeer): HighCommandPeer['address'] {
81 return typeof peer.address === 'string' ? { value: peer.address, at: peer.seenAt } : peer.address
82}
83
84/**
85 * Applies peer reports and keeps the LIMITS.peers peers reported most recently. Deliveries can finish
86 * recording out of order, so a state or an address older than the stored one leaves it, and `seenAt` never
87 * moves back. Address observations retain their application order to break timestamp ties.
88 */
89export function applyReports(peers: Readonly<Peers>, reports: readonly PeerReport[], at: number): Peers {
90 const isCurrent = (stored: { at: number } | undefined): boolean => stored === undefined || stored.at <= at
91 const next = new Map(Object.entries(peers).map(([name, peer]) => [
92 name, typeof peer.address === 'string' ? { ...peer, address: addressOf(peer) } : peer,
93 ]))
94 let order = 0
95 for (const peer of next.values()) order = Math.max(order, peer.address?.order ?? 0)
96 for (const { peer, state, address, source } of reports) {
97 const known = next.get(peer)
98 next.set(peer, {
99 ...known,
100 seenAt: Math.max(known?.seenAt ?? at, at),
101 ...(state !== undefined && isCurrent(known?.state) ? { state: { word: state, at, source: source ?? 'peer notice' } } : {}),
102 ...(address !== undefined && isCurrent(known?.address) ? { address: { value: address, at, order: ++order } } : {}),
103 })
104 }
105 const all = [...next]
106 return Object.fromEntries(all.length <= LIMITS.peers ? all : all.sort(([, a], [, b]) => b.seenAt - a.seenAt).slice(0, LIMITS.peers))
107}
108
109/**
110 * The key a send to a listed agent files under: its name, so the send joins that agent's messages to the main
111 * conversation, or its name and ID when the name is empty after peerKey, another listed agent has that name, or a
112 * peer of that name has an address (another session).
113 */
114function agentRecipient(agent: AgentInfo, agents: readonly AgentInfo[], peers: Readonly<Peers>): string {
115 if (agent.name === undefined) return ''
116 const name = peerKey(agent.name)
117 const isShared = name === '' || own(peers, name)?.address !== undefined ||
118 agents.some(other => other.id !== agent.id && peerKey(other.name ?? '') === name)
119 return isShared ? peerKey(`${agent.name} [${agent.id}]`) : name
120}
121
122/**
123 * The peer a send goes to: the peer whose newest address report matches `to`, else this session's agent whose
124 * id is `to` (agentRecipient), else the name `to` gives; undefined when that is no name.
125 * Equal address timestamps use observation order. Known names keep their brackets; an unknown recipient
126 * can carry one trailing listing reference.
127 */
128export function recipientOf(to: string, peers: Readonly<Peers>, agents: readonly AgentInfo[], entries: readonly HighCommandEntry[] = []): string | undefined {
129 let addressed: { name: string; at: number; order: number } | undefined
130 for (const [name, peer] of Object.entries(peers)) {
131 const address = addressOf(peer)
132 if (address?.value === to && (addressed === undefined || address.at > addressed.at ||
133 (address.at === addressed.at && (address.order ?? 0) > addressed.order))) {
134 addressed = { name, at: address.at, order: address.order ?? 0 }
135 }
136 }
137 if (addressed !== undefined) return addressed.name
138 const agent = agents.find(candidate => candidate.id === to)
139 let name = agent === undefined ? peerKey(to) : agentRecipient(agent, agents, peers)
140 const isKnown = Object.hasOwn(peers, name) || entries.some(entry => entry.peer === name) ||
141 agents.some(candidate => peerKey(candidate.name ?? '') === name)
142 if (agent === undefined && !isKnown) {
143 name = name.replace(RECIPIENT_REF, '')
144 }
145 return name === '' ? undefined : name
146}
147
148/** The observed send attempt, its reported outcome, and a bounded detailed refusal. */
149export function outboundEntry(peer: string, text: string, sent: SessionSendResult): NewEntry {
150 const body = sanitize(text)
151 return {
152 peer,
153 direction: 'out',
154 kind: 'message',
155 summary: summaryOf(undefined, body),
156 ...capBody(body),
157 ...(!sent.isDelivered ? { refusal: capBody(sanitize(sent.reason)) } : {}),
158 delivery: sent.isDelivered ? { isDelivered: true } : { isDelivered: false, reason: lineOf(sanitize(sent.reason)) },
159 }
160}
161
162/** An inbound entry newer than `mark`. Outbound entries and lane copies of the main conversation's sends are never new. */
163export function isNewer(entry: HighCommandEntry, mark: number): boolean {
164 return entry.direction === 'in' && entry.fromMain !== true && entry.seq > mark
165}
166
167/** The unread count of all rows. */
168export function unreadTotal(rows: readonly RosterRow[]): number {
169 return rows.reduce((sum, row) => sum + row.unread, 0)
170}
171
172/**
173 * One row per peer with entries: peers with unread entries first, then the newest activity first. While a peer
174 * has unread entries, its row shows the weakest trust level and any suspicious mark among them, so a newer
175 * message cannot hide a warning on one the person has not read; else it shows those of its newest inbound entry.
176 */
177export function rosterOf(entries: readonly HighCommandEntry[], peers: Readonly<Peers>, marks: Readonly<ReadMarks>): RosterRow[] {
178 const rows = new Map<string, RosterRow>()
179 for (const entry of entries) {
180 const row = rows.get(entry.peer) ?? { peer: entry.peer, unread: 0, last: entry, state: own(peers, entry.peer)?.state, isSuspicious: false }
181 if (entry.at > row.last.at || (entry.at === row.last.at && entry.seq > row.last.seq)) row.last = entry
182 if (entry.direction === 'in') {
183 const isUnread = isNewer(entry, markOf(marks, entry.peer))
184 // A peer's read entries come before its unread ones, so its first unread entry starts the fold. A lane copy of
185 // the main conversation's send is never unread and can follow unread entries; it leaves their fold alone.
186 if (isUnread || row.unread === 0) {
187 const isFolded = isUnread && row.unread > 0
188 row.trust = isFolded ? LEVELS.find(level => level === row.trust || level === entry.trust) : entry.trust
189 row.isSuspicious = (isFolded && row.isSuspicious) || entry.isSuspicious === true
190 }
191 if (isUnread) row.unread += 1
192 }
193 rows.set(entry.peer, row)
194 }
195 return [...rows.values()].sort((a, b) => Number(b.unread > 0) - Number(a.unread > 0) || b.last.at - a.last.at || b.last.seq - a.last.seq)
196}
197
198/** The unread count of all agent lanes. */
199export function lanesUnread(lanes: Readonly<Record<string, HighCommandLane>>): number {
200 return Object.values(lanes).reduce((sum, lane) => sum + unreadTotal(rosterOf(lane.entries, {}, lane.readThrough)), 0)
201}
202
203/** Per lane, the newest seq a press of Hide saw in `lanes`. A mark never moves back. */
204export function hideLanes(hidden: Readonly<Record<string, number>>, lanes: Readonly<Record<string, HighCommandLane>>): Record<string, number> {
205 const next = new Map(Object.entries(hidden))
206 for (const [id, lane] of Object.entries(lanes)) next.set(id, Math.max(next.get(id) ?? 0, newestSeq(lane.entries)))
207 return Object.fromEntries(next)
208}
209
210/** The seq of the newest lane entry that can be unread (isNewer), or 0 without one. */
211export function newestUnreadableSeq(list: readonly HighCommandEntry[]): number {
212 return list.findLast(entry => isNewer(entry, 0))?.seq ?? 0
213}
214
215/**
216 * Whether a Hide saw every lane's newest entry that can be unread, also one that left its lane since: the lane's
217 * `inboundThrough` keeps its seq, so an eviction cannot bring back a Hide that the entry ended.
218 */
219export function lanesSeen(lanes: Readonly<Record<string, HighCommandLane>>, hidden: Readonly<Record<string, number>>): boolean {
220 return Object.entries(lanes).every(([id, lane]) => Math.max(lane.inboundThrough ?? 0, newestUnreadableSeq(lane.entries)) <= markOf(hidden, id))
221}
222
223/**
224 * The versions of `entries` and `readThrough` that a status line count comes from, and the version of `lanes` when
225 * the line also counts agent lanes.
226 */
227type StatusView = { entries: number; marks: number; lanes?: number }
228
229/**
230 * Whether a status refresh may pin its count: its view is older in no value than the view behind the line and
231 * newer in one, the line has no view yet, or the line counts agent lanes and the refresh does not, or the reverse.
232 */
233export function isNewerView(view: StatusView, shown: StatusView | undefined): boolean {
234 if (shown === undefined || (view.lanes === undefined) !== (shown.lanes === undefined)) return true
235 const isOlder = view.entries < shown.entries || view.marks < shown.marks || (view.lanes ?? 0) < (shown.lanes ?? 0)
236 return !isOlder && (view.entries > shown.entries || view.marks > shown.marks || (view.lanes ?? 0) > (shown.lanes ?? 0))
237}
238
239/**
240 * Whether an append keeps the open thread: the person opened it after the append, or its peer
241 * has entries before and after it. So the thread on a peer the append drops closes, and so does a thread
242 * opened from an older view on a peer already gone, when that peer writes again.
243 */
244export function keepsThread(open: HighCommandThread, before: readonly HighCommandEntry[], after: readonly HighCommandEntry[]): boolean {
245 const has = (list: readonly HighCommandEntry[]): boolean => list.some(entry => entry.peer === open.peer)
246 return open.from >= newestSeq(after) || (has(before) && has(after))
247}
248
249/** The characters of `value` as JSON text: the measure that Claude Code holds a saved value to. */
250function jsonChars(value: unknown): number {
251 return JSON.stringify(value).length
252}
253
254/**
255 * The lane without its `drop` oldest entries, where `drop` is less than its entry count. Its newest entry stays and
256 * counts them as evicted, and the open thread closes as an append that dropped them would close it.
257 */
258function dropOldest(lane: HighCommandLane, drop: number): HighCommandLane {
259 if (drop <= 0) return lane
260 const kept = lane.entries.slice(drop)
261 const last = kept.at(-1)!
262 kept[kept.length - 1] = { ...last, evictedTotal: (last.evictedTotal ?? 0) + drop }
263 const open = lane.openThread
264 return { ...lane, entries: kept, openThread: open === null || keepsThread(open, lane.entries, kept) ? open : null }
265}
266
267/** Drops the lane's oldest entries while its entries pass LIMITS.laneChars; it keeps its newest entry. */
268function capLane(lane: HighCommandLane): HighCommandLane {
269 let size = jsonChars(lane.entries)
270 if (size <= LIMITS.laneChars) return lane
271 let drop = 0
272 while (size > LIMITS.laneChars && drop < lane.entries.length - 1) {
273 size -= jsonChars(lane.entries[drop]) + 1
274 drop += 1
275 }
276 const next = dropOldest(lane, drop)
277 // The evicted count on the newest entry adds a few characters.
278 return drop > 0 && jsonChars(next.entries) > LIMITS.laneChars ? capLane(next) : next
279}
280
281/**
282 * Fits the agent lanes in the space of one saved value. The lane `id`, the one that changed, first drops its own
283 * oldest entries past LIMITS.laneChars. Then, while all lanes pass LIMITS.lanesChars, the entries observed first
284 * drop from every lane. An entry drops only with the older entries of its lane, so it ranks by the latest observation
285 * among them. Each lane keeps its newest entry, so its seq numbers and read marks stay in step.
286 */
287export function fitLanes(lanes: Readonly<Record<string, HighCommandLane>>, id?: string): Record<string, HighCommandLane> {
288 const all = new Map(Object.entries(lanes))
289 const changed = id === undefined ? undefined : all.get(id)
290 if (changed !== undefined) all.set(id!, capLane(changed))
291 let over = jsonChars(Object.fromEntries(all)) - LIMITS.lanesChars
292 while (over > 0) {
293 const ranked: { lane: string; index: number; at: number; size: number }[] = []
294 for (const [name, lane] of all) {
295 let at = -Infinity
296 lane.entries.slice(0, -1).forEach((entry, index) => {
297 at = Math.max(at, entry.at)
298 ranked.push({ lane: name, index, at, size: jsonChars(entry) + 1 })
299 })
300 }
301 if (ranked.length === 0) break
302 ranked.sort((a, b) => a.at - b.at || a.index - b.index || (a.lane < b.lane ? -1 : a.lane > b.lane ? 1 : 0))
303 const drops = new Map<string, number>()
304 for (const { lane, index, size } of ranked) {
305 if (over <= 0) break
306 drops.set(lane, index + 1)
307 over -= size
308 }
309 for (const [name, drop] of drops) all.set(name, dropOldest(all.get(name)!, drop))
310 over = jsonChars(Object.fromEntries(all)) - LIMITS.lanesChars
311 }
312 return Object.fromEntries(all)
313}
314
315/** The thread of the open peer, or undefined when the pane draws the roster. */
316export function threadOf(entries: readonly HighCommandEntry[], rows: readonly RosterRow[], open: HighCommandThread | null): Thread | undefined {
317 const row = rows.find(candidate => candidate.peer === open?.peer)
318 if (open === null || row === undefined) return undefined
319 const all = entries.filter(entry => entry.peer === open.peer).sort((a, b) => b.at - a.at || b.seq - a.seq)
320 const groups: HighCommandEntry[][] = [[]]
321 let size = 0
322 for (const entry of all) {
323 const cost = JSON.stringify([entry.body, entry.summary]).length + 2000
324 if (groups.at(-1)!.length >= LIMITS.threadEntries || size + cost > 60_000) {
325 groups.push([])
326 size = 0
327 }
328 groups.at(-1)!.push(entry)
329 size += cost
330 }
331 const pages = groups.length
332 const page = Math.max(0, Math.min(open.page ?? 0, pages - 1))
333 const shown = groups[page]!
334 const fresh = all.filter(entry => isNewer(entry, open.since)).length
335 return { row, since: open.since, shown, hidden: all.length - shown.length, fresh, page, pages, identityPage: open.identityPage, reasonSeq: open.reasonSeq }
336}
337
338/**
339 * Marks `peer`, or every peer when none is given, read through its newest inbound entry in the window `entries`.
340 * A mark never moves back: `update` can apply an entries snapshot older than marks that another press already
341 * wrote. A mark below the window's oldest entry goes, so the marks of peers that left the window do not pile up:
342 * every entry of its peer, now or later, is newer, so it marked nothing. An older snapshot only drops less.
343 */
344export function markRead(marks: Readonly<ReadMarks>, entries: readonly HighCommandEntry[], peer?: string): ReadMarks {
345 const oldest = entries[0]?.seq ?? 0
346 const next = new Map(Object.entries(marks).filter(([, seq]) => seq >= oldest))
347 for (const entry of entries) {
348 if (entry.direction === 'in' && (peer === undefined || entry.peer === peer)) {
349 next.set(entry.peer, Math.max(next.get(entry.peer) ?? 0, entry.seq))
350 }
351 }
352 return Object.fromEntries(next)
353}
354
355/** The arrow and label for an inbound message or an observed send attempt. */
356export function labelOf(entry: HighCommandEntry): { arrow: '←' | '→'; word: string } {
357 if (entry.direction === 'out') return { arrow: '→', word: entry.delivery?.isDelivered === false ? 'send attempted (not delivered)' : 'send attempted' }
358 return { arrow: '←', word: entry.kind === 'result' ? 'finished' : entry.kind === 'failure' ? 'failed' : 'message' }
359}
360
361/** A settled core call proves only that call's queue outcome. */
362export function coreEvidence(trace: readonly TraceEntry<'session.send'>[]): HighCommandEntry['core'] {
363 const core = trace.find(item => item.plugin === 'engine' && item.tier === 'core' &&
364 (item.outcome === 'returned' || item.outcome === 'passed') && item.returned !== undefined)
365 if (core?.returned === undefined) return undefined
366 return {
367 to: sanitize(core.received.to).slice(0, LIMITS.bodyChars),
368 ...capBody(sanitize(core.received.text)),
369 isDelivered: core.returned.isDelivered,
370 ...(!core.returned.isDelivered ? { reason: lineOf(sanitize(core.returned.reason)) } : {}),
371 }
372}
373hooks/listing.ts 53 lines1import type { PeerReport } from './envelope'
2import { LIMITS } from './limits'
3import { peerKey, sanitize } from './text'
4
5/**
6 * The start of a listing row, ` name [ref]`, where the engine's `·` fields begin. The name runs to the last
7 * ` [ref]` that a `·` follows, so a session name that holds ` [ref] · ` stays whole and cannot place a field.
8 * The fields after it are split on `·`, not matched by a repeated group, so a hostile row cannot make the
9 * match backtrack across its fields.
10 */
11const HEAD = /^\s+(\S.*) \[[0-9a-z]+\](?=\s+·)/i
12/** A state word as the engine writes one: a letter, then letters, spaces, and hyphens. */
13const STATE_WORD = new RegExp(`^[a-z][a-z -]{0,${LIMITS.stateChars - 1}}$`, 'i')
14
15/** Peer states from a ListAgents listing, and how many lines looked like rows. */
16export function parseListing(listing: string): { reports: PeerReport[]; rowCount: number } {
17 const rows = sanitize(listing)
18 .split('\n')
19 .filter(line => /^\s+\S/.test(line))
20 const reports: PeerReport[] = []
21 for (const row of rows) {
22 const head = HEAD.exec(row)
23 if (head === null) continue
24 const peer = peerKey(head[1] ?? '')
25 // The text before the first `·` is the space after the ref; the fields keep their places, so an empty one stays empty.
26 const state = row.slice(head[0].length).split('·')[2]?.trim()
27 if (peer !== '' && state !== undefined && STATE_WORD.test(state)) reports.push({ peer, state })
28 }
29 return { reports, rowCount: rows.length }
30}
31
32/** Structured rows take precedence; duplicate names cannot establish one peer's state. */
33export function parseStructuredListing(result: unknown): { reports: PeerReport[]; rowCount: number } | undefined {
34 if (typeof result !== 'object' || result === null || !('sections' in result) || !Array.isArray(result.sections)) return undefined
35 const states = new Map<string, string | undefined>()
36 let rowCount = 0
37 for (const section of result.sections) {
38 if (typeof section !== 'object' || section === null || !Array.isArray(section.rows)) continue
39 for (const row of section.rows) {
40 rowCount++
41 if (typeof row !== 'object' || row === null || typeof row.name !== 'string') continue
42 const peer = peerKey(row.name)
43 if (peer === '') continue
44 const state = typeof row.status === 'string' ? sanitize(row.status).trim() : ''
45 states.set(peer, states.has(peer) || !STATE_WORD.test(state) ? undefined : state)
46 }
47 }
48 return {
49 reports: [...states].flatMap(([peer, state]) => state === undefined ? [] : [{ peer, state, source: 'structured listing' }]),
50 rowCount,
51 }
52}
53hooks/summary.ts 55 lines1import type { HighCommandAgent, HighCommandCapture, HighCommandLane } from '../types'
2import { labelOf, rosterOf, unreadTotal, type RosterRow } from './inbox'
3import { captureLabel, clockLabel, count, peerLabel, peerLabels, stateLabel, trustLabel } from './text'
4
5/** Names remain literal, including backticks and link-shaped text. */
6function literal(name: string): string {
7 const longest = Math.max(0, ...Array.from(name.matchAll(/`+/g), run => run[0].length))
8 const fence = '`'.repeat(longest + 1)
9 const pad = name.startsWith('`') || name.endsWith('`') ? ' ' : ''
10 return `${fence}${pad}${name}${pad}${fence}`
11}
12
13type SummaryContext = { evicted?: number; capture?: HighCommandCapture | null; agents?: readonly HighCommandAgent[]; lanes?: Record<string, HighCommandLane> }
14
15/** Counts cover retained messages only. Message bodies never enter command output. */
16export function summaryText(rows: readonly RosterRow[], entryCount: number, now: number, context: SummaryContext = {}): string {
17 const labels = peerLabels(rows.map(row => row.peer))
18 const useList = Array.from(labels.values()).some(name => {
19 let oddBackslashes = false
20 for (const char of name) {
21 if (char === '|' && oddBackslashes) return true
22 oddBackslashes = char === '\\' && !oddBackslashes
23 }
24 return false
25 })
26 const lines = rows.map(row => {
27 const label = trustLabel(row.trust)
28 const name = literal(labels.get(row.peer)!)
29 const peer = `${name}${label === undefined ? '' : ` (${label})`}`
30 const { arrow, word } = labelOf(row.last)
31 const fields = [String(row.unread), clockLabel(row.last.at, now), stateLabel(row.state, now) ?? '—', `${arrow} ${word}`]
32 if (useList) return `- ${peer}: ${fields[0]} new; last ${fields[1]}; state ${fields[2]}; latest ${fields[3]}`
33 const escaped = peer.replace(/\|/g, '\\|')
34 return `| ${escaped} | ${fields.join(' | ')} |`
35 })
36 const header = rows.length === 0 ? 'No messages from teammates or other sessions in this capture window.' :
37 `${unreadTotal(rows)} unread · ${count(rows.length, 'peer')} · ${count(entryCount, 'retained message')}`
38 const agents = context.agents ?? []
39 const lanes = Object.entries(context.lanes ?? {})
40 return [
41 ...(context.capture ? [captureLabel(context.capture, now)] : []),
42 header,
43 'Order: observation time; read marks and retention use recording sequence.',
44 ...((context.evicted ?? 0) > 0 ? [`History trimmed: ${count(context.evicted!, 'message')} permanently evicted; totals cover retained messages.`] : []),
45 ...(rows.length === 0 ? [] : ['', ...(useList ? [] : ['| Peer | New | Last | State | Latest |', '| --- | --- | --- | --- | --- |']), ...lines]),
46 ...(lanes.length === 0 ? [] : ['', 'Agent observations (separate counts; split-pane traffic unavailable):',
47 ...lanes.map(([id, lane]) => `- ${literal(peerLabel(id))}: ${unreadTotal(rosterOf(lane.entries, lane.peers, lane.readThrough))} unread; ${count(lane.entries.length, 'retained message')}; ${lane.entries.at(-1)?.evictedTotal ?? 0} permanently evicted`),
48 ]),
49 ...(agents.length === 0 ? [] : ['', 'Local agents (separate from peer names; split-pane liveness is unconfirmed):',
50 ...agents.slice(0, 100).map(agent => `- ${literal(peerLabel(agent.name ?? agent.id))} (${literal(peerLabel(agent.id))}): ${agent.status}; ${agent.source} at ${clockLabel(agent.at, now)}${agent.lastEvent ? `; lifecycle observation: ${agent.lastEvent.word} at ${clockLabel(agent.lastEvent.at, now)}` : ''}`),
51 ...(agents.length > 100 ? [`${agents.length - 100} more local agents; open the pane to page through them.`] : []),
52 ]),
53 ].join('\n')
54}
55hooks/text.ts 197 lines1import type { HighCommandState, HighCommandTrust } from '../types'
2import { LIMITS } from './limits'
3
4const CONTROL = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g
5const FORMAT = /\p{Cf}/gu
6/**
7 * The invisible characters that the suspicious-text check reads past: format characters (category Cf), the
8 * combining grapheme joiner U+034F, and the variation selectors U+FE00 to U+FE0F and U+E0100 to U+E01EF.
9 */
10const INVISIBLE = /[\p{Cf}\u034F\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu
11const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
12
13/**
14 * The suspicious-text list, in order: an instruction to ignore earlier instructions, to
15 * change the permission mode, to run a download in a shell (piped, or through `$(...)` or `<(...)`), or to
16 * read a credential file. Every repeat is bounded or over distinct words, so a match is linear in the text.
17 * `hasBase64Block` adds the last item of the list, a long base64 block.
18 */
19const SUSPICIOUS = [
20 /\b(?:ignore|disregard|forget)\s+(?:(?:all|any|the|your|of)\s+)*(?:(?:previous|prior|earlier|above)\s+(?:instructions?|prompts?|rules?)\b|(?:instructions?|prompts?|rules?)\s+above\b)/i,
21 /\b(?:bypassPermissions|dangerously-skip-permissions)\b|\b(?:change|switch|set)\b[^.\n]{0,30}\bpermission mode\b/i,
22 /\b(?:curl|wget)\b[^|\n]{0,200}\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b|\b(?:(?:ba|z|da|k)?sh|eval|source)\s+(?:-c\s+)?["']?(?:\$\(|<\()\s*(?:curl|wget)\b/i,
23 /\b(?:cat|read|print|send|upload|paste|share|copy|dump)\b[^\n]{0,60}(?:\.ssh\/|\bid_(?:rsa|ecdsa|ed25519)\b|\.aws\/credentials|\.netrc\b|\.git-credentials\b|\.env\b)/i,
24]
25/** Characters of one unbroken base64 run that make a long base64 block. */
26const BASE64_RUN = 200
27
28/** Removes control characters, keeping tab and line feed, so no terminal sequence reaches a drawing. */
29export function sanitize(text: string): string {
30 return text.replace(CONTROL, '')
31}
32
33/** Replaces each run of white space with one space and trims. */
34function oneLine(text: string): string {
35 return text.replace(/\s+/g, ' ').trim()
36}
37
38/** The first line that holds a non-space character, as one line; '' when there is none. */
39function firstLine(text: string): string {
40 const line = text.split('\n').find(part => part.trim() !== '')
41 return line === undefined ? '' : oneLine(line)
42}
43
44/** Keeps at most `max` UTF-16 units of a longer text, never splitting a surrogate pair. */
45function cut(text: string, max: number): string {
46 if (text.length <= max) return text
47 const kept = text.slice(0, max)
48 const last = kept.charCodeAt(max - 1)
49 return last >= 0xd800 && last <= 0xdbff ? kept.slice(0, -1) : kept
50}
51
52/** The text when it has at most `max` characters, else its start and '…'. */
53function clip(text: string, max: number): string {
54 return text.length <= max ? text : `${cut(text, max - 1)}…`
55}
56
57/** The text as one line of at most LIMITS.summaryChars characters. */
58export function lineOf(text: string): string {
59 return clip(oneLine(text), LIMITS.summaryChars)
60}
61
62/** One summary line: the preferred text, else the first line of the body, else '(empty message)'. */
63export function summaryOf(preferred: string | undefined, body: string): string {
64 return lineOf(preferred ?? '') || lineOf(firstLine(body)) || '(empty message)'
65}
66
67/** The body as stored, and its length before the cut. */
68export function capBody(text: string): { body: string; fullChars: number } {
69 return { body: cut(text, LIMITS.bodyChars), fullChars: text.length }
70}
71
72function twoDigits(value: number): string {
73 return String(value).padStart(2, '0')
74}
75
76/** `HH:MM` on the local day of `now`, else `Mon D HH:MM`. */
77export function clockLabel(at: number, now: number): string {
78 const time = new Date(at)
79 const today = new Date(now)
80 const hhmm = `${twoDigits(time.getHours())}:${twoDigits(time.getMinutes())}`
81 const isToday =
82 time.getFullYear() === today.getFullYear() &&
83 time.getMonth() === today.getMonth() &&
84 time.getDate() === today.getDate()
85 return isToday ? hhmm : `${MONTHS[time.getMonth()]} ${time.getDate()} ${hhmm}`
86}
87
88/** A state with the clock time it was reported, as in `idle (12:04)`; undefined without a state. */
89export function stateLabel(state: HighCommandState | undefined, now: number): string | undefined {
90 return state === undefined ? undefined : `${state.word} (${clockLabel(state.at, now)}${state.source ? `; ${state.source}; last reported` : ''})`
91}
92
93/**
94 * Whether the text holds BASE64_RUN or more base64 characters in a row that mix upper case, lower case,
95 * and digits, as encoded data does; a run of one letter or of hex digits is not a block. One pass.
96 */
97function hasBase64Block(text: string): boolean {
98 let run = 0
99 let kinds = 0
100 for (let index = 0; index < text.length; index += 1) {
101 const code = text.charCodeAt(index)
102 const kind = code >= 65 && code <= 90 ? 1 : code >= 97 && code <= 122 ? 2 : code >= 48 && code <= 57 ? 4 : code === 43 || code === 47 ? 8 : 0
103 run = kind === 0 ? 0 : run + 1
104 kinds = kind === 0 ? 0 : kinds | kind
105 if (run >= BASE64_RUN && (kinds & 7) === 7) return true
106 }
107 return false
108}
109
110/**
111 * Whether the text matches the suspicious-text list; the mark it gives is for display only. The list is tested on
112 * two forms of the text: each INVISIBLE character read as a space, as between two words, and each one removed,
113 * as inside a word. A match in either form marks the text. A space matches every class that an INVISIBLE
114 * character matches in the list, so the first form keeps each match of the text as it came.
115 */
116export function isSuspicious(text: string): boolean {
117 return [text.replace(INVISIBLE, ' '), text.replace(INVISIBLE, '')].some(
118 form => SUSPICIOUS.some(pattern => pattern.test(form)) || hasBase64Block(form),
119 )
120}
121
122/**
123 * How the roster, the thread, and `/highcommand` name a trust level, as in `verified sender`; no label for a peer
124 * that only received sends.
125 */
126export function trustLabel(trust: HighCommandTrust | undefined): string | undefined {
127 return trust === undefined ? undefined : `${trust} sender`
128}
129
130/** A count and its noun: `1 peer`, `2 peers`. */
131export function count(n: number, noun: string): string {
132 return `${n} ${noun}${n === 1 ? '' : 's'}`
133}
134
135/**
136 * The unread counts that the band and the status line show, as in `3 unread (retained) · 2 unread in agent lanes`:
137 * the main inbox's count, and the agent lanes' count when it is above zero; '' when both are zero.
138 */
139export function unreadText(main: number, lanes = 0): string {
140 return [main > 0 ? `${main} unread (retained)` : undefined, lanes > 0 ? `${lanes} unread in agent lanes` : undefined]
141 .filter(part => part !== undefined).join(' · ')
142}
143
144/**
145 * A full peer identity: one line without control or format characters
146 * (bidi controls, zero-width marks). Bracketed text belongs to the name.
147 */
148export function peerKey(name: string): string {
149 return oneLine(sanitize(name).replace(FORMAT, ''))
150}
151
152/** A display label for a normalized peer identity, clipped without changing its state key. */
153export function peerLabel(peer: string): string {
154 if (peer.length <= LIMITS.nameChars) return peer
155 let hash = 2166136261
156 for (let index = 0; index < peer.length; index++) hash = Math.imul(hash ^ peer.charCodeAt(index), 16777619)
157 return `${clip(peer, LIMITS.nameChars - 11)} [${(hash >>> 0).toString(16).padStart(8, '0')}]`
158}
159
160/** Resolves even hash collisions across the current retained roster. */
161export function peerLabels(peers: readonly string[]): Map<string, string> {
162 const groups = new Map<string, string[]>()
163 for (const peer of new Set(peers)) {
164 const label = peerLabel(peer)
165 groups.set(label, [...(groups.get(label) ?? []), peer])
166 }
167 const result = new Map<string, string>()
168 const reserved = new Set(groups.keys())
169 for (const [label, group] of groups) {
170 group.sort()
171 let index = 0
172 for (const peer of group) {
173 let resolved = label
174 if (group.length > 1) {
175 do { resolved = `${clip(label, 50)} [${++index}]` } while (reserved.has(resolved))
176 reserved.add(resolved)
177 }
178 result.set(peer, resolved)
179 }
180 }
181 return result
182}
183
184/** A conversation-local marker, preserved across module reloads. */
185export function captureLabel(capture: { at: number; reset: boolean }, now: number): string {
186 return `Capture window: ${clockLabel(capture.at, now)} · current conversation only. ${capture.reset ? 'Previous High Command history was not restored.' : 'Earlier history is not included.'}`
187}
188
189/** Fixed-width identity pages never separate the halves of a surrogate pair. */
190export function identityPart(peer: string, page: number): string {
191 const boundary = (at: number): number => {
192 const char = peer.charCodeAt(at)
193 return char >= 0xdc00 && char <= 0xdfff ? at - 1 : at
194 }
195 return peer.slice(boundary(page * 1000), boundary((page + 1) * 1000))
196}
197hooks/views.tsx 258 lines1import type { BoxProps, ButtonProps, ElementConstructor, RenderElement, TextProps } from 'claude-code'
2
3import type { HighCommandEntry, HighCommandAgent, HighCommandCapture } from '../types'
4import { isNewer, labelOf, unreadTotal, type RosterRow, type Thread } from './inbox'
5import { LIMITS } from './limits'
6import { captureLabel, identityPart, clockLabel, count, peerLabel, peerLabels, stateLabel, trustLabel, unreadText } from './text'
7
8/** The elements High Command draws with; every surface's table has them. */
9type HighCommandElements = {
10 Box: ElementConstructor<BoxProps>
11 Text: ElementConstructor<TextProps>
12 Button: ElementConstructor<ButtonProps>
13}
14
15/** What the pane draws: the roster, or the open thread. */
16type PaneModel = {
17 rows: RosterRow[]; now: number; thread: Thread | undefined
18 evicted?: number; capture?: HighCommandCapture | null
19 rosterPage?: number; laneId?: string; laneIds?: string[]; lanePage?: number; agents?: HighCommandAgent[]
20 /** Each agent lane's unread count, by agent id. */
21 laneUnread?: ReadonlyMap<string, number>
22}
23
24/**
25 * Each action returns its work, and each Button's onPress returns the action's promise: a press stays the
26 * person's own input only while that work runs.
27 */
28export type PaneActions = {
29 openThread: (peer: string) => Promise<void>
30 back: () => Promise<void>
31 markRead: (peer: string) => Promise<void>
32 readAll: () => Promise<void>
33 page: (page: number) => Promise<void>
34 identity: (page: number | undefined) => Promise<void>
35 reason: (seq: number) => Promise<void>
36 lane: (id: string) => Promise<void>
37 lanePage: (page: number) => Promise<void>
38 rosterPage: (page: number) => Promise<void>
39}
40
41export type BandActions = { open: () => Promise<void>; readAll: () => Promise<void>; hide: () => Promise<void> }
42
43/** The defined parts, joined with ` · `. */
44function joined(parts: readonly (string | undefined)[]): string {
45 return parts.filter(part => part !== undefined).join(' · ')
46}
47
48/** Labels send attempts and inbound results or failures before their summary. */
49function kindPrefix(entry: HighCommandEntry): string {
50 if (entry.direction === 'out') return 'send attempted: '
51 const { word } = labelOf(entry)
52 return word !== 'message' ? `${word}: ` : ''
53}
54
55/** The sender label and the suspicious mark come before the long state, so a narrow pane cuts the state first. */
56function rowText(row: RosterRow, now: number): string {
57 const { arrow } = labelOf(row.last)
58 return joined([
59 row.unread > 0 ? `${row.unread} new` : undefined,
60 trustLabel(row.trust),
61 row.isSuspicious ? 'suspicious' : undefined,
62 clockLabel(row.last.at, now),
63 stateLabel(row.state, now),
64 `${arrow} ${kindPrefix(row.last)}${row.last.summary}${row.last.delivery?.isDelivered === false ? ' (not delivered)' : ''}`,
65 ])
66}
67
68function entryHeader(entry: HighCommandEntry, isFresh: boolean, now: number): string {
69 const delivery =
70 entry.delivery === undefined ? '' : entry.delivery.isDelivered ? ' · reported send result: queued' : ` · reported send result: not delivered: ${entry.delivery.reason}`
71 const marks = joined([trustLabel(entry.trust), entry.isSuspicious === true ? 'suspicious' : undefined])
72 return `${labelOf(entry).arrow} ${clockLabel(entry.at, now)} ${kindPrefix(entry)}${entry.summary}${delivery}${marks === '' ? '' : ` · ${marks}`}${isFresh ? ' · new' : ''}`
73}
74
75function rosterView({ Box, Text, Button }: HighCommandElements, model: PaneModel, actions: PaneActions, labels: Map<string, string>): RenderElement {
76 const unread = unreadTotal(model.rows)
77 const pages = Math.max(1, Math.ceil(model.rows.length / LIMITS.rosterRows))
78 const page = Math.max(0, Math.min(model.rosterPage ?? 0, pages - 1))
79 return (
80 <Box flexDirection="column">
81 <Box flexDirection="row" columnGap={1}>
82 <Text bold>High Command</Text>
83 <Text dimColor>{`· ${unread} unread (retained) · ${count(model.rows.length, 'peer')}`}</Text>
84 {unread > 0 && <Button key="pane:read-all" label="Read all" hotkey="r" onPress={actions.readAll} />}
85 </Box>
86 {model.rows.length === 0 && <Text dimColor>No messages from teammates or other sessions in this capture window.</Text>}
87 {model.rows.slice(page * LIMITS.rosterRows, (page + 1) * LIMITS.rosterRows).map((row, index) => (
88 <Box flexDirection="row" columnGap={1}>
89 <Button
90 key={`open:${row.last.seq}`}
91 label={labels.get(row.peer)!}
92 plain
93 {...(index < LIMITS.hotkeys ? { hotkey: String(index + 1) } : {})}
94 onPress={() => actions.openThread(row.peer)}
95 />
96 <Text bold={row.unread > 0} wrap="truncate-end">
97 {rowText(row, model.now)}
98 </Text>
99 </Box>
100 ))}
101 {pages > 1 && <Text dimColor>{`Peer page ${page + 1} of ${pages}`}</Text>}
102 {page > 0 && <Button key="roster:previous" label="Previous peers" onPress={() => actions.rosterPage(page - 1)} />}
103 {page + 1 < pages && <Button key="roster:next" label="More peers" onPress={() => actions.rosterPage(page + 1)} />}
104 </Box>
105 )
106}
107
108function threadView({ Box, Text, Button }: HighCommandElements, now: number, thread: Thread, actions: PaneActions, label: string): RenderElement {
109 const { row, since, shown, hidden, fresh } = thread
110 const details = joined([
111 stateLabel(row.state, now),
112 trustLabel(row.trust),
113 count(shown.length + hidden, 'retained message'),
114 fresh > 0 ? `${fresh} new` : undefined,
115 ])
116 return (
117 <Box flexDirection="column">
118 <Box flexDirection="row" columnGap={1}>
119 <Button key="pane:back" label="Back" hotkey="b" onPress={actions.back} />
120 <Text bold>{label}</Text>
121 <Button key="pane:identity" label="Identity" onPress={() => actions.identity(thread.identityPage === undefined ? 0 : undefined)} />
122 <Text dimColor>{`· ${details}`}</Text>
123 {row.unread > 0 && <Button key="pane:mark-read" label="Mark read" hotkey="r" onPress={() => actions.markRead(row.peer)} />}
124 </Box>
125 {thread.identityPage !== undefined && <Box flexDirection="column">
126 <Text>{`Identity part ${thread.identityPage + 1} of ${Math.ceil(row.peer.length / 1000)} (${row.peer.length} characters):`}</Text>
127 <Text>{identityPart(row.peer, thread.identityPage)}</Text>
128 {thread.identityPage > 0 && <Button key="identity:previous" label="Previous identity part" onPress={() => actions.identity(thread.identityPage! - 1)} />}
129 {(thread.identityPage + 1) * 1000 < row.peer.length && <Button key="identity:next" label="Next identity part" onPress={() => actions.identity(thread.identityPage! + 1)} />}
130 </Box>}
131 {shown.some(entry => entry.direction === 'out') && (
132 <Text dimColor>Send attempts show observed text. Reported success is not a receipt for that text. Core evidence covers only the latest traced call, never reading or every send.</Text>
133 )}
134 {shown.map(entry => {
135 const isFresh = isNewer(entry, since)
136 return (
137 <Box flexDirection="column" marginTop={1}>
138 <Text bold={isFresh}>{entryHeader(entry, isFresh, now)}</Text>
139 <Text wrap="wrap">{entry.body}</Text>
140 {(entry.refusal !== undefined || entry.core !== undefined) && <Button key={`reason:${entry.seq}`} label="Send details" onPress={() => actions.reason(entry.seq)} />}
141 {thread.reasonSeq === entry.seq && entry.refusal !== undefined && <Box flexDirection="column">
142 <Text>{entry.refusal.body}</Text>
143 {entry.refusal.fullChars > entry.refusal.body.length && <Text dimColor>{`Refusal truncated: ${entry.refusal.fullChars} characters originally; ${entry.refusal.body.length} retained.`}</Text>}
144 </Box>}
145 {thread.reasonSeq === entry.seq && entry.core !== undefined && <Box flexDirection="column">
146 <Text>{`Latest traced core recipient (bounded): ${entry.core.to}`}</Text>
147 <Text>{`Core queue outcome: ${entry.core.isDelivered ? 'accepted, not read' : `refused: ${entry.core.reason}`}`}</Text>
148 <Text>{entry.core.body}</Text>
149 {entry.core.fullChars > entry.core.body.length && <Text>{`Core text truncated: ${entry.core.fullChars} characters originally.`}</Text>}
150 </Box>}
151 {entry.fullChars > entry.body.length && (
152 <Text dimColor>{`(${count(entry.fullChars - entry.body.length, 'more character')} in the transcript.)`}</Text>
153 )}
154 </Box>
155 )
156 })}
157 {thread.page > 0 && <Button key="pane:newer" label="Newer" onPress={() => actions.page(thread.page - 1)} />}
158 {thread.page + 1 < thread.pages && <Button key="pane:older" label="Older" onPress={() => actions.page(thread.page + 1)} />}
159 {hidden > 0 && (
160 <Box marginTop={1}>
161 <Text dimColor>{`Page ${thread.page + 1} of ${thread.pages}; ${count(hidden, 'retained message')} on other pages.`}</Text>
162 </Box>
163 )}
164 </Box>
165 )
166}
167
168/** Includes JSON structure, escaped strings, and space for serialized button handlers. */
169export function fitsBounds(tree: RenderElement): boolean {
170 const pending = [{ node: tree as unknown, depth: 1 }]
171 let nodes = 0
172 while (pending.length > 0) {
173 const { node, depth } = pending.pop()!
174 if (++nodes > LIMITS.drawingNodes || depth > LIMITS.drawingDepth) return false
175 if (typeof node !== 'object' || node === null) continue
176 const children: unknown = Reflect.get(node, 'children')
177 if (Array.isArray(children)) for (const child of children) pending.push({ node: child, depth: depth + 1 })
178 }
179 try {
180 return JSON.stringify(tree, (_key, value: unknown) => typeof value === 'function' ? 'handler'.padEnd(256, '_') : value).length <= LIMITS.drawingText
181 } catch {
182 return false
183 }
184}
185
186/** The pane's tree: the roster, or one peer's thread, newest entry first. */
187export function paneTree(ui: HighCommandElements, model: PaneModel, actions: PaneActions): RenderElement {
188 const { Box, Text, Button } = ui
189 const labels = peerLabels(model.rows.map(row => row.peer))
190 const agentMap = new Map((model.agents ?? []).map(agent => [agent.id, agent]))
191 const ids = [...new Set([...(model.laneIds ?? []), ...agentMap.keys()])].sort()
192 const pages = Math.max(1, Math.ceil(ids.length / 10))
193 const page = Math.max(0, Math.min(model.lanePage ?? 0, pages - 1))
194 return <Box flexDirection="column">
195 {model.capture && <Text dimColor>{captureLabel(model.capture, model.now)}</Text>}
196 <Text dimColor>Order: observation time. Counts cover retained messages.</Text>
197 {(model.evicted ?? 0) > 0 && <Text>{`History trimmed: ${count(model.evicted!, 'message')} permanently evicted.`}</Text>}
198 {ids.length > 0 && <Box flexDirection="column">
199 <Text bold>{model.laneId === undefined ? 'Main inbox' : `Agent observations: ${peerLabel(model.laneId)}`}</Text>
200 <Text dimColor>Send and receive observations are separate. Split-pane traffic is unavailable; roster snapshots do not confirm liveness.</Text>
201 <Button key="lane:main" label="Main inbox" onPress={() => actions.lane('')} />
202 {ids.slice(page * 10, page * 10 + 10).map((id, index) => {
203 const agent = agentMap.get(id)
204 const unread = model.laneUnread?.get(id) ?? 0
205 // The status goes on its own line under the button: beside a long button, a narrow dock left it a few columns.
206 return <Box flexDirection="column">
207 <Button key={`lane:${page * 10 + index}`} label={`${peerLabel(agent?.name ?? id)} (${peerLabel(id)})${unread > 0 ? ` · ${unread} unread` : ''}`} onPress={() => actions.lane(id)} />
208 {agent && <Box paddingLeft={2}>
209 <Text>{`${agent.status}; ${agent.source} at ${clockLabel(agent.at, model.now)}${agent.lastEvent ? `; lifecycle observation: ${agent.lastEvent.word} at ${clockLabel(agent.lastEvent.at, model.now)}` : ''}`}</Text>
210 </Box>}
211 </Box>
212 })}
213 {page > 0 && <Button key="lanes:previous" label="Previous agents" onPress={() => actions.lanePage(page - 1)} />}
214 {page + 1 < pages && <Button key="lanes:next" label="More agents" onPress={() => actions.lanePage(page + 1)} />}
215 </Box>}
216 {model.thread === undefined ? rosterView(ui, model, actions, labels) : threadView(ui, model.now, model.thread, actions, labels.get(model.thread.row.peer)!)}
217 </Box>
218}
219
220/**
221 * The band row above what other mods draw there. `waiting` holds only rows with unread entries; `laneUnread` is the
222 * unread count of all agent lanes, above zero only with agentLanesInBand on.
223 */
224export function bandTree(
225 { Box, Text, Button }: HighCommandElements,
226 waiting: readonly RosterRow[],
227 actions: BandActions,
228 below: RenderElement,
229 labels: Map<string, string>,
230 laneUnread = 0,
231): RenderElement {
232 const named = waiting.slice(0, LIMITS.bandPeers).map(row => `${labels.get(row.peer)} ${row.unread}`)
233 const more = waiting.length > LIMITS.bandPeers ? [`+${waiting.length - LIMITS.bandPeers} more`] : []
234 const peers = [...named, ...more]
235 return (
236 <Box flexDirection="column">
237 <Box flexDirection="row" columnGap={1}>
238 <Text wrap="truncate-end">{`High Command · ${unreadText(unreadTotal(waiting), laneUnread)}${peers.length === 0 ? '' : ` — ${peers.join(' · ')}`}`}</Text>
239 <Button key="band:open" label="Open" hotkey="o" onPress={actions.open} />
240 <Button key="band:read-all" label="Read all" hotkey="r" onPress={actions.readAll} />
241 <Button key="band:hide" label="Hide" hotkey="h" onPress={actions.hide} />
242 </Box>
243 {below}
244 </Box>
245 )
246}
247
248/** Keeps only the counts and Open when the full band exceeds the shared budget. */
249export function compactBandTree({ Box, Text, Button }: HighCommandElements, waiting: readonly RosterRow[], actions: BandActions, below: RenderElement, laneUnread = 0): RenderElement {
250 return <Box flexDirection="column">
251 <Box flexDirection="row">
252 <Text>{`High Command · ${unreadText(unreadTotal(waiting), laneUnread)}`}</Text>
253 <Button key="band:open" label="Open" hotkey="o" onPress={actions.open} />
254 </Box>
255 {below}
256 </Box>
257}
258hooks/limits.ts 40 lines1/** Every bound of High Command, in one place. */
2export const LIMITS = {
3 /** Entries kept in all; the oldest drop first. */
4 entries: 200,
5 /** Entries kept for one peer; a busy peer drops its own oldest entries first. */
6 peerEntries: 50,
7 /** Characters of JSON text that one agent lane's entries keep; past it, that lane drops its own oldest entries first. */
8 laneChars: 500_000,
9 /**
10 * Characters of JSON text that all agent lanes keep together: they are saved as one value, and Claude Code 2.1.296
11 * refuses a value over 4,194,304 characters. Past it, the entries observed first drop; each lane keeps its newest.
12 */
13 lanesChars: 4_000_000,
14 /** Peers kept; the ones reported least recently drop first. */
15 peers: 100,
16 /** Characters of a body kept; the transcript keeps the rest. */
17 bodyChars: 2000,
18 /** Characters of a summary line. */
19 summaryChars: 120,
20 /** Characters of a displayed peer label; identity keys keep the full normalized name. */
21 nameChars: 60,
22 /** Characters of a ListAgents state word. */
23 stateChars: 24,
24 /** Peer rows on one roster page. */
25 rosterRows: 50,
26 /** Entries drawn in one thread. */
27 threadEntries: 20,
28 /**
29 * The engine's bounds on one drawing: characters of text, nodes (strings count), and depth. The engine
30 * refuses a drawing past any of them, so the band checks the drawing it wraps against them.
31 */
32 drawingText: 100_000,
33 drawingNodes: 20_000,
34 drawingDepth: 32,
35 /** Peers named in the band. */
36 bandPeers: 3,
37 /** Roster rows that get a digit hotkey. */
38 hotkeys: 9,
39} as const
40types/index.d.ts 129 lines1/** A peer's last reported state. */
2export type HighCommandState = {
3 /** 'idle', 'interrupted', 'failed', 'ended', 'shut down', or a ListAgents state word. */
4 word: string
5 source?: string
6 /** $.clock.now() when the state was reported. */
7 at: number
8}
9
10/**
11 * How far Claude Code vouches for an inbound sender name: 'verified' for a team mailbox
12 * entry the harness wrote under that name, 'local' for another session's or agent's message that Claude
13 * Code delivered, 'unverified' for every other origin, a name only a plugin claims included.
14 */
15export type HighCommandTrust = 'verified' | 'local' | 'unverified'
16
17/** One receive observation ('in') or send attempt ('out') in its conversation lane. */
18export type HighCommandEntry = {
19 /** Lane-unique and increasing from 1: the recording order. */
20 seq: number
21 /** The full normalized peer identity: the thread this entry belongs to. */
22 peer: string
23 direction: 'in' | 'out'
24 /** 'result': a teammate's final answer. 'failure': a teammate's failure. */
25 kind: 'message' | 'result' | 'failure'
26 /** One line, at most LIMITS.summaryChars characters. */
27 summary: string
28 /** The sanitized body, cut at LIMITS.bodyChars characters. */
29 body: string
30 /** The length of the sanitized body before the cut. */
31 fullChars: number
32 /** $.clock.now() when High Command observed the event. */
33 at: number
34 /** Inbound entries only: the trust level of the sender name, set from origin fields alone. */
35 trust?: HighCommandTrust
36 /** Inbound entries only: the text matches the suspicious-text list; a display mark. */
37 isSuspicious?: true
38 /** Agent lane inbound entries only: the main conversation's own message to that agent, never unread. */
39 fromMain?: true
40 /** Cumulative retention loss, carried atomically by the newest recorded entry. */
41 evictedTotal?: number
42 /** The detailed refusal, bounded separately from its one-line preview. */
43 refusal?: { body: string; fullChars: number }
44 /** Evidence from the latest downstream call only, never a complete send history. */
45 core?: { to: string; body: string; fullChars: number; isDelivered: boolean; reason?: string }
46 /** Outbound entries only: the reported outcome, separate from core evidence. */
47 delivery?: { isDelivered: true } | { isDelivered: false; reason: string }
48}
49
50/** What High Command knows about a peer besides its messages. */
51export type HighCommandPeer = {
52 /** $.clock.now() of the last report about this peer; the peer cap keeps the newest. */
53 seenAt: number
54 state?: HighCommandState
55 /** The `from` address the newest cross-session frame named for this peer, and $.clock.now() when it was reported. */
56 address?: {
57 value: string
58 at: number
59 /** Address observation order for timestamp ties; absent in older saved state. */
60 order?: number
61 }
62}
63
64/** The thread the pane shows, with the peer's read mark from the moment it opened. */
65export type HighCommandThread = {
66 peer: string
67 page?: number
68 identityPage?: number
69 reasonSeq?: number
70 /** Inbound entries with a higher seq draw as new while the thread stays open. */
71 since: number
72 /** The newest seq when the person opened the thread: an append that it saw never closes it. */
73 from: number
74}
75
76export type HighCommandLane = {
77 rosterPage?: number
78 entries: HighCommandEntry[]
79 peers: Record<string, HighCommandPeer>
80 readThrough: Record<string, number>
81 openThread: HighCommandThread | null
82 /**
83 * The seq of the newest entry recorded that can be unread; it only grows and outlives the entry's eviction.
84 * Absent before the lane's first append.
85 */
86 inboundThrough?: number
87}
88
89export type HighCommandAgent = {
90 id: string
91 name?: string
92 status: string
93 at: number
94 source: 'roster snapshot' | 'lifecycle observation'
95 lastEvent?: { word: string; at: number }
96}
97
98export type HighCommandCapture = { at: number; reset: boolean }
99
100declare module 'claude-code' {
101 interface PluginState {
102 'high-command': {
103 lanes: Record<string, HighCommandLane>
104 selectedLane: string | null
105 lanePage: number
106 rosterPage: number
107 agents: Record<string, HighCommandAgent>
108 capture: HighCommandCapture | null
109 entries: HighCommandEntry[]
110 peers: Record<string, HighCommandPeer>
111 readThrough: Record<string, number>
112 openThread: HighCommandThread | null
113 /** The newest seq that a press of Hide had seen; 0 before any press. It only grows. */
114 hiddenThrough: number
115 /** The seq of the newest inbound entry recorded; it only grows and outlives the entry's eviction. */
116 inboundThrough: number
117 /**
118 * Per agent lane, the newest seq that a press of Hide had seen while agentLanesInBand is on; a mark only grows.
119 */
120 hiddenLanes: Record<string, number>
121 /**
122 * The status line as last pinned: the versions of `entries` and `readThrough` it counts, and its count. With
123 * agentLanesInBand on, also the version of `lanes` and the unread count of all agent lanes.
124 */
125 statusFrom: { entries: number; marks: number; unread: number; lanes?: number; laneUnread?: number }
126 }
127 }
128}
129