legion-tty 0.4.20 → 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: f2d9ef0fe2336f2750785f4742bc14661032836b05fab83062ba5de23c60cbbc
4
- data.tar.gz: cf9fe410cc87ba108cc52e994b5210351bb403b84aa43a27cbcb5ad94ff65232
3
+ metadata.gz: f33a9d4d3640c42054bc91b873ec04898386dc635d5b3097e037c7695a3a9594
4
+ data.tar.gz: 469499bb265ededb47d2c1a03778174b8cae152a163808ba5a606b1aaa3b94ee
5
5
  SHA512:
6
- metadata.gz: 194101f68a10f6e0c9b7a9c14d38690adcfe37724fcb5c2ee894d5a13b757a897aa3b4e85fe48e5db5b44b14f597ffffa9ce1a88ede2da54a1858ca42f759bc9
7
- data.tar.gz: 1eb5e7cafbdc7e1025fcdd8424256eea456c3b24f3d03077effd4417a694bddf5f77fd232b6514e89dad53d813161b2d753b6e9a4d7564c1b13ecea6dde1cd95
6
+ metadata.gz: 79090a1ed32486fa0e15f59312d34ee601c8cc92e8d596ccffda4cc7951cedd3d8461801b90669c1a34d22b32caa6c794836d7dc00633636d64c940dfc757633
7
+ data.tar.gz: 3ccb8fde2f3e285cbed4f06ddc46132d9f5fd45bad3c5cc5a80d22bca704c2a8e1e90bf799e8e7095228fe1c1330318156792392a67ddcaba4b3eea9f439d094
data/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
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
+
15
+ ## [0.4.22] - 2026-03-19
16
+
17
+ ### Added
18
+ - `/truncate [N|off]` command: display-only truncation of long messages (preserves originals)
19
+ - `/archive [name]` command: archive session to `~/.legionio/archives/` with timestamp and start fresh
20
+ - `/archives` command: list all archived sessions with file sizes
21
+ - `/tee <path>` command: copy new messages to file in real-time (like Unix tee)
22
+ - `/pipe <command>` command: pipe last assistant response through a shell command
23
+ - `/calc <expression>` command: safe math expression evaluator with Math functions
24
+ - `/rand [N|min..max]` command: generate random numbers (float, integer, or range)
25
+
26
+ ## [0.4.21] - 2026-03-19
27
+
28
+ ### Added
29
+ - `/annotate [N] <text>` command: add notes/annotations to specific messages with timestamps
30
+ - `/annotations` command: list all annotated messages with their notes
31
+ - `/filter [role|tag|pinned|clear]` command: filter displayed messages by role, tag, or pinned status
32
+ - `/multiline` command: toggle multi-line input mode (submit with empty line)
33
+ - `/export yaml` format: export chat history as YAML alongside existing md/json/html formats
34
+ - Annotation rendering in message stream (displayed after reactions)
35
+ - `[ML]` status bar indicator for multi-line input mode
36
+
3
37
  ## [0.4.20] - 2026-03-19
4
38
 
5
39
  ### Added
@@ -132,7 +132,7 @@ module Legion
132
132
 
133
133
  private
134
134
 
135
- def boot_legion_subsystems # rubocop:disable Metrics/MethodLength
135
+ def boot_legion_subsystems
136
136
  # Follow the same init order as Legion::Service:
137
137
  # 1. logging 2. settings 3. crypt 4. resolve secrets 5. LLM merge
138
138
  require 'legion/logging'
@@ -39,7 +39,7 @@ module Legion
39
39
  end
40
40
  # rubocop:enable Metrics/AbcSize
41
41
 
42
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
42
+ # rubocop:disable Metrics/AbcSize
43
43
  def run_async(queue, remotes: [], quick_profile: nil)
44
44
  Thread.new do
45
45
  @log&.log('github', "probing with #{remotes.size} remotes: #{remotes.first(5).inspect}")
@@ -59,7 +59,7 @@ module Legion
59
59
  queue.push({ type: :github_error, error: e.message })
60
60
  end
61
61
  end
62
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
62
+ # rubocop:enable Metrics/AbcSize
63
63
 
64
64
  private
65
65
 
@@ -282,7 +282,6 @@ module Legion
282
282
 
283
283
  # --- Token resolution ---
284
284
 
285
- # rubocop:disable Metrics/CyclomaticComplexity
286
285
  def resolve_token
287
286
  env_token = ENV.fetch('GITHUB_TOKEN', nil) ||
288
287
  ENV.fetch('GH_TOKEN', nil) ||
@@ -301,9 +300,7 @@ module Legion
301
300
  @log&.log('github', 'no token found (no env var, no gh CLI)')
302
301
  nil
303
302
  end
304
- # rubocop:enable Metrics/CyclomaticComplexity
305
303
 
306
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
307
304
  def token_from_gh_cli
308
305
  gh_path = `which gh 2>/dev/null`.strip
309
306
  return nil if gh_path.empty?
@@ -322,8 +319,6 @@ module Legion
322
319
  @log&.log('github', "gh CLI error: #{e.message}")
323
320
  nil
324
321
  end
325
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
326
-
327
322
  # --- HTTP ---
328
323
 
329
324
  def api_get(path)
@@ -62,7 +62,6 @@ module Legion
62
62
  query_ldap(username: username, host: dc_host, base_dn: base_dn)
63
63
  end
64
64
 
65
- # rubocop:disable Metrics/CyclomaticComplexity
66
65
  def discover_dc(realm)
67
66
  domain = realm.downcase
68
67
  srv_name = "_ldap._tcp.#{domain}"
@@ -74,7 +73,6 @@ module Legion
74
73
  @log&.log('kerberos', "SRV lookup failed: #{e.message}")
75
74
  nil
76
75
  end
77
- # rubocop:enable Metrics/CyclomaticComplexity
78
76
 
79
77
  def realm_to_base_dn(realm)
80
78
  realm.downcase.split('.').map { |part| "DC=#{part}" }.join(',')
@@ -149,7 +147,7 @@ module Legion
149
147
  }.compact
150
148
  end
151
149
 
152
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
150
+ # rubocop:disable Metrics/AbcSize
153
151
  def calculate_tenure(when_created)
154
152
  return nil unless when_created&.length&.>=(8)
155
153
 
@@ -184,7 +182,7 @@ module Legion
184
182
 
185
183
  { years: years, months: months, days: days }
186
184
  end
187
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
185
+ # rubocop:enable Metrics/AbcSize
188
186
 
189
187
  def days_in_month(month, year)
190
188
  Time.new(year, month, -1).day
@@ -93,7 +93,6 @@ module Legion
93
93
  false
94
94
  end
95
95
 
96
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
96
  def collect_repos(base, depth = 0)
98
97
  return [] unless File.directory?(base)
99
98
  return [build_repo_entry(base)] if File.directory?(File.join(base, '.git'))
@@ -110,7 +109,6 @@ module Legion
110
109
  rescue StandardError
111
110
  []
112
111
  end
113
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
114
112
 
115
113
  def build_repo_entry(path)
116
114
  { path: path, name: File.basename(path), remote: git_remote(path),
@@ -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
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,15 +81,40 @@ module Legion
77
81
  private
78
82
 
79
83
  def build_all_lines(width)
80
- @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
 
87
- def render_message(msg, width)
88
- role_lines(msg, width) + panel_lines(msg, width)
92
+ def filtered_messages
93
+ return @messages if @filter.nil?
94
+
95
+ case @filter[:type]
96
+ when :role
97
+ @messages.select { |m| m[:role].to_s == @filter[:value].to_s }
98
+ when :tag
99
+ @messages.select { |m| (m[:tags] || []).include?(@filter[:value]) }
100
+ when :pinned
101
+ @messages.select { |m| m[:pinned] }
102
+ else
103
+ @messages
104
+ end
105
+ end
106
+
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
89
118
  end
90
119
 
91
120
  def role_lines(msg, width)
@@ -104,6 +133,7 @@ module Legion
104
133
  content = apply_highlights(msg[:content].to_s)
105
134
  lines = ['', "#{header}: #{content}"]
106
135
  lines << reaction_line(msg) if msg[:reactions]&.any?
136
+ lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
107
137
  lines
108
138
  end
109
139
 
@@ -114,18 +144,34 @@ module Legion
114
144
  end
115
145
 
116
146
  def assistant_lines(msg, width)
117
- rendered = render_markdown(msg[:content], width)
147
+ content = display_content(msg[:content])
148
+ rendered = render_markdown(content, width)
118
149
  rendered = apply_highlights(rendered)
119
150
  lines = ['', *rendered.split("\n")]
120
151
  lines << reaction_line(msg) if msg[:reactions]&.any?
152
+ lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
121
153
  lines
122
154
  end
123
155
 
156
+ def display_content(content)
157
+ return content unless @truncate_limit
158
+ return content if content.to_s.length <= @truncate_limit
159
+
160
+ "#{content[0...@truncate_limit]}... [truncated]"
161
+ end
162
+
124
163
  def reaction_line(msg)
125
164
  reactions = msg[:reactions].map { |r| "[#{r}]" }.join(' ')
126
165
  " #{Theme.c(:muted, reactions)}"
127
166
  end
128
167
 
168
+ def annotation_lines(msg)
169
+ msg[:annotations].map do |a|
170
+ ts = a[:timestamp].to_s[11..15] || ''
171
+ " #{Theme.c(:muted, "note [#{ts}]: #{a[:text]}")}"
172
+ end
173
+ end
174
+
129
175
  def render_markdown(text, width)
130
176
  require_relative 'markdown_view'
131
177
  MarkdownView.render(text, width: width)
@@ -9,7 +9,7 @@ module Legion
9
9
  @current_model = current_model
10
10
  end
11
11
 
12
- def available_models # rubocop:disable Metrics/CyclomaticComplexity
12
+ def available_models
13
13
  return [] unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:settings)
14
14
 
15
15
  providers = Legion::LLM.settings[:providers]
@@ -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 }
13
+ debug_mode: false, message_count: 0, multiline: false, silent: false }
14
14
  @notifications = []
15
15
  end
16
16
 
@@ -42,6 +42,8 @@ module Legion
42
42
  [
43
43
  model_segment,
44
44
  plan_segment,
45
+ silent_segment,
46
+ multiline_segment,
45
47
  debug_segment,
46
48
  thinking_segment,
47
49
  notification_segment,
@@ -63,6 +65,18 @@ module Legion
63
65
  Theme.c(:warning, '[PLAN]')
64
66
  end
65
67
 
68
+ def multiline_segment
69
+ return nil unless @state[:multiline]
70
+
71
+ Theme.c(:accent, '[ML]')
72
+ end
73
+
74
+ def silent_segment
75
+ return nil unless @state[:silent]
76
+
77
+ Theme.c(:warning, '[SILENT]')
78
+ end
79
+
66
80
  def debug_segment
67
81
  return nil unless @state[:debug_mode]
68
82
 
@@ -4,7 +4,6 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module CustomCommands
9
8
  TEMPLATES = {
10
9
  'explain' => 'Explain the following concept in simple terms: ',
@@ -19,7 +18,6 @@ module Legion
19
18
 
20
19
  private
21
20
 
22
- # rubocop:disable Metrics/MethodLength
23
21
  def handle_template(input)
24
22
  name = input.split(nil, 2)[1]
25
23
  unless name
@@ -47,7 +45,6 @@ module Legion
47
45
  )
48
46
  :handled
49
47
  end
50
- # rubocop:enable Metrics/MethodLength
51
48
 
52
49
  def handle_alias(input)
53
50
  parts = input.split(nil, 3)
@@ -263,7 +260,6 @@ module Legion
263
260
  end
264
261
  # rubocop:enable Metrics/AbcSize
265
262
 
266
- # rubocop:disable Metrics/MethodLength
267
263
  def handle_macro(input)
268
264
  parts = input.split(nil, 3)
269
265
  subcommand = parts[1]
@@ -288,7 +284,6 @@ module Legion
288
284
  end
289
285
  :handled
290
286
  end
291
- # rubocop:enable Metrics/MethodLength
292
287
 
293
288
  def macro_record(name)
294
289
  unless name
@@ -378,7 +373,7 @@ module Legion
378
373
  end
379
374
  end
380
375
 
381
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
376
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
382
377
  def handle_chain(input)
383
378
  args = input.split(nil, 2)[1]
384
379
  unless args
@@ -412,9 +407,8 @@ module Legion
412
407
  )
413
408
  :handled
414
409
  end
415
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
410
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
416
411
  end
417
- # rubocop:enable Metrics/ModuleLength
418
412
  end
419
413
  end
420
414
  end
@@ -21,19 +21,22 @@ module Legion
21
21
 
22
22
  def build_export_path(input)
23
23
  format = input.split[1]&.downcase
24
- format = 'md' unless %w[json md html].include?(format)
24
+ format = 'md' unless %w[json md html yaml].include?(format)
25
25
  exports_dir = File.expand_path('~/.legionio/exports')
26
26
  FileUtils.mkdir_p(exports_dir)
27
27
  timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
28
- ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
28
+ ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html', 'yaml' => 'yaml' }[format]
29
29
  File.join(exports_dir, "chat-#{timestamp}.#{ext}")
30
30
  end
31
31
 
32
32
  def dispatch_export(path, format)
33
- if format == 'json'
33
+ case format
34
+ when 'json'
34
35
  export_json(path)
35
- elsif format == 'html'
36
+ when 'html'
36
37
  export_html(path)
38
+ when 'yaml'
39
+ export_yaml(path)
37
40
  else
38
41
  export_markdown(path)
39
42
  end
@@ -58,6 +61,17 @@ module Legion
58
61
  File.write(path, ::JSON.pretty_generate(data))
59
62
  end
60
63
 
64
+ def export_yaml(path)
65
+ require 'yaml'
66
+ data = {
67
+ 'exported_at' => Time.now.iso8601,
68
+ 'messages' => @message_stream.messages.map do |m|
69
+ { 'role' => m[:role].to_s, 'content' => m[:content], 'timestamp' => m[:timestamp]&.iso8601 }
70
+ end
71
+ }
72
+ File.write(path, ::YAML.dump(data))
73
+ end
74
+
61
75
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
62
76
  def export_html(path)
