Class: Rubino::UI::SubagentCards

Inherits:
Object
  • Object
show all
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).

""
APPROVAL =
""

Instance Method Summary collapse

Constructor Details

#initialize(pastel: Pastel.new) ⇒ SubagentCards

Returns a new instance of SubagentCards.



46
47
48
# File 'lib/rubino/ui/subagent_cards.rb', line 46

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.



89
90
91
92
93
94
95
96
97
98
# File 'lib/rubino/ui/subagent_cards.rb', line 89

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

#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.



102
103
104
105
106
107
108
# File 'lib/rubino/ui/subagent_cards.rb', line 102

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.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/rubino/ui/subagent_cards.rb', line 68

def card_line(entry)
  if 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.



55
56
57
58
59
60
61
62
63
64
65
# File 'lib/rubino/ui/subagent_cards.rb', line 55

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?
  lines << hint_line(live)
  lines
end