legion-tty 0.4.16 → 0.4.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70299176c70666c5c4a4e7a1e6ac8ae1a03050525e299b7fcc4cf6a4cb8146c4
4
- data.tar.gz: c3cf40f5acfa8e14da112380c93931340985d937aafd6667c638bdb4b3ed0913
3
+ metadata.gz: 6faa2820fd40c949a40eb6e336cf75067e3ab400d9631980fc49fda2f0b5dc29
4
+ data.tar.gz: 35c674132cb7f41f0c2c8d4150afe10c99f4ac30fed392e78477fb074c383d67
5
5
  SHA512:
6
- metadata.gz: 8df88d8c600985633a63d34c18c3d1366dd9868d4109c2240c7522fa905511d04d865a8a259af71009840571ec627dae22f0def660043ecbc63701a4a0564dcb
7
- data.tar.gz: 7b607c3304efb10d94866d23eabd122b1aee84e0cc6cb8ecd7329129ad6b18277867b4e74825788d0bbc171563ea6a60c73d75fd2bdeeb7be1e9227ec26515a0
6
+ metadata.gz: 3766b9312e3fa871e39cfc1b42eac013c82ab680509f10f6f034474fda23381d1bfa4bc67e9e7a95d1557ca546ae3cd6d209818d4fe814111b33cc35b167f46f
7
+ data.tar.gz: c0644da99c0c8cd8b0002366e2e9f46edf23d99a157a8e5652165e3112442516da2178e16ca91596afdbef04a57fe379a347bd6f5c6883903bae24f599a294dd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.18] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/focus` command: toggle minimal UI mode (hides status bar for distraction-free writing)
7
+ - `/retry` command: resend last user message to LLM, replacing previous assistant response
8
+ - `/merge <session>` command: import messages from another saved session into current conversation
9
+ - `/sort [length|role]` command: display messages sorted by character length or grouped by role
10
+
11
+ ## [0.4.17] - 2026-03-19
12
+
13
+ ### Added
14
+ - `/template` command: 8 predefined prompt templates (explain, review, summarize, refactor, test, debug, translate, compare)
15
+ - `/fav` and `/favs` commands: persistent favorites saved to `~/.legionio/favorites.json`
16
+ - `/log [N]` command: view last N lines of boot log (default 20)
17
+ - `/version` command: show legion-tty version, Ruby version, and platform
18
+
3
19
  ## [0.4.16] - 2026-03-19
4
20
 
5
21
  ### Added
@@ -6,8 +6,49 @@ module Legion
6
6
  class Chat < Base
7
7
  # rubocop:disable Metrics/ModuleLength
8
8
  module CustomCommands
9
+ TEMPLATES = {
10
+ 'explain' => 'Explain the following concept in simple terms: ',
11
+ 'review' => "Review this code for bugs, security issues, and improvements:\n```\n",
12
+ 'summarize' => "Summarize the following text in 3 bullet points:\n",
13
+ 'refactor' => "Refactor this code for readability and performance:\n```\n",
14
+ 'test' => "Write unit tests for this code:\n```\n",
15
+ 'debug' => "Help me debug this error:\n",
16
+ 'translate' => 'Translate the following to ',
17
+ 'compare' => "Compare and contrast the following:\n"
18
+ }.freeze
19
+
9
20
  private
10
21
 
22
+ # rubocop:disable Metrics/MethodLength
23
+ def handle_template(input)
24
+ name = input.split(nil, 2)[1]
25
+ unless name
26
+ lines = TEMPLATES.map { |k, v| " #{k}: #{v[0, 60]}" }
27
+ @message_stream.add_message(
28
+ role: :system,
29
+ content: "Available templates (#{TEMPLATES.size}):\n#{lines.join("\n")}\n\nUsage: /template <name>"
30
+ )
31
+ return :handled
32
+ end
33
+
34
+ template = TEMPLATES[name]
35
+ unless template
36
+ available = TEMPLATES.keys.join(', ')
37
+ @message_stream.add_message(
38
+ role: :system,
39
+ content: "Template '#{name}' not found. Available: #{available}"
40
+ )
41
+ return :handled
42
+ end
43
+
44
+ @message_stream.add_message(
45
+ role: :system,
46
+ content: "Template '#{name}':\n#{template}"
47
+ )
48
+ :handled
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+
11
52
  def handle_alias(input)
12
53
  parts = input.split(nil, 3)
13
54
  if parts.size < 2
@@ -298,6 +298,120 @@ module Legion
298
298
  )
299
299
  end
300
300
  end
301
+
302
+ def handle_sort(input)
303
+ arg = input.split(nil, 2)[1]
304
+ if arg == 'role'
305
+ sort_by_role
306
+ else
307
+ sort_by_length
308
+ end
309
+ :handled
310
+ end
311
+
312
+ def sort_by_length
313
+ msgs = @message_stream.messages
314
+ if msgs.empty?
315
+ @message_stream.add_message(role: :system, content: 'No messages to sort.')
316
+ return
317
+ end
318
+
319
+ sorted = msgs.sort_by { |m| -m[:content].to_s.length }.first(10)
320
+ lines = sorted.map { |m| format_length_line(m) }
321
+ @message_stream.add_message(
322
+ role: :system,
323
+ content: "Messages by length (top #{sorted.size}):\n#{lines.join("\n")}"
324
+ )
325
+ end
326
+
327
+ def format_length_line(msg)
328
+ len = msg[:content].to_s.length
329
+ preview = truncate_text(msg[:content].to_s, 60)
330
+ " [#{msg[:role]}] (#{len} chars) #{preview}"
331
+ end
332
+
333
+ def sort_by_role
334
+ msgs = @message_stream.messages
335
+ if msgs.empty?
336
+ @message_stream.add_message(role: :system, content: 'No messages to sort.')
337
+ return
338
+ end
339
+
340
+ counts = msgs.group_by { |m| m[:role] }
341
+ .transform_values(&:size)
342
+ .sort_by { |_, count| -count }
343
+ lines = counts.map { |role, count| " #{role}: #{count}" }
344
+ @message_stream.add_message(
345
+ role: :system,
346
+ content: "Messages by role:\n#{lines.join("\n")}"
347
+ )
348
+ end
349
+
350
+ def favorites_file
351
+ File.expand_path('~/.legionio/favorites.json')
352
+ end
353
+
354
+ def load_favorites
355
+ return [] unless File.exist?(favorites_file)
356
+
357
+ raw = File.read(favorites_file)
358
+ parsed = ::JSON.parse(raw, symbolize_names: true)
359
+ parsed.is_a?(Array) ? parsed : []
360
+ rescue StandardError
361
+ []
362
+ end
363
+
364
+ def save_favorites(favs)
365
+ require 'fileutils'
366
+ FileUtils.mkdir_p(File.dirname(favorites_file))
367
+ File.write(favorites_file, ::JSON.generate(favs))
368
+ end
369
+
370
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
371
+ def handle_fav(input)
372
+ idx_str = input.split(nil, 2)[1]
373
+ msg = if idx_str
374
+ @message_stream.messages[idx_str.to_i]
375
+ else
376
+ @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
377
+ end
378
+ unless msg
379
+ @message_stream.add_message(role: :system, content: 'No message to favorite.')
380
+ return :handled
381
+ end
382
+
383
+ @favorites ||= []
384
+ entry = {
385
+ role: msg[:role],
386
+ content: msg[:content].to_s,
387
+ saved_at: Time.now.iso8601,
388
+ session: @session_name
389
+ }
390
+ @favorites << entry
391
+ save_favorites(load_favorites + [entry])
392
+ preview = truncate_text(msg[:content].to_s, 60)
393
+ @message_stream.add_message(role: :system, content: "Favorited: #{preview}")
394
+ :handled
395
+ end
396
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
397
+
398
+ def handle_favs
399
+ all_favs = load_favorites
400
+ if all_favs.empty?
401
+ @message_stream.add_message(role: :system, content: 'No favorites saved.')
402
+ return :handled
403
+ end
404
+
405
+ lines = all_favs.each_with_index.map do |fav, i|
406
+ saved = fav[:saved_at] || ''
407
+ " #{i + 1}. [#{fav[:role]}] #{truncate_text(fav[:content].to_s, 60)} (#{saved})"
408
+ end
409
+ @message_stream.add_message(
410
+ role: :system,
411
+ content: "Favorites (#{all_favs.size}):\n#{lines.join("\n")}"
412
+ )
413
+ :handled
414
+ end
301
415
  end
302
416
  # rubocop:enable Metrics/ModuleLength
303
417
  end
@@ -4,6 +4,7 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
+ # rubocop:disable Metrics/ModuleLength
7
8
  module ModelCommands
8
9
  private
9
10
 
@@ -116,7 +117,24 @@ module Legion
116
117
  @message_stream.add_message(role: :system, content: "Personality set to: #{name} (no active LLM)")
117
118
  end
118
119
  end
120
+
121
+ def handle_retry
122
+ unless @last_user_input
123
+ @message_stream.add_message(role: :system, content: 'Nothing to retry.')
124
+ return :handled
125
+ end
126
+
127
+ msgs = @message_stream.messages
128
+ last_assistant_idx = msgs.rindex { |m| m[:role] == :assistant }
129
+ msgs.delete_at(last_assistant_idx) if last_assistant_idx
130
+
131
+ @status_bar.notify(message: 'Retrying...', level: :info, ttl: 2)
132
+ @message_stream.add_message(role: :assistant, content: '')
133
+ send_to_llm(@last_user_input)
134
+ :handled
135
+ end
119
136
  end
137
+ # rubocop:enable Metrics/ModuleLength
120
138
  end
121
139
  end
122
140
  end
@@ -156,6 +156,29 @@ module Legion
156
156
  rescue StandardError
157
157
  nil
158
158
  end
159
+
160
+ def handle_merge(input)
161
+ name = input.split(nil, 2)[1]
162
+ unless name
163
+ @message_stream.add_message(role: :system, content: 'Usage: /merge <session-name>')
164
+ return :handled
165
+ end
166
+
167
+ data = @session_store.load(name)
168
+ unless data
169
+ @message_stream.add_message(role: :system, content: 'Session not found.')
170
+ return :handled
171
+ end
172
+
173
+ imported = data[:messages]
174
+ @message_stream.messages.concat(imported)
175
+ @status_bar.update(message_count: @message_stream.messages.size)
176
+ @message_stream.add_message(
177
+ role: :system,
178
+ content: "Merged #{imported.size} messages from '#{name}'."
179
+ )
180
+ :handled
181
+ end
159
182
  end
160
183
  # rubocop:enable Metrics/ModuleLength
161
184
  end
@@ -277,6 +277,42 @@ module Legion
277
277
  def format_stat_number(num)
278
278
  num.to_s.chars.reverse.each_slice(3).map(&:join).join(',').reverse
279
279
  end
280
+
281
+ def handle_log(input)
282
+ n = (input.split(nil, 2)[1] || '20').to_i.clamp(1, 500)
283
+ log_path = File.expand_path('~/.legionio/logs/tty-boot.log')
284
+ unless File.exist?(log_path)
285
+ @message_stream.add_message(role: :system, content: 'No boot log found.')
286
+ return :handled
287
+ end
288
+
289
+ lines = File.readlines(log_path, chomp: true).last(n)
290
+ @message_stream.add_message(
291
+ role: :system,
292
+ content: "Boot log (last #{lines.size} lines):\n#{lines.join("\n")}"
293
+ )
294
+ :handled
295
+ end
296
+
297
+ def handle_version
298
+ ruby_ver = RUBY_VERSION
299
+ platform = RUBY_PLATFORM
300
+ @message_stream.add_message(
301
+ role: :system,
302
+ content: "legion-tty v#{Legion::TTY::VERSION}\nRuby: #{ruby_ver}\nPlatform: #{platform}"
303
+ )
304
+ :handled
305
+ end
306
+
307
+ def handle_focus
308
+ @focus_mode = !@focus_mode
309
+ if @focus_mode
310
+ @status_bar.notify(message: 'Focus mode ON', level: :info, ttl: 2)
311
+ else
312
+ @status_bar.notify(message: 'Focus mode OFF', level: :info, ttl: 2)
313
+ end
314
+ :handled
315
+ end
280
316
  end
281
317
  # rubocop:enable Metrics/ModuleLength
282
318
  end
@@ -29,7 +29,9 @@ module Legion
29
29
  /hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
30
30
  /theme /search /grep /stats /personality /undo /history /pin /pins /rename
31
31
  /context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
32
- /wc /import /mute /autosave /react /macro /tag /tags /repeat /count].freeze
32
+ /wc /import /mute /autosave /react /macro /tag /tags /repeat /count
33
+ /template /fav /favs /log /version
34
+ /focus /retry /merge /sort].freeze
33
35
 
34
36
  PERSONALITIES = {
35
37
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -67,6 +69,8 @@ module Legion
67
69
  @recording_macro = nil
68
70
  @macro_buffer = []
69
71
  @last_command = nil
72
+ @focus_mode = false
73
+ @last_user_input = nil
70
74
  end
71
75
 
72
76
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -130,6 +134,7 @@ module Legion
130
134
  end
131
135
 
132
136
  def handle_user_message(input)
137
+ @last_user_input = input
133
138
  @message_stream.add_message(role: :user, content: input)
134
139
  if @plan_mode
135
140
  @message_stream.add_message(role: :system, content: '(bookmarked)')
@@ -159,16 +164,9 @@ module Legion
159
164
  end
160
165
 
161
166
  def render(width, height)
162
- bar_line = @status_bar.render(width: width)
163
- divider = Theme.c(:muted, '-' * width)
164
- dbg = debug_segment
165
- extra_rows = dbg ? 1 : 0
166
- stream_height = [height - 2 - extra_rows, 1].max
167
- stream_lines = @message_stream.render(width: width, height: stream_height)
168
- @status_bar.update(scroll: @message_stream.scroll_position)
169
- lines = stream_lines + [divider, bar_line]
170
- lines << dbg if dbg
171
- lines
167
+ return render_focus(width, height) if @focus_mode
168
+
169
+ render_normal(width, height)
172
170
  end
173
171
 
174
172
  def handle_input(key)
@@ -186,6 +184,25 @@ module Legion
186
184
 
187
185
  private
188
186
 
187
+ def render_focus(width, height)
188
+ stream_lines = @message_stream.render(width: width, height: [height, 1].max)
189
+ @status_bar.update(scroll: @message_stream.scroll_position)
190
+ stream_lines
191
+ end
192
+
193
+ def render_normal(width, height)
194
+ bar_line = @status_bar.render(width: width)
195
+ divider = Theme.c(:muted, '-' * width)
196
+ dbg = debug_segment
197
+ extra_rows = dbg ? 1 : 0
198
+ stream_height = [height - 2 - extra_rows, 1].max
199
+ stream_lines = @message_stream.render(width: width, height: stream_height)
200
+ @status_bar.update(scroll: @message_stream.scroll_position)
201
+ lines = stream_lines + [divider, bar_line]
202
+ lines << dbg if dbg
203
+ lines
204
+ end
205
+
189
206
  def record_macro_step(input, cmd, result)
190
207
  return unless @recording_macro
191
208
  return if cmd == '/macro'
@@ -379,6 +396,15 @@ module Legion
379
396
  when '/tags' then handle_tags(input)
380
397
  when '/repeat' then handle_repeat
381
398
  when '/count' then handle_count(input)
399
+ when '/template' then handle_template(input)
400
+ when '/fav' then handle_fav(input)
401
+ when '/favs' then handle_favs
402
+ when '/log' then handle_log(input)
403
+ when '/version' then handle_version
404
+ when '/focus' then handle_focus
405
+ when '/retry' then handle_retry
406
+ when '/merge' then handle_merge(input)
407
+ when '/sort' then handle_sort(input)
382
408
  else :handled
383
409
  end
384
410
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.16'
5
+ VERSION = '0.4.18'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.16
4
+ version: 0.4.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity