Class: Rubino::UI::Notifier
- Inherits:
-
Object
- Object
- Rubino::UI::Notifier
- Defined in:
- lib/rubino/ui/notifier.rb
Overview
Attention notifications for the moments the agent needs human eyes: a long agentic turn finishing, an approval prompt parking the run on a human decision, or a background subagent blocking on the human (an escalated ask_parent).
Channels — mirroring the dominant pattern across coding agents (Claude Code’s terminal bell + hooks, Codex’s notify hook, aider’s –notifications):
* terminal bell (BEL, "\a") — default on. BEL never moves the cursor,
so it is safe even while the bottom composer owns the screen; it is
still routed to the composer's REAL output (never the StdoutProxy,
whose partial-line buffer would re-ring the byte on every repaint)
and NEVER into a pipe.
* OSC 9 ("\e]9;msg\a") — additionally emitted on iTerm2
(TERM_PROGRAM=iTerm.app), which renders it as a native macOS
notification.
* notifications.command — an optional shell command spawned
NON-BLOCKING and best-effort per event with RUBINO_EVENT
(turn_finished | needs_approval | blocked) and RUBINO_MESSAGE in
its environment; failures are swallowed to the structured log.
Covers osascript / notify-send users.
Spam control: events landing within COALESCE_SECONDS of the last emitted one are dropped, so a burst (several children blocking at once) rings at most once.
Constant Summary collapse
- EVENTS =
Event names the command hook sees via RUBINO_EVENT.
%i[turn_finished needs_approval blocked].freeze
- COALESCE_SECONDS =
Burst window: events within this many seconds of the last emitted notification coalesce (are dropped).
1.0
Instance Method Summary collapse
-
#blocked(message = "a subagent is waiting on you") ⇒ Object
A background child is blocked on the human (the ⛔ escalated ask_parent banner).
-
#initialize(config: nil) ⇒ Notifier
constructor
A new instance of Notifier.
-
#needs_approval(message = "approval required") ⇒ Object
An approval prompt is parked on the human — the main agent’s confirm card, or a background child flipped to :needs_approval.
-
#notify(event, message) ⇒ Object
Emits one notification through every enabled channel.
-
#turn_finished(seconds) ⇒ Object
A turn ended after
seconds.
Constructor Details
#initialize(config: nil) ⇒ Notifier
Returns a new instance of Notifier.
40 41 42 43 44 |
# File 'lib/rubino/ui/notifier.rb', line 40 def initialize(config: nil) @config = config @mutex = Mutex.new @last_emitted_at = nil end |
Instance Method Details
#blocked(message = "a subagent is waiting on you") ⇒ Object
A background child is blocked on the human (the ⛔ escalated ask_parent banner).
64 65 66 |
# File 'lib/rubino/ui/notifier.rb', line 64 def blocked( = "a subagent is waiting on you") notify(:blocked, ) end |
#needs_approval(message = "approval required") ⇒ Object
An approval prompt is parked on the human — the main agent’s confirm card, or a background child flipped to :needs_approval.
58 59 60 |
# File 'lib/rubino/ui/notifier.rb', line 58 def needs_approval( = "approval required") notify(:needs_approval, ) end |
#notify(event, message) ⇒ Object
Emits one notification through every enabled channel. Best-effort: a channel failure is logged and never raised into the turn.
70 71 72 73 74 75 76 77 78 |
# File 'lib/rubino/ui/notifier.rb', line 70 def notify(event, ) return unless enabled? return unless mark_emittable! emit_bell() spawn_command(event, ) rescue StandardError => e log_failure(e) end |
#turn_finished(seconds) ⇒ Object
A turn ended after seconds. Quick turns stay silent (notifications.min_turn_seconds): focus detection is unreliable in plain terminals, so duration is the proxy for “the human probably looked away”.
50 51 52 53 54 |
# File 'lib/rubino/ui/notifier.rb', line 50 def turn_finished(seconds) return if seconds.nil? || seconds.to_f < min_turn_seconds notify(:turn_finished, "turn finished after #{seconds.to_i}s") end |