legion-tty 0.4.9 → 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 +8 -0
- data/lib/legion/tty/components/status_bar.rb +9 -1
- data/lib/legion/tty/screens/chat.rb +212 -7
- 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,13 @@
|
|
|
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
|
+
|
|
3
11
|
## [0.4.9] - 2026-03-19
|
|
4
12
|
|
|
5
13
|
### Added
|
|
@@ -8,7 +8,8 @@ module Legion
|
|
|
8
8
|
module Components
|
|
9
9
|
class StatusBar
|
|
10
10
|
def initialize
|
|
11
|
-
@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 }
|
|
12
13
|
@notifications = []
|
|
13
14
|
end
|
|
14
15
|
|
|
@@ -40,6 +41,7 @@ module Legion
|
|
|
40
41
|
[
|
|
41
42
|
model_segment,
|
|
42
43
|
plan_segment,
|
|
44
|
+
debug_segment,
|
|
43
45
|
thinking_segment,
|
|
44
46
|
notification_segment,
|
|
45
47
|
tokens_segment,
|
|
@@ -59,6 +61,12 @@ module Legion
|
|
|
59
61
|
Theme.c(:warning, '[PLAN]')
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
def debug_segment
|
|
65
|
+
return nil unless @state[:debug_mode]
|
|
66
|
+
|
|
67
|
+
Theme.c(:muted, '[DBG]')
|
|
68
|
+
end
|
|
69
|
+
|
|
62
70
|
def thinking_segment
|
|
63
71
|
return nil unless @state[:thinking]
|
|
64
72
|
|
|
@@ -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 /undo /history /pin /pins /rename
|
|
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.',
|
|
@@ -39,6 +40,9 @@ module Legion
|
|
|
39
40
|
@session_name = 'default'
|
|
40
41
|
@plan_mode = false
|
|
41
42
|
@pinned_messages = []
|
|
43
|
+
@aliases = {}
|
|
44
|
+
@snippets = {}
|
|
45
|
+
@debug_mode = false
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
def activate
|
|
@@ -85,7 +89,12 @@ module Legion
|
|
|
85
89
|
return nil unless input.start_with?('/')
|
|
86
90
|
|
|
87
91
|
cmd = input.split.first
|
|
88
|
-
|
|
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
|
|
89
98
|
|
|
90
99
|
dispatch_slash(cmd, input)
|
|
91
100
|
end
|
|
@@ -120,10 +129,14 @@ module Legion
|
|
|
120
129
|
def render(width, height)
|
|
121
130
|
bar_line = @status_bar.render(width: width)
|
|
122
131
|
divider = Theme.c(:muted, '-' * width)
|
|
123
|
-
|
|
132
|
+
dbg = debug_segment
|
|
133
|
+
extra_rows = dbg ? 1 : 0
|
|
134
|
+
stream_height = [height - 2 - extra_rows, 1].max
|
|
124
135
|
stream_lines = @message_stream.render(width: width, height: stream_height)
|
|
125
136
|
@status_bar.update(scroll: @message_stream.scroll_position)
|
|
126
|
-
stream_lines + [divider, bar_line]
|
|
137
|
+
lines = stream_lines + [divider, bar_line]
|
|
138
|
+
lines << dbg if dbg
|
|
139
|
+
lines
|
|
127
140
|
end
|
|
128
141
|
|
|
129
142
|
def handle_input(key)
|
|
@@ -264,7 +277,7 @@ module Legion
|
|
|
264
277
|
nil
|
|
265
278
|
end
|
|
266
279
|
|
|
267
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
280
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
268
281
|
def dispatch_slash(cmd, input)
|
|
269
282
|
case cmd
|
|
270
283
|
when '/quit' then :quit
|
|
@@ -298,11 +311,16 @@ module Legion
|
|
|
298
311
|
when '/pin' then handle_pin(input)
|
|
299
312
|
when '/pins' then handle_pins
|
|
300
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
|
|
301
318
|
else :handled
|
|
302
319
|
end
|
|
303
320
|
end
|
|
304
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
321
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
305
322
|
|
|
323
|
+
# rubocop:disable Metrics/MethodLength
|
|
306
324
|
def handle_help
|
|
307
325
|
@message_stream.add_message(
|
|
308
326
|
role: :system,
|
|
@@ -320,11 +338,16 @@ module Legion
|
|
|
320
338
|
"/history -- show recent input history\n " \
|
|
321
339
|
"/pin [N] -- pin last assistant message (or message at index N)\n " \
|
|
322
340
|
"/pins -- show all pinned messages\n " \
|
|
323
|
-
"/rename <name> -- rename current session\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" \
|
|
324
346
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
325
347
|
)
|
|
326
348
|
:handled
|
|
327
349
|
end
|
|
350
|
+
# rubocop:enable Metrics/MethodLength
|
|
328
351
|
|
|
329
352
|
def handle_clear
|
|
330
353
|
@message_stream.messages.clear
|
|
@@ -945,6 +968,188 @@ module Legion
|
|
|
945
968
|
:handled
|
|
946
969
|
end
|
|
947
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
|
+
|
|
948
1153
|
def build_default_input_bar
|
|
949
1154
|
cfg = safe_config
|
|
950
1155
|
name = cfg[:name] || 'User'
|
data/lib/legion/tty/version.rb
CHANGED