63
77
  lines = [
@@ -118,6 +132,35 @@ module Legion
118
132
  :handled
119
133
  end
120
134
  # rubocop:enable Metrics/AbcSize
135
+
136
+ def handle_tee(input)
137
+ arg = input.split(nil, 2)[1]
138
+ if arg.nil?
139
+ status = @tee_path ? "Tee active: #{@tee_path}" : 'Tee inactive.'
140
+ @message_stream.add_message(role: :system, content: status)
141
+ return :handled
142
+ end
143
+
144
+ if arg.strip == 'off'
145
+ @tee_path = nil
146
+ @message_stream.add_message(role: :system, content: 'Tee stopped.')
147
+ else
148
+ @tee_path = File.expand_path(arg.strip)
149
+ @message_stream.add_message(role: :system, content: "Tee started: #{@tee_path}")
150
+ end
151
+ :handled
152
+ rescue StandardError => e
153
+ @message_stream.add_message(role: :system, content: "Tee error: #{e.message}")
154
+ :handled
155
+ end
156
+
157
+ def tee_message(line)
158
+ return unless @tee_path
159
+
160
+ File.open(@tee_path, 'a') { |f| f.puts(line) }
161
+ rescue StandardError
162
+ nil
163
+ end
121
164
  end
122
165
  end
123
166
  end
@@ -4,7 +4,6 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module MessageCommands
9
8
  private
10
9
 
@@ -402,7 +401,7 @@ module Legion
402
401
  File.write(favorites_file, ::JSON.generate(favs))
403
402
  end
404
403
 
405
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
404
+ # rubocop:disable Metrics/AbcSize
406
405
  def handle_fav(input)
407
406
  idx_str = input.split(nil, 2)[1]
408
407
  msg = if idx_str
@@ -428,7 +427,51 @@ module Legion
428
427
  @message_stream.add_message(role: :system, content: "Favorited: #{preview}")
429
428
  :handled
430
429
  end
431
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
430
+ # rubocop:enable Metrics/AbcSize
431
+
432
+ # rubocop:disable Metrics/AbcSize
433
+ def handle_annotate(input)
434
+ parts = input.split(nil, 3)
435
+ if parts.size >= 3 && parts[1].match?(/\A\d+\z/)
436
+ idx = parts[1].to_i
437
+ text = parts[2]
438
+ msg = @message_stream.messages[idx]
439
+ elsif parts.size == 2 && !parts[1].match?(/\A\d+\z/)
440
+ text = parts[1]
441
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
442
+ elsif parts.size >= 3
443
+ text = parts[1..].join(' ')
444
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
445
+ else
446
+ @message_stream.add_message(role: :system, content: 'Usage: /annotate [N] <text>')
447
+ return :handled
448
+ end
449
+
450
+ unless msg
451
+ @message_stream.add_message(role: :system, content: 'No message to annotate.')
452
+ return :handled
453
+ end
454
+
455
+ msg[:annotations] ||= []
456
+ msg[:annotations] << { text: text, timestamp: Time.now.iso8601 }
457
+ @message_stream.add_message(role: :system, content: "Annotation added: #{text}")
458
+ :handled
459
+ end
460
+ # rubocop:enable Metrics/AbcSize
461
+
462
+ def handle_annotations(_input)
463
+ annotated = @message_stream.messages.each_with_index.select { |m, _| m[:annotations]&.any? }
464
+ if annotated.empty?
465
+ @message_stream.add_message(role: :system, content: 'No annotated messages.')
466
+ return :handled
467
+ end
468
+
469
+ lines = annotated.flat_map do |msg, idx|
470
+ msg[:annotations].map { |a| " [#{idx}][#{msg[:role]}] #{a[:text]} (#{a[:timestamp]})" }
471
+ end
472
+ @message_stream.add_message(role: :system, content: "Annotations:\n#{lines.join("\n")}")
473
+ :handled
474
+ end
432
475
 
433
476
  def handle_favs
434
477
  all_favs = load_favorites
@@ -447,8 +490,77 @@ module Legion
447
490
  )
448
491
  :handled
449
492
  end
493
+
494
+ def handle_filter(input)
495
+ parts = input.split(nil, 3)
496
+ subcommand = parts[1]
497
+ case subcommand
498
+ when 'role'
499
+ apply_role_filter(parts[2])
500
+ when 'tag'
501
+ apply_tag_filter(parts[2])
502
+ when 'pinned'
503
+ @message_stream.filter = { type: :pinned }
504
+ @message_stream.add_message(role: :system, content: 'Filter: pinned messages only.')
505
+ when 'clear'
506
+ @message_stream.filter = nil
507
+ @message_stream.add_message(role: :system, content: 'Filter cleared.')
508
+ when nil
509
+ show_filter_status
510
+ else
511
+ @message_stream.add_message(
512
+ role: :system,
513
+ content: 'Usage: /filter [role|tag|pinned|clear] [value]'
514
+ )
515
+ end
516
+ :handled
517
+ end
518
+
519
+ def apply_role_filter(value)
520
+ unless value
521
+ @message_stream.add_message(role: :system, content: 'Usage: /filter role <user|assistant|system>')
522
+ return
523
+ end
524
+
525
+ @message_stream.filter = { type: :role, value: value }
526
+ @message_stream.add_message(role: :system, content: "Filter: role=#{value}.")
527
+ end
528
+
529
+ def apply_tag_filter(value)
530
+ unless value
531
+ @message_stream.add_message(role: :system, content: 'Usage: /filter tag <label>')
532
+ return
533
+ end
534
+
535
+ @message_stream.filter = { type: :tag, value: value }
536
+ @message_stream.add_message(role: :system, content: "Filter: tag=#{value}.")
537
+ end
538
+
539
+ def show_filter_status
540
+ f = @message_stream.filter
541
+ content = if f.nil?
542
+ 'No active filter.'
543
+ elsif f[:type] == :pinned
544
+ 'Active filter: pinned.'
545
+ else
546
+ "Active filter: #{f[:type]}=#{f[:value]}."
547
+ end
548
+ @message_stream.add_message(role: :system, content: content)
549
+ end
550
+
551
+ def handle_truncate(input)
552
+ n = (input.split(nil, 2)[1] || '10').to_i.clamp(1, 500)
553
+ msgs = @message_stream.messages
554
+ if msgs.size <= n
555
+ @message_stream.add_message(role: :system, content: "Already #{msgs.size} messages (<=#{n}).")
556
+ return :handled
557
+ end
558
+
559
+ @message_stream.messages.replace(msgs.last(n))
560
+ @message_stream.add_message(role: :system, content: "Truncated to last #{n} messages.")
561
+ :handled
562
+ end
450
563
  end
