Class: Rubino::UI::Base

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

Overview

Abstract base class for all UI adapters. Defines the interface that CLI, API, and Null must implement. No output method should be called directly from core logic; all output flows through one of these methods.

Direct Known Subclasses

API, Null, PrinterBase, SubagentView

Instance Method Summary collapse

Instance Method Details

#ask(prompt) ⇒ Object

Raises:

  • (NotImplementedError)


118
119
120
# File 'lib/rubino/ui/base.rb', line 118

def ask(prompt)
  raise NotImplementedError, "#{self.class}#ask not implemented"
end

#assistant_text(text) ⇒ Object

A finished assistant message. The CLI renders it as markdown; other adapters fall back to plain body text. Part of the UI contract so the session-history replay (resume/continue) works on every adapter.



61
62
63
# File 'lib/rubino/ui/base.rb', line 61

def assistant_text(text)
  body(text)
end

#blank_lineObject

Raises:

  • (NotImplementedError)


201
202
203
# File 'lib/rubino/ui/base.rb', line 201

def blank_line
  raise NotImplementedError, "#{self.class}#blank_line not implemented"
end

#blocking_human_input?Boolean

True when this adapter parks the run on a cross-thread gate for human approvals/clarifications (the HTTP/API path) rather than prompting inline on a terminal. The agent loop uses this to run an interactive turn NON-STREAMING so no upstream LLM socket is held open during the wait. Default false: CLI/Null prompt inline (or auto-answer) and never park, so they keep streaming.

Returns:

  • (Boolean)


247
248
249
# File 'lib/rubino/ui/base.rb', line 247

def blocking_human_input?
  false
end

#body(text) ⇒ Object

Emits the payload that goes under a header (replay assistant body, one-shot text dumps, anything that isn’t a stream chunk). Routing it through the UI rather than $stdout means the Null adapter still records it for tests and the CLI can later style/wrap it.

Raises:

  • (NotImplementedError)


54
55
56
# File 'lib/rubino/ui/base.rb', line 54

def body(text)
  raise NotImplementedError, "#{self.class}#body not implemented"
end

#box_close(*pieces, color: nil) ⇒ Object

Closes the currently-open box with ‘└─ pieces ─────`. When no pieces are given, emits a bare `└────` line — used for boxes that have no trailing metric (user, assistant, thinking). For tool boxes the caller passes `“done”, name, metrics` so the bottom border carries the cost/scope of the call.

Raises:

  • (NotImplementedError)


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

def box_close(*pieces, color: nil)
  raise NotImplementedError, "#{self.class}#box_close not implemented"
end

#box_open(*pieces, at: nil, color: nil) ⇒ Object

Opens a box: ‘┌─ HH:MM · type · pieces ─────` filling the box width. Every visible scrollback block (user, thinking, tool, assistant, replay) is rendered as a box; body lines below get a `│ ` prefix and the box is closed with `box_close`. Pieces are joined with `·`. `at:` overrides the timestamp so replay preserves the original time of each historical step instead of showing “now” for the whole resumed session. `color:` overrides the auto-color (used by tool_finished to flip done to red).

Raises:

  • (NotImplementedError)


37
38
39
# File 'lib/rubino/ui/base.rb', line 37

def box_open(*pieces, at: nil, color: nil)
  raise NotImplementedError, "#{self.class}#box_open not implemented"
end

#compression_finished(metadata, at: nil) ⇒ Object

Raises:

  • (NotImplementedError)


181
182
183
# File 'lib/rubino/ui/base.rb', line 181

def compression_finished(, at: nil)
  raise NotImplementedError, "#{self.class}#compression_finished not implemented"
end

#compression_started(at: nil) ⇒ Object

‘at:` overrides the timestamp shown on the compaction free line. Live events leave it nil and pick up current time; replay (if compaction events ever become stored in history) can pin the original moment.

Raises:

  • (NotImplementedError)


177
178
179
# File 'lib/rubino/ui/base.rb', line 177

def compression_started(at: nil)
  raise NotImplementedError, "#{self.class}#compression_started not implemented"
end

#confirm(question, scope: nil, **context) ⇒ Object

‘scope:` is part of the contract for ALL adapters (not just API): ToolExecutor#request_approval always passes it. CLI/Null ignore it; API uses it as the session-approval cache key. Keeping the keyword in the shared signature is what stops UI::CLI from raising `ArgumentError: unknown keyword: :scope` on every interactive tool approval. `**context` absorbs the enriched approval fields (tool/ command/pattern_key/description) that ToolExecutor passes for the /v1 event — only UI::API consumes them; CLI/Null/SubagentView ignore them.

Raises:

  • (NotImplementedError)


138
139
140
# File 'lib/rubino/ui/base.rb', line 138

def confirm(question, scope: nil, **context)
  raise NotImplementedError, "#{self.class}#confirm not implemented"
end

