legion-tty 0.4.8 → 0.4.10
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 +17 -0
- data/lib/legion/tty/components/status_bar.rb +23 -1
- data/lib/legion/tty/screens/chat.rb +313 -16
- 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: b4f6ee315fe6ff9851580bebff6f0187665a2c53f1719c48ad807879651eee44
|
|
4
|
+
data.tar.gz: 938e195e473fb65a27840f31b82cefa9032c9f500d828f2844d8ca58b7f9bc95
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27167cd635e065fca190930586ba383ffe8b8595c772eb917dd243aa7832b6d23807198e2e73b25b942154fb155c455e239c52e51b68ad16675beb363224be28
|
|
7
|
+
data.tar.gz: cdbcae2d8a592ea9b6316f3069f593c49b11f242ddfedd5306313a53b6bce48e5c718c06fb2cbb21e44f4c8b323a4fae06839b47fda2cb41bd8c7312a405be55
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.10] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/context` command: display active session state summary (model, personality, plan mode, system prompt, session, message count, pinned count, token usage)
|
|
7
|
+
- `/alias` command: create short aliases for frequently used slash commands; aliases expand and re-dispatch transparently
|
|
8
|
+
- `/snippet save|load|list|delete <name>` command: save last assistant message as a named snippet, insert snippets as user messages, persist to `~/.legionio/snippets/`
|
|
9
|
+
- `/debug` command: toggle debug mode; adds `[DEBUG]` line to render output showing msgs/scroll/plan/personality/aliases/snippets/pinned counts; StatusBar shows `[DBG]` indicator
|
|
10
|
+
|
|
11
|
+
## [0.4.9] - 2026-03-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- StatusBar notifications: transient toast-style messages with TTL expiry (wired to save/load/export/theme)
|
|
15
|
+
- `/undo` command: remove last user+assistant message pair
|
|
16
|
+
- `/history` command: show last 20 input entries
|
|
17
|
+
- `/pin` and `/pins` commands: pin important messages, view pinned list
|
|
18
|
+
- `/rename <name>` command: rename current session (deletes old, saves new)
|
|
19
|
+
|
|
3
20
|
## [0.4.8] - 2026-03-19
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../theme'
|
|
4
|
+
require_relative 'notification'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module TTY
|
|
7
8
|
module Components
|
|
8
9
|
class StatusBar
|
|
9
10
|
def initialize
|
|
10
|
-
@state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false
|
|
11
|
+
@state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false,
|
|
12
|
+
debug_mode: false }
|
|
13
|
+
@notifications = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def notify(message:, level: :info, ttl: 5)
|
|
17
|
+
@notifications << Notification.new(message: message, level: level, ttl: ttl)
|
|
11
18
|
end
|
|
12
19
|
|
|
13
20
|
def update(**fields)
|
|
@@ -34,7 +41,9 @@ module Legion
|
|
|
34
41
|
[
|
|
35
42
|
model_segment,
|
|
36
43
|
plan_segment,
|
|
44
|
+
debug_segment,
|
|
37
45
|
thinking_segment,
|
|
46
|
+
notification_segment,
|
|
38
47
|
tokens_segment,
|
|
39
48
|
cost_segment,
|
|
40
49
|
session_segment,
|
|
@@ -52,6 +61,12 @@ module Legion
|
|
|
52
61
|
Theme.c(:warning, '[PLAN]')
|
|
53
62
|
end
|
|
54
63
|
|
|
64
|
+
def debug_segment
|
|
65
|
+
return nil unless @state[:debug_mode]
|
|
66
|
+
|
|
67
|
+
Theme.c(:muted, '[DBG]')
|
|
68
|
+
end
|
|
69
|
+
|
|
55
70
|
def thinking_segment
|
|
56
71
|
return nil unless @state[:thinking]
|
|
57
72
|
|
|
@@ -60,6 +75,13 @@ module Legion
|
|
|
60
75
|
Theme.c(:warning, "#{frame} thinking...")
|
|
61
76
|
end
|
|
62
77
|
|
|
78
|
+
def notification_segment
|
|
79
|
+
@notifications.reject!(&:expired?)
|
|
80
|
+
return nil if @notifications.empty?
|
|
81
|
+
|
|
82
|
+
@notifications.first.render
|
|
83
|
+
end
|
|
84
|
+
|
|
63
85
|
def tokens_segment
|
|
64
86
|
Theme.c(:secondary, "#{format_number(@state[:tokens])} tokens") if @state[:tokens].to_i.positive?
|
|
65
87
|
end
|
|
@@ -14,7 +14,8 @@ module Legion
|
|
|
14
14
|
class Chat < Base
|
|
15
15
|
SLASH_COMMANDS = %w[/help /quit /clear /compact /copy /diff /model /session /cost /export /tools /dashboard
|
|
16
16
|
/hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
|
|
17
|
-
/theme /search /stats /personality
|
|
17
|
+
/theme /search /stats /personality /undo /history /pin /pins /rename
|
|
18
|
+
/context /alias /snippet /debug].freeze
|
|
18
19
|
|
|
19
20
|
PERSONALITIES = {
|
|
20
21
|
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
@@ -38,6 +39,10 @@ module Legion
|
|
|
38
39
|
@session_store = SessionStore.new
|
|
39
40
|
@session_name = 'default'
|
|
40
41
|
@plan_mode = false
|
|
42
|
+
@pinned_messages = []
|
|
43
|
+
@aliases = {}
|
|
44
|
+
@snippets = {}
|
|
45
|
+
@debug_mode = false
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def activate
|
|
@@ -84,7 +89,12 @@ module Legion
|
|
|
84
89
|
return nil unless input.start_with?('/')
|
|
85
90
|
|
|
86
91
|
cmd = input.split.first
|
|
87
|
-
|
|
92
|
+
unless SLASH_COMMANDS.include?(cmd)
|
|
93
|
+
expanded = @aliases[cmd]
|
|
94
|
+
return nil unless expanded
|
|
95
|
+
|
|
96
|
+
return handle_slash_command("#{expanded} #{input.split(nil, 2)[1]}".strip)
|
|
97
|
+
end
|
|
88
98
|
|
|
89
99
|
dispatch_slash(cmd, input)
|
|
90
100
|
end
|
|
@@ -119,10 +129,14 @@ module Legion
|
|
|
119
129
|
def render(width, height)
|
|
120
130
|
bar_line = @status_bar.render(width: width)
|
|
121
131
|
divider = Theme.c(:muted, '-' * width)
|
|
122
|
-
|
|
132
|
+
dbg = debug_segment
|
|
133
|
+
extra_rows = dbg ? 1 : 0
|
|
134
|
+
stream_height = [height - 2 - extra_rows, 1].max
|
|
123
135
|
stream_lines = @message_stream.render(width: width, height: stream_height)
|
|
124
136
|
@status_bar.update(scroll: @message_stream.scroll_position)
|
|
125
|
-
stream_lines + [divider, bar_line]
|
|
137
|
+
lines = stream_lines + [divider, bar_line]
|
|
138
|
+
lines << dbg if dbg
|
|
139
|
+
lines
|
|
126
140
|
end
|
|
127
141
|
|
|
128
142
|
def handle_input(key)
|
|
@@ -263,7 +277,7 @@ module Legion
|
|
|
263
277
|
nil
|
|
264
278
|
end
|
|
265
279
|
|
|
266
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
280
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
267
281
|
def dispatch_slash(cmd, input)
|
|
268
282
|
case cmd
|
|
269
283
|
when '/quit' then :quit
|
|
@@ -292,11 +306,21 @@ module Legion
|
|
|
292
306
|
when '/search' then handle_search(input)
|
|
293
307
|
when '/stats' then handle_stats
|
|
294
308
|
when '/personality' then handle_personality(input)
|
|
309
|
+
when '/undo' then handle_undo
|
|
310
|
+
when '/history' then handle_history
|
|
311
|
+
when '/pin' then handle_pin(input)
|
|
312
|
+
when '/pins' then handle_pins
|
|
313
|
+
when '/rename' then handle_rename(input)
|
|
314
|
+
when '/context' then handle_context
|
|
315
|
+
when '/alias' then handle_alias(input)
|
|
316
|
+
when '/snippet' then handle_snippet(input)
|
|
317
|
+
when '/debug' then handle_debug
|
|
295
318
|
else :handled
|
|
296
319
|
end
|
|
297
320
|
end
|
|
298
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
321
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
299
322
|
|
|
323
|
+
# rubocop:disable Metrics/MethodLength
|
|
300
324
|
def handle_help
|
|
301
325
|
@message_stream.add_message(
|
|
302
326
|
role: :system,
|
|
@@ -309,11 +333,21 @@ module Legion
|
|
|
309
333
|
"/copy -- copy last assistant message to clipboard\n " \
|
|
310
334
|
"/diff -- show new messages since session was loaded\n " \
|
|
311
335
|
"/stats -- show conversation statistics\n " \
|
|
312
|
-
"/personality [name] -- switch assistant personality\n
|
|
336
|
+
"/personality [name] -- switch assistant personality\n " \
|
|
337
|
+
"/undo -- remove last user+assistant message pair\n " \
|
|
338
|
+
"/history -- show recent input history\n " \
|
|
339
|
+
"/pin [N] -- pin last assistant message (or message at index N)\n " \
|
|
340
|
+
"/pins -- show all pinned messages\n " \
|
|
341
|
+
"/rename <name> -- rename current session\n " \
|
|
342
|
+
"/context -- show active session context summary\n " \
|
|
343
|
+
"/alias [shortname /command] -- create or list command aliases\n " \
|
|
344
|
+
"/snippet save|load|list|delete <name> -- manage reusable text snippets\n " \
|
|
345
|
+
"/debug -- toggle debug mode (shows internal state)\n\n" \
|
|
313
346
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
314
347
|
)
|
|
315
348
|
:handled
|
|
316
349
|
end
|
|
350
|
+
# rubocop:enable Metrics/MethodLength
|
|
317
351
|
|
|
318
352
|
def handle_clear
|
|
319
353
|
@message_stream.messages.clear
|
|
@@ -403,6 +437,7 @@ module Legion
|
|
|
403
437
|
@session_name = name
|
|
404
438
|
@session_store.save(name, messages: @message_stream.messages)
|
|
405
439
|
@status_bar.update(session: name)
|
|
440
|
+
@status_bar.notify(message: "Saved '#{name}'", level: :success, ttl: 3)
|
|
406
441
|
@message_stream.add_message(role: :system, content: "Session saved as '#{name}'.")
|
|
407
442
|
:handled
|
|
408
443
|
end
|
|
@@ -422,6 +457,7 @@ module Legion
|
|
|
422
457
|
@loaded_message_count = @message_stream.messages.size
|
|
423
458
|
@session_name = name
|
|
424
459
|
@status_bar.update(session: name)
|
|
460
|
+
@status_bar.notify(message: "Loaded '#{name}'", level: :info, ttl: 3)
|
|
425
461
|
@message_stream.add_message(role: :system,
|
|
426
462
|
content: "Session '#{name}' loaded (#{data[:messages].size} messages).")
|
|
427
463
|
:handled
|
|
@@ -454,16 +490,29 @@ module Legion
|
|
|
454
490
|
:handled
|
|
455
491
|
end
|
|
456
492
|
|
|
457
|
-
# rubocop:disable Metrics/AbcSize
|
|
458
493
|
def handle_export(input)
|
|
459
494
|
require 'fileutils'
|
|
495
|
+
path = build_export_path(input)
|
|
496
|
+
dispatch_export(path, input.split[1]&.downcase)
|
|
497
|
+
@status_bar.notify(message: 'Exported', level: :success, ttl: 3)
|
|
498
|
+
@message_stream.add_message(role: :system, content: "Exported to: #{path}")
|
|
499
|
+
:handled
|
|
500
|
+
rescue StandardError => e
|
|
501
|
+
@message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
|
|
502
|
+
:handled
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def build_export_path(input)
|
|
460
506
|
format = input.split[1]&.downcase
|
|
461
507
|
format = 'md' unless %w[json md html].include?(format)
|
|
462
508
|
exports_dir = File.expand_path('~/.legionio/exports')
|
|
463
509
|
FileUtils.mkdir_p(exports_dir)
|
|
464
510
|
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
465
511
|
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
|
|
466
|
-
|
|
512
|
+
File.join(exports_dir, "chat-#{timestamp}.#{ext}")
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def dispatch_export(path, format)
|
|
467
516
|
if format == 'json'
|
|
468
517
|
export_json(path)
|
|
469
518
|
elsif format == 'html'
|
|
@@ -471,15 +520,8 @@ module Legion
|
|
|
471
520
|
else
|
|
472
521
|
export_markdown(path)
|
|
473
522
|
end
|
|
474
|
-
@message_stream.add_message(role: :system, content: "Exported to: #{path}")
|
|
475
|
-
:handled
|
|
476
|
-
rescue StandardError => e
|
|
477
|
-
@message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
|
|
478
|
-
:handled
|
|
479
523
|
end
|
|
480
524
|
|
|
481
|
-
# rubocop:enable Metrics/AbcSize
|
|
482
|
-
|
|
483
525
|
# rubocop:disable Metrics/AbcSize
|
|
484
526
|
def handle_tools
|
|
485
527
|
lex_gems = Gem::Specification.select { |s| s.name.start_with?('lex-') }
|
|
@@ -605,6 +647,7 @@ module Legion
|
|
|
605
647
|
name = input.split(nil, 2)[1]
|
|
606
648
|
if name
|
|
607
649
|
if Theme.switch(name)
|
|
650
|
+
@status_bar.notify(message: "Theme: #{name}", level: :info, ttl: 2)
|
|
608
651
|
@message_stream.add_message(role: :system, content: "Theme switched to: #{name}")
|
|
609
652
|
else
|
|
610
653
|
available = Theme.available_themes.join(', ')
|
|
@@ -853,6 +896,260 @@ module Legion
|
|
|
853
896
|
text.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
854
897
|
end
|
|
855
898
|
|
|
899
|
+
def handle_undo
|
|
900
|
+
msgs = @message_stream.messages
|
|
901
|
+
last_user_idx = msgs.rindex { |m| m[:role] == :user }
|
|
902
|
+
unless last_user_idx
|
|
903
|
+
@message_stream.add_message(role: :system, content: 'Nothing to undo.')
|
|
904
|
+
return :handled
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
msgs.slice!(last_user_idx..)
|
|
908
|
+
:handled
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def handle_history
|
|
912
|
+
entries = @input_bar.history
|
|
913
|
+
if entries.empty?
|
|
914
|
+
@message_stream.add_message(role: :system, content: 'No input history.')
|
|
915
|
+
else
|
|
916
|
+
recent = entries.last(20)
|
|
917
|
+
lines = recent.each_with_index.map { |entry, i| " #{i + 1}. #{entry}" }
|
|
918
|
+
@message_stream.add_message(role: :system,
|
|
919
|
+
content: "Input history (last #{recent.size}):\n#{lines.join("\n")}")
|
|
920
|
+
end
|
|
921
|
+
:handled
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def handle_pin(input)
|
|
925
|
+
idx_str = input.split(nil, 2)[1]
|
|
926
|
+
msg = if idx_str
|
|
927
|
+
@message_stream.messages[idx_str.to_i]
|
|
928
|
+
else
|
|
929
|
+
@message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
930
|
+
end
|
|
931
|
+
unless msg
|
|
932
|
+
@message_stream.add_message(role: :system, content: 'No message to pin.')
|
|
933
|
+
return :handled
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
@pinned_messages << msg
|
|
937
|
+
preview = truncate_text(msg[:content].to_s, 60)
|
|
938
|
+
@message_stream.add_message(role: :system, content: "Pinned: #{preview}")
|
|
939
|
+
:handled
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def handle_pins
|
|
943
|
+
if @pinned_messages.empty?
|
|
944
|
+
@message_stream.add_message(role: :system, content: 'No pinned messages.')
|
|
945
|
+
else
|
|
946
|
+
lines = @pinned_messages.each_with_index.map do |msg, i|
|
|
947
|
+
" #{i + 1}. [#{msg[:role]}] #{truncate_text(msg[:content].to_s, 70)}"
|
|
948
|
+
end
|
|
949
|
+
@message_stream.add_message(role: :system,
|
|
950
|
+
content: "Pinned messages (#{@pinned_messages.size}):\n#{lines.join("\n")}")
|
|
951
|
+
end
|
|
952
|
+
:handled
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def handle_rename(input)
|
|
956
|
+
name = input.split(nil, 2)[1]
|
|
957
|
+
unless name
|
|
958
|
+
@message_stream.add_message(role: :system, content: 'Usage: /rename <new-name>')
|
|
959
|
+
return :handled
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
old_name = @session_name
|
|
963
|
+
@session_store.delete(old_name) if old_name != 'default'
|
|
964
|
+
@session_name = name
|
|
965
|
+
@status_bar.update(session: name)
|
|
966
|
+
@session_store.save(name, messages: @message_stream.messages)
|
|
967
|
+
@message_stream.add_message(role: :system, content: "Session renamed to '#{name}'.")
|
|
968
|
+
:handled
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# rubocop:disable Metrics/AbcSize
|
|
972
|
+
def handle_context
|
|
973
|
+
cfg = safe_config
|
|
974
|
+
model_info = @llm_chat.respond_to?(:model) ? @llm_chat.model.to_s : (cfg[:provider] || 'none')
|
|
975
|
+
sys_prompt = if @llm_chat.respond_to?(:instructions) && @llm_chat.instructions
|
|
976
|
+
truncate_text(@llm_chat.instructions.to_s, 80)
|
|
977
|
+
else
|
|
978
|
+
'default'
|
|
979
|
+
end
|
|
980
|
+
lines = [
|
|
981
|
+
'Session Context:',
|
|
982
|
+
" Model/Provider : #{model_info}",
|
|
983
|
+
" Personality : #{@personality || 'default'}",
|
|
984
|
+
" Plan mode : #{@plan_mode ? 'on' : 'off'}",
|
|
985
|
+
" System prompt : #{sys_prompt}",
|
|
986
|
+
" Session : #{@session_name}",
|
|
987
|
+
" Messages : #{@message_stream.messages.size}",
|
|
988
|
+
" Pinned : #{@pinned_messages.size}",
|
|
989
|
+
" Tokens : #{@token_tracker.summary}"
|
|
990
|
+
]
|
|
991
|
+
@message_stream.add_message(role: :system, content: lines.join("\n"))
|
|
992
|
+
:handled
|
|
993
|
+
end
|
|
994
|
+
# rubocop:enable Metrics/AbcSize
|
|
995
|
+
|
|
996
|
+
def handle_alias(input)
|
|
997
|
+
parts = input.split(nil, 3)
|
|
998
|
+
if parts.size < 2
|
|
999
|
+
if @aliases.empty?
|
|
1000
|
+
@message_stream.add_message(role: :system, content: 'No aliases defined.')
|
|
1001
|
+
else
|
|
1002
|
+
lines = @aliases.map { |k, v| " #{k} => #{v}" }
|
|
1003
|
+
@message_stream.add_message(role: :system, content: "Aliases:\n#{lines.join("\n")}")
|
|
1004
|
+
end
|
|
1005
|
+
return :handled
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
shortname = parts[1]
|
|
1009
|
+
expansion = parts[2]
|
|
1010
|
+
unless expansion
|
|
1011
|
+
@message_stream.add_message(role: :system, content: 'Usage: /alias <shortname> <command and args>')
|
|
1012
|
+
return :handled
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
alias_key = shortname.start_with?('/') ? shortname : "/#{shortname}"
|
|
1016
|
+
@aliases[alias_key] = expansion
|
|
1017
|
+
@message_stream.add_message(role: :system, content: "Alias created: #{alias_key} => #{expansion}")
|
|
1018
|
+
:handled
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def handle_snippet(input)
|
|
1022
|
+
parts = input.split(nil, 3)
|
|
1023
|
+
subcommand = parts[1]
|
|
1024
|
+
name = parts[2]
|
|
1025
|
+
|
|
1026
|
+
case subcommand
|
|
1027
|
+
when 'save'
|
|
1028
|
+
snippet_save(name)
|
|
1029
|
+
when 'load'
|
|
1030
|
+
snippet_load(name)
|
|
1031
|
+
when 'list'
|
|
1032
|
+
snippet_list
|
|
1033
|
+
when 'delete'
|
|
1034
|
+
snippet_delete(name)
|
|
1035
|
+
else
|
|
1036
|
+
@message_stream.add_message(
|
|
1037
|
+
role: :system,
|
|
1038
|
+
content: 'Usage: /snippet save|load|list|delete <name>'
|
|
1039
|
+
)
|
|
1040
|
+
end
|
|
1041
|
+
:handled
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def handle_debug
|
|
1045
|
+
@debug_mode = !@debug_mode
|
|
1046
|
+
if @debug_mode
|
|
1047
|
+
@status_bar.update(debug_mode: true)
|
|
1048
|
+
@message_stream.add_message(role: :system, content: 'Debug mode ON -- internal state shown below.')
|
|
1049
|
+
else
|
|
1050
|
+
@status_bar.update(debug_mode: false)
|
|
1051
|
+
@message_stream.add_message(role: :system, content: 'Debug mode OFF.')
|
|
1052
|
+
end
|
|
1053
|
+
:handled
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
def debug_segment
|
|
1057
|
+
return nil unless @debug_mode
|
|
1058
|
+
|
|
1059
|
+
"[DEBUG] msgs:#{@message_stream.messages.size} " \
|
|
1060
|
+
"scroll:#{@message_stream.scroll_position&.dig(:current) || 0} " \
|
|
1061
|
+
"plan:#{@plan_mode} " \
|
|
1062
|
+
"personality:#{@personality || 'default'} " \
|
|
1063
|
+
"aliases:#{@aliases.size} " \
|
|
1064
|
+
"snippets:#{@snippets.size} " \
|
|
1065
|
+
"pinned:#{@pinned_messages.size}"
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def snippet_dir
|
|
1069
|
+
File.expand_path('~/.legionio/snippets')
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
# rubocop:disable Metrics/AbcSize
|
|
1073
|
+
def snippet_save(name)
|
|
1074
|
+
unless name
|
|
1075
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet save <name>')
|
|
1076
|
+
return
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
1080
|
+
unless last_assistant
|
|
1081
|
+
@message_stream.add_message(role: :system, content: 'No assistant message to save as snippet.')
|
|
1082
|
+
return
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
require 'fileutils'
|
|
1086
|
+
FileUtils.mkdir_p(snippet_dir)
|
|
1087
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
1088
|
+
File.write(path, last_assistant[:content].to_s)
|
|
1089
|
+
@snippets[name] = last_assistant[:content].to_s
|
|
1090
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' saved.")
|
|
1091
|
+
end
|
|
1092
|
+
# rubocop:enable Metrics/AbcSize
|
|
1093
|
+
|
|
1094
|
+
def snippet_load(name)
|
|
1095
|
+
unless name
|
|
1096
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet load <name>')
|
|
1097
|
+
return
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
content = @snippets[name]
|
|
1101
|
+
if content.nil?
|
|
1102
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
1103
|
+
content = File.read(path) if File.exist?(path)
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
unless content
|
|
1107
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
1108
|
+
return
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
@snippets[name] = content
|
|
1112
|
+
@message_stream.add_message(role: :user, content: content)
|
|
1113
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' inserted.")
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
# rubocop:disable Metrics/AbcSize
|
|
1117
|
+
def snippet_list
|
|
1118
|
+
disk_snippets = Dir.glob(File.join(snippet_dir, '*.txt')).map { |f| File.basename(f, '.txt') }
|
|
1119
|
+
all_names = (@snippets.keys + disk_snippets).uniq.sort
|
|
1120
|
+
|
|
1121
|
+
if all_names.empty?
|
|
1122
|
+
@message_stream.add_message(role: :system, content: 'No snippets saved.')
|
|
1123
|
+
return
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
lines = all_names.map do |sname|
|
|
1127
|
+
content = @snippets[sname] || begin
|
|
1128
|
+
path = File.join(snippet_dir, "#{sname}.txt")
|
|
1129
|
+
File.exist?(path) ? File.read(path) : ''
|
|
1130
|
+
end
|
|
1131
|
+
" #{sname}: #{truncate_text(content.to_s, 60)}"
|
|
1132
|
+
end
|
|
1133
|
+
@message_stream.add_message(role: :system, content: "Snippets (#{all_names.size}):\n#{lines.join("\n")}")
|
|
1134
|
+
end
|
|
1135
|
+
# rubocop:enable Metrics/AbcSize
|
|
1136
|
+
|
|
1137
|
+
def snippet_delete(name)
|
|
1138
|
+
unless name
|
|
1139
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
|
|
1140
|
+
return
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
@snippets.delete(name)
|
|
1144
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
1145
|
+
if File.exist?(path)
|
|
1146
|
+
File.delete(path)
|
|
1147
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' deleted.")
|
|
1148
|
+
else
|
|
1149
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
1150
|
+
end
|
|
1151
|
+
end
|
|
1152
|
+
|
|
856
1153
|
def build_default_input_bar
|
|
857
1154
|
cfg = safe_config
|
|
858
1155
|
name = cfg[:name] || 'User'
|
data/lib/legion/tty/version.rb
CHANGED