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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a862cd998ed05b3eb9a633c41602683425e79ea59f9634f0c2826e24e981291
4
- data.tar.gz: 7cda3afc03dfa47d640d7fd4eca2dce80c8701fea820e3077e7bf3f4a37fc30d
3
+ metadata.gz: b4f6ee315fe6ff9851580bebff6f0187665a2c53f1719c48ad807879651eee44
4
+ data.tar.gz: 938e195e473fb65a27840f31b82cefa9032c9f500d828f2844d8ca58b7f9bc95
5
5
  SHA512:
6
- metadata.gz: 9f65b4afeeed674a49e59a43e4116eee383db5bdc2083cd648342c9134ba2e971d8403d8d3fc57ebbcacacbf9dcec293d482cce818a97c84ce4485d843ad1490
7
- data.tar.gz: cd51d5ede4592418fe9a25226c054a2cf26adec493f7efc94763c6165c4bef50b1fdfe74f039759fb4cabdf0fec36ab679b643f28273f8a8573011e8a063c3d8
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].freeze
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
- return nil unless SLASH_COMMANDS.include?(cmd)
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
- stream_height = [height - 2, 1].max
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\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'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.9'
5
+ VERSION = '0.4.10'
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.9
4
+ version: 0.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity