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_COLLAPSE_CHARS =

A paste longer than this many CHARS collapses to the chip even on a single line. ~80 cols × 5 rows ≈ the line-count trigger’s footprint, so a big one-line paste (a long URL/token/minified JSON) collapses just like a multi-line one instead of flooding the composer.

400
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.



61
62
63
64
65
66
# File 'lib/rubino/ui/paste_store.rb', line 61

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.



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

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, OR more CHARACTERS than paste.collapse_chars. The char trigger makes the chip fire CONSISTENTLY —a big one-line paste (a long URL, token, or minified JSON) has few/no newlines, so the line-count rule alone never collapsed it and it flooded the composer (#437 chip should trigger for any large paste).

Returns:

  • (Boolean)


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

def collapse?(body)
  s = body.to_s
  s.lines.length > collapse_lines || s.length > collapse_chars
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.



99
100
101
102
103
# File 'lib/rubino/ui/paste_store.rb', line 99

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.



110
111
112
113
114
115
116
117
# File 'lib/rubino/ui/paste_store.rb', line 110

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.



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/rubino/ui/paste_store.rb', line 125

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.



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

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