Class: Rubino::UI::InputHistory

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

Overview

Prompt history for the bottom composer, backed by the SAME store the old Reline idle prompt used (Reline::HISTORY) so continuity is preserved when the composer becomes the single idle input path — a session’s earlier entries (and anything Reline itself recorded) stay navigable.

Navigation model mirrors a shell / Reline: ↑ walks BACK toward older entries, ↓ walks FORWARD toward newer ones and finally back to the live draft the user was typing. The in-progress draft is stashed on the first ↑so ↓-ing all the way down restores exactly what the user had typed, never losing it.

Like LineInput#remember, consecutive duplicates are de-duped on push so a repeated command doesn’t clutter the ring.

PERSISTENCE (#2): like a shell (bash/zsh) and Hermes — which persists its input history to a .hermes_history file (see hermes_cli/profiles.py / profile_distribution.py) — rubino persists submitted lines to a plain-text file under RUBINO_HOME (default <RUBINO_HOME>/history, one entry per line) so they survive a restart. The file is LOADED into the ring at construction (composer/REPL startup) and each remembered line is APPENDED, capped to the last DEFAULT_CAP entries. EVERYTHING submitted is recorded — real prompts AND slash commands (/help, /agents, …) — matching the field standard (bash/zsh/Claude Code) where ↑ recalls the whole input line. All disk access is best-effort: a missing, unreadable or unwritable history file must never crash startup or a turn, so every file op is rescued and the in-memory ring keeps working.

Constant Summary collapse

DEFAULT_CAP =

Default number of most-recent entries kept on disk (and trimmed to on save). A shell-sized ring: large enough to recall across sessions, bounded so the file can’t grow without limit.

1000

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store: Reline::HISTORY, path: :default, cap: DEFAULT_CAP) ⇒ InputHistory

Returns a new instance of InputHistory.

Parameters:

  • store (#push, #to_a) (defaults to: Reline::HISTORY)

    the in-memory history ring (Reline::HISTORY by default, for continuity with the old idle prompt).

  • path (String, nil, :default) (defaults to: :default)

    the on-disk history file. When left :default, persistence is tied to the DEFAULT global store: the real chat ring (Reline::HISTORY) persists to <RUBINO_HOME>/history, while an INJECTED private store (tests / standalone) stays purely in-memory — so a private ring never reads/writes the shared file. Pass an explicit path to force persistence, or nil to force it off.

  • cap (Integer) (defaults to: DEFAULT_CAP)

    most-recent entries kept on disk.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rubino/ui/input_history.rb', line 57

def initialize(store: Reline::HISTORY, path: :default, cap: DEFAULT_CAP)
  @store  = store
  @path   = if path == :default
              store.equal?(Reline::HISTORY) ? self.class.default_path : nil
            else
              path
            end
  @cap    = cap
  # Cursor into the history ring. nil = "on the live draft" (not navigating
  # history). 0 = most recent entry, increasing = older.
  @index  = nil
  @draft  = nil
  load_from_disk
end

Class Method Details

.default_pathObject

Resolve the default history file under the SAME home the rest of rubino uses (RUBINO_HOME → ~/.rubino, via the config Loader), so an isolated or relocated home keeps its own history alongside config/.env/skills.



42
43
44
45
46
# File 'lib/rubino/ui/input_history.rb', line 42

def self.default_path
  File.join(Rubino::Config::Loader.default_home_path, "history")
rescue StandardError
  nil
end

Instance Method Details

#down(_current = nil) ⇒ Object

Move toward NEWER entries (↓). Returns the newer entry, or the stashed draft when stepping back below the newest entry, or nil when not currently navigating history (caller keeps the current buffer).



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rubino/ui/input_history.rb', line 113

def down(_current = nil)
  return nil if @index.nil?

  entries = to_a
  if @index.positive?
    @index -= 1
    entries[entries.size - 1 - @index]
  else
    # Stepped below the newest entry → back to the live draft.
    @index = nil
    d = @draft.to_s
    @draft = nil
    d
  end
end

True while the cursor is walking the history ring (not on the draft).

Returns:

  • (Boolean)


130
131
132
# File 'lib/rubino/ui/input_history.rb', line 130

def navigating?
  !@index.nil?
end

#remember(line) ⇒ Object

Append a submitted line, de-duping a consecutive duplicate (matches LineInput#remember). Blank lines are not recorded. Resets navigation so the next ↑ starts from the newest entry again. EVERYTHING typed is recorded — real prompts AND slash commands — so ↑ recalls the whole input line like bash/zsh/Claude Code (#2). Also appended to the on-disk history (best-effort) so it survives a restart.



78
79
80
81
82
83
84
85
86
87
# File 'lib/rubino/ui/input_history.rb', line 78

def remember(line)
  reset!
  return if line.nil?

  stripped = line.strip
  return if stripped.empty? || last == stripped

  @store.push(stripped)
  append_to_disk(stripped)
end

#reset!Object

Drop navigation state (called on submit / any direct edit so a fresh ↑starts from the newest entry and a typed edit isn’t treated as history).



136
137
138
139
# File 'lib/rubino/ui/input_history.rb', line 136

def reset!
  @index = nil
  @draft = nil
end

#up(current) ⇒ Object

Move toward OLDER entries (↑). current is the buffer the user is editing right now; it’s stashed as the draft on the first move up so ↓can restore it. Returns the entry to show, or nil when there’s nothing older (caller keeps the current buffer).



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rubino/ui/input_history.rb', line 93

def up(current)
  entries = to_a
  return nil if entries.empty?

  if @index.nil?
    # dup, not to_s: String#to_s returns self, so a later in-place
    # @buffer.replace by the caller would mutate the stashed draft too.
    @draft = current.to_s.dup
    @index = 0
  elsif @index < entries.size - 1
    @index += 1
  else
    return nil # already on the oldest entry — clamp
  end
  entries[entries.size - 1 - @index]
end