Cards for herdr-inbox tool calls and operator answers in the Claude Code transcript (early-access mods API)

An asynchronous question inbox for Claude Code agents running in herdr: every agent's open questions in one popup, every answer delivered back to the pane that asked.
<a href="https://herdr.dev"><img alt="herdr" src="https://img.shields.io/badge/herdr-%E2%89%A5%200.9-5865a3?style=flat"></a> <a href="https://herdr.dev/plugins/"><img alt="herdr marketplace" src="https://img.shields.io/badge/herdr_marketplace-listed-5865a3?style=flat"></a> <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757?style=flat&logo=claude&logoColor=white"> <img alt="MCP server" src="https://img.shields.io/badge/MCP-server-1f2937?style=flat&logo=modelcontextprotocol&logoColor=white"> <img alt="Python" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776ab?style=flat&logo=python&logoColor=white"> <img alt="Linux and macOS" src="https://img.shields.io/badge/Linux%20%C2%B7%20macOS-supported-2ea44f?style=flat&logo=linux&logoColor=white"> <a href="https://github.com/LeaveMyYard/herdr-inbox/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/LeaveMyYard/herdr-inbox/ci.yml?branch=main&style=flat&logo=githubactions&logoColor=white&label=CI"></a> <img alt="Version" src="https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2FLeaveMyYard%2Fherdr-inbox%2Fmain%2Fpyproject.toml&query=%24.project.version&prefix=v&label=version&color=8b5cf6&style=flat"> <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/github/license/LeaveMyYard/herdr-inbox?style=flat&color=blue"></a>
<a href="docs/media/herdr-demo.mp4"><img src="docs/screenshots/herdr/answering.gif" alt="Answering a group in herdr: open the inbox, pick SQLite, tick redis and CDN, submit; the tab's ❓ marker goes and the answer appears in the agent's conversation" width="920"></a>
<sub>Click for the 36-second demo with <a href="https://github.com/LeaveMyYard/herdr-panorama">herdr-panorama</a> (MP4).</sub>
Ever had an agent stop dead to ask you a question, when it could have kept going with the rest of its work? Or one that asked in chat, carried on, and then asked the same thing again three turns later because it never knew you'd seen it?
herdr-inbox makes agent questions asynchronous. It works like Claude Code's own question prompt (same options, notes and multi-question groups), except the agent doesn't wait for you. It files the question once, gets a receipt (Q12) and keeps working on everything that doesn't depend on the answer. You answer whenever you're ready, from one popup that collects the open questions of every agent in your herdr session, and the answer lands back in the right agent's conversation.
❓N count while their agent is waiting on you.claude plugin marketplace add LeaveMyYard/herdr-inbox
claude plugin install herdr-inbox@herdr-inbox
herdr plugin install LeaveMyYard/herdr-inbox
Then bind a key to the popup (Install has the snippet). The repo is two plugins at once: a Claude Code plugin (.claude-plugin/: MCP server, hooks, the herdr-inbox:operator-questions skill) and a herdr plugin (herdr-plugin.toml: popup, open and sync-tokens actions, startup hook), listed on the herdr plugin marketplace. You install both.
<img src="docs/screenshots/hero.png" alt="herdr with a Claude Code agent that has just filed demo questions; the inbox popup is open over it on a question with described options" width="920">
Running many Claude Code agents at once? herdr-panorama shows them all on one live screen: every orchestrator, the threads it drives and the subagents it spawned. The two plugins know about each other. An agent that asks through the inbox gets a ?N on its panorama card, a on that card opens the inbox right on its question, and your answer goes back to the agent, which carries on.
<a href="docs/media/herdr-demo.mp4"><img src="docs/media/herdr-demo.gif" alt="Four demo agents in herdr-panorama; the Release notes agent asks a question, its card and tab get a ❓ mark, the operator presses a, answers in the inbox popup, and the agent's pane shows the answer and the agent continuing" width="920"></a>
herdr plugin install LeaveMyYard/herdr-panorama # then bind prefix+space to leavemyyard.herdr-panorama.open
herdr-inbox-ui cards need 2.1.301+.PATH for both herdr and Claude Code (it brings its own Python 3.11+); the hooks use the system python3 (3.11+, else uv).Press prefix+i. A group is one ask_operator call: a single question, or up to four related ones that the agent names with a short title.
<img src="docs/screenshots/herdr/list.png" alt="The group list: one titled row per ask_operator call, with the tab, the asking agent and the question headers on a second line" width="920">
With several open groups the popup opens on a list styled like Claude Code's option list: one row per group, named by its title (❯ 1. Invoice storage and caching · 2 questions · 4m), with the tab label, asked_by and the question headers on a dim second line. Groups from the pane you were in come first, then the rest oldest first; answers that could not be delivered are their own ⚠ rows. Enter opens a group, g jumps to its agent, d dismisses the whole group (y confirms). With exactly one open group the popup skips the list and opens straight into it.
<img src="docs/screenshots/herdr/submit.png" alt="Inside a group: the question tabs, ticked once answered, and the Submit tab reviewing both answers" width="920">
A group's questions are tabs under its title, followed by its own Submit tab. Answering drafts the question and moves to the next tab. Submit reviews everything ("You have not answered all questions" when some are blank) and sends the group's answered questions in one message; blank ones stay open. Drafts are stored, so closing the popup loses nothing. Esc goes back to the list.
<img src="docs/screenshots/herdr/single.png" alt="A single question with a git-graph preview beside the highlighted option" width="920">
A group of one question sends as soon as you select an answer. Options can be plain strings or {label, description, preview}; a preview (plain monospace text such as a config snippet or a git graph) is shown beside the list while that option is highlighted. There is always Type something. (a free answer; in multi-select it is added to the picks) and Chat about this (jumps to the asking agent's pane and leaves the question open). n adds a note to the highlighted option, e.g. Postgres (note: keep the old schema).
Nothing that happens in the background changes what your next key does. New groups are appended to the list without moving your cursor, and a row that just arrived ignores Enter and digits for 1 s. Every view ignores Enter, digits and Space for 0.4 s after it appears. A group or question closed meanwhile (withdrawn, answered elsewhere) stays as an inert placeholder ("Q12 was closed") until you move. With no open questions the popup shows "No open questions" and closes on any key; groups arriving then are announced, and Tab or → shows them. An undelivered answer's ⚠ row offers Retry delivery and Stop showing it here (the agent can still fetch it with check_answers).
| Key | Action |
|---|---|
↑ ↓ / j k | Move the ❯ cursor |
1–9 | Jump to that row or option |
Enter | Open the group; select the option (single-select) or toggle it (multi-select) |
Space | Toggle (multi-select) |
n / x | Add a note to the option / clear the draft |
Tab → / Shift-Tab ← | Next / previous tab inside a group |
g | Go to the asking agent's pane |
d | Dismiss the group or question (y confirms); on an alert, acknowledge it |
Esc | Back to the list; on the list, close without sending |
<img src="docs/screenshots/herdr/answered.png" alt="The Billing API agent has received the [inbox] answer and carries on; its tab no longer has a ❓ marker" width="920">
An answer arrives in the agent's conversation as [inbox] Q12 answered — <question>: Postgres (note: keep the old schema), or for several [inbox] answered — Q12 <q>: A · Q13 <q>: B, only while that pane still holds the conversation that asked.
How it gets there depends on the agent.
Note on consent. Claude Code marks an answer that arrives through its hooks as an automated notification, not as something the user typed. Agents act on such answers as information. But an agent may refuse to treat one as your approval for something risky, like "deploy? yes". Ask for such confirmations in chat, as the inbox rules already say.
Either way the inbox counts an answer as seen only once the conversation transcript shows it.
If an answer can't be delivered right away, one background delivery worker retries every few seconds. This covers an agent at a dialog, an agent busy, Claude Code's background-agents panel having focus, or a typed answer that never showed up in the conversation. The worker serves every pane, whichever inbox version each session runs. It starts with herdr and on demand (inbox worker --ensure). It stays up while herdr reports agents, and stops after 10 idle minutes without any.
The popup shows why an answer is waiting. If an answer is still undelivered after 5 minutes, or runs out of tries, herdr shows a notification. The agent can always fetch it itself with check_answers.
<img src="docs/screenshots/herdr/tab-marker.png" alt="herdr's tab bar before and after the agents asked: tabs with open questions get a ❓N prefix" width="920">
herdr has no per-tab tokens, so while a tab has open questions its label gets a ❓N prefix (herdr tab rename), updated as the count changes and removed at 0. Your own label is never stored: it is recovered by stripping the prefix each time, so renaming a prefixed tab is respected. Tabs without a custom name (herdr shows their position, e.g. 6) are left alone by default, because a rename cannot give them back automatic naming.
Optionally, every pane with open questions carries the pane token inbox_q = "?N", cleared when the last one closes. Add $inbox_q to a row of the agent sidebar:
[ui.sidebar.agents]
rows = [["state_icon", "tab", { token = "$inbox_q", fg = "#c04a4a", bold = true }], ["agent"]]
With herdr-radar, which replaces the agent rows with its own and has no slot for other plugins' tokens yet (hhdebb/herdr-radar#57 proposes an extra_tokens setting), run the action Inbox: show ?N in herdr-radar's sidebar (herdr plugin action invoke leavemyyard.herdr-inbox.radar-sidebar, or inbox radar-sidebar). It adds the $inbox_q cell to each of radar's agent rows, inside radar's managed block only, and reloads herdr's config. It runs only when you ask: radar rewrites its block on its configure action, and the popup then shows a one-line hint to run it again (radar_sidebar_hint = false turns the hint off). It leaves the file alone if it does not parse, if a row would exceed herdr's 16-token limit, or once radar's config carries $inbox_q in extra_tokens itself; a backup of the previous file is kept as config.toml.inbox-bak.
See Requirements first.
Claude Code:
claude plugin marketplace add LeaveMyYard/herdr-inbox
claude plugin install herdr-inbox@herdr-inbox
This registers the MCP server (tools appear as mcp__plugin_herdr-inbox_herdr-inbox__ask_operator etc.) and the hooks. Restart running Claude sessions (or /reload-plugins). Claude Code installs plugins as a copy, so the MCP server keeps its Python environment in ${CLAUDE_PLUGIN_DATA}/venv-<version> (created by uv on first use, about 0.5 s from a warm cache; older versions' venvs are removed automatically once no server uses them; after restarting sessions that predate 0.5.3, inbox clean-venvs removes their older venvs too). The hooks need only the system python3 (3.11+; older falls back to uv) and always exit 0. MCP server only, without hooks: run claude mcp add --scope user herdr-inbox -- uv run --project /path/to/herdr-inbox inbox mcp from a checkout and add docs/agent-instructions.md to your CLAUDE.md; use one route, not both.
herdr:
herdr plugin install LeaveMyYard/herdr-inbox
The plugin is also on the herdr plugin marketplace (through the herdr-plugin GitHub topic); installing from there is the same command. herdr runs the manifest's [[build]] step (uv sync --frozen --no-dev) during install, after showing you what it will run. Add --ref <tag> to pin a revision. Then bind a key in ~/.config/herdr/config.toml and run herdr server reload-config:
[[keys.command]]
key = "prefix+i"
type = "plugin_action"
command = "leavemyyard.herdr-inbox.open"
description = "question inbox"
herdr-inbox-ui is a separate plugin in the same marketplace that draws herdr-inbox in the Claude Code transcript: ask_operator as a card with the title, questions, options, asked_by and the filed Q ids; alert_operator as a card coloured by severity (info cyan, warning yellow, urgent red); check_answers, list_my_questions and the withdraw tools as one dim line (a folded run of inbox calls is unfolded when it asks or alerts, so the cards show); and an arriving [inbox] answer (single, batch or pasted) as a green block listing each Q id with its answer, plus a toast. ctrl+o shows the raw message. It changes only what is drawn, never what the model reads. It uses Claude Code's early-access mods API (2.1.301+; terminal and desktop draw it, other surfaces keep the plain rows), which is why it is not part of herdr-inbox:
claude plugin install herdr-inbox-ui@herdr-inbox
Settings live in config.toml in the plugin's config directory (herdr plugin config-dir leavemyyard.herdr-inbox):
tab_prefix = "❓" # "" turns the tab prefix off
prefix_unnamed_tabs = false # true: also prefix tabs named by position (they keep the number)
radar_sidebar_hint = true # false: no popup hint when herdr-radar's rows lack $inbox_q
The popup and every Claude session read this file, and edits apply without a restart. The database is ${XDG_STATE_HOME:-~/.local/state}/herdr-inbox/inbox.db; HERDR_INBOX_DB overrides it.
| Tool | Purpose |
|---|---|
ask_operator(question, options?, header?, multi_select?, context?, asked_by?, title?) | File a question (or questions=[...], up to 4, named by title); returns a receipt immediately |
check_answers | Answers not seen yet (after a missed delivery or compaction) |
list_my_questions(include_closed=false) | The session's questions, to recover after /compact or /clear |
withdraw_question(id, reason) | The agent resolved it itself |
alert_operator(title, message?, severity?, asked_by?) | "Please check X": no answer expected; shows as an alert until acknowledged |
withdraw_alert(id) | The issue resolved itself |
An alert (A7) is something the operator should check, with no answer expected. It heads the popup list (⚑, most severe first, unnumbered: digits and Enter keep acting on your questions exactly as before), raises a notification and counts in the ❓N badge until acknowledged with d (↑/↓ to reach it). The agent learns of the acknowledgement through check_answers; nothing is typed into its pane.
The delivery worker also raises an alert by itself (⏸ <tab> is waiting for you) for an agent herdr reports as blocked for 15 s — stuck at a permission, AskUserQuestion or plan prompt, where it cannot call any tool. A deliberate g (a pause after your last key) goes to it, never Enter or a digit; the alert clears when the prompt is answered. The inbox never types into that pane.
A SessionStart hook teaches every Claude Code session to use them, so no CLAUDE.md edit is needed. Outside herdr the hooks stay silent and ask_operator tells the agent to ask in chat.
From a shell (from a checkout, as uv run inbox ...):
inbox list [--all]
inbox answer Q12 "squash it"
inbox dismiss 12
inbox sync-tokens
inbox radar-sidebar [--remove] # $inbox_q in herdr-radar's sidebar rows (on demand)
Another plugin (or a script) can open the popup straight on one question or group:
root=$(herdr plugin list --json | jq -r '.result.plugins[] | select(.plugin_id == "leavemyyard.herdr-inbox") | .plugin_root')
uv run --quiet --frozen --no-dev --project "$root" inbox open --question Q12 # or:
# inbox open --group 7 the group (one ask_operator call)
# inbox open --pane w6:p3A that pane's oldest open group
# inbox open --session <id> that Claude session's oldest open group
Run it only on an explicit operator action (a key binding, a click), never from an event or a timer. A popup takes all keyboard input, so one that appears under the operator's typing would swallow their keys.
A caller whose open always comes from an explicit operator key or click (herdr-panorama's a, say) can pass --no-hold. The popup then opens like a normal one: its views arm for 0.4 s, then Enter works. Pass --no-hold only in that case.
Without it, as a second guard, a deep-linked popup sends nothing until the operator presses Tab or →, at least half a second after it appeared. ↑/↓ don't count, since they are what shell history under the popup would send. Until then it shows "Opened from another plugin — press Tab or → to start answering", and Enter and digits do nothing.
How it works:
herdr plugin pane open cannot pass arguments to the popup. So inbox open writes a one-shot request (open-request.json next to the database: target, time and nonce, written atomically, newest wins).inbox open exits with code 3 and the message "the inbox popup is already open", and writes nothing.--question takes Qn or n and --group takes a number; anything else is a usage error (exit 2).Agents on a saved herdr SSH machine (herdr machine list) can ask you too.
sh inbox remote install <machine> # later: inbox remote upgrade <machine> `` This needs key-based ssh; it never prompts for a password.Their questions then show up in this popup as <machine>:Q12. Answers are queued here and sent by the delivery worker, and the remote's own inbox types them into its agent. Settings in the inbox config.toml: remote_sync_seconds = 5 and remote_answer_expiry_hours = 12. Details: docs/REMOTE.md.
herdr plugin update # herdr side (or: herdr plugin update LeaveMyYard/herdr-inbox)
claude plugin marketplace update herdr-inbox
claude plugin update herdr-inbox@herdr-inbox
Then restart Claude sessions: a session started before the update keeps its old MCP server. While 0.2.x and 0.3 sessions overlap, an answer sent at the same moment as an old session's retry tick can (rarely) be delivered twice; the window closes once every session has restarted.
Remove the tab prefixes first (pane tokens vanish with herdr's next restart). Set tab_prefix = "" in the config file above, then reconcile every tab:
herdr plugin action invoke leavemyyard.herdr-inbox.sync-tokens
Then remove the plugins:
herdr plugin uninstall leavemyyard.herdr-inbox # or: herdr plugin unlink ... if you linked it
claude plugin uninstall herdr-inbox@herdr-inbox
claude plugin marketplace remove herdr-inbox
From a checkout, uv run inbox sync-tokens --remove-prefixes does the first step in one command. If you added $inbox_q to herdr-radar's rows, take it out first with inbox radar-sidebar --remove. Remove the prefix+i binding yourself. The database stays in ~/.local/state/herdr-inbox/ until you delete it.
The MCP server (spawned per Claude session, so it inherits the pane's HERDR_PANE_ID) writes questions to a shared SQLite database. The popup reads it, and answering calls herdr agent prompt <pane>. Markers use herdr tab rename and herdr pane report-metadata. A SessionStart hook injects a short usage block into each session. Details, data model and design decisions: docs/DESIGN.md.
prefix+i does nothing. The key is yours to bind (see Install); after editing config.toml run herdr server reload-config. herdr plugin list should show leavemyyard.herdr-inbox enabled, and herdr plugin log shows its action and startup logs, including a missing uv on herdr's PATH.
My agents still ask in chat. Restart the Claude Code sessions (or /reload-plugins) after installing: the MCP server and the session hook load at session start. Outside herdr, or in a claude nested inside an agent's pane, the tool tells the agent to ask in chat on purpose.
An answer did not arrive. The popup shows it as a ⚠ Q12 not delivered row with Retry delivery; inbox list --all from a checkout shows every question's state. The agent can always fetch its answers itself with
hooks/register.tsx 114 lines1import type { Register } from 'claude-code'
2
3// herdr-inbox's MCP tools: `mcp__plugin_herdr-inbox_herdr-inbox__ask_operator` when installed
4// as the plugin, `mcp__herdr-inbox__ask_operator` when the server is registered by hand.
5const TOOL = /^mcp__(?:plugin_herdr-inbox_)?herdr-inbox__(\w+)$/
6const SEVERITY: Record<string, string> = { info: 'cyan', warning: 'yellow', urgent: 'red' }
7const DRAWN = new Set(['terminal', 'desktop'])
8
9type Option = string | { label?: string; description?: string }
10type Item = { question?: string; header?: string; options?: Option[]; multi_select?: boolean }
11type Answer = { id: string; question: string; answer: string }
12
13// Control characters (an escape, a NUL) would get the tree refused and the card lost: drop them.
14const clean = (s: string) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '')
15const str = (v: unknown) => (typeof v === 'string' ? clean(v) : '')
16const label = (o: Option) => str(typeof o === 'string' ? o : o?.label)
17
18/** The `[inbox]` answers a message carries, in herdr-inbox's own delivery format
19 * (service.delivery_text): `[inbox] Q7 answered — q: a` or `[inbox] answered — Q7 q: a · Q8 q: b`.
20 * Read only at the start, like transcript.header_ids: a message that merely quotes an `[inbox]`
21 * line is not one. A big paste is recorded as `<pasted_content …>[inbox] …</pasted_content>`. */
22export function parseAnswers(raw: string): Answer[] {
23 if (!raw.includes('[inbox]')) return []
24 const text = clean(raw.replace(/^\s*(?:<pasted_content[^>]*>\s*)?/, '').replace(/\s*<\/pasted_content>\s*$/, ''))
25 const split = (seg: string, id: string): Answer => {
26 const at = seg.indexOf(': ')
27 return at < 0 ? { id, question: '', answer: seg.trim() } : { id, question: seg.slice(0, at).trim(), answer: seg.slice(at + 2).trim() }
28 }
29 const single = /^\[inbox\] (Q\d+) answered — ([\s\S]*)/.exec(text)
30 if (single) return [split(single[2]!, single[1]!)]
31 const batch = /^\[inbox\] answered — ([\s\S]*)/.exec(text)
32 if (!batch) return []
33 return batch[1]!.split(/ · (?=Q\d+ )/).flatMap(seg => {
34 const m = /^\s*(Q\d+) ([\s\S]*)$/.exec(seg)
35 return m ? [split(m[2]!, m[1]!)] : []
36 })
37}
38
39export const register: Register = on => {
40 on('ui.render', { component: 'ToolUse' }, ($, e, next) => {
41 const name = TOOL.exec(e.props.tool)?.[1]
42 if (!name || !DRAWN.has(e.surface)) return next(e)
43 const { Box, Text } = $.ui.resolve(e)
44 const input = (e.props.input ?? {}) as Record<string, unknown>
45 const by = str(input.asked_by) ? <Text dimColor>asked by {str(input.asked_by)}</Text> : null
46 if (name === 'ask_operator') {
47 const items: Item[] = Array.isArray(input.questions) ? (input.questions as Item[])
48 : [{ question: str(input.question), header: str(input.header), options: input.options as Option[] }]
49 const filed = [...JSON.stringify(e.props.output ?? '').matchAll(/Q(\d+) \((\w+)\)/g)].map(m => `Q${m[1]} ${m[2]}`)
50 return (
51 <Box borderStyle="round" borderColor="yellow" paddingX={1} flexDirection="column">
52 <Text bold color="yellow">? Question for the operator{str(input.title) ? `: ${str(input.title)}` : ''}</Text>
53 {items.map((q, i) => (
54 <Box key={`q${i}`} flexDirection="column">
55 <Text>{str(q.header) ? <Text bold>{str(q.header)} · </Text> : ''}{str(q.question)}</Text>
56 {(q.options ?? []).length > 0 ? <Text dimColor> {(q.options ?? []).map(label).join(' · ')}{q.multi_select ? ' (multi)' : ''}</Text> : null}
57 </Box>
58 ))}
59 {by}
60 {filed.length > 0 ? <Text color="green">filed {filed.join(', ')}</Text> : null}
61 </Box>
62 )
63 }
64 if (name === 'alert_operator') {
65 const severity = str(input.severity) || 'warning'
66 const color = SEVERITY[severity] ?? 'yellow'
67 return (
68 <Box borderStyle="round" borderColor={color} paddingX={1} flexDirection="column">
69 <Text bold color={color}>⚑ Alert · {severity}: {str(input.title)}</Text>
70 {str(input.message) ? <Text>{str(input.message)}</Text> : null}
71 {by}
72 </Box>
73 )
74 }
75 const compact: Record<string, string> = {
76 check_answers: 'checked for operator answers',
77 list_my_questions: 'listed my questions',
78 withdraw_question: `withdrew Q${String(input.id ?? '?')}${str(input.reason) ? ` — ${str(input.reason)}` : ''}`,
79 withdraw_alert: `withdrew alert A${String(input.id ?? '?')}`,
80 }
81 if (!compact[name]) return next(e)
82 return <Text dimColor>✉ inbox: {clean(compact[name])}</Text>
83 })
84
85 // The engine folds a run of MCP calls into one line ("Called herdr-inbox 3 times"); unfold a
86 // run that asks or alerts so its cards show.
87 on('ui.render', { component: 'ToolGroup' }, ($, e, next) => {
88 const asks = e.props.calls.some(c => /^(ask|alert)_operator$/.test(TOOL.exec(c.tool)?.[1] ?? ''))
89 if (!asks || e.props.isExpanded || !DRAWN.has(e.surface)) return next(e)
90 return next({ ...e, props: { ...e.props, isExpanded: true } })
91 })
92
93 on('ui.render', { component: 'UserMessage' }, ($, e, next) => {
94 if (e.props.isExpanded || !DRAWN.has(e.surface)) return next(e)
95 const answers = parseAnswers(e.props.text)
96 if (answers.length === 0) return next(e)
97 const { Box, Text } = $.ui.resolve(e)
98 return (
99 <Box borderStyle="round" borderColor="green" paddingX={1} flexDirection="column">
100 <Text bold color="green">✓ Answer from the operator</Text>
101 {answers.map(a => (
102 <Text key={a.id}><Text bold color="green">{a.id}</Text>{a.question ? <Text dimColor> {a.question}</Text> : ''}{' → '}{a.answer}</Text>
103 ))}
104 </Box>
105 )
106 })
107
108 on('prompt.submit', ($, e, next) => {
109 const ids = parseAnswers(e.text).map(a => a.id)
110 if (ids.length > 0) $.ui.toast(`Operator answered ${ids.join(', ')}`)
111 return next(e)
112 })
113}
114