legion-tty 0.4.22 → 0.4.23

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: 45ef88ce519140b5056ce0b934f3c3512781a4f26e30dc74203c431e6f20cc3c
4
- data.tar.gz: ba632f83ea2022683c097b5451849bf129287c46959a050a8db90b356abe1f29
3
+ metadata.gz: f33a9d4d3640c42054bc91b873ec04898386dc635d5b3097e037c7695a3a9594
4
+ data.tar.gz: 469499bb265ededb47d2c1a03778174b8cae152a163808ba5a606b1aaa3b94ee
5
5
  SHA512:
6
- metadata.gz: affafac87b03a47e29ae08bfe5815ba9eb8f72de5fc3feda9350b2871021537f6319261177de1a20f349b8d38cda3295d6b5cca6ff941dbe9414744fa27562e5
7
- data.tar.gz: bf14853b34688cd959cf6301a56542ec9448fc9f4db2fb2489e7c6aeec3d56e08fc6e44c1627dc31c33631a8b9291717786032dee7ab72b566c545693ca2b57e
6
+ metadata.gz: 79090a1ed32486fa0e15f59312d34ee601c8cc92e8d596ccffda4cc7951cedd3d8461801b90669c1a34d22b32caa6c794836d7dc00633636d64c940dfc757633
7
+ data.tar.gz: 3ccb8fde2f3e285cbed4f06ddc46132d9f5fd45bad3c5cc5a80d22bca704c2a8e1e90bf799e8e7095228fe1c1330318156792392a67ddcaba4b3eea9f439d094
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.23] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/wrap [N|off]` command: set custom word wrap width for message display
7
+ - `/number [on|off]` command: toggle message numbering with `[N]` prefix
8
+ - `/echo <text>` command: add user-defined system messages (notes/markers)
9
+ - `/env` command: show environment info (Ruby version, platform, terminal, PID, Legion gems)
10
+ - `/speak [on|off]` command: toggle text-to-speech for assistant messages (macOS only, via `say`)
11
+ - `/silent` command: toggle silent mode (responses tracked but not displayed), `[SILENT]` indicator
12
+ - `/ls [path]` command: list directory contents with directory markers
13
+ - `/pwd` command: show current working directory
14
+
3
15
  ## [0.4.22] - 2026-03-19
4
16
 
5
17
  ### Added
@@ -9,7 +9,7 @@ module Legion
9
9
  # rubocop:disable Metrics/ClassLength
10
10
  class MessageStream
11
11
  attr_reader :messages, :scroll_offset
12
- attr_accessor :mute_system, :highlights, :filter, :truncate_limit
12
+ attr_accessor :mute_system, :silent_mode, :highlights, :filter, :truncate_limit, :wrap_width, :show_numbers
13
13
 
14
14
  HIGHLIGHT_COLOR = "\e[1;33m"
15
15
  HIGHLIGHT_RESET = "\e[0m"
@@ -18,7 +18,10 @@ module Legion
18
18
  @messages = []
19
19
  @scroll_offset = 0
20
20
  @mute_system = false
21
+ @silent_mode = false
21
22
  @highlights = []
23
+ @wrap_width = nil
24
+ @show_numbers = false
22
25
  end
23
26
 
24
27
  def add_message(role:, content:)
@@ -61,7 +64,8 @@ module Legion
61
64
  end
62
65
 
63
66
  def render(width:, height:)
64
- all_lines = build_all_lines(width)
67
+ effective_width = @wrap_width || width
68
+ all_lines = build_all_lines(effective_width)
65
69
  total = all_lines.size
66
70
  start_idx = [total - height - @scroll_offset, 0].max
67
71
  start_idx = [start_idx, total].min
@@ -77,10 +81,11 @@ module Legion
77
81
  private
78
82
 
79
83
  def build_all_lines(width)
80
- filtered_messages.flat_map do |msg|
84
+ filtered_messages.each_with_index.flat_map do |msg, idx|
81
85
  next [] if @mute_system && msg[:role] == :system
86
+ next [] if @silent_mode && msg[:role] == :assistant
82
87
 
83
- render_message(msg, width)
88
+ render_message(msg, width, @show_numbers ? idx + 1 : nil)
84
89
  end
85
90
  end
86
91
 
@@ -99,8 +104,17 @@ module Legion
99
104
  end
100
105
  end
101
106
 
102
- def render_message(msg, width)
103
- role_lines(msg, width) + panel_lines(msg, width)
107
+ def render_message(msg, width, number = nil)
108
+ lines = role_lines(msg, width) + panel_lines(msg, width)
109
+ prepend_number(lines, number)
110
+ end
111
+
112
+ def prepend_number(lines, number)
113
+ return lines unless number
114
+
115
+ lines.each_with_index.map do |line, i|
116
+ i == 1 ? "[#{number}] #{line}" : line
117
+ end
104
118
  end
105
119
 
106
120
  def role_lines(msg, width)
@@ -10,7 +10,7 @@ module Legion
10
10
  class StatusBar
11
11
  def initialize
12
12
  @state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false,
13
- debug_mode: false, message_count: 0, multiline: false }
13
+ debug_mode: false, message_count: 0, multiline: false, silent: false }
14
14
  @notifications = []
15
15
  end
16
16
 
@@ -42,6 +42,7 @@ module Legion
42
42
  [
43
43
  model_segment,
44
44
  plan_segment,
45
+ silent_segment,
45
46
  multiline_segment,
46
47
  debug_segment,
47
48
  thinking_segment,
@@ -70,6 +71,12 @@ module Legion
70
71
  Theme.c(:accent, '[ML]')
71
72
  end
72
73
 
74
+ def silent_segment
75
+ return nil unless @state[:silent]
76
+
77
+ Theme.c(:warning, '[SILENT]')
78
+ end
79
+
73
80
  def debug_segment
74
81
  return nil unless @state[:debug_mode]
75
82
 
@@ -132,6 +132,28 @@ module Legion
132
132
  send_to_llm(@last_user_input)
133
133
  :handled
134
134
  end
135
+
136
+ def handle_speak(input)
137
+ unless RUBY_PLATFORM =~ /darwin/
138
+ @message_stream.add_message(role: :system, content: 'Text-to-speech is only available on macOS.')
139
+ return :handled
140
+ end
141
+
142
+ arg = input.split(nil, 2)[1]&.strip&.downcase
143
+ case arg
144
+ when 'on'
145
+ @speak_mode = true
146
+ @message_stream.add_message(role: :system, content: 'Text-to-speech ON.')
147
+ when 'off'
148
+ @speak_mode = false
149
+ @message_stream.add_message(role: :system, content: 'Text-to-speech OFF.')
150
+ else
151
+ @speak_mode = !@speak_mode
152
+ state = @speak_mode ? 'ON' : 'OFF'
153
+ @message_stream.add_message(role: :system, content: "Text-to-speech #{state}.")
154
+ end
155
+ :handled
156
+ end
135
157
  end
136
158
  end
137
159
  end
@@ -471,10 +471,75 @@ module Legion
471
471
  end
472
472
  end
473
473
 
474
+ def handle_wrap(input)
475
+ arg = input.split(nil, 2)[1]&.strip
476
+ if arg.nil?
477
+ status = @message_stream.wrap_width ? "#{@message_stream.wrap_width} columns" : 'off'
478
+ @message_stream.add_message(role: :system, content: "Wrap: #{status}")
479
+ elsif arg == 'off'
480
+ @message_stream.wrap_width = nil
481
+ @message_stream.add_message(role: :system, content: 'Word wrap disabled.')
482
+ else
483
+ n = arg.to_i
484
+ if n >= 20
485
+ @message_stream.wrap_width = n
486
+ @message_stream.add_message(role: :system, content: "Word wrap set to #{n} columns.")
487
+ else
488
+ @message_stream.add_message(role: :system, content: 'Usage: /wrap [N|off]')
489
+ end
490
+ end
491
+ :handled
492
+ end
493
+
494
+ def handle_number(input)
495
+ arg = input.split(nil, 2)[1]&.strip
496
+ case arg
497
+ when 'on'
498
+ @message_stream.show_numbers = true
499
+ @message_stream.add_message(role: :system, content: 'Message numbering ON.')
500
+ when 'off'
501
+ @message_stream.show_numbers = false
502
+ @message_stream.add_message(role: :system, content: 'Message numbering OFF.')
503
+ else
504
+ @message_stream.show_numbers = !@message_stream.show_numbers
505
+ state = @message_stream.show_numbers ? 'ON' : 'OFF'
506
+ @message_stream.add_message(role: :system, content: "Message numbering #{state}.")
507
+ end
508
+ :handled
509
+ end
510
+
474
511
  def safe_calc_expr?(expr)
475
512
  CALC_SAFE_PATTERN.match?(expr) || CALC_MATH_PATTERN.match?(expr)
476
513
  end
477
514
 
515
+ def handle_echo(input)
516
+ text = input.split(nil, 2)[1]&.strip
517
+ unless text && !text.empty?
518
+ @message_stream.add_message(role: :system, content: 'Usage: /echo <text>')
519
+ return :handled
520
+ end
521
+
522
+ @message_stream.add_message(role: :system, content: text)
523
+ :handled
524
+ end
525
+
526
+ def handle_env
527
+ width = terminal_width
528
+ height = terminal_height
529
+ legion_gems = Gem::Specification.select { |s| s.name.start_with?('legion-', 'lex-') }
530
+ .map { |s| "#{s.name} #{s.version}" }
531
+ .sort
532
+ lines = [
533
+ "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})",
534
+ "Terminal: #{width}x#{height}",
535
+ "PID: #{::Process.pid}",
536
+ "TTY: legion-tty v#{Legion::TTY::VERSION}",
537
+ "Gems (#{legion_gems.size}): #{legion_gems.join(', ')}"
538
+ ]
539
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
540
+ :handled
541
+ end
542
+
478
543
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
479
544
  def handle_summary
480
545
  msgs = @message_stream.messages
@@ -541,6 +606,37 @@ module Legion
541
606
  rescue StandardError => e
542
607
  raise "command failed: #{e.message}"
543
608
  end
609
+
610
+ # rubocop:disable Metrics/AbcSize
611
+ def handle_ls(input)
612
+ path = File.expand_path(input.split(nil, 2)[1]&.strip || '.')
613
+ entries = Dir.entries(path).sort.reject { |e| ['.', '..'].include?(e) }
614
+ entries = entries.map { |e| File.directory?(File.join(path, e)) ? "#{e}/" : e }
615
+ @message_stream.add_message(role: :system, content: "#{path}:\n#{entries.join("\n")}")
616
+ :handled
617
+ rescue Errno::ENOENT, Errno::EACCES => e
618
+ @message_stream.add_message(role: :system, content: "ls: #{e.message}")
619
+ :handled
620
+ end
621
+ # rubocop:enable Metrics/AbcSize
622
+
623
+ def handle_pwd
624
+ @message_stream.add_message(role: :system, content: Dir.pwd)
625
+ :handled
626
+ end
627
+
628
+ def handle_silent
629
+ @silent_mode = !@silent_mode
630
+ @message_stream.silent_mode = @silent_mode
631
+ if @silent_mode
632
+ @status_bar.update(silent: true)
633
+ @message_stream.add_message(role: :system, content: 'Silent mode ON -- assistant responses hidden.')
634
+ else
635
+ @status_bar.update(silent: false)
636
+ @message_stream.add_message(role: :system, content: 'Silent mode OFF -- assistant responses visible.')
637
+ end
638
+ :handled
639
+ end
544
640
  end
545
641
  end
546
642
  end
@@ -37,7 +37,11 @@ module Legion
37
37
  /annotate /annotations /filter /truncate
38
38
  /tee /pipe
39
39
  /archive /archives
40
- /calc /rand].freeze
40
+ /calc /rand
41
+ /echo /env
42
+ /ls /pwd
43
+ /wrap /number
44
+ /speak /silent].freeze
41
45
 
42
46
  PERSONALITIES = {
43
47
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -79,6 +83,8 @@ module Legion
79
83
  @last_user_input = nil
80
84
  @highlights = []
81
85
  @multiline_mode = false
86
+ @speak_mode = false
87
+ @silent_mode = false
82
88
  end
83
89
 
84
90
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -244,20 +250,35 @@ module Legion
244
250
  send_via_direct(message)
245
251
  end
246
252
 
253
+ # rubocop:disable Metrics/AbcSize
247
254
  def send_via_direct(message)
248
255
  return unless @llm_chat
249
256
 
250
257
  @status_bar.update(thinking: true)
251
258
  render_screen
252
259
  start_time = Time.now
260
+ response_text = +''
253
261
  response = @llm_chat.ask(message) do |chunk|
254
262
  @status_bar.update(thinking: false)
255
- @message_stream.append_streaming(chunk.content) if chunk.content
263
+ if chunk.content
264
+ response_text << chunk.content
265
+ @message_stream.append_streaming(chunk.content)
266
+ end
256
267
  render_screen
257
268
  end
258
269
  record_response_time(Time.now - start_time)
259
270
  @status_bar.update(thinking: false)
260
271
  track_response_tokens(response)
272
+ speak_response(response_text) if @speak_mode
273
+ end
274
+ # rubocop:enable Metrics/AbcSize
275
+
276
+ def speak_response(text)
277
+ return unless RUBY_PLATFORM =~ /darwin/
278
+
279
+ ::Process.spawn('say', text[0..500], err: '/dev/null', out: '/dev/null')
280
+ rescue StandardError
281
+ nil
261
282
  end
262
283
 
263
284
  def record_response_time(elapsed)
@@ -448,6 +469,14 @@ module Legion
448
469
  when '/archives' then handle_archives
449
470
  when '/calc' then handle_calc(input)
450
471
  when '/rand' then handle_rand(input)
472
+ when '/echo' then handle_echo(input)
473
+ when '/env' then handle_env
474
+ when '/ls' then handle_ls(input)
475
+ when '/pwd' then handle_pwd
476
+ when '/wrap' then handle_wrap(input)
477
+ when '/number' then handle_number(input)
478
+ when '/speak' then handle_speak(input)
479
+ when '/silent' then handle_silent
451
480
  else :handled
452
481
  end
453
482
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.22'
5
+ VERSION = '0.4.23'
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.22
4
+ version: 0.4.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity