Class: Rubino::UI::AgentMenu

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/ui/agent_menu.rb

Overview

Live subagent picker hosted by the bottom composer. It only chooses an entry; the /agents command remains the single owner of drill-in semantics.

Constant Summary collapse

MAX_ROWS =
5
MAIN_ROW =

The synthetic “return to the main session” row, shown at the BOTTOM of the picker so the list is a switcher: pick a subagent to attach/switch to it, or pick “main” to leave an attached agent (a no-op at the main prompt). A tiny struct so it answers #id like a real row (the refresh re-selection finds it by id). Identity-compared via .main_row?.

Struct.new(:id).new("__main__").freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(entries: -> { Tools::BackgroundTasks.instance.running }, pastel: Pastel.new) ⇒ AgentMenu

Returns a new instance of AgentMenu.



21
22
23
24
25
# File 'lib/rubino/ui/agent_menu.rb', line 21

def initialize(entries: -> { Tools::BackgroundTasks.instance.running }, pastel: Pastel.new)
  @entries = entries
  @pastel = pastel
  @state = nil
end

Class Method Details

.main_row?(entry) ⇒ Boolean

Returns:

  • (Boolean)


19
# File 'lib/rubino/ui/agent_menu.rb', line 19

def self.main_row?(entry) = entry.equal?(MAIN_ROW)

Instance Method Details

#acceptObject



92
93
94
95
96
# File 'lib/rubino/ui/agent_menu.rb', line 92

def accept
  entry = selected
  close!
  entry
end

#close!Object



56
57
58
# File 'lib/rubino/ui/agent_menu.rb', line 56

def close!
  @state = nil
end

#downObject



78
79
80
81
82
83
84
# File 'lib/rubino/ui/agent_menu.rb', line 78

def down
  return open! unless open?

  @state[:selected] = [@state[:selected] + 1, @state[:items].size - 1].min
  sync_top
  true
end

The picker rows: the live subagents, then the “◂ main” row at the bottom. Empty (so the picker stays closed) when no subagent is live — there is nothing to switch between and “main” alone would be a pointless prompt.



41
42
43
44
45
46
# File 'lib/rubino/ui/agent_menu.rb', line 41

def menu_items
  live = live_entries
  return [] if live.empty?

  live + [MAIN_ROW]
end

#open!Object



31
32
33
34
35
36
# File 'lib/rubino/ui/agent_menu.rb', line 31

def open!
  items = menu_items
  return if items.empty?

  @state = { items: items, selected: 0, top: 0 }
end

#open?Boolean

Returns:

  • (Boolean)


27
28
29
# File 'lib/rubino/ui/agent_menu.rb', line 27

def open?
  !@state.nil?
end

#refresh!Object



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rubino/ui/agent_menu.rb', line 98

def refresh!
  return unless open?

  previous = selected&.id
  items = menu_items
  if items.empty?
    close!
    return
  end

  selected = items.index { |entry| entry.id == previous } || 0
  @state = { items: items, selected: selected, top: window_top(selected, items.size) }
end

#rows(cols) ⇒ Object

The rendered picker rows, or [] when closed. Delegates the look — the ‘┄ subagents ┄` header, the scroll-window slice, the cyan ❯ + inverse highlight, the dim rest, and the overflow footer — to the shared MenuView, so this picker and the `/` command palette render alike (#562). This menu still owns its rows: the status-coloured `id · subagent · status` label and the `◂ main` row.



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rubino/ui/agent_menu.rb', line 118

def rows(cols)
  return [] unless open?

  refresh!
  return [] unless open?

  descriptors = @state[:items].map { |entry| descriptor(entry) }
  MenuView.render(descriptors, cols,
                  window: { selected: @state[:selected], top: @state[:top], max_rows: MAX_ROWS },
                  header: "subagents", hints: "Enter attaches · ← back")
end

#selectedObject



86
87
88
89
90
# File 'lib/rubino/ui/agent_menu.rb', line 86

def selected
  return unless open?

  @state[:items][@state[:selected]]
end

#single_liveObject

The single live subagent, or nil when there are zero or several. Lets the composer honor the “Enter to view” hint with a true one-press attach when there is nothing to choose between (#42).



51
52
53
54
# File 'lib/rubino/ui/agent_menu.rb', line 51

def single_live
  live = live_entries
  live.size == 1 ? live.first : nil
end

#up!Object

Move the highlight up one. ↑ off the TOP of the list EXITS the picker: the menu closes itself (it owns its own lifecycle) so focus returns to the input — no stranded ❯ marker. Returns true while it stayed open and moved, false when it closed (or was already closed), so the caller can just ‘up!; redraw` without re-implementing the focus hand-off.



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/rubino/ui/agent_menu.rb', line 65

def up! # rubocop:disable Naming/PredicateMethod -- a bang mutator that also reports whether it stayed open, not a pure query
  return false unless open?

  if @state[:selected].zero?
    close!
    return false
  end

  @state[:selected] -= 1
  sync_top
  true
end