legion-tty 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/legion/tty/app.rb +40 -3
- data/lib/legion/tty/components/input_bar.rb +10 -0
- data/lib/legion/tty/components/message_stream.rb +25 -2
- data/lib/legion/tty/screens/chat.rb +2 -4
- data/lib/legion/tty/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdea3050f87e6b47ea8d03d9d3575acdecb2a3968c120cf3f9302068d21a88d9
|
|
4
|
+
data.tar.gz: d90acad8892d80dfab3ba292cb024a969037a4ddbde376535c26418d17cfb5a3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9bc985a693932f6cd1ec9682306dc55eb282c3d7bc6e430305afdeb05962c2bd2c64d70ad6efba4177bf895519a3636ef659508c8182ead8b63410d5bd8bf78
|
|
7
|
+
data.tar.gz: cecd20a8b7842d4c41671458853a030a45e4276b00be917de6578c8c982024ef8c1b6a333ca43ffbbd7f48850c72416e78f47b4217420570dccf721d0ed8d691
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.3] - 2026-04-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Scroll performance** — `MessageStream` now caches rendered lines per message keyed on `(object_id, content_hash, role, width, reactions, annotations, tags, pinned)`; cache invalidates automatically on content change (streaming), display option change, or width change — markdown re-parsing only runs for new/changed messages (closes #23)
|
|
7
|
+
- **Input coalescing** — `App#run_loop` drains all queued keys before the next `render_frame`, so rapid typing no longer triggers O(total-messages) renders per keystroke (closes #23)
|
|
8
|
+
- **PgUp/PgDn scroll step** — now scrolls by half-viewport (`(height-3)/2`) instead of a fixed 10 lines, matching Ctrl+B/Ctrl+F behavior (closes #23)
|
|
9
|
+
|
|
3
10
|
## [0.5.1] - 2026-04-18
|
|
4
11
|
|
|
5
12
|
### Fixed
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -40,6 +40,8 @@ module Legion
|
|
|
40
40
|
DISABLE_ALT_SCREEN = "\e[?1049l"
|
|
41
41
|
ENABLE_MOUSE = "\e[?1000h\e[?1006h"
|
|
42
42
|
DISABLE_MOUSE = "\e[?1000h\e[?1006l"
|
|
43
|
+
ENABLE_BRACKETED_PASTE = "\e[?2004h"
|
|
44
|
+
DISABLE_BRACKETED_PASTE = "\e[?2004l"
|
|
43
45
|
SGR_MOUSE_RE = /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
|
|
44
46
|
|
|
45
47
|
attr_reader :config, :credentials, :screen_manager, :hotkeys, :llm_chat, :input_bar
|
|
@@ -158,7 +160,7 @@ module Legion
|
|
|
158
160
|
|
|
159
161
|
# --- Event Loop ---
|
|
160
162
|
|
|
161
|
-
# rubocop:disable Metrics/AbcSize
|
|
163
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
162
164
|
def run_loop
|
|
163
165
|
require 'io/console'
|
|
164
166
|
|
|
@@ -166,6 +168,7 @@ module Legion
|
|
|
166
168
|
@raw_mode = true
|
|
167
169
|
$stdout.print ENABLE_ALT_SCREEN
|
|
168
170
|
$stdout.print ENABLE_MOUSE
|
|
171
|
+
$stdout.print ENABLE_BRACKETED_PASTE
|
|
169
172
|
$stdout.print cursor.hide
|
|
170
173
|
$stdout.print cursor.clear_screen
|
|
171
174
|
|
|
@@ -178,25 +181,38 @@ module Legion
|
|
|
178
181
|
|
|
179
182
|
key = normalize_key(raw_key)
|
|
180
183
|
dispatch_key(key)
|
|
184
|
+
drain_burst_keys(raw_in)
|
|
181
185
|
end
|
|
182
186
|
end
|
|
183
187
|
rescue Interrupt
|
|
184
188
|
nil
|
|
185
189
|
ensure
|
|
186
190
|
@raw_mode = false
|
|
191
|
+
$stdout.print DISABLE_BRACKETED_PASTE
|
|
187
192
|
$stdout.print DISABLE_MOUSE
|
|
188
193
|
$stdout.print cursor.show
|
|
189
194
|
$stdout.print DISABLE_ALT_SCREEN
|
|
190
195
|
shutdown
|
|
191
196
|
end
|
|
192
|
-
# rubocop:enable Metrics/AbcSize
|
|
197
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
193
198
|
|
|
194
199
|
def needs_refresh?
|
|
195
200
|
active = @screen_manager.active_screen
|
|
196
201
|
active.respond_to?(:streaming?) && active.streaming?
|
|
197
202
|
end
|
|
198
203
|
|
|
204
|
+
def drain_burst_keys(raw_in)
|
|
205
|
+
while @running && raw_in.wait_readable(0)
|
|
206
|
+
burst_key = read_raw_key(raw_in, timeout: 0)
|
|
207
|
+
break unless burst_key
|
|
208
|
+
|
|
209
|
+
dispatch_key(normalize_key(burst_key))
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
199
213
|
def normalize_key(raw)
|
|
214
|
+
return raw if raw.is_a?(Hash)
|
|
215
|
+
|
|
200
216
|
mouse = parse_sgr_mouse(raw)
|
|
201
217
|
return mouse if mouse
|
|
202
218
|
|
|
@@ -205,6 +221,7 @@ module Legion
|
|
|
205
221
|
|
|
206
222
|
# --- Key Dispatch ---
|
|
207
223
|
|
|
224
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
208
225
|
def dispatch_key(key)
|
|
209
226
|
if key == :ctrl_c
|
|
210
227
|
@running = false
|
|
@@ -225,7 +242,7 @@ module Legion
|
|
|
225
242
|
active = @screen_manager.active_screen
|
|
226
243
|
return unless active
|
|
227
244
|
|
|
228
|
-
if %i[scroll_up scroll_down].include?(key)
|
|
245
|
+
if key.is_a?(Symbol) && %i[scroll_up scroll_down].include?(key)
|
|
229
246
|
dispatch_to_screen(active, key)
|
|
230
247
|
return
|
|
231
248
|
end
|
|
@@ -236,6 +253,7 @@ module Legion
|
|
|
236
253
|
dispatch_to_screen(active, key)
|
|
237
254
|
end
|
|
238
255
|
end
|
|
256
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
239
257
|
|
|
240
258
|
def dispatch_to_input_screen(screen, key)
|
|
241
259
|
result = @input_bar.handle_key(key)
|
|
@@ -311,9 +329,28 @@ module Legion
|
|
|
311
329
|
seq << c
|
|
312
330
|
break if c.ord.between?(0x40, 0x7E)
|
|
313
331
|
end
|
|
332
|
+
|
|
333
|
+
return read_paste_content(io) if seq == "\e[200~"
|
|
334
|
+
|
|
314
335
|
seq
|
|
315
336
|
end
|
|
316
337
|
|
|
338
|
+
def read_paste_content(io)
|
|
339
|
+
buf = +''
|
|
340
|
+
end_marker = "\e[201~"
|
|
341
|
+
loop do
|
|
342
|
+
break unless io.wait_readable(1)
|
|
343
|
+
|
|
344
|
+
c = io.getc
|
|
345
|
+
break unless c
|
|
346
|
+
|
|
347
|
+
buf << c
|
|
348
|
+
break if buf.end_with?(end_marker)
|
|
349
|
+
end
|
|
350
|
+
buf.delete_suffix!(end_marker)
|
|
351
|
+
{ paste: buf }
|
|
352
|
+
end
|
|
353
|
+
|
|
317
354
|
def parse_sgr_mouse(raw)
|
|
318
355
|
match = SGR_MOUSE_RE.match(raw)
|
|
319
356
|
return nil unless match
|
|
@@ -32,6 +32,8 @@ module Legion
|
|
|
32
32
|
|
|
33
33
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
34
34
|
def handle_key(key)
|
|
35
|
+
return handle_paste(key[:paste]) if key.is_a?(Hash) && key.key?(:paste)
|
|
36
|
+
|
|
35
37
|
case key
|
|
36
38
|
when :enter
|
|
37
39
|
submit_line
|
|
@@ -202,6 +204,14 @@ module Legion
|
|
|
202
204
|
:handled
|
|
203
205
|
end
|
|
204
206
|
|
|
207
|
+
def handle_paste(text)
|
|
208
|
+
sanitized = text.to_s.gsub(/\r\n?/, "\n").gsub("\n", ' ')
|
|
209
|
+
@buffer.insert(@cursor_pos, sanitized)
|
|
210
|
+
@cursor_pos += sanitized.length
|
|
211
|
+
@tab_matches = []
|
|
212
|
+
:handled
|
|
213
|
+
end
|
|
214
|
+
|
|
205
215
|
def insert_char(key)
|
|
206
216
|
return :pass unless key.is_a?(String) && key.length == 1 && key.ord >= 32
|
|
207
217
|
|
|
@@ -28,10 +28,13 @@ module Legion
|
|
|
28
28
|
@show_numbers = false
|
|
29
29
|
@colorize = true
|
|
30
30
|
@show_timestamps = true
|
|
31
|
+
@line_cache = {}
|
|
32
|
+
@cache_options_hash = nil
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def add_message(role:, content:)
|
|
34
36
|
@messages << { role: role, content: content, tool_panels: [], timestamp: Time.now }
|
|
37
|
+
compact_cache if @line_cache.size > @messages.size * 2
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def append_streaming(text)
|
|
@@ -87,15 +90,35 @@ module Legion
|
|
|
87
90
|
|
|
88
91
|
private
|
|
89
92
|
|
|
90
|
-
def build_all_lines(width)
|
|
93
|
+
def build_all_lines(width) # rubocop:disable Metrics/AbcSize
|
|
94
|
+
current_opts = options_hash
|
|
95
|
+
if current_opts != @cache_options_hash
|
|
96
|
+
@line_cache.clear
|
|
97
|
+
@cache_options_hash = current_opts
|
|
98
|
+
end
|
|
99
|
+
|
|
91
100
|
filtered_messages.each_with_index.flat_map do |msg, idx|
|
|
92
101
|
next [] if @mute_system && msg[:role] == :system
|
|
93
102
|
next [] if @silent_mode && msg[:role] == :assistant
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
cache_key = message_cache_key(msg, width)
|
|
105
|
+
@line_cache[cache_key] ||= render_message(msg, width, @show_numbers ? idx + 1 : nil)
|
|
96
106
|
end
|
|
97
107
|
end
|
|
98
108
|
|
|
109
|
+
def options_hash
|
|
110
|
+
[@wrap_width, @show_numbers, @colorize, @show_timestamps, @highlights, @truncate_limit].hash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def message_cache_key(msg, width)
|
|
114
|
+
[msg.object_id, msg[:content], msg[:role], width,
|
|
115
|
+
msg[:reactions], msg[:annotations], msg[:tags], msg[:pinned]].hash
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def compact_cache
|
|
119
|
+
@line_cache.clear
|
|
120
|
+
end
|
|
121
|
+
|
|
99
122
|
def filtered_messages
|
|
100
123
|
return @messages if @filter.nil?
|
|
101
124
|
|
|
@@ -208,12 +208,10 @@ module Legion
|
|
|
208
208
|
|
|
209
209
|
def handle_scroll_key(key)
|
|
210
210
|
case key
|
|
211
|
-
when :page_up
|
|
212
|
-
when :page_down
|
|
211
|
+
when :page_up, :ctrl_b then @message_stream.scroll_up(half_page_lines)
|
|
212
|
+
when :page_down, :ctrl_f then @message_stream.scroll_down(half_page_lines)
|
|
213
213
|
when :scroll_up then @message_stream.scroll_up(3)
|
|
214
214
|
when :scroll_down then @message_stream.scroll_down(3)
|
|
215
|
-
when :ctrl_b then @message_stream.scroll_up(half_page_lines)
|
|
216
|
-
when :ctrl_f then @message_stream.scroll_down(half_page_lines)
|
|
217
215
|
when :home then @message_stream.scroll_up(@message_stream.messages.size * 5)
|
|
218
216
|
when :end then @message_stream.scroll_down(@message_stream.scroll_offset)
|
|
219
217
|
else return nil
|
data/lib/legion/tty/version.rb
CHANGED