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

Constructor Details

#initialize(pastel: Pastel.new) ⇒ SubagentCards

Returns a new instance of SubagentCards.



39
40
41
# File 'lib/rubino/ui/subagent_cards.rb', line 39

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.



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

def approval_card_line(entry)
  glyph   = @pastel.yellow(APPROVAL)
  command = entry.approval_command.to_s
  command = entry.approval_question.to_s if command.empty?
  "  #{glyph} #{entry.id} · #{entry.subagent} · " +
    @pastel.yellow("needs approval") + ": #{first_line(command, 60)} " \
                                       "· /agents #{entry.id}"
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, and points at /reply <id> (the answer verb), distinct from the approval row’s /agents <id>.



81
82
83
84
85
86
87
# File 'lib/rubino/ui/subagent_cards.rb', line 81

def blocked_card_line(entry)
  glyph    = @pastel.red(BLOCKED)
  question = entry.ask_question.to_s
  "  #{glyph} #{entry.id} · #{entry.subagent} · " +
    @pastel.red("waiting on you") + ": #{first_line(question, 60)} " \
                                    "· /reply #{entry.id}"
end

#card_line(entry) ⇒ Object

One collapsed card row for a single entry.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rubino/ui/subagent_cards.rb', line 61

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
    body  = "#{entry.id} · #{entry.subagent} · #{state} · " \
            "#{count} tool#{"s" if count != 1} · #{elapsed(entry)}"
    body += " · #{entry.last_activity}" unless entry.last_activity.to_s.empty?
    "  #{glyph} #{body}"
  end
end

#card_lines(entries) ⇒ 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.



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/rubino/ui/subagent_cards.rb', line 48

def card_lines(entries)
  live = entries.select { |e| live?(e) }
  return [] if live.empty?

  shown    = live.first(MAX_CARDS)
  overflow = live.size - shown.size
  lines    = shown.map { |e| card_line(e) }
  lines << @pastel.dim("  + #{overflow} more · /agents") if overflow.positive?
  lines << hint_line(shown)
  lines
end