Class: Rubino::Commands::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/commands/executor.rb

Overview

Executes a slash command, rendering its template and feeding it to the agent.

‘runner:` (optional) is the live Agent::Runner for the interactive REPL. When present, `/status` and `/sessions` can read the current session / model straight off it. It is nil for non-interactive callers (and unit tests that don’t exercise those commands), in which case those commands degrade gracefully instead of raising.

Constant Summary collapse

MODEL_LIST_LIMIT =

How many model ids the bare ‘/model` listing renders before deferring the rest to the completion dropdown.

12
IMMEDIATE_WHILE_BUSY =

Local meta-commands SAFE to run IMMEDIATELY while a turn is active (the type-ahead QUEUE-by-default is overridden for these). Read-only/control only: they INSPECT or SIGNAL the running tree without mutating session / conversation / config / turn state, so they run on the composer’s reader thread concurrently with the turn thread without a race (output routes through the SAME render-mutex-serialized UI). /stop reuses the cancel machinery Esc / ‘–stop` use (already concurrent-safe). /reply is kept BLOCKED: its interactive form `/reply <id>` (-> @ui.ask) can’t be told apart by NAME from the safe inline form, and would steal the reader’s stdin (default-to-blocked on a concurrency hazard). Single source of truth for the busy-time classification — #busy_disposition reads it.

%w[agents tasks stop status jobs help commands dirs].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(loader: nil, ui: nil, runner: nil) ⇒ Executor

Returns a new instance of Executor.



56
57
58
59
60
# File 'lib/rubino/commands/executor.rb', line 56

def initialize(loader: nil, ui: nil, runner: nil)
  @loader = loader || Loader.new
  @ui = ui || Rubino.ui
  @runner = runner
end

Class Method Details

.welcome(runner: nil, ui: nil) ⇒ Object

Renders the welcome variant on first interactive boot. Best-effort: a welcome banner must never block the REPL from starting, so any assembler hiccup degrades to no banner rather than a crash. The boot header (workspace/branch/model) is printed by the chat command; this adds only the orientation, with no duplicate identity/session-id renderings.



99
100
101
102
103
# File 'lib/rubino/commands/executor.rb', line 99

def self.welcome(runner: nil, ui: nil)
  new(ui: ui, runner: runner).send(:show_welcome)
rescue StandardError
  nil
end

Instance Method Details

#busy_disposition(input) ⇒ Object

Classifies an input line for the BUSY (turn-active) input gate:

:immediate - a registered local meta-command in IMMEDIATE_WHILE_BUSY;
             the composer dispatches it NOW (does not queue) via
             #try_execute.
:blocked   - a registered local built-in NOT in the immediate set
             (state-mutating / turn-affecting); the composer neither
             queues nor runs it, and shows the not-available notice.
:pass      - not a recognized local built-in (free text, a `?` probe,
             a `!` shell escape, an @file line, an agent name, a custom
             .md command, an unknown slash): fall through to the normal
             QUEUE-by-default behavior, handled by the post-turn path
             exactly as today.


42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rubino/commands/executor.rb', line 42

def busy_disposition(input)
  return :pass unless @loader.slash_command?(input)

  name, = @loader.parse(input)
  return :pass unless name

  return :immediate if IMMEDIATE_WHILE_BUSY.include?(name)
  # Only a KNOWN local built-in is blocked; anything else passes through to
  # queue so the existing post-turn dispatch handles it unchanged.
  return :blocked if BuiltIns::NAMES.include?("/#{name}")

  :pass
end

#try_execute(input) ⇒ Object

Attempts to execute input as a slash command. Returns the rendered prompt if it’s a command, nil otherwise.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/rubino/commands/executor.rb', line 64

def try_execute(input)
  return nil unless @loader.slash_command?(input)

  name, arguments = @loader.parse(input)
  return nil unless name

  # Check built-in commands first
  built_in_result = handle_built_in(name, arguments)
  return built_in_result if built_in_result

  # Agent switching (#320): a bare `/<primary>` pins it, a `/<agent>
  # <message>` routes one turn to it. Resolved against the live registry so
  # built-in (build/plan/explore/general) AND user-registered agents are
  # reachable — checked BEFORE custom .md commands so an agent name wins.
  agent_result = agent_switch_handler.handle_command(name, arguments)
  return agent_result if agent_result

  # Look up custom command
  command = @loader.find(name)
  unless command
    @ui.error("unknown command: /#{name}")
    # "Did you mean /X?" on the closest known command before the full
    # roster (FRICTION-4) — the affordance Thor/git/bundler ship for typos.
    # Returns :handled so try_execute reports it handled (even if failed).
    return help_handler.suggest_and_list(name)
  end

  run_custom_command(command, name, arguments)
end