legion-tty 0.4.19 → 0.4.22

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: 34931409185817b874f6234bb8e81b3fb9144764b53273b990e6c64168f0f087
4
- data.tar.gz: 84e0e03f8dffb27b927792c5d53d49843206fc9a0b88a859b5f6cab190df29e0
3
+ metadata.gz: 45ef88ce519140b5056ce0b934f3c3512781a4f26e30dc74203c431e6f20cc3c
4
+ data.tar.gz: ba632f83ea2022683c097b5451849bf129287c46959a050a8db90b356abe1f29
5
5
  SHA512:
6
- metadata.gz: b2005ab12dbb4159e9ec13118de48de9a3dfc3b555f3bceb3ccde24cf4d2466836d33d157eb2558b3303f5e61b597e3894bdb910fe9d043cc2057f6b53647392
7
- data.tar.gz: b9db6191d5b9425f1fa2eb16b222bf96937d702439432199a948a56b44a14a2b6826e14f5f49ca570885b3f3dd91dd3e56c5de378a337d8b719e887f62134823
6
+ metadata.gz: affafac87b03a47e29ae08bfe5815ba9eb8f72de5fc3feda9350b2871021537f6319261177de1a20f349b8d38cda3295d6b5cca6ff941dbe9414744fa27562e5
7
+ data.tar.gz: bf14853b34688cd959cf6301a56542ec9448fc9f4db2fb2489e7c6aeec3d56e08fc6e44c1627dc31c33631a8b9291717786032dee7ab72b566c545693ca2b57e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.22] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/truncate [N|off]` command: display-only truncation of long messages (preserves originals)
7
+ - `/archive [name]` command: archive session to `~/.legionio/archives/` with timestamp and start fresh
8
+ - `/archives` command: list all archived sessions with file sizes
9
+ - `/tee <path>` command: copy new messages to file in real-time (like Unix tee)
10
+ - `/pipe <command>` command: pipe last assistant response through a shell command
11
+ - `/calc <expression>` command: safe math expression evaluator with Math functions
12
+ - `/rand [N|min..max]` command: generate random numbers (float, integer, or range)
13
+
14
+ ## [0.4.21] - 2026-03-19
15
+
16
+ ### Added
17
+ - `/annotate [N] <text>` command: add notes/annotations to specific messages with timestamps
18
+ - `/annotations` command: list all annotated messages with their notes
19
+ - `/filter [role|tag|pinned|clear]` command: filter displayed messages by role, tag, or pinned status
20
+ - `/multiline` command: toggle multi-line input mode (submit with empty line)
21
+ - `/export yaml` format: export chat history as YAML alongside existing md/json/html formats
22
+ - Annotation rendering in message stream (displayed after reactions)
23
+ - `[ML]` status bar indicator for multi-line input mode
24
+
25
+ ## [0.4.20] - 2026-03-19
26
+
27
+ ### Added
28
+ - `/prompt save|load|list|delete` command: persist and reuse custom system prompts
29
+ - `/reset` command: reset session to clean state (clears messages, modes, aliases, macros)
30
+ - `/replace old >>> new` command: find and replace text across all messages
31
+ - `/highlight` command: highlight text patterns in message rendering with ANSI color
32
+
3
33
  ## [0.4.19] - 2026-03-19
4
34
 
5
35
  ### 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),
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  require_relative '../theme'
4
5
 
5
6
  module Legion
@@ -8,12 +9,16 @@ module Legion
8
9
  # rubocop:disable Metrics/ClassLength
9
10
  class MessageStream
10
11
  attr_reader :messages, :scroll_offset
11
- attr_accessor :mute_system
12
+ attr_accessor :mute_system, :highlights, :filter, :truncate_limit
13
+
14
+ HIGHLIGHT_COLOR = "\e[1;33m"
15
+ HIGHLIGHT_RESET = "\e[0m"
12
16
 
13
17
  def initialize
14
18
  @messages = []
15
19
  @scroll_offset = 0
16
20
  @mute_system = false
21
+ @highlights = []
17
22
  end
18
23
 
19
24
  def add_message(role:, content:)
@@ -72,13 +77,28 @@ module Legion
72
77
  private
73
78
 
74
79
  def build_all_lines(width)
75
- @messages.flat_map do |msg|
80
+ filtered_messages.flat_map do |msg|
76
81
  next [] if @mute_system && msg[:role] == :system
77
82
 
78
83
  render_message(msg, width)
79
84
  end
80
85
  end
81
86
 
87
+ def filtered_messages
88
+ return @messages if @filter.nil?
89
+
90
+ case @filter[:type]
91
+ when :role
92
+ @messages.select { |m| m[:role].to_s == @filter[:value].to_s }
93
+ when :tag
94
+ @messages.select { |m| (m[:tags] || []).include?(@filter[:value]) }
95
+ when :pinned
96
+ @messages.select { |m| m[:pinned] }
97
+ else
98
+ @messages
99
+ end
100
+ end
101
+
82
102
  def render_message(msg, width)
83
103
  role_lines(msg, width) + panel_lines(msg, width)
84
104
  end
@@ -96,8 +116,10 @@ module Legion
96
116
  def user_lines(msg, _width)
97
117
  ts = format_timestamp(msg[:timestamp])
98
118
  header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
99
- lines = ['', "#{header}: #{msg[:content]}"]
119
+ content = apply_highlights(msg[:content].to_s)
120
+ lines = ['', "#{header}: #{content}"]
100
121
  lines << reaction_line(msg) if msg[:reactions]&.any?
122
+ lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
101
123
  lines
102
124
  end
103
125
 
@@ -108,17 +130,34 @@ module Legion
108
130
  end
109
131
 
110
132
  def assistant_lines(msg, width)
111
- rendered = render_markdown(msg[:content], width)
133
+ content = display_content(msg[:content])
134
+ rendered = render_markdown(content, width)
135
+ rendered = apply_highlights(rendered)
112
136
  lines = ['', *rendered.split("\n")]
113
137
  lines << reaction_line(msg) if msg[:reactions]&.any?
138
+ lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
114
139
  lines
115
140
  end
116
141
 
142
+ def display_content(content)
143
+ return content unless @truncate_limit
144
+ return content if content.to_s.length <= @truncate_limit
145
+
146
+ "#{content[0...@truncate_limit]}... [truncated]"
147
+ end
148
+
117
149
  def reaction_line(msg)
118
150
  reactions = msg[:reactions].map { |r| "[#{r}]" }.join(' ')
119
151
  " #{Theme.c(:muted, reactions)}"
120
152
  end
121
153
 
154
+ def annotation_lines(msg)
155
+ msg[:annotations].map do |a|
156
+ ts = a[:timestamp].to_s[11..15] || ''
157
+ " #{Theme.c(:muted, "note [#{ts}]: #{a[:text]}")}"
158
+ end
159
+ end
160
+
122
161
  def render_markdown(text, width)
123
162
  require_relative 'markdown_view'
124
163
  MarkdownView.render(text, width: width)
@@ -140,6 +179,16 @@ module Legion
140
179
  msg[:tool_panels].flat_map { |panel| panel.render(width: width).split("\n") }
141
180
  end
142
181
 
182
+ def apply_highlights(text)
183
+ return text if @highlights.nil? || @highlights.empty?
184
+
185
+ @highlights.reduce(text) do |result, pattern|
186
+ result.gsub(pattern) { "#{HIGHLIGHT_COLOR}#{$LAST_MATCH_INFO}#{HIGHLIGHT_RESET}" }
187
+ end
188
+ rescue StandardError
189
+ text
190
+ end
191
+
143
192
  def apply_tool_panel_update(panel, status:, duration:, result:, error:)
144
193
  panel.instance_variable_set(:@status, status)
145
194
  panel.instance_variable_set(:@duration, duration) if duration
@@ -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 }
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
+ multiline_segment,
45
46
  debug_segment,
46
47
  thinking_segment,
47
48
  notification_segment,
@@ -63,6 +64,12 @@ module Legion
63
64
  Theme.c(:warning, '[PLAN]')
64
65
  end
65
66
 
67
+ def multiline_segment
68
+ return nil unless @state[:multiline]
69
+
70
+ Theme.c(:accent, '[ML]')
71
+ end
72
+
66
73
  def debug_segment
67
74
  return nil unless @state[:debug_mode]
68
75
 
@@ -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)
@@ -97,6 +94,103 @@ module Legion
97
94
  :handled
98
95
  end
99
96
 
97
+ def handle_prompt(input)
98
+ parts = input.split(nil, 3)
99
+ subcommand = parts[1]
100
+ name = parts[2]
101
+
102
+ case subcommand
103
+ when 'save'
104
+ prompt_save(name)
105
+ when 'load'
106
+ prompt_load(name)
107
+ when 'list'
108
+ prompt_list
109
+ when 'delete'
110
+ prompt_delete(name)
111
+ else
112
+ @message_stream.add_message(
113
+ role: :system,
114
+ content: 'Usage: /prompt save|load|list|delete <name>'
115
+ )
116
+ end
117
+ :handled
118
+ end
119
+
120
+ def prompt_dir
121
+ File.expand_path('~/.legionio/prompts')
122
+ end
123
+
124
+ def prompt_save(name)
125
+ unless name
126
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt save <name>')
127
+ return
128
+ end
129
+
130
+ current = @llm_chat.respond_to?(:instructions) ? @llm_chat.instructions.to_s : ''
131
+ if current.empty?
132
+ @message_stream.add_message(role: :system, content: 'No system prompt is currently set.')
133
+ return
134
+ end
135
+
136
+ require 'fileutils'
137
+ FileUtils.mkdir_p(prompt_dir)
138
+ path = File.join(prompt_dir, "#{name}.txt")
139
+ File.write(path, current)
140
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' saved.")
141
+ end
142
+
143
+ def prompt_load(name)
144
+ unless name
145
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt load <name>')
146
+ return
147
+ end
148
+
149
+ path = File.join(prompt_dir, "#{name}.txt")
150
+ unless File.exist?(path)
151
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
152
+ return
153
+ end
154
+
155
+ content = File.read(path)
156
+ @llm_chat.with_instructions(content) if @llm_chat.respond_to?(:with_instructions)
157
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' loaded as system prompt.")
158
+ end
159
+
160
+ # rubocop:disable Metrics/AbcSize
161
+ def prompt_list
162
+ disk_prompts = Dir.glob(File.join(prompt_dir, '*.txt')).map { |f| File.basename(f, '.txt') }.sort
163
+
164
+ if disk_prompts.empty?
165
+ @message_stream.add_message(role: :system, content: 'No prompts saved.')
166
+ return
167
+ end
168
+
169
+ lines = disk_prompts.map do |pname|
170
+ path = File.join(prompt_dir, "#{pname}.txt")
171
+ preview = File.exist?(path) ? truncate_text(File.read(path), 60) : ''
172
+ " #{pname}: #{preview}"
173
+ end
174
+ @message_stream.add_message(role: :system,
175
+ content: "Prompts (#{disk_prompts.size}):\n#{lines.join("\n")}")
176
+ end
177
+ # rubocop:enable Metrics/AbcSize
178
+
179
+ def prompt_delete(name)
180
+ unless name
181
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt delete <name>')
182
+ return
183
+ end
184
+
185
+ path = File.join(prompt_dir, "#{name}.txt")
186
+ if File.exist?(path)
187
+ File.delete(path)
188
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' deleted.")
189
+ else
190
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
191
+ end
192
+ end
193
+
100
194
  def snippet_dir
101
195
  File.expand_path('~/.legionio/snippets')
102
196
  end
@@ -166,7 +260,6 @@ module Legion
166
260
  end
167
261
  # rubocop:enable Metrics/AbcSize
168
262
 
169
- # rubocop:disable Metrics/MethodLength
170
263
  def handle_macro(input)
171
264
  parts = input.split(nil, 3)
172
265
  subcommand = parts[1]
@@ -191,7 +284,6 @@ module Legion
191
284
  end
192
285
  :handled
193
286
  end
194
- # rubocop:enable Metrics/MethodLength
195
287
 
196
288
  def macro_record(name)
197
289
  unless name
@@ -281,7 +373,7 @@ module Legion
281
373
  end
282
374
  end
283
375
 
284
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
376
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
285
377
  def handle_chain(input)
286
378
  args = input.split(nil, 2)[1]
287
379
  unless args
@@ -315,9 +407,8 @@ module Legion
315
407
  )
316
408
  :handled
317
409
  end
318
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
410
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
319
411
  end
320
- # rubocop:enable Metrics/ModuleLength
321
412
  end
322
413
  end
323
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
 
@@ -248,6 +247,41 @@ module Legion
248
247
  :handled
249
248
  end
250
249
 
250
+ def handle_replace(input)
251
+ args = input.split(nil, 2)[1]
252
+ unless args&.include?(' >>> ')
253
+ @message_stream.add_message(role: :system, content: 'Usage: /replace old >>> new')
254
+ return :handled
255
+ end
256
+
257
+ parts = args.split(' >>> ', 2)
258
+ count = apply_replace(parts[0], parts[1] || '')
259
+ report_replace_result(count, parts[0], parts[1] || '')
260
+ :handled
261
+ end
262
+
263
+ def apply_replace(old_text, new_text)
264
+ count = 0
265
+ @message_stream.messages.each do |msg|
266
+ next unless msg[:content].is_a?(::String) && msg[:content].include?(old_text)
267
+
268
+ count += msg[:content].scan(old_text).size
269
+ msg[:content] = msg[:content].gsub(old_text, new_text)
270
+ end
271
+ count
272
+ end
273
+
274
+ def report_replace_result(count, old_text, new_text)
275
+ if count.zero?
276
+ @message_stream.add_message(role: :system, content: "No occurrences of '#{old_text}' found.")
277
+ else
278
+ @message_stream.add_message(
279
+ role: :system,
280
+ content: "Replaced #{count} occurrence#{'s' unless count == 1} of '#{old_text}' with '#{new_text}'."
281
+ )
282
+ end
283
+ end
284
+
251
285
  def search_messages(query)
252
286
  pattern = query.downcase
253
287
  @message_stream.messages.select do |msg|
@@ -367,7 +401,7 @@ module Legion
367
401
  File.write(favorites_file, ::JSON.generate(favs))
368
402
  end
369
403
 
370
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
404
+ # rubocop:disable Metrics/AbcSize
371
405
  def handle_fav(input)
372
406
  idx_str = input.split(nil, 2)[1]
373
407
  msg = if idx_str
@@ -393,7 +427,51 @@ module Legion
393
427
  @message_stream.add_message(role: :system, content: "Favorited: #{preview}")
394
428
  :handled
395
429
  end
396
- # 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
397
475
 
398
476
  def handle_favs
399
477
  all_favs = load_favorites
@@ -412,8 +490,77 @@ module Legion
412
490
  )
413
491
  :handled
414
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
415
563
  end
416
- # rubocop:enable Metrics/ModuleLength
417
564
  end
418
565
  end
419
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
 
@@ -134,7 +133,6 @@ module Legion
134
133
  :handled
135
134
  end
136
135
  end
137
- # rubocop:enable Metrics/ModuleLength
138
136
  end
139
137
  end
140
138
  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
 
@@ -211,6 +210,56 @@ module Legion
211
210
  end
212
211
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
213
212
 
213
+ def handle_reset
214
+ @message_stream.messages.clear
215
+ @plan_mode = false
216
+ @focus_mode = false
217
+ @debug_mode = false
218
+ @muted_system = false
219
+ @pinned_messages = []
220
+ @aliases = {}
221
+ @macros = {}
222
+ @recording_macro = nil
223
+ @macro_buffer = []
224
+ @session_name = 'default'
225
+ @status_bar.update(session: 'default', plan_mode: false, debug_mode: false)
226
+ cfg = safe_config
227
+ @message_stream.add_message(
228
+ role: :system,
229
+ content: "Welcome#{", #{cfg[:name]}" if cfg[:name]}. Type /help for commands."
230
+ )
231
+ @status_bar.notify(message: 'Session reset', level: :info, ttl: 3)
232
+ :handled
233
+ end
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
+
214
263
  def handle_merge(input)
215
264
  name = input.split(nil, 2)[1]
216
265
  unless name
@@ -234,7 +283,6 @@ module Legion
234
283
  :handled
235
284
  end
236
285
  end
237
- # rubocop:enable Metrics/ModuleLength
238
286
  end
239
287
  end
240
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]
@@ -351,7 +387,95 @@ module Legion
351
387
  end
352
388
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
353
389
 
354
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
390
+ def handle_highlight(input)
391
+ arg = input.split(nil, 2)[1]
392
+ @highlights ||= []
393
+
394
+ unless arg
395
+ @message_stream.add_message(role: :system, content: 'Usage: /highlight <pattern> | clear | list')
396
+ return :handled
397
+ end
398
+
399
+ case arg.strip
400
+ when 'clear' then highlight_clear
401
+ when 'list' then highlight_list
402
+ else highlight_add(arg.strip)
403
+ end
404
+ :handled
405
+ end
406
+
407
+ def highlight_clear
408
+ @highlights = []
409
+ @message_stream.highlights = @highlights
410
+ @message_stream.add_message(role: :system, content: 'Highlights cleared.')
411
+ end
412
+
413
+ def highlight_list
414
+ if @highlights.empty?
415
+ @message_stream.add_message(role: :system, content: 'No active highlights.')
416
+ else
417
+ lines = @highlights.each_with_index.map { |p, i| " #{i + 1}. #{p}" }
418
+ @message_stream.add_message(role: :system,
419
+ content: "Active highlights (#{@highlights.size}):\n#{lines.join("\n")}")
420
+ end
421
+ end
422
+
423
+ def highlight_add(pattern)
424
+ @highlights << pattern
425
+ @message_stream.highlights = @highlights
426
+ @message_stream.add_message(role: :system, content: "Highlight added: '#{pattern}'")
427
+ end
428
+
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 safe_calc_expr?(expr)
475
+ CALC_SAFE_PATTERN.match?(expr) || CALC_MATH_PATTERN.match?(expr)
476
+ end
477
+
478
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
355
479
  def handle_summary
356
480
  msgs = @message_stream.messages
357
481
  elapsed = Time.now - @session_start
@@ -384,9 +508,40 @@ module Legion
384
508
  @message_stream.add_message(role: :system, content: lines.join("\n"))
385
509
  :handled
386
510
  end
387
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
511
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
512
+
513
+ def handle_pipe(input)
514
+ cmd = input.split(nil, 2)[1]
515
+ unless cmd
516
+ @message_stream.add_message(role: :system, content: 'Usage: /pipe <shell command>')
517
+ return :handled
518
+ end
519
+
520
+ last_msg = @message_stream.messages.select { |m| m[:role] == :assistant }.last
521
+ unless last_msg
522
+ @message_stream.add_message(role: :system, content: 'No assistant message to pipe.')
523
+ return :handled
524
+ end
525
+
526
+ output = pipe_through_command(cmd, last_msg[:content].to_s)
527
+ @message_stream.add_message(role: :system, content: "pipe | #{cmd}:\n#{output}")
528
+ :handled
529
+ rescue StandardError => e
530
+ @message_stream.add_message(role: :system, content: "Pipe error: #{e.message}")
531
+ :handled
532
+ end
533
+
534
+ def pipe_through_command(cmd, content)
535
+ result = IO.popen(cmd, 'r+') do |io|
536
+ io.write(content)
537
+ io.close_write
538
+ io.read
539
+ end
540
+ result.to_s.chomp
541
+ rescue StandardError => e
542
+ raise "command failed: #{e.message}"
543
+ end
388
544
  end
389
- # rubocop:enable Metrics/ModuleLength
390
545
  end
391
546
  end
392
547
  end
@@ -32,7 +32,12 @@ module Legion
32
32
  /wc /import /mute /autosave /react /macro /tag /tags /repeat /count
33
33
  /template /fav /favs /log /version
34
34
  /focus /retry /merge /sort
35
- /chain /info /scroll /summary].freeze
35
+ /chain /info /scroll /summary
36
+ /prompt /reset /replace /highlight /multiline
37
+ /annotate /annotations /filter /truncate
38
+ /tee /pipe
39
+ /archive /archives
40
+ /calc /rand].freeze
36
41
 
37
42
  PERSONALITIES = {
38
43
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -72,6 +77,8 @@ module Legion
72
77
  @last_command = nil
73
78
  @focus_mode = false
74
79
  @last_user_input = nil
80
+ @highlights = []
81
+ @multiline_mode = false
75
82
  end
76
83
 
77
84
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -92,7 +99,7 @@ module Legion
92
99
  @running
93
100
  end
94
101
 
95
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
102
+ # rubocop:disable Metrics/AbcSize
96
103
  def run
97
104
  activate
98
105
  while @running
@@ -115,7 +122,7 @@ module Legion
115
122
  end
116
123
  end
117
124
  end
118
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
125
+ # rubocop:enable Metrics/AbcSize
119
126
 
120
127
  def handle_slash_command(input)
121
128
  return nil unless input.start_with?('/')
@@ -137,6 +144,7 @@ module Legion
137
144
  def handle_user_message(input)
138
145
  @last_user_input = input
139
146
  @message_stream.add_message(role: :user, content: input)
147
+ tee_message("[user] #{input}") if @tee_path
140
148
  if @plan_mode
141
149
  @message_stream.add_message(role: :system, content: '(bookmarked)')
142
150
  else
@@ -337,12 +345,27 @@ module Legion
337
345
 
338
346
  def read_input
339
347
  return nil unless @input_bar.respond_to?(:read_line)
348
+ return read_multiline_input if @multiline_mode
340
349
 
341
350
  @input_bar.read_line
342
351
  rescue Interrupt
343
352
  nil
344
353
  end
345
354
 
355
+ def read_multiline_input
356
+ lines = []
357
+ loop do
358
+ line = @input_bar.read_line
359
+ return nil if line.nil? && lines.empty?
360
+ break if line.nil? || line.empty?
361
+
362
+ lines << line
363
+ end
364
+ lines.empty? ? nil : lines.join("\n")
365
+ rescue Interrupt
366
+ nil
367
+ end
368
+
346
369
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
347
370
  def dispatch_slash(cmd, input)
348
371
  case cmd
@@ -410,6 +433,21 @@ module Legion
410
433
  when '/info' then handle_info
411
434
  when '/scroll' then handle_scroll(input)
412
435
  when '/summary' then handle_summary
436
+ when '/prompt' then handle_prompt(input)
437
+ when '/reset' then handle_reset
438
+ when '/replace' then handle_replace(input)
439
+ when '/highlight' then handle_highlight(input)
440
+ when '/multiline' then handle_multiline
441
+ when '/annotate' then handle_annotate(input)
442
+ when '/annotations' then handle_annotations(input)
443
+ when '/filter' then handle_filter(input)
444
+ when '/truncate' then handle_truncate(input)
445
+ when '/tee' then handle_tee(input)
446
+ when '/pipe' then handle_pipe(input)
447
+ when '/archive' then handle_archive(input)
448
+ when '/archives' then handle_archives
449
+ when '/calc' then handle_calc(input)
450
+ when '/rand' then handle_rand(input)
413
451
  else :handled
414
452
  end
415
453
  end
@@ -494,6 +532,7 @@ module Legion
494
532
  "[DEBUG] msgs:#{@message_stream.messages.size} " \
495
533
  "scroll:#{@message_stream.scroll_position&.dig(:current) || 0} " \
496
534
  "plan:#{@plan_mode} " \
535
+ "multiline:#{@multiline_mode} " \
497
536
  "personality:#{@personality || 'default'} " \
498
537
  "aliases:#{@aliases.size} " \
499
538
  "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.19'
5
+ VERSION = '0.4.22'
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.19
4
+ version: 0.4.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity