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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/legion/tty/components/status_bar.rb +9 -1
- data/lib/legion/tty/screens/chat.rb +256 -7
- data/lib/legion/tty/screens/dashboard.rb +96 -5
- 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: 3e2b8a679110725634b3db870bb623fc8f5cef89706890f9c57c579490c85114
|
|
4
|
+
data.tar.gz: 6487e27647ed48406731a9c0c1bf1664b64831b60139dd698ae794d2dc8d3871
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
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
|
{
|
data/lib/legion/tty/version.rb
CHANGED