451
- # rubocop:enable Metrics/ModuleLength
452
564
  end
453
565
  end
454
566
  end
@@ -4,7 +4,6 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module ModelCommands
9
8
  private
10
9
 
@@ -133,8 +132,29 @@ module Legion
133
132
  send_to_llm(@last_user_input)
134
133
  :handled
135
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
136
157
  end
137
- # rubocop:enable Metrics/ModuleLength
138
158
  end
139
159
  end
140
160
  end
@@ -4,7 +4,6 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module SessionCommands
9
8
  private
10
9
 
@@ -233,6 +232,34 @@ module Legion
233
232
  :handled
234
233
  end
235
234
 
235
+ def handle_archive(input)
236
+ name = input.split(nil, 2)[1] || "#{@session_name}-#{Time.now.strftime('%Y%m%d-%H%M%S')}"
237
+ archive_dir = File.expand_path('~/.legionio/archives')
238
+ FileUtils.mkdir_p(archive_dir)
239
+ path = File.join(archive_dir, "#{name}.json")
240
+ data = { name: name, messages: @message_stream.messages, archived_at: Time.now.iso8601 }
241
+ File.write(path, ::JSON.generate(data))
242
+ @message_stream.messages.clear
243
+ @message_stream.add_message(role: :system, content: "Session archived as: #{name}")
244
+ @status_bar.notify(message: "Archived: #{name}", level: :success, ttl: 3)
245
+ :handled
246
+ end
247
+
248
+ def handle_archives
249
+ archive_dir = File.expand_path('~/.legionio/archives')
250
+ files = Dir.glob(File.join(archive_dir, '*.json'))
251
+ if files.empty?
252
+ @message_stream.add_message(role: :system, content: 'No archives found.')
253
+ else
254
+ lines = files.sort.map do |f|
255
+ size = File.size(f)
256
+ " #{File.basename(f, '.json')} (#{size} bytes)"
257
+ end
258
+ @message_stream.add_message(role: :system, content: "Archives:\n#{lines.join("\n")}")
259
+ end
260
+ :handled
261
+ end
262
+
236
263
  def handle_merge(input)
237
264
  name = input.split(nil, 2)[1]
238
265
  unless name
@@ -256,7 +283,6 @@ module Legion
256
283
  :handled
257
284
  end
258
285
  end
259
- # rubocop:enable Metrics/ModuleLength
260
286
  end
261
287
  end
262
288
  end
@@ -4,7 +4,6 @@ module Legion
4
4
  module TTY
5
5
  module Screens
6
6
  class Chat < Base
7
- # rubocop:disable Metrics/ModuleLength
8
7
  module UiCommands
9
8
  TIPS = [
10
9
  'Press Tab after / to auto-complete commands',
@@ -31,10 +30,14 @@ module Legion
31
30
  'NAV : /dashboard /extensions /config /palette /hotkeys',
32
31
  'DISPLAY : /theme /plan /debug /context /time /uptime',
33
32
  'TOOLS : /tools /export /bookmark /pin /pins /alias /snippet /history',
33
+ 'UTILS : /calc /rand',
34
34
  '',
35
35
  'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
36
36
  ].freeze
37
37
 
38
+ CALC_SAFE_PATTERN = %r{\A[\d\s+\-*/.()%]*\z}
39
+ CALC_MATH_PATTERN = %r{\A[\d\s+\-*/.()%]*(Math\.\w+\([\d\s+\-*/.()%,]*\)[\d\s+\-*/.()%]*)*\z}
40
+
38
41
  private
39
42
 
40
43
  def handle_help
@@ -314,6 +317,39 @@ module Legion
314
317
  :handled
315
318
  end
316
319
 
