Class: Rubino::UI::PasteStore

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

Overview

The per-session PASTE store behind the composer’s file-backed paste pipeline (Hermes-style, two tiers).

A large bracketed paste does not flood the composer: the body is registered here and a single compact PLACEHOLDER token —“[Pasted text #1 +123 lines]” — is inserted into the editable buffer instead. The token rides the draft like normal text (editable around, history-recalled, queueable) and is EXPANDED to the full body only at the message-build seam, where the line leaves the composer for the agent loop (ChatCommand#run_turn): the model sees everything, while the transcript echo keeps the placeholder so scrollback stays clean.

Two tiers, both behind one placeholder shape:

* Tier 1 — PLACEHOLDER COLLAPSE: a paste longer than
  `paste.collapse_lines` lines (default 5) is held in memory and the
  token expands to the verbatim body at submit.
* Tier 2 — FILE OVERFLOW: a paste bigger than
  `paste.file_threshold_tokens` (default 8000, estimated at the same
  chars/4 rule Context::TokenBudget uses) is written to a session-
  scoped file — <RUBINO_HOME>/sessions/<id>/paste_N.txt — and the
  token expands to a one-line pointer telling the model to read the
  file with the read tool. The home sessions dir is where session
  artifacts already live, it never pollutes the workspace tree, and
  the read tool is deliberately un-sandboxed (only WRITES are gated
  by Workspace roots), so the model can read it from any cwd.

Lifecycle: a tier-1 body is consumed when its token is expanded into an outgoing message (re-submitting the line from history later leaves the literal placeholder, matching Hermes); tier-2 files persist for the session so the model can re-read them in later turns. Pastes at or under the collapse threshold never reach the store — they inline into the buffer exactly as before.

Constant Summary collapse

TOKEN_RE =

The placeholder shape, shared with the CompletionSource highlight and the composer’s whole-token backspace.

/\[Pasted text #\d+ \+\d+ lines\]/
DEFAULT_COLLAPSE_LINES =

Built-in fallbacks when config is missing/garbage.

5
DEFAULT_THRESHOLD_TOKENS =
8000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config: nil, session_source: nil) ⇒ PasteStore

Returns a new instance of PasteStore.

Parameters:

  • config (Config::Configuration, nil) (defaults to: nil)

    resolved lazily from Rubino.configuration when nil, so a long-lived store follows config reloads.

  • session_source (#call, String, nil) (defaults to: nil)

    the session id the tier-2 files are scoped under. A callable is resolved at WRITE time, so the chat loop can hand a closure over its (re-assignable) runner and /new //sessions //branch swaps are honored without re-wiring.



56
57
58
59
60
61
# File 'lib/rubino/ui/paste_store.rb', line 56

def initialize(config: nil, session_source: nil)
  @config         = config
  @session_source = session_source
  @entries        = {} # placeholder token => expansion text
  @counter        = 0
end

Instance Attribute Details

#session_source=(value) ⇒ Object (writeonly)

Late wiring for the session scope (see #initialize) — the chat command builds the store before the runner exists.



65
66
67
# File 'lib/rubino/ui/paste_store.rb', line 65

def session_source=(value)
  @session_source = value
end

Instance Method Details

#collapse?(body) ⇒ Boolean

True when body should collapse to a placeholder instead of inlining: strictly more lines than paste.collapse_lines.

Returns:

  • (Boolean)


69
70
71
# File 'lib/rubino/ui/paste_store.rb', line 69

def collapse?(body)
  body.to_s.lines.length > collapse_lines
end

#expand(text) ⇒ Object

Expands every registered placeholder in text to its stored body (tier 1) or file pointer (tier 2) — the message-build seam. Consumed entries are dropped (“cleared on submit”); unknown placeholder-shaped text is left verbatim, so user-typed literals are never rewritten.



89
90
91
92
93
# File 'lib/rubino/ui/paste_store.rb', line 89

def expand(text)
  return text unless text.is_a?(String) && @entries.keys.any? { |t| text.include?(t) }

  text.gsub(TOKEN_RE) { |token| @entries.delete(token) || token }
end

#expansions_in(text) ⇒ Object

The registered [token, body] pairs whose placeholder appears in text, CONSUMING them like #expand does (re-submitting from history later leaves the literal placeholder). Returned as an array of pairs so the tokens survive JSON round-trips intact (a token is not a valid symbol key). Empty array when text carries no live placeholder.



100
101
102
103
104
105
106
107
# File 'lib/rubino/ui/paste_store.rb', line 100

def expansions_in(text)
  return [] unless text.is_a?(String)

  text.scan(TOKEN_RE).uniq.filter_map do |token|
    body = @entries.delete(token)
    [token, body] if body
  end
end

#placeholder_span(buffer, cursor) ⇒ Object

The [start, length] (codepoint) span of the registered placeholder covering the char just BEFORE cursor in buffer, or nil. The composer’s backspace uses it to delete a placeholder WHOLE — a half-eaten token would neither read nor expand. Only spans the store actually registered qualify; lookalike text the user typed is edited char-by-char as usual.



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

def placeholder_span(buffer, cursor)
  return nil if @entries.empty? || buffer.nil?

  pos = 0
  while (m = TOKEN_RE.match(buffer, pos))
    start  = m.begin(0)
    length = m[0].length
    return [start, length] if @entries.key?(m[0]) && cursor > start && cursor <= start + length

    pos = start + length
  end
  nil
end

#register(body) ⇒ Object

Registers a pasted body and returns the placeholder token to insert into the buffer. Oversized bodies (tier 2) are written to the session paste file here, at paste time; their token expands to the file pointer instead of the content.



77
78
79
80
81
82
83
# File 'lib/rubino/ui/paste_store.rb', line 77

def register(body)
  body  = body.to_s
  n     = (@counter += 1)
  token = "[Pasted text ##{n} +#{body.lines.length} lines]"
  @entries[token] = oversize?(body) ? overflow_to_file(n, body) : body
  token
end