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).
- #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 plan` or 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 ‘done` rules.
- #tool_finished(name, result: nil) ⇒ Object
-
#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”.
-
#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).
- #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
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.
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.
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
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.
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.
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.
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
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.
232 |
# File 'lib/rubino/ui/base.rb', line 232 def input_injected(text); end |
#job_enqueued(type) ⇒ Object
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
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
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”).
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.
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.
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
197 198 199 |
# File 'lib/rubino/ui/base.rb', line 197 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).
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
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.
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_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.
239 |
# File 'lib/rubino/ui/base.rb', line 239 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 |