Class: Rubino::UI::SubagentCards
- Inherits:
-
Object
- Object
- Rubino::UI::SubagentCards
- Defined in:
- lib/rubino/ui/subagent_cards.rb
Overview
Formats BackgroundTasks registry entries into the COLLAPSED LIVE CARDS the parent shows while one or more background subagents run (Variant A of the orchestration-UX blueprint). This is the single source of card text: the live region (UI::CLI#set_subagent_cards → BottomComposer) renders it while a turn runs, and the /agents drill-in reuses the same formatter for the expanded view. Pure formatting — it never touches the registry mutex itself (callers pass a snapshot) and writes nothing; the renderer decides where the lines go.
Collapsed card (one row per running subagent, updates in place):
▸ sa_9ae4 · explore · running · 14 tools · 38s · grep "def authenticate"
plus a single shared hint line under the block.
An entry parked on a human approval shows the approval prominently instead:
● sa_9ae4 · explore · needs approval · shell rm -rf build
Up to MAX_CARDS cards stack; a longer list collapses the overflow into a “+N more” tail so the live region stays bounded (and the single-row clamp in the composer never has to host an unbounded block).
Constant Summary collapse
- MAX_CARDS =
Cap the live block so it never grows past the registry’s own MAX_CONCURRENT (3) live children — but defend against a stale/over-long list anyway with an explicit overflow tail.
Tools::BackgroundTasks::MAX_CONCURRENT
- DEFAULT_CARD_WIDTH =
The display-column budget every card row is bounded to when the caller does not pass the live pane width. Several concurrent ‘needs approval` cards previously rendered at WHATEVER length their (model-chosen) command made them — so two parked children sat at different right edges and the longer one wrapped mid-word onto a second physical line at a stray column. Bounding EVERY row to one budget (and eliding on a glyph boundary, never mid-word) keeps the concurrent toasts a calm, left-aligned, single-line stack. A real pane width (when threaded through) overrides this default.
100- COLLAPSED =
Collapsed glyph (a running card) / approval glyph (needs the human) / BLOCKED glyph (an escalated ask_parent waiting on the human — RESERVED for “the tree is blocked on you” and nothing else, the distinct-signal rule).
"▸"- APPROVAL =
"●"- BLOCKED =
"⛔"
Instance Method Summary collapse
-
#approval_card_line(entry) ⇒ Object
A card for a child parked on a human approval — the approval is the most important thing on the row, so it leads (amber ●) with the command.
-
#blocked_card_line(entry) ⇒ Object
A card for a child parked on an escalated ask_parent — the ⛔ “tree is blocked on YOU” row, the loudest state.
-
#budget_card_line(entry) ⇒ Object
A card for a child parked asking for MORE budget (#574): it hit its tool-iteration ceiling and wants the human to grant more iterations.
-
#card_line(entry) ⇒ Object
One collapsed card row for a single entry.
-
#card_lines(entries, width: DEFAULT_CARD_WIDTH) ⇒ Object
Renders the live CARD BLOCK for the running (or approval-pending) children in
entriesas an array of ready-to-print lines. -
#initialize(pastel: Pastel.new) ⇒ SubagentCards
constructor
A new instance of SubagentCards.
Constructor Details
#initialize(pastel: Pastel.new) ⇒ SubagentCards
Returns a new instance of SubagentCards.
49 50 51 |
# File 'lib/rubino/ui/subagent_cards.rb', line 49 def initialize(pastel: Pastel.new) @pastel = pastel end |
Instance Method Details
#approval_card_line(entry) ⇒ Object
A card for a child parked on a human approval — the approval is the most important thing on the row, so it leads (amber ●) with the command. A BUDGET request (#574) reuses the same parked state but reads as a budget grant, not a tool approval, so the human knows what they’re granting.
109 110 111 112 113 114 115 116 117 118 |
# File 'lib/rubino/ui/subagent_cards.rb', line 109 def approval_card_line(entry) return budget_card_line(entry) if entry.budget_request glyph = @pastel.yellow(APPROVAL) command = entry.approval_command.to_s command = entry.approval_question.to_s if command.empty? " #{glyph} #{entry.id} · #{safe(card_label(entry))} · " + @pastel.yellow("needs approval") + ": #{safe(first_line(command, 60))} " \ "· ↓ to approve" end |
#blocked_card_line(entry) ⇒ Object
A card for a child parked on an escalated ask_parent — the ⛔ “tree is blocked on YOU” row, the loudest state. Leads with the red ⛔ glyph and the question. The reply prompt AUTO-OPENS (#510/#513); the card just signals the state and points at the same arrow navigation (↓).
97 98 99 100 101 102 103 |
# File 'lib/rubino/ui/subagent_cards.rb', line 97 def blocked_card_line(entry) glyph = @pastel.red(BLOCKED) question = entry.ask_question.to_s " #{glyph} #{entry.id} · #{safe(card_label(entry))} · " + @pastel.red("waiting on you") + ": #{safe(first_line(question, 60))} " \ "· ↓ to answer" end |
#budget_card_line(entry) ⇒ Object
A card for a child parked asking for MORE budget (#574): it hit its tool-iteration ceiling and wants the human to grant more iterations.
122 123 124 125 126 127 128 |
# File 'lib/rubino/ui/subagent_cards.rb', line 122 def budget_card_line(entry) glyph = @pastel.yellow(APPROVAL) question = entry.approval_question.to_s " #{glyph} #{entry.id} · #{safe(card_label(entry))} · " + @pastel.yellow("wants +budget") + ": #{safe(first_line(question, 60))} " \ "· ↓ to grant" end |
#card_line(entry) ⇒ Object
One collapsed card row for a single entry.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/rubino/ui/subagent_cards.rb', line 74 def card_line(entry) if entry.status == :blocked_on_human blocked_card_line(entry) elsif entry.status == :needs_approval approval_card_line(entry) else glyph = @pastel.cyan(COLLAPSED) state = entry.status == :stopping ? "stopping" : "running" count = entry.tool_count.to_i # Compact card: id · label · state · N tools · elapsed. The per-tool # last_activity (often a long grep/glob arg or absolute path) is NOT # shown here — too noisy on the always-visible card; the live detail # lives in the agent's own view (Enter) / drill-in. body = "#{entry.id} · #{safe(card_label(entry))} · #{state} · " \ "#{count} tool#{"s" if count != 1} · #{elapsed(entry)}" " #{glyph} #{body}" end end |
#card_lines(entries, width: DEFAULT_CARD_WIDTH) ⇒ Object
Renders the live CARD BLOCK for the running (or approval-pending) children in entries as an array of ready-to-print lines. Returns [] when nothing is live, so the renderer can clear the region. entries is a snapshot (BackgroundTasks#running) taken under the registry mutex by the caller — this method only reads the plain struct fields.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/rubino/ui/subagent_cards.rb', line 58 def card_lines(entries, width: DEFAULT_CARD_WIDTH) live = entries.select { |e| live?(e) } return [] if live.empty? shown = live.first(MAX_CARDS) overflow = live.size - shown.size lines = shown.map { |e| clamp_row(card_line(e), width) } lines << @pastel.dim(" + #{overflow} more · /agents") if overflow.positive? # Count blocked children over the FULL live list (pre-cap), not just the # shown cards, so the aggregated ⛔N is the true number waiting on the # human even when some are hidden behind the MAX_CARDS overflow (#475-4). lines << hint_line(live) lines end |