legion-tty 0.4.18 → 0.4.20

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: 6faa2820fd40c949a40eb6e336cf75067e3ab400d9631980fc49fda2f0b5dc29
4
- data.tar.gz: 35c674132cb7f41f0c2c8d4150afe10c99f4ac30fed392e78477fb074c383d67
3
+ metadata.gz: f2d9ef0fe2336f2750785f4742bc14661032836b05fab83062ba5de23c60cbbc
4
+ data.tar.gz: cf9fe410cc87ba108cc52e994b5210351bb403b84aa43a27cbcb5ad94ff65232
5
5
  SHA512:
6
- metadata.gz: 3766b9312e3fa871e39cfc1b42eac013c82ab680509f10f6f034474fda23381d1bfa4bc67e9e7a95d1557ca546ae3cd6d209818d4fe814111b33cc35b167f46f
7
- data.tar.gz: c0644da99c0c8cd8b0002366e2e9f46edf23d99a157a8e5652165e3112442516da2178e16ca91596afdbef04a57fe379a347bd6f5c6883903bae24f599a294dd
6
+ metadata.gz: 194101f68a10f6e0c9b7a9c14d38690adcfe37724fcb5c2ee894d5a13b757a897aa3b4e85fe48e5db5b44b14f597ffffa9ce1a88ede2da54a1858ca42f759bc9
7
+ data.tar.gz: 1eb5e7cafbdc7e1025fcdd8424256eea456c3b24f3d03077effd4417a694bddf5f77fd232b6514e89dad53d813161b2d753b6e9a4d7564c1b13ecea6dde1cd95
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.20] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/prompt save|load|list|delete` command: persist and reuse custom system prompts
7
+ - `/reset` command: reset session to clean state (clears messages, modes, aliases, macros)
8
+ - `/replace old >>> new` command: find and replace text across all messages
9
+ - `/highlight` command: highlight text patterns in message rendering with ANSI color
10
+
11
+ ## [0.4.19] - 2026-03-19
12
+
13
+ ### Added
14
+ - `/chain` command: send a sequence of pipe-separated prompts to the LLM sequentially
15
+ - `/info` command: comprehensive session info (modes, counts, aliases, snippets, macros, provider)
16
+ - `/scroll [top|bottom|N]` command: navigate to specific scroll position in message stream
17
+ - `/summary` command: generate a local conversation summary (topics, lengths, duration)
18
+
3
19
  ## [0.4.18] - 2026-03-19
4
20
 
5
21
  ### Added
data/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Rich terminal UI for the LegionIO async cognition engine.
4
4
 
5
- **Version**: 0.4.12
5
+ **Version**: 0.4.18
6
6
 
7
- Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with 40 slash commands, operational dashboard, extensions browser, config editor, and session persistence - all rendered with the [tty-ruby](https://ttytoolkit.org/) gem ecosystem.
7
+ Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with 60 slash commands, operational dashboard, extensions browser, config editor, and session persistence - all rendered with the [tty-ruby](https://ttytoolkit.org/) gem ecosystem.
8
8
 
9
9
  ## Features
10
10
 
11
11
  - **Onboarding wizard** - First-run setup with Kerberos identity detection, GitHub profile probing, environment scanning, and LLM provider selection
12
12
  - **Digital rain intro** - Matrix-style rain using discovered LEX extension names
13
- - **AI chat shell** - Streaming LLM chat with 40 slash commands, tab completion, markdown rendering, and tool panels
13
+ - **AI chat shell** - Streaming LLM chat with 60 slash commands, tab completion, markdown rendering, and tool panels
14
14
  - **Operational dashboard** - Service/LLM status, extension inventory, system info, panel navigation (Ctrl+D or `/dashboard`)
15
15
  - **Extensions browser** - Browse installed LEX gems by category with detail view and homepage opener ('o' key)
16
16
  - **Config viewer/editor** - View and edit `~/.legionio/settings/*.json` with vault:// masking and JSON validation
@@ -115,6 +115,25 @@ legion chat prompt "explain async cognition"
115
115
  | `/uptime` | Show session elapsed time |
116
116
  | `/bookmark` | Export pinned messages to file |
117
117
  | `/time` | Show current date and time |
118
+ | `/autosave [N\|off]` | Toggle periodic auto-save with interval |
119
+ | `/react <emoji>` | Add emoji reaction to a message |
120
+ | `/macro <action>` | Record/stop/play/list/delete command macros |
121
+ | `/tag <label>` | Tag a message with a label |
122
+ | `/tags [label]` | Show tag statistics or filter by tag |
123
+ | `/repeat` | Re-execute the last slash command |
124
+ | `/count <pattern>` | Count messages matching a pattern |
125
+ | `/template [name]` | List or use prompt templates |
126
+ | `/fav [N]` | Favorite a message (persists to disk) |
127
+ | `/favs` | Show all favorited messages |
128
+ | `/log [N]` | View last N lines of boot log |
129
+ | `/version` | Show version and platform info |
130
+ | `/focus` | Toggle minimal UI (hide status bar) |
131
+ | `/retry` | Resend last message to LLM |
132
+ | `/merge <session>` | Merge another session into current |
133
+ | `/sort [length\|role]` | Show messages sorted by length or role |
134
+ | `/import <path>` | Import session from a JSON file |
135
+ | `/mute` | Toggle system message display |
136
+ | `/wc` | Show word count statistics |
118
137
 
119
138
  ## Hotkeys
120
139
 
@@ -139,7 +158,7 @@ legion-tty
139
158
 
140
159
  Screens/
141
160
  Onboarding # First-run wizard (rain -> intro -> wizard -> reveal)
142
- Chat # AI chat REPL with streaming + 40 slash commands
161
+ Chat # AI chat REPL with streaming + 60 slash commands
143
162
  SessionCommands # save/load/sessions/delete/rename
144
163
  ExportCommands # export/bookmark/html/json/markdown
145
164
  MessageCommands # compact/copy/diff/search/grep/undo/pin/pins
@@ -194,8 +213,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
194
213
 
195
214
  ```bash
196
215
  bundle install
197
- bundle exec rspec # 836 examples, 0 failures
198
- bundle exec rubocop # 92 files, 0 offenses
216
+ bundle exec rspec # 1143 examples, 0 failures
217
+ bundle exec rubocop # 106 files, 0 offenses
199
218
  ```
200
219
 
201
220
  ## License
@@ -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
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:)
@@ -96,7 +101,8 @@ module Legion
96
101
  def user_lines(msg, _width)
97
102
  ts = format_timestamp(msg[:timestamp])
98
103
  header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
99
- lines = ['', "#{header}: #{msg[:content]}"]
104
+ content = apply_highlights(msg[:content].to_s)
105
+ lines = ['', "#{header}: #{content}"]
100
106
  lines << reaction_line(msg) if msg[:reactions]&.any?
101
107
  lines
102
108
  end
@@ -109,6 +115,7 @@ module Legion
109
115
 
110
116
  def assistant_lines(msg, width)
111
117
  rendered = render_markdown(msg[:content], width)
118
+ rendered = apply_highlights(rendered)
112
119
  lines = ['', *rendered.split("\n")]
113
120
  lines << reaction_line(msg) if msg[:reactions]&.any?
114
121
  lines
@@ -140,6 +147,16 @@ module Legion
140
147
  msg[:tool_panels].flat_map { |panel| panel.render(width: width).split("\n") }
141
148
  end
142
149
 
150
+ def apply_highlights(text)
151
+ return text if @highlights.nil? || @highlights.empty?
152
+
153
+ @highlights.reduce(text) do |result, pattern|
154
+ result.gsub(pattern) { "#{HIGHLIGHT_COLOR}#{$LAST_MATCH_INFO}#{HIGHLIGHT_RESET}" }
155
+ end
156
+ rescue StandardError
157
+ text
158
+ end
159
+
143
160
  def apply_tool_panel_update(panel, status:, duration:, result:, error:)
144
161
  panel.instance_variable_set(:@status, status)
145
162
  panel.instance_variable_set(:@duration, duration) if duration
@@ -97,6 +97,103 @@ module Legion
97
97
  :handled
98
98
  end
99
99
 
100
+ def handle_prompt(input)
101
+ parts = input.split(nil, 3)
102
+ subcommand = parts[1]
103
+ name = parts[2]
104
+
105
+ case subcommand
106
+ when 'save'
107
+ prompt_save(name)
108
+ when 'load'
109
+ prompt_load(name)
110
+ when 'list'
111
+ prompt_list
112
+ when 'delete'
113
+ prompt_delete(name)
114
+ else
115
+ @message_stream.add_message(
116
+ role: :system,
117
+ content: 'Usage: /prompt save|load|list|delete <name>'
118
+ )
119
+ end
120
+ :handled
121
+ end
122
+
123
+ def prompt_dir
124
+ File.expand_path('~/.legionio/prompts')
125
+ end
126
+
127
+ def prompt_save(name)
128
+ unless name
129
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt save <name>')
130
+ return
131
+ end
132
+
133
+ current = @llm_chat.respond_to?(:instructions) ? @llm_chat.instructions.to_s : ''
134
+ if current.empty?
135
+ @message_stream.add_message(role: :system, content: 'No system prompt is currently set.')
136
+ return
137
+ end
138
+
139
+ require 'fileutils'
140
+ FileUtils.mkdir_p(prompt_dir)
141
+ path = File.join(prompt_dir, "#{name}.txt")
142
+ File.write(path, current)
143
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' saved.")
144
+ end
145
+
146
+ def prompt_load(name)
147
+ unless name
148
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt load <name>')
149
+ return
150
+ end
151
+
152
+ path = File.join(prompt_dir, "#{name}.txt")
153
+ unless File.exist?(path)
154
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
155
+ return
156
+ end
157
+
158
+ content = File.read(path)
159
+ @llm_chat.with_instructions(content) if @llm_chat.respond_to?(:with_instructions)
160
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' loaded as system prompt.")
161
+ end
162
+
163
+ # rubocop:disable Metrics/AbcSize
164
+ def prompt_list
165
+ disk_prompts = Dir.glob(File.join(prompt_dir, '*.txt')).map { |f| File.basename(f, '.txt') }.sort
166
+
167
+ if disk_prompts.empty?
168
+ @message_stream.add_message(role: :system, content: 'No prompts saved.')
169
+ return
170
+ end
171
+
172
+ lines = disk_prompts.map do |pname|
173
+ path = File.join(prompt_dir, "#{pname}.txt")
174
+ preview = File.exist?(path) ? truncate_text(File.read(path), 60) : ''
175
+ " #{pname}: #{preview}"
176
+ end
177
+ @message_stream.add_message(role: :system,
178
+ content: "Prompts (#{disk_prompts.size}):\n#{lines.join("\n")}")
179
+ end
180
+ # rubocop:enable Metrics/AbcSize
181
+
182
+ def prompt_delete(name)
183
+ unless name
184
+ @message_stream.add_message(role: :system, content: 'Usage: /prompt delete <name>')
185
+ return
186
+ end
187
+
188
+ path = File.join(prompt_dir, "#{name}.txt")
189
+ if File.exist?(path)
190
+ File.delete(path)
191
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' deleted.")
192
+ else
193
+ @message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
194
+ end
195
+ end
196
+
100
197
  def snippet_dir
101
198
  File.expand_path('~/.legionio/snippets')
102
199
  end
@@ -280,6 +377,42 @@ module Legion
280
377
  @message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
281
378
  end
282
379
  end
380
+
381
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
382
+ def handle_chain(input)
383
+ args = input.split(nil, 2)[1]
384
+ unless args
385
+ @message_stream.add_message(
386
+ role: :system,
387
+ content: 'Usage: /chain prompt1 | prompt2 | prompt3'
388
+ )
389
+ return :handled
390
+ end
391
+
392
+ unless @llm_chat || daemon_available?
393
+ @message_stream.add_message(role: :system, content: 'LLM not configured. Cannot run chain.')
394
+ return :handled
395
+ end
396
+
397
+ prompts = args.split('|').map(&:strip).reject(&:empty?)
398
+ if prompts.empty?
399
+ @message_stream.add_message(role: :system, content: 'Usage: /chain prompt1 | prompt2 | prompt3')
400
+ return :handled
401
+ end
402
+
403
+ prompts.each do |prompt|
404
+ @message_stream.add_message(role: :user, content: prompt)
405
+ @message_stream.add_message(role: :assistant, content: '')
406
+ send_to_llm(prompt)
407
+ end
408
+
409
+ @message_stream.add_message(
410
+ role: :system,
411
+ content: "Chain complete: #{prompts.size} prompt#{'s' unless prompts.size == 1} sent."
412
+ )
413
+ :handled
414
+ end
415
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
283
416
  end
284
417
  # rubocop:enable Metrics/ModuleLength
285
418
  end
@@ -248,6 +248,41 @@ module Legion
248
248
  :handled
249
249
  end
250
250
 
251
+ def handle_replace(input)
252
+ args = input.split(nil, 2)[1]
253
+ unless args&.include?(' >>> ')
254
+ @message_stream.add_message(role: :system, content: 'Usage: /replace old >>> new')
255
+ return :handled
256
+ end
257
+
258
+ parts = args.split(' >>> ', 2)
259
+ count = apply_replace(parts[0], parts[1] || '')
260
+ report_replace_result(count, parts[0], parts[1] || '')
261
+ :handled
262
+ end
263
+
264
+ def apply_replace(old_text, new_text)
265
+ count = 0
266
+ @message_stream.messages.each do |msg|
267
+ next unless msg[:content].is_a?(::String) && msg[:content].include?(old_text)
268
+
269
+ count += msg[:content].scan(old_text).size
270
+ msg[:content] = msg[:content].gsub(old_text, new_text)
271
+ end
272
+ count
273
+ end
274
+
275
+ def report_replace_result(count, old_text, new_text)
276
+ if count.zero?
277
+ @message_stream.add_message(role: :system, content: "No occurrences of '#{old_text}' found.")
278
+ else
279
+ @message_stream.add_message(
280
+ role: :system,
281
+ content: "Replaced #{count} occurrence#{'s' unless count == 1} of '#{old_text}' with '#{new_text}'."
282
+ )
283
+ end
284
+ end
285
+
251
286
  def search_messages(query)
252
287
  pattern = query.downcase
253
288
  @message_stream.messages.select do |msg|
@@ -157,6 +157,82 @@ module Legion
157
157
  nil
158
158
  end
159
159
 
160
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
161
+ def handle_info
162
+ cfg = safe_config
163
+ elapsed = Time.now - @session_start
164
+ hours = (elapsed / 3600).to_i
165
+ minutes = ((elapsed % 3600) / 60).to_i
166
+ seconds = (elapsed % 60).to_i
167
+ uptime_str = "#{hours}h #{minutes}m #{seconds}s"
168
+
169
+ msgs = @message_stream.messages
170
+ counts = %i[user assistant system tool].to_h { |r| [r, msgs.count { |m| m[:role] == r }] }
171
+ total_chars = msgs.sum { |m| m[:content].to_s.length }
172
+ avg_len = (total_chars.to_f / [msgs.size, 1].max).round
173
+
174
+ model_info = if @llm_chat.respond_to?(:model)
175
+ @llm_chat.model.to_s
176
+ else
177
+ cfg[:provider] || 'none'
178
+ end
179
+
180
+ tagged_count = msgs.count { |m| m[:tags]&.any? }
181
+ fav_count = msgs.count { |m| m[:favorited] }
182
+
183
+ lines = [
184
+ "Session: #{@session_name}",
185
+ "Started: #{@session_start.strftime('%Y-%m-%d %H:%M:%S')}",
186
+ "Uptime: #{uptime_str}",
187
+ '',
188
+ "Messages: #{msgs.size} total",
189
+ " User: #{counts[:user]}, Assistant: #{counts[:assistant]}, System: #{counts[:system]}",
190
+ " Tool: #{counts[:tool]}",
191
+ '',
192
+ "Total characters: #{total_chars}",
193
+ "Avg message length: #{avg_len} chars",
194
+ '',
195
+ "Pinned: #{@pinned_messages.size}",
196
+ "Tagged: #{tagged_count}",
197
+ "Favorited: #{fav_count}",
198
+ "Aliases: #{@aliases.size}",
199
+ "Snippets: #{@snippets.size}",
200
+ "Macros: #{@macros.size}",
201
+ '',
202
+ "Autosave: #{@autosave_enabled ? "ON (every #{@autosave_interval}s)" : 'OFF'}",
203
+ "Focus mode: #{@focus_mode ? 'on' : 'off'}",
204
+ "Muted system: #{@muted_system ? 'on' : 'off'}",
205
+ "Plan mode: #{@plan_mode ? 'on' : 'off'}",
206
+ "Debug mode: #{@debug_mode ? 'on' : 'off'}",
207
+ "LLM: #{model_info}"
208
+ ]
209
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
210
+ :handled
211
+ end
212
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
213
+
214
+ def handle_reset
215
+ @message_stream.messages.clear
216
+ @plan_mode = false
217
+ @focus_mode = false
218
+ @debug_mode = false
219
+ @muted_system = false
220
+ @pinned_messages = []
221
+ @aliases = {}
222
+ @macros = {}
223
+ @recording_macro = nil
224
+ @macro_buffer = []
225
+ @session_name = 'default'
226
+ @status_bar.update(session: 'default', plan_mode: false, debug_mode: false)
227
+ cfg = safe_config
228
+ @message_stream.add_message(
229
+ role: :system,
230
+ content: "Welcome#{", #{cfg[:name]}" if cfg[:name]}. Type /help for commands."
231
+ )
232
+ @status_bar.notify(message: 'Session reset', level: :info, ttl: 3)
233
+ :handled
234
+ end
235
+
160
236
  def handle_merge(input)
161
237
  name = input.split(nil, 2)[1]
162
238
  unless name
@@ -313,6 +313,117 @@ module Legion
313
313
  end
314
314
  :handled
315
315
  end
316
+
317
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
318
+ def handle_scroll(input)
319
+ arg = input.split(nil, 2)[1]
320
+ unless arg
321
+ pos = @message_stream.scroll_position
322
+ @message_stream.add_message(
323
+ role: :system,
324
+ content: "Scroll position: offset=#{pos[:current]}, messages=#{pos[:total]}"
325
+ )
326
+ return :handled
327
+ end
328
+
329
+ case arg.strip
330
+ when 'top'
331
+ @message_stream.scroll_up(@message_stream.messages.size * 5)
332
+ @message_stream.add_message(role: :system, content: 'Scrolled to top.')
333
+ when 'bottom'
334
+ @message_stream.scroll_down(@message_stream.scroll_offset)
335
+ @message_stream.add_message(role: :system, content: 'Scrolled to bottom.')
336
+ else
337
+ idx = arg.strip.to_i
338
+ if idx >= 0 && idx < @message_stream.messages.size
339
+ @message_stream.scroll_down(@message_stream.scroll_offset)
340
+ target_offset = [@message_stream.messages.size - idx - 1, 0].max
341
+ @message_stream.scroll_up(target_offset)
342
+ @message_stream.add_message(role: :system, content: "Scrolled to message #{idx}.")
343
+ else
344
+ @message_stream.add_message(
345
+ role: :system,
346
+ content: 'Invalid index. Usage: /scroll top|bottom|<N>'
347
+ )
348
+ end
349
+ end
350
+ :handled
351
+ end
352
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
353
+
354
+ def handle_highlight(input)
355
+ arg = input.split(nil, 2)[1]
356
+ @highlights ||= []
357
+
358
+ unless arg
359
+ @message_stream.add_message(role: :system, content: 'Usage: /highlight <pattern> | clear | list')
360
+ return :handled
361
+ end
362
+
363
+ case arg.strip
364
+ when 'clear' then highlight_clear
365
+ when 'list' then highlight_list
366
+ else highlight_add(arg.strip)
367
+ end
368
+ :handled
369
+ end
370
+
371
+ def highlight_clear
372
+ @highlights = []
373
+ @message_stream.highlights = @highlights
374
+ @message_stream.add_message(role: :system, content: 'Highlights cleared.')
375
+ end
376
+
377
+ def highlight_list
378
+ if @highlights.empty?
379
+ @message_stream.add_message(role: :system, content: 'No active highlights.')
380
+ else
381
+ lines = @highlights.each_with_index.map { |p, i| " #{i + 1}. #{p}" }
382
+ @message_stream.add_message(role: :system,
383
+ content: "Active highlights (#{@highlights.size}):\n#{lines.join("\n")}")
384
+ end
385
+ end
386
+
387
+ def highlight_add(pattern)
388
+ @highlights << pattern
389
+ @message_stream.highlights = @highlights
390
+ @message_stream.add_message(role: :system, content: "Highlight added: '#{pattern}'")
391
+ end
392
+
393
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
394
+ def handle_summary
395
+ msgs = @message_stream.messages
396
+ elapsed = Time.now - @session_start
397
+ hours = (elapsed / 3600).to_i
398
+ minutes = ((elapsed % 3600) / 60).to_i
399
+ seconds = (elapsed % 60).to_i
400
+ uptime_str = "#{hours}h #{minutes}m #{seconds}s"
401
+
402
+ counts = %i[user assistant system].to_h { |r| [r, msgs.count { |m| m[:role] == r }] }
403
+ most_active = counts.max_by { |_, v| v }&.first || :none
404
+
405
+ user_msgs = msgs.select { |m| m[:role] == :user }
406
+ top_words = user_msgs.flat_map { |m| m[:content].to_s.split.first(1) }
407
+ .tally.sort_by { |_, c| -c }.first(5).map(&:first)
408
+
409
+ longest = msgs.max_by { |m| m[:content].to_s.length }
410
+ longest_preview = longest ? truncate_text(longest[:content].to_s, 60) : 'none'
411
+
412
+ last_user = user_msgs.last
413
+ recent_topic = last_user ? truncate_text(last_user[:content].to_s, 40) : 'none'
414
+
415
+ lines = [
416
+ 'Conversation Summary',
417
+ " Messages: #{msgs.size}, Duration: #{uptime_str}",
418
+ " Most active role: #{most_active}",
419
+ " Top starting words: #{top_words.empty? ? 'none' : top_words.join(', ')}",
420
+ " Longest message: #{longest_preview}",
421
+ " Most recent topic: #{recent_topic}"
422
+ ]
423
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
424
+ :handled
425
+ end
426
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
316
427
  end
317
428
  # rubocop:enable Metrics/ModuleLength
318
429
  end
@@ -31,7 +31,9 @@ module Legion
31
31
  /context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
32
32
  /wc /import /mute /autosave /react /macro /tag /tags /repeat /count
33
33
  /template /fav /favs /log /version
34
- /focus /retry /merge /sort].freeze
34
+ /focus /retry /merge /sort
35
+ /chain /info /scroll /summary
36
+ /prompt /reset /replace /highlight].freeze
35
37
 
36
38
  PERSONALITIES = {
37
39
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -71,6 +73,7 @@ module Legion
71
73
  @last_command = nil
72
74
  @focus_mode = false
73
75
  @last_user_input = nil
76
+ @highlights = []
74
77
  end
75
78
 
76
79
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -405,6 +408,14 @@ module Legion
405
408
  when '/retry' then handle_retry
406
409
  when '/merge' then handle_merge(input)
407
410
  when '/sort' then handle_sort(input)
411
+ when '/chain' then handle_chain(input)
412
+ when '/info' then handle_info
413
+ when '/scroll' then handle_scroll(input)
414
+ when '/summary' then handle_summary
415
+ when '/prompt' then handle_prompt(input)
416
+ when '/reset' then handle_reset
417
+ when '/replace' then handle_replace(input)
418
+ when '/highlight' then handle_highlight(input)
408
419
  else :handled
409
420
  end
410
421
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.18'
5
+ VERSION = '0.4.20'
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.18
4
+ version: 0.4.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity