legion-tty 0.4.9 → 0.4.11

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: 3e2b8a679110725634b3db870bb623fc8f5cef89706890f9c57c579490c85114
4
+ data.tar.gz: 6487e27647ed48406731a9c0c1bf1664b64831b60139dd698ae794d2dc8d3871
5
5
  SHA512:
6
- metadata.gz: 9f65b4afeeed674a49e59a43e4116eee383db5bdc2083cd648342c9134ba2e971d8403d8d3fc57ebbcacacbf9dcec293d482cce818a97c84ce4485d843ad1490
7
- data.tar.gz: cd51d5ede4592418fe9a25226c054a2cf26adec493f7efc94763c6165c4bef50b1fdfe74f039759fb4cabdf0fec36ab679b643f28273f8a8573011e8a063c3d8
6
+ metadata.gz: 9c89fb3dbcb807c628889b2b60566b3b23cad1356c26de42cd1d02d75a2977e7856528b0193e8bb9dd630ee4cf52b2b55e06e009c8fc3e34550a1712d6768b68
7
+ data.tar.gz: ae426495d1eb8708723a5c2804a4a77e4a557480324c23b370b0ebe324ea1a7ece957665b133f7810952f03f586cb0e87dcef3e1c78e9fcde81fe13e0e6e4760
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.11] - 2026-03-19
4
+
5
+ ### Added
6
+ - Dashboard LLM status panel: shows provider, model, started/daemon status with green/red icons
7
+ - Dashboard panel navigation: j/k/arrows to move between panels, 1-5 to jump, 'e' to open extensions
8
+ - `/uptime` command: show current chat session elapsed time
9
+ - `/bookmark` command: export all pinned messages to markdown file
10
+
11
+ ## [0.4.10] - 2026-03-19
12
+
13
+ ### Added
14
+ - `/context` command: display active session state summary (model, personality, plan mode, system prompt, session, message count, pinned count, token usage)
15
+ - `/alias` command: create short aliases for frequently used slash commands; aliases expand and re-dispatch transparently
16
+ - `/snippet save|load|list|delete <name>` command: save last assistant message as a named snippet, insert snippets as user messages, persist to `~/.legionio/snippets/`
17
+ - `/debug` command: toggle debug mode; adds `[DEBUG]` line to render output showing msgs/scroll/plan/personality/aliases/snippets/pinned counts; StatusBar shows `[DBG]` indicator
18
+
3
19
  ## [0.4.9] - 2026-03-19
4
20
 
5
21
  ### 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 /uptime /bookmark].freeze
18
19
 
19
20
  PERSONALITIES = {
20
21
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -26,6 +27,7 @@ module Legion
26
27
 
27
28
  attr_reader :message_stream, :status_bar
28
29
 
30
+ # rubocop:disable Metrics/AbcSize
29
31
  def initialize(app, output: $stdout, input_bar: nil)
30
32
  super(app)
31
33
  @output = output
@@ -39,8 +41,14 @@ module Legion
39
41
  @session_name = 'default'
40
42
  @plan_mode = false
41
43
  @pinned_messages = []
44
+ @aliases = {}
45
+ @snippets = {}
46
+ @debug_mode = false
47
+ @session_start = Time.now
42
48
  end
43
49
 
50
+ # rubocop:enable Metrics/AbcSize
51
+
44
52
  def activate
45
53
  @running = true
46
54
  cfg = safe_config
@@ -85,7 +93,12 @@ module Legion
85
93
  return nil unless input.start_with?('/')
86
94
 
87
95
  cmd = input.split.first
88
- return nil unless SLASH_COMMANDS.include?(cmd)
96
+ unless SLASH_COMMANDS.include?(cmd)
97
+ expanded = @aliases[cmd]
98
+ return nil unless expanded
99
+
100
+ return handle_slash_command("#{expanded} #{input.split(nil, 2)[1]}".strip)
101
+ end
89
102
 
90
103
  dispatch_slash(cmd, input)
91
104
  end
@@ -120,10 +133,14 @@ module Legion
120
133
  def render(width, height)
121
134
  bar_line = @status_bar.render(width: width)
122
135
  divider = Theme.c(:muted, '-' * width)
123
- stream_height = [height - 2, 1].max
136
+ dbg = debug_segment
137
+ extra_rows = dbg ? 1 : 0
138
+ stream_height = [height - 2 - extra_rows, 1].max
124
139
  stream_lines = @message_stream.render(width: width, height: stream_height)
125
140
  @status_bar.update(scroll: @message_stream.scroll_position)
126
- stream_lines + [divider, bar_line]
141
+ lines = stream_lines + [divider, bar_line]
142
+ lines << dbg if dbg
143
+ lines
127
144
  end
128
145
 
129
146
  def handle_input(key)
@@ -264,7 +281,7 @@ module Legion
264
281
  nil
265
282
  end
266
283
 
267
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
284
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
268
285
  def dispatch_slash(cmd, input)
269
286
  case cmd
270
287
  when '/quit' then :quit
@@ -298,11 +315,18 @@ module Legion
298
315
  when '/pin' then handle_pin(input)
299
316
  when '/pins' then handle_pins
300
317
  when '/rename' then handle_rename(input)
318
+ when '/context' then handle_context
319
+ when '/alias' then handle_alias(input)
320
+ when '/snippet' then handle_snippet(input)
321
+ when '/debug' then handle_debug
322
+ when '/uptime' then handle_uptime
323
+ when '/bookmark' then handle_bookmark
301
324
  else :handled
302
325
  end
303
326
  end
304
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
327
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
305
328
 
329
+ # rubocop:disable Metrics/MethodLength
306
330
  def handle_help
307
331
  @message_stream.add_message(
308
332
  role: :system,
@@ -320,11 +344,18 @@ module Legion
320
344
  "/history -- show recent input history\n " \
321
345
  "/pin [N] -- pin last assistant message (or message at index N)\n " \
322
346
  "/pins -- show all pinned messages\n " \
323
- "/rename <name> -- rename current session\n\n" \
347
+ "/rename <name> -- rename current session\n " \
348
+ "/context -- show active session context summary\n " \
349
+ "/alias [shortname /command] -- create or list command aliases\n " \
350
+ "/snippet save|load|list|delete <name> -- manage reusable text snippets\n " \
351
+ "/debug -- toggle debug mode (shows internal state)\n " \
352
+ "/uptime -- show how long this session has been active\n " \
353
+ "/bookmark -- export pinned messages to a markdown file\n\n" \
324
354
  'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
325
355
  )
326
356
  :handled
327
357
  end
358
+ # rubocop:enable Metrics/MethodLength
328
359
 
329
360
  def handle_clear
330
361
  @message_stream.messages.clear
@@ -945,6 +976,224 @@ module Legion
945
976
  :handled
946
977
  end
947
978
 
979
+ # rubocop:disable Metrics/AbcSize
980
+ def handle_context
981
+ cfg = safe_config
982
+ model_info = @llm_chat.respond_to?(:model) ? @llm_chat.model.to_s : (cfg[:provider] || 'none')
983
+ sys_prompt = if @llm_chat.respond_to?(:instructions) && @llm_chat.instructions
984
+ truncate_text(@llm_chat.instructions.to_s, 80)
985
+ else
986
+ 'default'
987
+ end
988
+ lines = [
989
+ 'Session Context:',
990
+ " Model/Provider : #{model_info}",
991
+ " Personality : #{@personality || 'default'}",
992
+ " Plan mode : #{@plan_mode ? 'on' : 'off'}",
993
+ " System prompt : #{sys_prompt}",
994
+ " Session : #{@session_name}",
995
+ " Messages : #{@message_stream.messages.size}",
996
+ " Pinned : #{@pinned_messages.size}",
997
+ " Tokens : #{@token_tracker.summary}"
998
+ ]
999
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
1000
+ :handled
1001
+ end
1002
+ # rubocop:enable Metrics/AbcSize
1003
+
1004
+ def handle_alias(input)
1005
+ parts = input.split(nil, 3)
1006
+ if parts.size < 2
1007
+ if @aliases.empty?
1008
+ @message_stream.add_message(role: :system, content: 'No aliases defined.')
1009
+ else
1010
+ lines = @aliases.map { |k, v| " #{k} => #{v}" }
1011
+ @message_stream.add_message(role: :system, content: "Aliases:\n#{lines.join("\n")}")
1012
+ end
1013
+ return :handled
1014
+ end
1015
+
1016
+ shortname = parts[1]
1017
+ expansion = parts[2]
1018
+ unless expansion
1019
+ @message_stream.add_message(role: :system, content: 'Usage: /alias <shortname> <command and args>')
1020
+ return :handled
1021
+ end
1022
+
1023
+ alias_key = shortname.start_with?('/') ? shortname : "/#{shortname}"
1024
+ @aliases[alias_key] = expansion
1025
+ @message_stream.add_message(role: :system, content: "Alias created: #{alias_key} => #{expansion}")
1026
+ :handled
1027
+ end
1028
+
1029
+ def handle_snippet(input)
1030
+ parts = input.split(nil, 3)
1031
+ subcommand = parts[1]
1032
+ name = parts[2]
1033
+
1034
+ case subcommand
1035
+ when 'save'
1036
+ snippet_save(name)
1037
+ when 'load'
1038
+ snippet_load(name)
1039
+ when 'list'
1040
+ snippet_list
1041
+ when 'delete'
1042
+ snippet_delete(name)
1043
+ else
1044
+ @message_stream.add_message(
1045
+ role: :system,
1046
+ content: 'Usage: /snippet save|load|list|delete <name>'
1047
+ )
1048
+ end
1049
+ :handled
1050
+ end
1051
+
1052
+ def handle_debug
1053
+ @debug_mode = !@debug_mode
1054
+ if @debug_mode
1055
+ @status_bar.update(debug_mode: true)
1056
+ @message_stream.add_message(role: :system, content: 'Debug mode ON -- internal state shown below.')
1057
+ else
1058
+ @status_bar.update(debug_mode: false)
1059
+ @message_stream.add_message(role: :system, content: 'Debug mode OFF.')
1060
+ end
1061
+ :handled
1062
+ end
1063
+
1064
+ def handle_uptime
1065
+ elapsed = Time.now - @session_start
1066
+ hours = (elapsed / 3600).to_i
1067
+ minutes = ((elapsed % 3600) / 60).to_i
1068
+ seconds = (elapsed % 60).to_i
1069
+ @message_stream.add_message(role: :system, content: "Session uptime: #{hours}h #{minutes}m #{seconds}s")
1070
+ :handled
1071
+ end
1072
+
1073
+ # rubocop:disable Metrics/AbcSize
1074
+ def handle_bookmark
1075
+ require 'fileutils'
1076
+ if @pinned_messages.empty?
1077
+ @message_stream.add_message(role: :system, content: 'No pinned messages to export.')
1078
+ return :handled
1079
+ end
1080
+
1081
+ exports_dir = File.expand_path('~/.legionio/exports')
1082
+ FileUtils.mkdir_p(exports_dir)
1083
+ timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
1084
+ path = File.join(exports_dir, "bookmarks-#{timestamp}.md")
1085
+ lines = ["# Pinned Messages\n", "_Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}_\n\n---\n"]
1086
+ @pinned_messages.each_with_index do |msg, i|
1087
+ role_label = msg[:role].to_s.capitalize
1088
+ lines << "\n## Bookmark #{i + 1} (#{role_label})\n\n#{msg[:content]}\n"
1089
+ end
1090
+ File.write(path, lines.join)
1091
+ @message_stream.add_message(role: :system, content: "Bookmarks exported to: #{path}")
1092
+ :handled
1093
+ rescue StandardError => e
1094
+ @message_stream.add_message(role: :system, content: "Bookmark export failed: #{e.message}")
1095
+ :handled
1096
+ end
1097
+
1098
+ # rubocop:enable Metrics/AbcSize
1099
+
1100
+ def debug_segment
1101
+ return nil unless @debug_mode
1102
+
1103
+ "[DEBUG] msgs:#{@message_stream.messages.size} " \
1104
+ "scroll:#{@message_stream.scroll_position&.dig(:current) || 0} " \
1105
+ "plan:#{@plan_mode} " \
1106
+ "personality:#{@personality || 'default'} " \
1107
+ "aliases:#{@aliases.size} " \
1108
+ "snippets:#{@snippets.size} " \
1109
+ "pinned:#{@pinned_messages.size}"
1110
+ end
1111
+
1112
+ def snippet_dir
1113
+ File.expand_path('~/.legionio/snippets')
1114
+ end
1115
+
1116
+ # rubocop:disable Metrics/AbcSize
1117
+ def snippet_save(name)
1118
+ unless name
1119
+ @message_stream.add_message(role: :system, content: 'Usage: /snippet save <name>')
1120
+ return
1121
+ end
1122
+
1123
+ last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
1124
+ unless last_assistant
1125
+ @message_stream.add_message(role: :system, content: 'No assistant message to save as snippet.')
1126
+ return
1127
+ end
1128
+
1129
+ require 'fileutils'
1130
+ FileUtils.mkdir_p(snippet_dir)
1131
+ path = File.join(snippet_dir, "#{name}.txt")
1132
+ File.write(path, last_assistant[:content].to_s)
1133
+ @snippets[name] = last_assistant[:content].to_s
1134
+ @message_stream.add_message(role: :system, content: "Snippet '#{name}' saved.")
1135
+ end
1136
+ # rubocop:enable Metrics/AbcSize
1137
+
1138
+ def snippet_load(name)
1139
+ unless name
1140
+ @message_stream.add_message(role: :system, content: 'Usage: /snippet load <name>')
1141
+ return
1142
+ end
1143
+
1144
+ content = @snippets[name]
1145
+ if content.nil?
1146
+ path = File.join(snippet_dir, "#{name}.txt")
1147
+ content = File.read(path) if File.exist?(path)
1148
+ end
1149
+
1150
+ unless content
1151
+ @message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
1152
+ return
1153
+ end
1154
+
1155
+ @snippets[name] = content
1156
+ @message_stream.add_message(role: :user, content: content)
1157
+ @message_stream.add_message(role: :system, content: "Snippet '#{name}' inserted.")
1158
+ end
1159
+
1160
+ # rubocop:disable Metrics/AbcSize
1161
+ def snippet_list
1162
+ disk_snippets = Dir.glob(File.join(snippet_dir, '*.txt')).map { |f| File.basename(f, '.txt') }
1163
+ all_names = (@snippets.keys + disk_snippets).uniq.sort
1164
+
1165
+ if all_names.empty?
1166
+ @message_stream.add_message(role: :system, content: 'No snippets saved.')
1167
+ return
1168
+ end
1169
+
1170
+ lines = all_names.map do |sname|
1171
+ content = @snippets[sname] || begin
1172
+ path = File.join(snippet_dir, "#{sname}.txt")
1173
+ File.exist?(path) ? File.read(path) : ''
1174
+ end
1175
+ " #{sname}: #{truncate_text(content.to_s, 60)}"
1176
+ end
1177
+ @message_stream.add_message(role: :system, content: "Snippets (#{all_names.size}):\n#{lines.join("\n")}")
1178
+ end
1179
+ # rubocop:enable Metrics/AbcSize
1180
+
1181
+ def snippet_delete(name)
1182
+ unless name
1183
+ @message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
1184
+ return
1185
+ end
1186
+
1187
+ @snippets.delete(name)
1188
+ path = File.join(snippet_dir, "#{name}.txt")
1189
+ if File.exist?(path)
1190
+ File.delete(path)
1191
+ @message_stream.add_message(role: :system, content: "Snippet '#{name}' deleted.")
1192
+ else
1193
+ @message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
1194
+ end
1195
+ end
1196
+
948
1197
  def build_default_input_bar
949
1198
  cfg = safe_config
950
1199
  name = cfg[:name] || 'User'
@@ -8,11 +8,14 @@ module Legion
8
8
  module Screens
9
9
  # rubocop:disable Metrics/ClassLength
10
10
  class Dashboard < Base
11
+ PANELS = %i[services llm extensions system activity].freeze
12
+
11
13
  def initialize(app)
12
14
  super
13
15
  @last_refresh = nil
14
16
  @refresh_interval = 5
15
17
  @cached_data = {}
18
+ @selected_panel = 0
16
19
  end
17
20
 
18
21
  def activate
@@ -26,6 +29,7 @@ module Legion
26
29
  rows = []
27
30
  rows.concat(render_header(width))
28
31
  rows.concat(render_services_panel(width))
32
+ rows.concat(render_llm_panel(width))
29
33
  rows.concat(render_extensions_panel(width))
30
34
  rows.concat(render_system_panel(width))
31
35
  rows.concat(render_activity_panel(width, remaining_height(height, rows.size)))
@@ -36,6 +40,7 @@ module Legion
36
40
 
37
41
  # rubocop:enable Metrics/AbcSize
38
42
 
43
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
39
44
  def handle_input(key)
40
45
  case key
41
46
  when 'r', :f5
@@ -43,15 +48,35 @@ module Legion
43
48
  :handled
44
49
  when 'q', :escape
45
50
  :pop_screen
51
+ when 'j', :down
52
+ @selected_panel = (@selected_panel + 1) % PANELS.size
53
+ :handled
54
+ when 'k', :up
55
+ @selected_panel = (@selected_panel - 1) % PANELS.size
56
+ :handled
57
+ when '1' then navigate_to_panel(0)
58
+ when '2' then navigate_to_panel(1)
59
+ when '3' then navigate_to_panel(2)
60
+ when '4' then navigate_to_panel(3)
61
+ when '5' then navigate_to_panel(4)
62
+ when 'e'
63
+ extensions_shortcut
46
64
  else
47
65
  :pass
48
66
  end
49
67
  end
50
68
 
69
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
70
+
71
+ def selected_panel
72
+ PANELS[@selected_panel]
73
+ end
74
+
51
75
  def refresh_data
52
76
  @last_refresh = Time.now
53
77
  @cached_data = {
54
78
  services: probe_services,
79
+ llm: llm_info,
55
80
  extensions: discover_extensions,
56
81
  system: system_info,
57
82
  activity: recent_activity
@@ -73,7 +98,8 @@ module Legion
73
98
 
74
99
  def render_services_panel(_width)
75
100
  services = @cached_data[:services] || {}
76
- lines = [Theme.c(:accent, ' Services')]
101
+ prefix = panel_prefix(:services)
102
+ lines = [Theme.c(:accent, "#{prefix}Services")]
77
103
  services.each do |name, info|
78
104
  icon = info[:running] ? Theme.c(:success, "\u2713") : Theme.c(:error, "\u2717")
79
105
  port_str = Theme.c(:muted, ":#{info[:port]}")
@@ -83,10 +109,29 @@ module Legion
83
109
  lines
84
110
  end
85
111
 
112
+ # rubocop:disable Metrics/AbcSize
113
+ def render_llm_panel(_width)
114
+ llm = @cached_data[:llm] || {}
115
+ prefix = panel_prefix(:llm)
116
+ lines = [Theme.c(:accent, "#{prefix}LLM")]
117
+ started_icon = llm[:started] ? Theme.c(:success, "\u2713") : Theme.c(:error, "\u2717")
118
+ daemon_icon = llm[:daemon] ? Theme.c(:success, "\u2713") : Theme.c(:error, "\u2717")
119
+ lines << " #{started_icon} Legion::LLM started"
120
+ lines << " #{daemon_icon} Daemon available"
121
+ lines << " Provider: #{Theme.c(:secondary, llm[:provider] || 'none')}"
122
+ lines << " Model: #{Theme.c(:secondary, llm[:model] || 'none')}" if llm[:model]
123
+ lines << ''
124
+ lines
125
+ end
126
+
127
+ # rubocop:enable Metrics/AbcSize
128
+
129
+ # rubocop:disable Metrics/AbcSize
86
130
  def render_extensions_panel(_width)
87
131
  extensions = @cached_data[:extensions] || []
88
132
  count = extensions.size
89
- lines = [Theme.c(:accent, " Extensions (#{count})")]
133
+ prefix = panel_prefix(:extensions)
134
+ lines = [Theme.c(:accent, "#{prefix}Extensions (#{count})")]
90
135
  if extensions.empty?
91
136
  lines << Theme.c(:muted, ' No lex-* gems found')
92
137
  else
@@ -100,10 +145,13 @@ module Legion
100
145
  lines
101
146
  end
102
147
 
148
+ # rubocop:enable Metrics/AbcSize
149
+
103
150
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
104
151
  def render_system_panel(_width)
105
152
  sys = @cached_data[:system] || {}
106
- lines = [Theme.c(:accent, ' System')]
153
+ prefix = panel_prefix(:system)
154
+ lines = [Theme.c(:accent, "#{prefix}System")]
107
155
  lines << " Ruby: #{Theme.c(:secondary, sys[:ruby_version] || 'unknown')}"
108
156
  lines << " OS: #{Theme.c(:secondary, sys[:os] || 'unknown')}"
109
157
  lines << " Host: #{Theme.c(:secondary, sys[:hostname] || 'unknown')}"
@@ -117,7 +165,8 @@ module Legion
117
165
 
118
166
  def render_activity_panel(_width, max_lines)
119
167
  activity = @cached_data[:activity] || []
120
- lines = [Theme.c(:accent, ' Recent Activity')]
168
+ prefix = panel_prefix(:activity)
169
+ lines = [Theme.c(:accent, "#{prefix}Recent Activity")]
121
170
  if activity.empty?
122
171
  lines << Theme.c(:muted, ' No recent activity')
123
172
  else
@@ -132,7 +181,8 @@ module Legion
132
181
 
133
182
  def render_help_bar(width)
134
183
  help = " #{Theme.c(:muted, 'r')}=refresh #{Theme.c(:muted, 'q')}=back " \
135
- "#{Theme.c(:muted, 'Ctrl+D')}=dashboard #{Theme.c(:muted, 'Ctrl+C')}=quit"
184
+ "#{Theme.c(:muted, 'j/k')}=navigate #{Theme.c(:muted, '1-5')}=jump " \
185
+ "#{Theme.c(:muted, 'e')}=extensions #{Theme.c(:muted, 'Ctrl+C')}=quit"
136
186
  [Theme.c(:muted, '-' * width), help]
137
187
  end
138
188
 
@@ -148,6 +198,47 @@ module Legion
148
198
  end
149
199
  end
150
200
 
201
+ def panel_prefix(panel_name)
202
+ PANELS[@selected_panel] == panel_name ? '>> ' : ' '
203
+ end
204
+
205
+ def navigate_to_panel(index)
206
+ @selected_panel = index
207
+ :handled
208
+ end
209
+
210
+ def extensions_shortcut
211
+ if PANELS[@selected_panel] == :extensions && @app.respond_to?(:screen_manager)
212
+ require_relative '../screens/extensions'
213
+ @app.screen_manager.push(Screens::Extensions.new(@app))
214
+ :handled
215
+ else
216
+ :pass
217
+ end
218
+ rescue LoadError, StandardError
219
+ :pass
220
+ end
221
+
222
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
223
+ def llm_info
224
+ info = { provider: 'none', model: nil, started: false, daemon: false }
225
+ if defined?(Legion::LLM)
226
+ info[:started] = Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
227
+ settings = Legion::LLM.respond_to?(:settings) ? Legion::LLM.settings : {}
228
+ info[:provider] = settings[:default_provider]&.to_s || 'none'
229
+ info[:model] = settings[:model]&.to_s
230
+ end
231
+ if defined?(Legion::LLM::DaemonClient)
232
+ info[:daemon] = Legion::LLM::DaemonClient.respond_to?(:available?) &&
233
+ Legion::LLM::DaemonClient.available?
234
+ end
235
+ info
236
+ rescue StandardError
237
+ info
238
+ end
239
+
240
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
241
+
151
242
  def probe_services
152
243
  require 'socket'
153
244
  {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.9'
5
+ VERSION = '0.4.11'
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.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity