Class: Rubino::UI::Base
- Inherits:
-
Object
- Object
- Rubino::UI::Base
- 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
Instance Method Summary collapse
- #ask(prompt) ⇒ Object
-
#assistant_text(text) ⇒ Object
A finished assistant message.
- #blank_line ⇒ Object
-
#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.
-
#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).
-
#box_close(*pieces, color: nil) ⇒ Object
Closes the currently-open box with
└─ pieces ─────. -
#box_open(*pieces, at: nil, color: nil) ⇒ Object
Opens a box:
┌─ HH:MM · type · pieces ─────filling the box width. - #compression_finished(metadata, at: nil) ⇒ Object
-
#compression_started(at: nil) ⇒ Object
at:overrides the timestamp shown on the compaction free line. -
#confirm(question, scope: nil, **context) ⇒ Object
scope:is part of the contract for ALL adapters (not just API): ToolExecutor#request_approval always passes it. -
#confirm_destructive(question) ⇒ Object
A destructive yes/No confirm, default No — distinct from the tool-approval #confirm above (#218).
- #error(message) ⇒ Object
-
#hint_row(command, description) ⇒ Object
A "try this" hint row: an actionable command plus its description (
/status what's going on right now). - #info(message) ⇒ Object
-
#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).
-
#interactive? ⇒ Boolean
True when this adapter can actually put an approval prompt in front of a human and block for an answer — a real interactive terminal (UI::CLI) or the HTTP approval gate (UI::API).
- #job_enqueued(type) ⇒ Object
- #job_finished(type) ⇒ Object
- #job_started(type) ⇒ Object
-
#mode_changed(name, previous: nil) ⇒ Object
Signals a Modes transition (e.g. user typed
/mode planor an API caller invoked Modes.set). -
#note(text) ⇒ Object
Small metadata line, dim, no header.
-
#panel_line(label, value, pointer: nil) ⇒ Object
A status-panel key/value row (
model minimax-m3), optionally followed by an actionable pointer ((use /mcp)). -
#queued(text) ⇒ Object
Echoes a message the user typed during a running turn — the steering / "talk while it works" affordance.
-
#replay_user_input(text) ⇒ Object
Replays a user message from session history (resume / continue).
-
#select(prompt, choices) ⇒ Object
Arrow-key single-select menu.
- #separator ⇒ Object
- #status(message) ⇒ Object
- #stream(chunk) ⇒ Object
- #stream_end ⇒ Object
- #success(message) ⇒ Object
- #table(headers:, rows:) ⇒ Object
-
#thinking_started ⇒ Object
Called when the model call starts but no chunk has arrived yet.
-
#tool_body(text, kind: :plain) ⇒ Object
Body block printed inside the open tool box, between the top and
donerules. - #tool_finished(name, result: nil) ⇒ Object
-
#tool_started(name, arguments: nil, at: nil, call_id: 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". -
#turn_interrupted ⇒ Object
Commits the standardized
⎿ interruptedmarker 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). - #warning(message) ⇒ Object
Instance Method Details
#ask(prompt) ⇒ Object
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_line ⇒ Object
212 213 214 |
# File 'lib/rubino/ui/base.rb', line 212 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.
258 259 260 |
# File 'lib/rubino/ui/base.rb', line 258 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.
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.
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).
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
192 193 194 |
# File 'lib/rubino/ui/base.rb', line 192 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.
188 189 190 |
# File 'lib/rubino/ui/base.rb', line 188 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 ignore them.
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.
158 159 160 |
# File 'lib/rubino/ui/base.rb', line 158 def confirm_destructive(question) raise NotImplementedError, "#{self.class}#confirm_destructive not implemented" end |
#error(message) ⇒ Object
22 23 24 |
# File 'lib/rubino/ui/base.rb', line 22 def error() 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
10 11 12 |
# File 'lib/rubino/ui/base.rb', line 10 def info() 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.
243 |
# File 'lib/rubino/ui/base.rb', line 243 def input_injected(text); end |
#interactive? ⇒ Boolean
True when this adapter can actually put an approval prompt in front of a human and block for an answer — a real interactive terminal (UI::CLI) or the HTTP approval gate (UI::API). The headless one-shot adapter (UI::Null) returns false, which is the signal ToolExecutor uses to FAIL CLOSED: a tool that needs approval is DENIED rather than auto-run, since there is no one to ask (the security floor behind #260). Default true so any custom adapter that hosts a human keeps the prompt path.
149 150 151 |
# File 'lib/rubino/ui/base.rb', line 149 def interactive? true end |
#job_enqueued(type) ⇒ Object
196 197 198 |
# File 'lib/rubino/ui/base.rb', line 196 def job_enqueued(type) raise NotImplementedError, "#{self.class}#job_enqueued not implemented" end |
#job_finished(type) ⇒ Object
204 205 206 |
# File 'lib/rubino/ui/base.rb', line 204 def job_finished(type) raise NotImplementedError, "#{self.class}#job_finished not implemented" end |
#job_started(type) ⇒ Object
200 201 202 |
# File 'lib/rubino/ui/base.rb', line 200 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").
222 223 224 |
# File 'lib/rubino/ui/base.rb', line 222 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.
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.
233 |
# File 'lib/rubino/ui/base.rb', line 233 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.
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.
126 127 128 |
# File 'lib/rubino/ui/base.rb', line 126 def select(prompt, choices) raise NotImplementedError, "#{self.class}#select not implemented" end |
#separator ⇒ Object
208 209 210 |
# File 'lib/rubino/ui/base.rb', line 208 def separator raise NotImplementedError, "#{self.class}#separator not implemented" end |
#status(message) ⇒ Object
26 27 28 |
# File 'lib/rubino/ui/base.rb', line 26 def status() raise NotImplementedError, "#{self.class}#status not implemented" end |
#stream(chunk) ⇒ Object
90 91 92 |
# File 'lib/rubino/ui/base.rb', line 90 def stream(chunk) raise NotImplementedError, "#{self.class}#stream not implemented" end |
#stream_end ⇒ Object
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
14 15 16 |
# File 'lib/rubino/ui/base.rb', line 14 def success() raise NotImplementedError, "#{self.class}#success not implemented" end |
#table(headers:, rows:) ⇒ Object
114 115 116 |
# File 'lib/rubino/ui/base.rb', line 114 def table(headers:, rows:) raise NotImplementedError, "#{self.class}#table not implemented" end |
#thinking_started ⇒ Object
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).
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).
180 181 182 |
# File 'lib/rubino/ui/base.rb', line 180 def tool_body(text, kind: :plain) raise NotImplementedError, "#{self.class}#tool_body not implemented" end |
#tool_finished(name, result: nil) ⇒ Object
169 170 171 |
# File 'lib/rubino/ui/base.rb', line 169 def tool_finished(name, result: nil) raise NotImplementedError, "#{self.class}#tool_finished not implemented" end |
#tool_started(name, arguments: nil, at: nil, call_id: 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.
165 166 167 |
# File 'lib/rubino/ui/base.rb', line 165 def tool_started(name, arguments: nil, at: nil, call_id: nil) raise NotImplementedError, "#{self.class}#tool_started not implemented" end |
#turn_interrupted ⇒ Object
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.
250 |
# File 'lib/rubino/ui/base.rb', line 250 def turn_interrupted; end |
#warning(message) ⇒ Object
18 19 20 |
# File 'lib/rubino/ui/base.rb', line 18 def warning() raise NotImplementedError, "#{self.class}#warning not implemented" end |