#confirm_destructive(question) ⇒ Object

A destructive yes/No confirm, default No — distinct from the tool-approval #confirm above (#218). Used for the in-chat/CLI destructive verbs (session delete, memory forget), so only the CLI and Null adapters implement it; both fail closed (decline) off a real terminal so a piped/EOF answer can never destroy. The API/subagent adapters don’t host these verbs and raise.

Raises:

  • (NotImplementedError)


147
148
149
# File 'lib/rubino/ui/base.rb', line 147

def confirm_destructive(question)
  raise NotImplementedError, "#{self.class}#confirm_destructive not implemented"
end

#error(message) ⇒ Object

Raises:

  • (NotImplementedError)


22
23
24
# File 'lib/rubino/ui/base.rb', line 22

def error(message)
  raise NotImplementedError, "#{self.class}#error not implemented"
end

#hint_row(command, description) ⇒ Object

A “try this” hint row: an actionable command plus its description (‘ /status what’s going on right now`). The CLI renders the command cyan (the one accent color) and the description plain.



77
78
79
# File 'lib/rubino/ui/base.rb', line 77

def hint_row(command, description)
  info("    #{command.to_s.ljust(9)} #{description}")
end

#info(message) ⇒ Object

Raises:

  • (NotImplementedError)


10
11
12
# File 'lib/rubino/ui/base.rb', line 10

def info(message)
  raise NotImplementedError, "#{self.class}#info not implemented"
end

#input_injected(text) ⇒ Object

Echoes a message that was picked up MID-TURN at an agent-loop iteration boundary and injected as a user message into the current turn (the Phase-2 steering / “Enter injects into the current turn” affordance). Distinct from #queued, which parks text for the NEXT turn: this text is already part of the live turn, so the CLI renders a dim ‘↳ received while working: …` confirmation. Concrete no-op by default; only the CLI has something to render. API surfaces it via the INPUT_INJECTED bus event, not this echo.



232
# File 'lib/rubino/ui/base.rb', line 232

def input_injected(text); end

#job_enqueued(type) ⇒ Object

Raises:

  • (NotImplementedError)


185
186
187
# File 'lib/rubino/ui/base.rb', line 185

def job_enqueued(type)
  raise NotImplementedError, "#{self.class}#job_enqueued not implemented"
end

#job_finished(type) ⇒ Object

Raises:

  • (NotImplementedError)


193
194
195
# File 'lib/rubino/ui/base.rb', line 193

def job_finished(type)
  raise NotImplementedError, "#{self.class}#job_finished not implemented"
end

#job_started(type) ⇒ Object

Raises:

  • (NotImplementedError)


189
190
191
# File 'lib/rubino/ui/base.rb', line 189

def job_started(type)
  raise NotImplementedError, "#{self.class}#job_started not implemented"
end

#mode_changed(name, previous: nil) ⇒ Object

Signals a Modes transition (e.g. user typed ‘/mode plan` or an API caller invoked Modes.set). CLI renders a `┄ HH:MM · mode → plan ┄` free line; API emits a `mode_changed` event the orchestrator can forward to the web client; Null records it for tests. `previous:` is the mode active before the transition, used to render the arrow (“default → plan”).

Raises:

  • (NotImplementedError)


211
212
213
# File 'lib/rubino/ui/base.rb', line 211

def mode_changed(name, previous: nil)
  raise NotImplementedError, "#{self.class}#mode_changed not implemented"
end

#note(text) ⇒ Object

Small metadata line, dim, no header. Used for the ‘turn · Xs · N tools · Y tok` summary after the final assistant message (on adapters without a dedicated #turn_footer), and any similar low-priority annotation that should sit close to the block it describes without competing visually.

Raises:

  • (NotImplementedError)


86
87
88
# File 'lib/rubino/ui/base.rb', line 86

def note(text)
  raise NotImplementedError, "#{self.class}#note not implemented"
end

#panel_line(label, value, pointer: nil) ⇒ Object

A status-panel key/value row (‘ model minimax-m3`), optionally followed by an actionable pointer (`(use /mcp)`). The CLI styles it per the panel color diet (dim label, plain value, cyan pointer — P8); the default assembles one plain info line so recording adapters keep the full content.



70
71
72
# File 'lib/rubino/ui/base.rb', line 70

def panel_line(label, value, pointer: nil)
  info(["  #{label.to_s.ljust(10)} #{value}", pointer].compact.join("   "))
end

#queued(text) ⇒ Object

Echoes a message the user typed during a running turn — the steering / “talk while it works” affordance. The background reader captured the line and parked it for the next turn; this just confirms it visually so the keystrokes don’t disappear into the streaming output. Concrete (not abstract) and a no-op by default: only the CLI shows the dim ‘queued ▸ …` echo; API/Null have nothing meaningful to render and inherit the no-op rather than each restating it.



222
# File 'lib/rubino/ui/base.rb', line 222

def queued(text); end

#replay_user_input(text) ⇒ Object

Replays a user message from session history (resume / continue). Lets the CLI render past turns with a stable “you >” label so the scrolled-back transcript matches what the user typed at the time.

Raises:

  • (NotImplementedError)


101
102
103
# File 'lib/rubino/ui/base.rb', line 101

def replay_user_input(text)
  raise NotImplementedError, "#{self.class}#replay_user_input not implemented"
end

#select(prompt, choices) ⇒ Object

Arrow-key single-select menu. choices is an array of

label, value

pairs; returns the chosen value, or nil when no

interactive selection is possible (non-TTY / Null adapter) so callers fall back to a non-interactive path.

Raises:

  • (NotImplementedError)


126
127
128
# File 'lib/rubino/ui/base.rb', line 126

def select(prompt, choices)
  raise NotImplementedError, "#{self.class}#select not implemented"
end

#separatorObject

Raises:

  • (NotImplementedError)


197
198
199
# File 'lib/rubino/ui/base.rb', line 197

def separator
  raise NotImplementedError, "#{self.class}#separator not implemented"
end

#status(message) ⇒ Object

Raises:

  • (NotImplementedError)


26
27
28
# File 'lib/rubino/ui/base.rb', line 26

def status(message)
  raise NotImplementedError, "#{self.class}#status not implemented"
end

#stream(chunk) ⇒ Object

Raises:

  • (NotImplementedError)


90
91
92
# File 'lib/rubino/ui/base.rb', line 90

def stream(chunk)
  raise NotImplementedError, "#{self.class}#stream not implemented"
end

#stream_endObject

Raises:

  • (NotImplementedError)


94
95
96
# File 'lib/rubino/ui/base.rb', line 94

def stream_end
  raise NotImplementedError, "#{self.class}#stream_end not implemented"
end

#success(message) ⇒ Object

Raises:

  • (NotImplementedError)


14
15
16
# File 'lib/rubino/ui/base.rb', line 14

def success(message)
  raise NotImplementedError, "#{self.class}#success not implemented"
end

#table(headers:, rows:) ⇒ Object

Raises:

  • (NotImplementedError)


114
115
116
# File 'lib/rubino/ui/base.rb', line 114

def table(headers:, rows:)
  raise NotImplementedError, "#{self.class}#table not implemented"
end

#thinking_startedObject

Called when the model call starts but no chunk has arrived yet. Lets the UI show a transient “thinking…” affordance so the user sees something is happening during TTFB and when show_reasoning is disabled (otherwise the terminal sits silent until the first content chunk lands).

Raises:

  • (NotImplementedError)


110
111
112
# File 'lib/rubino/ui/base.rb', line 110

def thinking_started
  raise NotImplementedError, "#{self.class}#thinking_started not implemented"
end

#tool_body(text, kind: :plain) ⇒ Object

Body block printed inside the open tool box, between the top and ‘done` rules. `kind:` controls coloring:

:plain — every line dim (default; for shell/grep/glob/read
         previews where a leading `-` is `ls -la` permissions,
         not a diff removal)
:diff  — `+ ` lines green, `- ` lines red, rest dim (for edit)

Caller is responsible for trimming the text first (Util::Output.preview).

Raises:

  • (NotImplementedError)


169
170
171
# File 'lib/rubino/ui/base.rb', line 169

def tool_body(text, kind: :plain)
  raise NotImplementedError, "#{self.class}#tool_body not implemented"
end

#tool_finished(name, result: nil) ⇒ Object

Raises:

  • (NotImplementedError)


158
159
160
# File 'lib/rubino/ui/base.rb', line 158

def tool_finished(name, result: nil)
  raise NotImplementedError, "#{self.class}#tool_finished not implemented"
end

#tool_started(name, arguments: nil, at: nil) ⇒ Object

‘at:` overrides the timestamp on the tool box top — replay uses it so historical tool calls show when they actually happened, not “now”. Live calls leave `at:` nil and get current time.

Raises:

  • (NotImplementedError)


154
155
156
# File 'lib/rubino/ui/base.rb', line 154

def tool_started(name, arguments: nil, at: nil)
  raise NotImplementedError, "#{self.class}#tool_started not implemented"
end

#turn_interruptedObject

Commits the standardized ‘⎿ interrupted` marker right after the partial answer that’s kept when a turn is cancelled (Ctrl+C, or the interrupt-by- default Enter on a type-ahead line). Concrete no-op by default; only the CLI renders the dim marker. API surfaces the cancel via its own events; Null records nothing.



239
# File 'lib/rubino/ui/base.rb', line 239

def turn_interrupted; end

#warning(message) ⇒ Object

Raises:

  • (NotImplementedError)


18
19
20
# File 'lib/rubino/ui/base.rb', line 18

def warning(message)
  raise NotImplementedError, "#{self.class}#warning not implemented"
end