legion-tty 0.4.19 → 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: 34931409185817b874f6234bb8e81b3fb9144764b53273b990e6c64168f0f087
4
- data.tar.gz: 84e0e03f8dffb27b927792c5d53d49843206fc9a0b88a859b5f6cab190df29e0
3
+ metadata.gz: f2d9ef0fe2336f2750785f4742bc14661032836b05fab83062ba5de23c60cbbc
4
+ data.tar.gz: cf9fe410cc87ba108cc52e994b5210351bb403b84aa43a27cbcb5ad94ff65232
5
5
  SHA512:
6
- metadata.gz: b2005ab12dbb4159e9ec13118de48de9a3dfc3b555f3bceb3ccde24cf4d2466836d33d157eb2558b3303f5e61b597e3894bdb910fe9d043cc2057f6b53647392
7
- data.tar.gz: b9db6191d5b9425f1fa2eb16b222bf96937d702439432199a948a56b44a14a2b6826e14f5f49ca570885b3f3dd91dd3e56c5de378a337d8b719e887f62134823
6
+ metadata.gz: 194101f68a10f6e0c9b7a9c14d38690adcfe37724fcb5c2ee894d5a13b757a897aa3b4e85fe48e5db5b44b14f597ffffa9ce1a88ede2da54a1858ca42f759bc9
7
+ data.tar.gz: 1eb5e7cafbdc7e1025fcdd8424256eea456c3b24f3d03077effd4417a694bddf5f77fd232b6514e89dad53d813161b2d753b6e9a4d7564c1b13ecea6dde1cd95
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.4.19] - 2026-03-19
4
12
 
5
13
  ### Added
@@ -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
@@ -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|
@@ -211,6 +211,28 @@ module Legion
211
211
  end
212
212
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
213
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
+
214
236
  def handle_merge(input)
215
237
  name = input.split(nil, 2)[1]
216
238
  unless name
@@ -351,6 +351,45 @@ module Legion
351
351
  end
352
352
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
353
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
+
354
393
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
355
394
  def handle_summary
356
395
  msgs = @message_stream.messages
@@ -32,7 +32,8 @@ 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].freeze
36
37
 
37
38
  PERSONALITIES = {
38
39
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -72,6 +73,7 @@ module Legion
72
73
  @last_command = nil
73
74
  @focus_mode = false
74
75
  @last_user_input = nil
76
+ @highlights = []
75
77
  end
76
78
 
77
79
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -410,6 +412,10 @@ module Legion
410
412
  when '/info' then handle_info
411
413
  when '/scroll' then handle_scroll(input)
412
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)
413
419
  else :handled
414
420
  end
415
421
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.19'
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.19
4
+ version: 0.4.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity