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 +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/legion/tty/components/message_stream.rb +19 -2
- data/lib/legion/tty/screens/chat/custom_commands.rb +97 -0
- data/lib/legion/tty/screens/chat/message_commands.rb +35 -0
- data/lib/legion/tty/screens/chat/session_commands.rb +22 -0
- data/lib/legion/tty/screens/chat/ui_commands.rb +39 -0
- data/lib/legion/tty/screens/chat.rb +7 -1
- data/lib/legion/tty/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2d9ef0fe2336f2750785f4742bc14661032836b05fab83062ba5de23c60cbbc
|
|
4
|
+
data.tar.gz: cf9fe410cc87ba108cc52e994b5210351bb403b84aa43a27cbcb5ad94ff65232
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/legion/tty/version.rb
CHANGED