320
+ def handle_truncate(input)
321
+ arg = input.split(nil, 2)[1]&.strip
322
+ if arg.nil?
323
+ status = @message_stream.truncate_limit ? "#{@message_stream.truncate_limit} chars" : 'off'
324
+ @message_stream.add_message(role: :system, content: "Truncation: #{status}")
325
+ elsif arg == 'off'
326
+ @message_stream.truncate_limit = nil
327
+ @message_stream.add_message(role: :system, content: 'Truncation disabled.')
328
+ else
329
+ limit = arg.to_i
330
+ if limit.positive?
331
+ @message_stream.truncate_limit = limit
332
+ @message_stream.add_message(role: :system, content: "Truncation set to #{limit} chars.")
333
+ else
334
+ @message_stream.add_message(role: :system, content: 'Usage: /truncate [N|off]')
335
+ end
336
+ end
337
+ :handled
338
+ end
339
+
340
+ def handle_multiline
341
+ @multiline_mode = !@multiline_mode
342
+ if @multiline_mode
343
+ @status_bar.update(multiline: true)
344
+ @message_stream.add_message(role: :system,
345
+ content: 'Multi-line mode ON. Submit with empty line.')
346
+ else
347
+ @status_bar.update(multiline: false)
348
+ @message_stream.add_message(role: :system, content: 'Multi-line mode OFF.')
349
+ end
350
+ :handled
351
+ end
352
+
317
353
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
318
354
  def handle_scroll(input)
319
355
  arg = input.split(nil, 2)[1]
@@ -390,7 +426,121 @@ module Legion
390
426
  @message_stream.add_message(role: :system, content: "Highlight added: '#{pattern}'")
391
427
  end
392
428
 
393
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
429
+ def handle_calc(input)
430
+ expr = input.split(nil, 2)[1]&.strip
431
+ unless expr
432
+ @message_stream.add_message(role: :system, content: 'Usage: /calc <expression>')
433
+ return :handled
434
+ end
435
+
436
+ unless safe_calc_expr?(expr)
437
+ @message_stream.add_message(role: :system, content: "Unsafe expression blocked: #{expr}")
438
+ return :handled
439
+ end
440
+
441
+ result = binding.send(:eval, expr)
442
+ @message_stream.add_message(role: :system, content: "= #{result}")
443
+ :handled
444
+ rescue SyntaxError, ZeroDivisionError, Math::DomainError => e
445
+ @message_stream.add_message(role: :system, content: "Error: #{e.message}")
446
+ :handled
447
+ end
448
+
449
+ def handle_rand(input)
450
+ arg = input.split(nil, 2)[1]&.strip
451
+ result = parse_rand_arg(arg)
452
+ if result == :invalid
453
+ @message_stream.add_message(role: :system, content: 'Usage: /rand [N|min..max]')
454
+ return :handled
455
+ end
456
+
457
+ @message_stream.add_message(role: :system, content: "Random: #{result}")
458
+ :handled
459
+ end
460
+
461
+ def parse_rand_arg(arg)
462
+ if arg.nil? || arg.empty?
463
+ rand
464
+ elsif arg.match?(/\A\d+\.\.\d+\z/)
465
+ parts = arg.split('..').map(&:to_i)
466
+ rand(parts[0]..parts[1])
467
+ elsif arg.match?(/\A\d+\z/)
468
+ rand(arg.to_i)
469
+ else
470
+ :invalid
471
+ end
472
+ end
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
+
511
+ def safe_calc_expr?(expr)
512
+ CALC_SAFE_PATTERN.match?(expr) || CALC_MATH_PATTERN.match?(expr)
513
+ end
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
+
543
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
394
544
  def handle_summary
395
545
  msgs = @message_stream.messages
396
546
  elapsed = Time.now - @session_start
@@ -423,9 +573,71 @@ module Legion
423
573
  @message_stream.add_message(role: :system, content: lines.join("\n"))
424
574
  :handled
425
575
  end
426
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
576
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
577
+
578
+ def handle_pipe(input)
579
+ cmd = input.split(nil, 2)[1]
580
+ unless cmd
581
+ @message_stream.add_message(role: :system, content: 'Usage: /pipe <shell command>')
582
+ return :handled
583
+ end
584
+
585
+ last_msg = @message_stream.messages.select { |m| m[:role] == :assistant }.last
586
+ unless last_msg
587
+ @message_stream.add_message(role: :system, content: 'No assistant message to pipe.')
588
+ return :handled
589
+ end
590
+
591
+ output = pipe_through_command(cmd, last_msg[:content].to_s)
592
+ @message_stream.add_message(role: :system, content: "pipe | #{cmd}:\n#{output}")
593
+ :handled
594
+ rescue StandardError => e
595
+ @message_stream.add_message(role: :system, content: "Pipe error: #{e.message}")
596
+ :handled
597
+ end
598
+
599
+ def pipe_through_command(cmd, content)
600
+ result = IO.popen(cmd, 'r+') do |io|
601
+ io.write(content)
602
+ io.close_write
603
+ io.read
604
+ end
605
+ result.to_s.chomp
606
+ rescue StandardError => e
607
+ raise "command failed: #{e.message}"
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
427
640
  end
428
- # rubocop:enable Metrics/ModuleLength
429
641
  end
430
642
  end
431
643
  end
@@ -33,7 +33,15 @@ module Legion
33
33
  /template /fav /favs /log /version
34
34
  /focus /retry /merge /sort
35
35
  /chain /info /scroll /summary
36
- /prompt /reset /replace /highlight].freeze
36
+ /prompt /reset /replace /highlight /multiline
37
+ /annotate /annotations /filter /truncate
38
+ /tee /pipe
39
+ /archive /archives
40
+ /calc /rand
41
+ /echo /env
42
+ /ls /pwd
43
+ /wrap /number
44
+ /speak /silent].freeze
37
45
 
38
46
  PERSONALITIES = {
39
47
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -74,6 +82,9 @@ module Legion
74
82
  @focus_mode = false
75
83
  @last_user_input = nil
76
84
  @highlights = []
85
+ @multiline_mode = false
86
+ @speak_mode = false
87
+ @silent_mode = false
77
88
  end
78
89
 
79
90
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -94,7 +105,7 @@ module Legion
94
105
  @running
95
106
  end
96
107
 
97
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
108
+ # rubocop:disable Metrics/AbcSize
98
109
  def run
99
110
  activate
100
111
  while @running
@@ -117,7 +128,7 @@ module Legion
117
128
  end
118
129
  end
119
130
  end
120
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
131
+ # rubocop:enable Metrics/AbcSize
121
132
 
122
133
  def handle_slash_command(input)
123
134
  return nil unless input.start_with?('/')
@@ -139,6 +150,7 @@ module Legion
139
150
  def handle_user_message(input)
140
151
  @last_user_input = input
141
152
  @message_stream.add_message(role: :user, content: input)
153
+ tee_message("[user] #{input}") if @tee_path
142
154
  if @plan_mode
143
155
  @message_stream.add_message(role: :system, content: '(bookmarked)')
144
156
  else
@@ -238,20 +250,35 @@ module Legion
238
250
  send_via_direct(message)
239
251
  end
240
252
 
253
+ # rubocop:disable Metrics/AbcSize
241
254
  def send_via_direct(message)
242
255
  return unless @llm_chat
243
256
 
244
257
  @status_bar.update(thinking: true)
245
258
  render_screen
246
259
  start_time = Time.now
260
+ response_text = +''
247
261
  response = @llm_chat.ask(message) do |chunk|
248
262
  @status_bar.update(thinking: false)
249
- @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
250
267
  render_screen
251
268
  end
252
269
  record_response_time(Time.now - start_time)
253
270
  @status_bar.update(thinking: false)
254
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
255
282
  end
256
283
 
257
284
  def record_response_time(elapsed)
@@ -339,12 +366,27 @@ module Legion
339
366
 
340
367
  def read_input
341
368
  return nil unless @input_bar.respond_to?(:read_line)
369
+ return read_multiline_input if @multiline_mode
342
370
 
343
371
  @input_bar.read_line
344
372
  rescue Interrupt
345
373
  nil
346
374
  end
347
375
 
376
+ def read_multiline_input
377
+ lines = []
378
+ loop do
379
+ line = @input_bar.read_line
380
+ return nil if line.nil? && lines.empty?
381
+ break if line.nil? || line.empty?
382
+
383
+ lines << line
384
+ end
385
+ lines.empty? ? nil : lines.join("\n")
386
+ rescue Interrupt
387
+ nil
388
+ end
389
+
348
390
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
349
391
  def dispatch_slash(cmd, input)
350
392
  case cmd
@@ -416,6 +458,25 @@ module Legion
416
458
  when '/reset' then handle_reset
417
459
  when '/replace' then handle_replace(input)
418
460
  when '/highlight' then handle_highlight(input)
461
+ when '/multiline' then handle_multiline
462
+ when '/annotate' then handle_annotate(input)
463
+ when '/annotations' then handle_annotations(input)
464
+ when '/filter' then handle_filter(input)
465
+ when '/truncate' then handle_truncate(input)
466
+ when '/tee' then handle_tee(input)
467
+ when '/pipe' then handle_pipe(input)
468
+ when '/archive' then handle_archive(input)
469
+ when '/archives' then handle_archives
470
+ when '/calc' then handle_calc(input)
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
419
480
  else :handled
420
481
  end
421
482
  end
@@ -500,6 +561,7 @@ module Legion
500
561
  "[DEBUG] msgs:#{@message_stream.messages.size} " \
501
562
  "scroll:#{@message_stream.scroll_position&.dig(:current) || 0} " \
502
563
  "plan:#{@plan_mode} " \
564
+ "multiline:#{@multiline_mode} " \
503
565
  "personality:#{@personality || 'default'} " \
504
566
  "aliases:#{@aliases.size} " \
505
567
  "snippets:#{@snippets.size} " \
@@ -105,7 +105,7 @@ module Legion
105
105
  @viewing_file = true
106
106
  end
107
107
 
108
- def edit_selected_key # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
108
+ def edit_selected_key # rubocop:disable Metrics/AbcSize
109
109
  keys = @file_data.keys
110
110
  return unless keys[@selected_key]
111
111
 
@@ -40,7 +40,7 @@ module Legion
40
40
 
41
41
  # rubocop:enable Metrics/AbcSize
42
42
 
43
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
43
+ # rubocop:disable Metrics/CyclomaticComplexity
44
44
  def handle_input(key)
45
45
  case key
46
46
  when 'r', :f5
@@ -66,7 +66,7 @@ module Legion
66
66
  end
67
67
  end
68
68
 
69
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
69
+ # rubocop:enable Metrics/CyclomaticComplexity
70
70
 
71
71
  def selected_panel
72
72
  PANELS[@selected_panel]
@@ -147,7 +147,7 @@ module Legion
147
147
 
148
148
  # rubocop:enable Metrics/AbcSize
149
149
 
150
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
150
+ # rubocop:disable Metrics/AbcSize
151
151
  def render_system_panel(_width)
152
152
  sys = @cached_data[:system] || {}
153
153
  prefix = panel_prefix(:system)
@@ -161,7 +161,7 @@ module Legion
161
161
  lines
162
162
  end
163
163
 
164
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
164
+ # rubocop:enable Metrics/AbcSize
165
165
 
166
166
  def render_activity_panel(_width, max_lines)
167
167
  activity = @cached_data[:activity] || []
@@ -219,7 +219,7 @@ module Legion
219
219
  :pass
220
220
  end
221
221
 
222
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
222
+ # rubocop:disable Metrics/AbcSize
223
223
  def llm_info
224
224
  info = { provider: 'none', model: nil, started: false, daemon: false }
225
225
  if defined?(Legion::LLM)
@@ -237,7 +237,7 @@ module Legion
237
237
  info
238
238
  end
239
239
 
240
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
240
+ # rubocop:enable Metrics/AbcSize
241
241
 
242
242
  def probe_services
243
243
  require 'socket'
@@ -195,7 +195,7 @@ module Legion
195
195
  end
196
196
  end
197
197
 
198
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
198
+ # rubocop:disable Metrics/AbcSize
199
199
  def run_gaia_awakening
200
200
  typed_output('Scanning for active cognition threads...')
201
201
  sleep 1.2
@@ -225,7 +225,7 @@ module Legion
225
225
 
226
226
  @output.puts
227
227
  end
228
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
228
+ # rubocop:enable Metrics/AbcSize
229
229
 
230
230
  def collect_background_results
231
231
  @log.log('collect', 'waiting for scanner results (10s timeout)')
@@ -438,7 +438,7 @@ module Legion
438
438
  end
439
439
  end
440
440
 
441
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
441
+ # rubocop:disable Metrics/AbcSize
442
442
  def run_intro_with_github
443
443
  gh = @github_quick
444
444
  name = gh[:name] || gh[:username]
@@ -462,7 +462,7 @@ module Legion
462
462
 
463
463
  @output.puts
464
464
  end
465
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
465
+ # rubocop:enable Metrics/AbcSize
466
466
 
467
467
  def collect_kerberos_identity
468
468
  @log.log('kerberos', 'collecting identity (2s timeout)')
@@ -475,7 +475,7 @@ module Legion
475
475
  end
476
476
  end
477
477
 
478
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
478
+ # rubocop:disable Metrics/AbcSize
479
479
  def run_intro_with_identity
480
480
  id = @kerberos_identity
481
481
  typed_output("I see you, #{id[:first_name]}.")
@@ -505,7 +505,7 @@ module Legion
505
505
  @output.puts
506
506
  @output.puts
507
507
  end
508
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
508
+ # rubocop:enable Metrics/AbcSize
509
509
 
510
510
  def ask_for_name
511
511
  if @kerberos_identity
@@ -515,7 +515,7 @@ module Legion
515
515
  end
516
516
  end
517
517
 
518
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
518
+ # rubocop:disable Metrics/AbcSize
519
519
  def identity_summary_lines
520
520
  return [] unless @kerberos_identity
521
521
 
@@ -529,7 +529,7 @@ module Legion
529
529
  lines << " Email: #{id[:email]}" if id[:email]
530
530
  lines
531
531
  end
532
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
532
+ # rubocop:enable Metrics/AbcSize
533
533
 
534
534
  def scan_summary_lines(scan_data)
535
535
  return [] unless scan_data.is_a?(Hash)
@@ -622,7 +622,7 @@ module Legion
622
622
  ['', "Terraform: #{dotfiles_tf[:hosts].join(', ')}"]
623
623
  end
624
624
 
625
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
625
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
626
626
  def github_summary_lines(github_data)
627
627
  return [] unless github_data.is_a?(Hash)
628
628
 
@@ -655,9 +655,9 @@ module Legion
655
655
 
656
656
  lines
657
657
  end
658
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
658
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
659
659
 
660
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
660
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
661
661
  def format_tenure(tenure)
662
662
  return tenure.to_s unless tenure.is_a?(Hash)
663
663
 
@@ -670,7 +670,7 @@ module Legion
670
670
  parts << "#{d} day#{'s' if d != 1}" if d&.positive?
671
671
  parts.join(', ')
672
672
  end
673
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
673
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
674
674
 
675
675
  def typed_output(text, delay: TYPED_DELAY)
676
676
  text.chars.each do |char|
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- # rubocop:disable Metrics/ModuleLength
6
5
  module Theme
7
6
  # rubocop:disable Naming/VariableNumber
8
7
  THEMES = {
@@ -125,6 +124,5 @@ module Legion
125
124
  end
126
125
  end
127
126
  end
128
- # rubocop:enable Metrics/ModuleLength
129
127
  end
130
128
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.20'
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.20
4
+ version: 0.4.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity