legion-tty 0.4.20 → 0.4.22
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 +22 -0
- data/lib/legion/tty/app.rb +1 -1
- data/lib/legion/tty/background/github_probe.rb +2 -7
- data/lib/legion/tty/background/kerberos_probe.rb +2 -4
- data/lib/legion/tty/background/scanner.rb +0 -2
- data/lib/legion/tty/components/message_stream.rb +35 -3
- data/lib/legion/tty/components/model_picker.rb +1 -1
- data/lib/legion/tty/components/status_bar.rb +8 -1
- data/lib/legion/tty/screens/chat/custom_commands.rb +2 -8
- data/lib/legion/tty/screens/chat/export_commands.rb +47 -4
- data/lib/legion/tty/screens/chat/message_commands.rb +116 -4
- data/lib/legion/tty/screens/chat/model_commands.rb +0 -2
- data/lib/legion/tty/screens/chat/session_commands.rb +28 -2
- data/lib/legion/tty/screens/chat/ui_commands.rb +120 -4
- data/lib/legion/tty/screens/chat.rb +36 -3
- data/lib/legion/tty/screens/config.rb +1 -1
- data/lib/legion/tty/screens/dashboard.rb +6 -6
- data/lib/legion/tty/screens/onboarding.rb +12 -12
- data/lib/legion/tty/theme.rb +0 -2
- 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: 45ef88ce519140b5056ce0b934f3c3512781a4f26e30dc74203c431e6f20cc3c
|
|
4
|
+
data.tar.gz: ba632f83ea2022683c097b5451849bf129287c46959a050a8db90b356abe1f29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: affafac87b03a47e29ae08bfe5815ba9eb8f72de5fc3feda9350b2871021537f6319261177de1a20f349b8d38cda3295d6b5cca6ff941dbe9414744fa27562e5
|
|
7
|
+
data.tar.gz: bf14853b34688cd959cf6301a56542ec9448fc9f4db2fb2489e7c6aeec3d56e08fc6e44c1627dc31c33631a8b9291717786032dee7ab72b566c545693ca2b57e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.22] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/truncate [N|off]` command: display-only truncation of long messages (preserves originals)
|
|
7
|
+
- `/archive [name]` command: archive session to `~/.legionio/archives/` with timestamp and start fresh
|
|
8
|
+
- `/archives` command: list all archived sessions with file sizes
|
|
9
|
+
- `/tee <path>` command: copy new messages to file in real-time (like Unix tee)
|
|
10
|
+
- `/pipe <command>` command: pipe last assistant response through a shell command
|
|
11
|
+
- `/calc <expression>` command: safe math expression evaluator with Math functions
|
|
12
|
+
- `/rand [N|min..max]` command: generate random numbers (float, integer, or range)
|
|
13
|
+
|
|
14
|
+
## [0.4.21] - 2026-03-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `/annotate [N] <text>` command: add notes/annotations to specific messages with timestamps
|
|
18
|
+
- `/annotations` command: list all annotated messages with their notes
|
|
19
|
+
- `/filter [role|tag|pinned|clear]` command: filter displayed messages by role, tag, or pinned status
|
|
20
|
+
- `/multiline` command: toggle multi-line input mode (submit with empty line)
|
|
21
|
+
- `/export yaml` format: export chat history as YAML alongside existing md/json/html formats
|
|
22
|
+
- Annotation rendering in message stream (displayed after reactions)
|
|
23
|
+
- `[ML]` status bar indicator for multi-line input mode
|
|
24
|
+
|
|
3
25
|
## [0.4.20] - 2026-03-19
|
|
4
26
|
|
|
5
27
|
### Added
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -132,7 +132,7 @@ module Legion
|
|
|
132
132
|
|
|
133
133
|
private
|
|
134
134
|
|
|
135
|
-
def boot_legion_subsystems
|
|
135
|
+
def boot_legion_subsystems
|
|
136
136
|
# Follow the same init order as Legion::Service:
|
|
137
137
|
# 1. logging 2. settings 3. crypt 4. resolve secrets 5. LLM merge
|
|
138
138
|
require 'legion/logging'
|
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
end
|
|
40
40
|
# rubocop:enable Metrics/AbcSize
|
|
41
41
|
|
|
42
|
-
# rubocop:disable Metrics/AbcSize
|
|
42
|
+
# rubocop:disable Metrics/AbcSize
|
|
43
43
|
def run_async(queue, remotes: [], quick_profile: nil)
|
|
44
44
|
Thread.new do
|
|
45
45
|
@log&.log('github', "probing with #{remotes.size} remotes: #{remotes.first(5).inspect}")
|
|
@@ -59,7 +59,7 @@ module Legion
|
|
|
59
59
|
queue.push({ type: :github_error, error: e.message })
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
-
# rubocop:enable Metrics/AbcSize
|
|
62
|
+
# rubocop:enable Metrics/AbcSize
|
|
63
63
|
|
|
64
64
|
private
|
|
65
65
|
|
|
@@ -282,7 +282,6 @@ module Legion
|
|
|
282
282
|
|
|
283
283
|
# --- Token resolution ---
|
|
284
284
|
|
|
285
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
286
285
|
def resolve_token
|
|
287
286
|
env_token = ENV.fetch('GITHUB_TOKEN', nil) ||
|
|
288
287
|
ENV.fetch('GH_TOKEN', nil) ||
|
|
@@ -301,9 +300,7 @@ module Legion
|
|
|
301
300
|
@log&.log('github', 'no token found (no env var, no gh CLI)')
|
|
302
301
|
nil
|
|
303
302
|
end
|
|
304
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
305
303
|
|
|
306
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
307
304
|
def token_from_gh_cli
|
|
308
305
|
gh_path = `which gh 2>/dev/null`.strip
|
|
309
306
|
return nil if gh_path.empty?
|
|
@@ -322,8 +319,6 @@ module Legion
|
|
|
322
319
|
@log&.log('github', "gh CLI error: #{e.message}")
|
|
323
320
|
nil
|
|
324
321
|
end
|
|
325
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
326
|
-
|
|
327
322
|
# --- HTTP ---
|
|
328
323
|
|
|
329
324
|
def api_get(path)
|
|
@@ -62,7 +62,6 @@ module Legion
|
|
|
62
62
|
query_ldap(username: username, host: dc_host, base_dn: base_dn)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
66
65
|
def discover_dc(realm)
|
|
67
66
|
domain = realm.downcase
|
|
68
67
|
srv_name = "_ldap._tcp.#{domain}"
|
|
@@ -74,7 +73,6 @@ module Legion
|
|
|
74
73
|
@log&.log('kerberos', "SRV lookup failed: #{e.message}")
|
|
75
74
|
nil
|
|
76
75
|
end
|
|
77
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
78
76
|
|
|
79
77
|
def realm_to_base_dn(realm)
|
|
80
78
|
realm.downcase.split('.').map { |part| "DC=#{part}" }.join(',')
|
|
@@ -149,7 +147,7 @@ module Legion
|
|
|
149
147
|
}.compact
|
|
150
148
|
end
|
|
151
149
|
|
|
152
|
-
# rubocop:disable Metrics/AbcSize
|
|
150
|
+
# rubocop:disable Metrics/AbcSize
|
|
153
151
|
def calculate_tenure(when_created)
|
|
154
152
|
return nil unless when_created&.length&.>=(8)
|
|
155
153
|
|
|
@@ -184,7 +182,7 @@ module Legion
|
|
|
184
182
|
|
|
185
183
|
{ years: years, months: months, days: days }
|
|
186
184
|
end
|
|
187
|
-
# rubocop:enable Metrics/AbcSize
|
|
185
|
+
# rubocop:enable Metrics/AbcSize
|
|
188
186
|
|
|
189
187
|
def days_in_month(month, year)
|
|
190
188
|
Time.new(year, month, -1).day
|
|
@@ -93,7 +93,6 @@ module Legion
|
|
|
93
93
|
false
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
97
96
|
def collect_repos(base, depth = 0)
|
|
98
97
|
return [] unless File.directory?(base)
|
|
99
98
|
return [build_repo_entry(base)] if File.directory?(File.join(base, '.git'))
|
|
@@ -110,7 +109,6 @@ module Legion
|
|
|
110
109
|
rescue StandardError
|
|
111
110
|
[]
|
|
112
111
|
end
|
|
113
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
114
112
|
|
|
115
113
|
def build_repo_entry(path)
|
|
116
114
|
{ path: path, name: File.basename(path), remote: git_remote(path),
|
|
@@ -9,7 +9,7 @@ module Legion
|
|
|
9
9
|
# rubocop:disable Metrics/ClassLength
|
|
10
10
|
class MessageStream
|
|
11
11
|
attr_reader :messages, :scroll_offset
|
|
12
|
-
attr_accessor :mute_system, :highlights
|
|
12
|
+
attr_accessor :mute_system, :highlights, :filter, :truncate_limit
|
|
13
13
|
|
|
14
14
|
HIGHLIGHT_COLOR = "\e[1;33m"
|
|
15
15
|
HIGHLIGHT_RESET = "\e[0m"
|
|
@@ -77,13 +77,28 @@ module Legion
|
|
|
77
77
|
private
|
|
78
78
|
|
|
79
79
|
def build_all_lines(width)
|
|
80
|
-
|
|
80
|
+
filtered_messages.flat_map do |msg|
|
|
81
81
|
next [] if @mute_system && msg[:role] == :system
|
|
82
82
|
|
|
83
83
|
render_message(msg, width)
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
def filtered_messages
|
|
88
|
+
return @messages if @filter.nil?
|
|
89
|
+
|
|
90
|
+
case @filter[:type]
|
|
91
|
+
when :role
|
|
92
|
+
@messages.select { |m| m[:role].to_s == @filter[:value].to_s }
|
|
93
|
+
when :tag
|
|
94
|
+
@messages.select { |m| (m[:tags] || []).include?(@filter[:value]) }
|
|
95
|
+
when :pinned
|
|
96
|
+
@messages.select { |m| m[:pinned] }
|
|
97
|
+
else
|
|
98
|
+
@messages
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
87
102
|
def render_message(msg, width)
|
|
88
103
|
role_lines(msg, width) + panel_lines(msg, width)
|
|
89
104
|
end
|
|
@@ -104,6 +119,7 @@ module Legion
|
|
|
104
119
|
content = apply_highlights(msg[:content].to_s)
|
|
105
120
|
lines = ['', "#{header}: #{content}"]
|
|
106
121
|
lines << reaction_line(msg) if msg[:reactions]&.any?
|
|
122
|
+
lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
|
|
107
123
|
lines
|
|
108
124
|
end
|
|
109
125
|
|
|
@@ -114,18 +130,34 @@ module Legion
|
|
|
114
130
|
end
|
|
115
131
|
|
|
116
132
|
def assistant_lines(msg, width)
|
|
117
|
-
|
|
133
|
+
content = display_content(msg[:content])
|
|
134
|
+
rendered = render_markdown(content, width)
|
|
118
135
|
rendered = apply_highlights(rendered)
|
|
119
136
|
lines = ['', *rendered.split("\n")]
|
|
120
137
|
lines << reaction_line(msg) if msg[:reactions]&.any?
|
|
138
|
+
lines.concat(annotation_lines(msg)) if msg[:annotations]&.any?
|
|
121
139
|
lines
|
|
122
140
|
end
|
|
123
141
|
|
|
142
|
+
def display_content(content)
|
|
143
|
+
return content unless @truncate_limit
|
|
144
|
+
return content if content.to_s.length <= @truncate_limit
|
|
145
|
+
|
|
146
|
+
"#{content[0...@truncate_limit]}... [truncated]"
|
|
147
|
+
end
|
|
148
|
+
|
|
124
149
|
def reaction_line(msg)
|
|
125
150
|
reactions = msg[:reactions].map { |r| "[#{r}]" }.join(' ')
|
|
126
151
|
" #{Theme.c(:muted, reactions)}"
|
|
127
152
|
end
|
|
128
153
|
|
|
154
|
+
def annotation_lines(msg)
|
|
155
|
+
msg[:annotations].map do |a|
|
|
156
|
+
ts = a[:timestamp].to_s[11..15] || ''
|
|
157
|
+
" #{Theme.c(:muted, "note [#{ts}]: #{a[:text]}")}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
129
161
|
def render_markdown(text, width)
|
|
130
162
|
require_relative 'markdown_view'
|
|
131
163
|
MarkdownView.render(text, width: width)
|
|
@@ -9,7 +9,7 @@ module Legion
|
|
|
9
9
|
@current_model = current_model
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def available_models
|
|
12
|
+
def available_models
|
|
13
13
|
return [] unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:settings)
|
|
14
14
|
|
|
15
15
|
providers = Legion::LLM.settings[:providers]
|
|
@@ -10,7 +10,7 @@ module Legion
|
|
|
10
10
|
class StatusBar
|
|
11
11
|
def initialize
|
|
12
12
|
@state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false,
|
|
13
|
-
debug_mode: false, message_count: 0 }
|
|
13
|
+
debug_mode: false, message_count: 0, multiline: false }
|
|
14
14
|
@notifications = []
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -42,6 +42,7 @@ module Legion
|
|
|
42
42
|
[
|
|
43
43
|
model_segment,
|
|
44
44
|
plan_segment,
|
|
45
|
+
multiline_segment,
|
|
45
46
|
debug_segment,
|
|
46
47
|
thinking_segment,
|
|
47
48
|
notification_segment,
|
|
@@ -63,6 +64,12 @@ module Legion
|
|
|
63
64
|
Theme.c(:warning, '[PLAN]')
|
|
64
65
|
end
|
|
65
66
|
|
|
67
|
+
def multiline_segment
|
|
68
|
+
return nil unless @state[:multiline]
|
|
69
|
+
|
|
70
|
+
Theme.c(:accent, '[ML]')
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
def debug_segment
|
|
67
74
|
return nil unless @state[:debug_mode]
|
|
68
75
|
|
|
@@ -4,7 +4,6 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module CustomCommands
|
|
9
8
|
TEMPLATES = {
|
|
10
9
|
'explain' => 'Explain the following concept in simple terms: ',
|
|
@@ -19,7 +18,6 @@ module Legion
|
|
|
19
18
|
|
|
20
19
|
private
|
|
21
20
|
|
|
22
|
-
# rubocop:disable Metrics/MethodLength
|
|
23
21
|
def handle_template(input)
|
|
24
22
|
name = input.split(nil, 2)[1]
|
|
25
23
|
unless name
|
|
@@ -47,7 +45,6 @@ module Legion
|
|
|
47
45
|
)
|
|
48
46
|
:handled
|
|
49
47
|
end
|
|
50
|
-
# rubocop:enable Metrics/MethodLength
|
|
51
48
|
|
|
52
49
|
def handle_alias(input)
|
|
53
50
|
parts = input.split(nil, 3)
|
|
@@ -263,7 +260,6 @@ module Legion
|
|
|
263
260
|
end
|
|
264
261
|
# rubocop:enable Metrics/AbcSize
|
|
265
262
|
|
|
266
|
-
# rubocop:disable Metrics/MethodLength
|
|
267
263
|
def handle_macro(input)
|
|
268
264
|
parts = input.split(nil, 3)
|
|
269
265
|
subcommand = parts[1]
|
|
@@ -288,7 +284,6 @@ module Legion
|
|
|
288
284
|
end
|
|
289
285
|
:handled
|
|
290
286
|
end
|
|
291
|
-
# rubocop:enable Metrics/MethodLength
|
|
292
287
|
|
|
293
288
|
def macro_record(name)
|
|
294
289
|
unless name
|
|
@@ -378,7 +373,7 @@ module Legion
|
|
|
378
373
|
end
|
|
379
374
|
end
|
|
380
375
|
|
|
381
|
-
# rubocop:disable Metrics/AbcSize, Metrics/
|
|
376
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
382
377
|
def handle_chain(input)
|
|
383
378
|
args = input.split(nil, 2)[1]
|
|
384
379
|
unless args
|
|
@@ -412,9 +407,8 @@ module Legion
|
|
|
412
407
|
)
|
|
413
408
|
:handled
|
|
414
409
|
end
|
|
415
|
-
# rubocop:enable Metrics/AbcSize, Metrics/
|
|
410
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
416
411
|
end
|
|
417
|
-
# rubocop:enable Metrics/ModuleLength
|
|
418
412
|
end
|
|
419
413
|
end
|
|
420
414
|
end
|
|
@@ -21,19 +21,22 @@ module Legion
|
|
|
21
21
|
|
|
22
22
|
def build_export_path(input)
|
|
23
23
|
format = input.split[1]&.downcase
|
|
24
|
-
format = 'md' unless %w[json md html].include?(format)
|
|
24
|
+
format = 'md' unless %w[json md html yaml].include?(format)
|
|
25
25
|
exports_dir = File.expand_path('~/.legionio/exports')
|
|
26
26
|
FileUtils.mkdir_p(exports_dir)
|
|
27
27
|
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
28
|
-
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
|
|
28
|
+
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html', 'yaml' => 'yaml' }[format]
|
|
29
29
|
File.join(exports_dir, "chat-#{timestamp}.#{ext}")
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def dispatch_export(path, format)
|
|
33
|
-
|
|
33
|
+
case format
|
|
34
|
+
when 'json'
|
|
34
35
|
export_json(path)
|
|
35
|
-
|
|
36
|
+
when 'html'
|
|
36
37
|
export_html(path)
|
|
38
|
+
when 'yaml'
|
|
39
|
+
export_yaml(path)
|
|
37
40
|
else
|
|
38
41
|
export_markdown(path)
|
|
39
42
|
end
|
|
@@ -58,6 +61,17 @@ module Legion
|
|
|
58
61
|
File.write(path, ::JSON.pretty_generate(data))
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
def export_yaml(path)
|
|
65
|
+
require 'yaml'
|
|
66
|
+
data = {
|
|
67
|
+
'exported_at' => Time.now.iso8601,
|
|
68
|
+
'messages' => @message_stream.messages.map do |m|
|
|
69
|
+
{ 'role' => m[:role].to_s, 'content' => m[:content], 'timestamp' => m[:timestamp]&.iso8601 }
|
|
70
|
+
end
|
|
71
|
+
}
|
|
72
|
+
File.write(path, ::YAML.dump(data))
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
62
76
|
def export_html(path)
|
|
63
77
|
lines = [
|
|
@@ -118,6 +132,35 @@ module Legion
|
|
|
118
132
|
:handled
|
|
119
133
|
end
|
|
120
134
|
# rubocop:enable Metrics/AbcSize
|
|
135
|
+
|
|
136
|
+
def handle_tee(input)
|
|
137
|
+
arg = input.split(nil, 2)[1]
|
|
138
|
+
if arg.nil?
|
|
139
|
+
status = @tee_path ? "Tee active: #{@tee_path}" : 'Tee inactive.'
|
|
140
|
+
@message_stream.add_message(role: :system, content: status)
|
|
141
|
+
return :handled
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if arg.strip == 'off'
|
|
145
|
+
@tee_path = nil
|
|
146
|
+
@message_stream.add_message(role: :system, content: 'Tee stopped.')
|
|
147
|
+
else
|
|
148
|
+
@tee_path = File.expand_path(arg.strip)
|
|
149
|
+
@message_stream.add_message(role: :system, content: "Tee started: #{@tee_path}")
|
|
150
|
+
end
|
|
151
|
+
:handled
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
@message_stream.add_message(role: :system, content: "Tee error: #{e.message}")
|
|
154
|
+
:handled
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def tee_message(line)
|
|
158
|
+
return unless @tee_path
|
|
159
|
+
|
|
160
|
+
File.open(@tee_path, 'a') { |f| f.puts(line) }
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
121
164
|
end
|
|
122
165
|
end
|
|
123
166
|
end
|
|
@@ -4,7 +4,6 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module MessageCommands
|
|
9
8
|
private
|
|
10
9
|
|
|
@@ -402,7 +401,7 @@ module Legion
|
|
|
402
401
|
File.write(favorites_file, ::JSON.generate(favs))
|
|
403
402
|
end
|
|
404
403
|
|
|
405
|
-
# rubocop:disable Metrics/AbcSize
|
|
404
|
+
# rubocop:disable Metrics/AbcSize
|
|
406
405
|
def handle_fav(input)
|
|
407
406
|
idx_str = input.split(nil, 2)[1]
|
|
408
407
|
msg = if idx_str
|
|
@@ -428,7 +427,51 @@ module Legion
|
|
|
428
427
|
@message_stream.add_message(role: :system, content: "Favorited: #{preview}")
|
|
429
428
|
:handled
|
|
430
429
|
end
|
|
431
|
-
# rubocop:enable Metrics/AbcSize
|
|
430
|
+
# rubocop:enable Metrics/AbcSize
|
|
431
|
+
|
|
432
|
+
# rubocop:disable Metrics/AbcSize
|
|
433
|
+
def handle_annotate(input)
|
|
434
|
+
parts = input.split(nil, 3)
|
|
435
|
+
if parts.size >= 3 && parts[1].match?(/\A\d+\z/)
|
|
436
|
+
idx = parts[1].to_i
|
|
437
|
+
text = parts[2]
|
|
438
|
+
msg = @message_stream.messages[idx]
|
|
439
|
+
elsif parts.size == 2 && !parts[1].match?(/\A\d+\z/)
|
|
440
|
+
text = parts[1]
|
|
441
|
+
msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
442
|
+
elsif parts.size >= 3
|
|
443
|
+
text = parts[1..].join(' ')
|
|
444
|
+
msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
445
|
+
else
|
|
446
|
+
@message_stream.add_message(role: :system, content: 'Usage: /annotate [N] <text>')
|
|
447
|
+
return :handled
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
unless msg
|
|
451
|
+
@message_stream.add_message(role: :system, content: 'No message to annotate.')
|
|
452
|
+
return :handled
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
msg[:annotations] ||= []
|
|
456
|
+
msg[:annotations] << { text: text, timestamp: Time.now.iso8601 }
|
|
457
|
+
@message_stream.add_message(role: :system, content: "Annotation added: #{text}")
|
|
458
|
+
:handled
|
|
459
|
+
end
|
|
460
|
+
# rubocop:enable Metrics/AbcSize
|
|
461
|
+
|
|
462
|
+
def handle_annotations(_input)
|
|
463
|
+
annotated = @message_stream.messages.each_with_index.select { |m, _| m[:annotations]&.any? }
|
|
464
|
+
if annotated.empty?
|
|
465
|
+
@message_stream.add_message(role: :system, content: 'No annotated messages.')
|
|
466
|
+
return :handled
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
lines = annotated.flat_map do |msg, idx|
|
|
470
|
+
msg[:annotations].map { |a| " [#{idx}][#{msg[:role]}] #{a[:text]} (#{a[:timestamp]})" }
|
|
471
|
+
end
|
|
472
|
+
@message_stream.add_message(role: :system, content: "Annotations:\n#{lines.join("\n")}")
|
|
473
|
+
:handled
|
|
474
|
+
end
|
|
432
475
|
|
|
433
476
|
def handle_favs
|
|
434
477
|
all_favs = load_favorites
|
|
@@ -447,8 +490,77 @@ module Legion
|
|
|
447
490
|
)
|
|
448
491
|
:handled
|
|
449
492
|
end
|
|
493
|
+
|
|
494
|
+
def handle_filter(input)
|
|
495
|
+
parts = input.split(nil, 3)
|
|
496
|
+
subcommand = parts[1]
|
|
497
|
+
case subcommand
|
|
498
|
+
when 'role'
|
|
499
|
+
apply_role_filter(parts[2])
|
|
500
|
+
when 'tag'
|
|
501
|
+
apply_tag_filter(parts[2])
|
|
502
|
+
when 'pinned'
|
|
503
|
+
@message_stream.filter = { type: :pinned }
|
|
504
|
+
@message_stream.add_message(role: :system, content: 'Filter: pinned messages only.')
|
|
505
|
+
when 'clear'
|
|
506
|
+
@message_stream.filter = nil
|
|
507
|
+
@message_stream.add_message(role: :system, content: 'Filter cleared.')
|
|
508
|
+
when nil
|
|
509
|
+
show_filter_status
|
|
510
|
+
else
|
|
511
|
+
@message_stream.add_message(
|
|
512
|
+
role: :system,
|
|
513
|
+
content: 'Usage: /filter [role|tag|pinned|clear] [value]'
|
|
514
|
+
)
|
|
515
|
+
end
|
|
516
|
+
:handled
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def apply_role_filter(value)
|
|
520
|
+
unless value
|
|
521
|
+
@message_stream.add_message(role: :system, content: 'Usage: /filter role <user|assistant|system>')
|
|
522
|
+
return
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
@message_stream.filter = { type: :role, value: value }
|
|
526
|
+
@message_stream.add_message(role: :system, content: "Filter: role=#{value}.")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def apply_tag_filter(value)
|
|
530
|
+
unless value
|
|
531
|
+
@message_stream.add_message(role: :system, content: 'Usage: /filter tag <label>')
|
|
532
|
+
return
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
@message_stream.filter = { type: :tag, value: value }
|
|
536
|
+
@message_stream.add_message(role: :system, content: "Filter: tag=#{value}.")
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def show_filter_status
|
|
540
|
+
f = @message_stream.filter
|
|
541
|
+
content = if f.nil?
|
|
542
|
+
'No active filter.'
|
|
543
|
+
elsif f[:type] == :pinned
|
|
544
|
+
'Active filter: pinned.'
|
|
545
|
+
else
|
|
546
|
+
"Active filter: #{f[:type]}=#{f[:value]}."
|
|
547
|
+
end
|
|
548
|
+
@message_stream.add_message(role: :system, content: content)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def handle_truncate(input)
|
|
552
|
+
n = (input.split(nil, 2)[1] || '10').to_i.clamp(1, 500)
|
|
553
|
+
msgs = @message_stream.messages
|
|
554
|
+
if msgs.size <= n
|
|
555
|
+
@message_stream.add_message(role: :system, content: "Already #{msgs.size} messages (<=#{n}).")
|
|
556
|
+
return :handled
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
@message_stream.messages.replace(msgs.last(n))
|
|
560
|
+
@message_stream.add_message(role: :system, content: "Truncated to last #{n} messages.")
|
|
561
|
+
:handled
|
|
562
|
+
end
|
|
450
563
|
end
|
|
451
|
-
# rubocop:enable Metrics/ModuleLength
|
|
452
564
|
end
|
|
453
565
|
end
|
|
454
566
|
end
|
|
@@ -4,7 +4,6 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module ModelCommands
|
|
9
8
|
private
|
|
10
9
|
|
|
@@ -134,7 +133,6 @@ module Legion
|
|
|
134
133
|
:handled
|
|
135
134
|
end
|
|
136
135
|
end
|
|
137
|
-
# rubocop:enable Metrics/ModuleLength
|
|
138
136
|
end
|
|
139
137
|
end
|
|
140
138
|
end
|
|
@@ -4,7 +4,6 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module SessionCommands
|
|
9
8
|
private
|
|
10
9
|
|
|
@@ -233,6 +232,34 @@ module Legion
|
|
|
233
232
|
:handled
|
|
234
233
|
end
|
|
235
234
|
|
|
235
|
+
def handle_archive(input)
|
|
236
|
+
name = input.split(nil, 2)[1] || "#{@session_name}-#{Time.now.strftime('%Y%m%d-%H%M%S')}"
|
|
237
|
+
archive_dir = File.expand_path('~/.legionio/archives')
|
|
238
|
+
FileUtils.mkdir_p(archive_dir)
|
|
239
|
+
path = File.join(archive_dir, "#{name}.json")
|
|
240
|
+
data = { name: name, messages: @message_stream.messages, archived_at: Time.now.iso8601 }
|
|
241
|
+
File.write(path, ::JSON.generate(data))
|
|
242
|
+
@message_stream.messages.clear
|
|
243
|
+
@message_stream.add_message(role: :system, content: "Session archived as: #{name}")
|
|
244
|
+
@status_bar.notify(message: "Archived: #{name}", level: :success, ttl: 3)
|
|
245
|
+
:handled
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def handle_archives
|
|
249
|
+
archive_dir = File.expand_path('~/.legionio/archives')
|
|
250
|
+
files = Dir.glob(File.join(archive_dir, '*.json'))
|
|
251
|
+
if files.empty?
|
|
252
|
+
@message_stream.add_message(role: :system, content: 'No archives found.')
|
|
253
|
+
else
|
|
254
|
+
lines = files.sort.map do |f|
|
|
255
|
+
size = File.size(f)
|
|
256
|
+
" #{File.basename(f, '.json')} (#{size} bytes)"
|
|
257
|
+
end
|
|
258
|
+
@message_stream.add_message(role: :system, content: "Archives:\n#{lines.join("\n")}")
|
|
259
|
+
end
|
|
260
|
+
:handled
|
|
261
|
+
end
|
|
262
|
+
|
|
236
263
|
def handle_merge(input)
|
|
237
264
|
name = input.split(nil, 2)[1]
|
|
238
265
|
unless name
|
|
@@ -256,7 +283,6 @@ module Legion
|
|
|
256
283
|
:handled
|
|
257
284
|
end
|
|
258
285
|
end
|
|
259
|
-
# rubocop:enable Metrics/ModuleLength
|
|
260
286
|
end
|
|
261
287
|
end
|
|
262
288
|
end
|
|
@@ -4,7 +4,6 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module UiCommands
|
|
9
8
|
TIPS = [
|
|
10
9
|
'Press Tab after / to auto-complete commands',
|
|
@@ -31,10 +30,14 @@ module Legion
|
|
|
31
30
|
'NAV : /dashboard /extensions /config /palette /hotkeys',
|
|
32
31
|
'DISPLAY : /theme /plan /debug /context /time /uptime',
|
|
33
32
|
'TOOLS : /tools /export /bookmark /pin /pins /alias /snippet /history',
|
|
33
|
+
'UTILS : /calc /rand',
|
|
34
34
|
'',
|
|
35
35
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
36
36
|
].freeze
|
|
37
37
|
|
|
38
|
+
CALC_SAFE_PATTERN = %r{\A[\d\s+\-*/.()%]*\z}
|
|
39
|
+
CALC_MATH_PATTERN = %r{\A[\d\s+\-*/.()%]*(Math\.\w+\([\d\s+\-*/.()%,]*\)[\d\s+\-*/.()%]*)*\z}
|
|
40
|
+
|
|
38
41
|
private
|
|
39
42
|
|
|
40
43
|
def handle_help
|
|
@@ -314,6 +317,39 @@ module Legion
|
|
|
314
317
|
:handled
|
|
315
318
|
end
|
|
316
319
|
|
|
320
|
+
def handle_truncate(input)
|
|
321
|
+
arg = input.split(nil, 2)[1]&.strip
|
|
322
|
+
if arg.nil?
|
|
323
|
+
status = @message_stream.truncate_limit ? "#{@message_stream.truncate_limit} chars" : 'off'
|
|
324
|
+
@message_stream.add_message(role: :system, content: "Truncation: #{status}")
|
|
325
|
+
elsif arg == 'off'
|
|
326
|
+
@message_stream.truncate_limit = nil
|
|
327
|
+
@message_stream.add_message(role: :system, content: 'Truncation disabled.')
|
|
328
|
+
else
|
|
329
|
+
limit = arg.to_i
|
|
330
|
+
if limit.positive?
|
|
331
|
+
@message_stream.truncate_limit = limit
|
|
332
|
+
@message_stream.add_message(role: :system, content: "Truncation set to #{limit} chars.")
|
|
333
|
+
else
|
|
334
|
+
@message_stream.add_message(role: :system, content: 'Usage: /truncate [N|off]')
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
:handled
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def handle_multiline
|
|
341
|
+
@multiline_mode = !@multiline_mode
|
|
342
|
+
if @multiline_mode
|
|
343
|
+
@status_bar.update(multiline: true)
|
|
344
|
+
@message_stream.add_message(role: :system,
|
|
345
|
+
content: 'Multi-line mode ON. Submit with empty line.')
|
|
346
|
+
else
|
|
347
|
+
@status_bar.update(multiline: false)
|
|
348
|
+
@message_stream.add_message(role: :system, content: 'Multi-line mode OFF.')
|
|
349
|
+
end
|
|
350
|
+
:handled
|
|
351
|
+
end
|
|
352
|
+
|
|
317
353
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
318
354
|
def handle_scroll(input)
|
|
319
355
|
arg = input.split(nil, 2)[1]
|
|
@@ -390,7 +426,56 @@ module Legion
|
|
|
390
426
|
@message_stream.add_message(role: :system, content: "Highlight added: '#{pattern}'")
|
|
391
427
|
end
|
|
392
428
|
|
|
393
|
-
|
|
429
|
+
def handle_calc(input)
|
|
430
|
+
expr = input.split(nil, 2)[1]&.strip
|
|
431
|
+
unless expr
|
|
432
|
+
@message_stream.add_message(role: :system, content: 'Usage: /calc <expression>')
|
|
433
|
+
return :handled
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
unless safe_calc_expr?(expr)
|
|
437
|
+
@message_stream.add_message(role: :system, content: "Unsafe expression blocked: #{expr}")
|
|
438
|
+
return :handled
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
result = binding.send(:eval, expr)
|
|
442
|
+
@message_stream.add_message(role: :system, content: "= #{result}")
|
|
443
|
+
:handled
|
|
444
|
+
rescue SyntaxError, ZeroDivisionError, Math::DomainError => e
|
|
445
|
+
@message_stream.add_message(role: :system, content: "Error: #{e.message}")
|
|
446
|
+
:handled
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def handle_rand(input)
|
|
450
|
+
arg = input.split(nil, 2)[1]&.strip
|
|
451
|
+
result = parse_rand_arg(arg)
|
|
452
|
+
if result == :invalid
|
|
453
|
+
@message_stream.add_message(role: :system, content: 'Usage: /rand [N|min..max]')
|
|
454
|
+
return :handled
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
@message_stream.add_message(role: :system, content: "Random: #{result}")
|
|
458
|
+
:handled
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def parse_rand_arg(arg)
|
|
462
|
+
if arg.nil? || arg.empty?
|
|
463
|
+
rand
|
|
464
|
+
elsif arg.match?(/\A\d+\.\.\d+\z/)
|
|
465
|
+
parts = arg.split('..').map(&:to_i)
|
|
466
|
+
rand(parts[0]..parts[1])
|
|
467
|
+
elsif arg.match?(/\A\d+\z/)
|
|
468
|
+
rand(arg.to_i)
|
|
469
|
+
else
|
|
470
|
+
:invalid
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def safe_calc_expr?(expr)
|
|
475
|
+
CALC_SAFE_PATTERN.match?(expr) || CALC_MATH_PATTERN.match?(expr)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
394
479
|
def handle_summary
|
|
395
480
|
msgs = @message_stream.messages
|
|
396
481
|
elapsed = Time.now - @session_start
|
|
@@ -423,9 +508,40 @@ module Legion
|
|
|
423
508
|
@message_stream.add_message(role: :system, content: lines.join("\n"))
|
|
424
509
|
:handled
|
|
425
510
|
end
|
|
426
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/
|
|
511
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
512
|
+
|
|
513
|
+
def handle_pipe(input)
|
|
514
|
+
cmd = input.split(nil, 2)[1]
|
|
515
|
+
unless cmd
|
|
516
|
+
@message_stream.add_message(role: :system, content: 'Usage: /pipe <shell command>')
|
|
517
|
+
return :handled
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
last_msg = @message_stream.messages.select { |m| m[:role] == :assistant }.last
|
|
521
|
+
unless last_msg
|
|
522
|
+
@message_stream.add_message(role: :system, content: 'No assistant message to pipe.')
|
|
523
|
+
return :handled
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
output = pipe_through_command(cmd, last_msg[:content].to_s)
|
|
527
|
+
@message_stream.add_message(role: :system, content: "pipe | #{cmd}:\n#{output}")
|
|
528
|
+
:handled
|
|
529
|
+
rescue StandardError => e
|
|
530
|
+
@message_stream.add_message(role: :system, content: "Pipe error: #{e.message}")
|
|
531
|
+
:handled
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def pipe_through_command(cmd, content)
|
|
535
|
+
result = IO.popen(cmd, 'r+') do |io|
|
|
536
|
+
io.write(content)
|
|
537
|
+
io.close_write
|
|
538
|
+
io.read
|
|
539
|
+
end
|
|
540
|
+
result.to_s.chomp
|
|
541
|
+
rescue StandardError => e
|
|
542
|
+
raise "command failed: #{e.message}"
|
|
543
|
+
end
|
|
427
544
|
end
|
|
428
|
-
# rubocop:enable Metrics/ModuleLength
|
|
429
545
|
end
|
|
430
546
|
end
|
|
431
547
|
end
|
|
@@ -33,7 +33,11 @@ module Legion
|
|
|
33
33
|
/template /fav /favs /log /version
|
|
34
34
|
/focus /retry /merge /sort
|
|
35
35
|
/chain /info /scroll /summary
|
|
36
|
-
/prompt /reset /replace /highlight
|
|
36
|
+
/prompt /reset /replace /highlight /multiline
|
|
37
|
+
/annotate /annotations /filter /truncate
|
|
38
|
+
/tee /pipe
|
|
39
|
+
/archive /archives
|
|
40
|
+
/calc /rand].freeze
|
|
37
41
|
|
|
38
42
|
PERSONALITIES = {
|
|
39
43
|
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
@@ -74,6 +78,7 @@ module Legion
|
|
|
74
78
|
@focus_mode = false
|
|
75
79
|
@last_user_input = nil
|
|
76
80
|
@highlights = []
|
|
81
|
+
@multiline_mode = false
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -94,7 +99,7 @@ module Legion
|
|
|
94
99
|
@running
|
|
95
100
|
end
|
|
96
101
|
|
|
97
|
-
# rubocop:disable Metrics/AbcSize
|
|
102
|
+
# rubocop:disable Metrics/AbcSize
|
|
98
103
|
def run
|
|
99
104
|
activate
|
|
100
105
|
while @running
|
|
@@ -117,7 +122,7 @@ module Legion
|
|
|
117
122
|
end
|
|
118
123
|
end
|
|
119
124
|
end
|
|
120
|
-
# rubocop:enable Metrics/AbcSize
|
|
125
|
+
# rubocop:enable Metrics/AbcSize
|
|
121
126
|
|
|
122
127
|
def handle_slash_command(input)
|
|
123
128
|
return nil unless input.start_with?('/')
|
|
@@ -139,6 +144,7 @@ module Legion
|
|
|
139
144
|
def handle_user_message(input)
|
|
140
145
|
@last_user_input = input
|
|
141
146
|
@message_stream.add_message(role: :user, content: input)
|
|
147
|
+
tee_message("[user] #{input}") if @tee_path
|
|
142
148
|
if @plan_mode
|
|
143
149
|
@message_stream.add_message(role: :system, content: '(bookmarked)')
|
|
144
150
|
else
|
|
@@ -339,12 +345,27 @@ module Legion
|
|
|
339
345
|
|
|
340
346
|
def read_input
|
|
341
347
|
return nil unless @input_bar.respond_to?(:read_line)
|
|
348
|
+
return read_multiline_input if @multiline_mode
|
|
342
349
|
|
|
343
350
|
@input_bar.read_line
|
|
344
351
|
rescue Interrupt
|
|
345
352
|
nil
|
|
346
353
|
end
|
|
347
354
|
|
|
355
|
+
def read_multiline_input
|
|
356
|
+
lines = []
|
|
357
|
+
loop do
|
|
358
|
+
line = @input_bar.read_line
|
|
359
|
+
return nil if line.nil? && lines.empty?
|
|
360
|
+
break if line.nil? || line.empty?
|
|
361
|
+
|
|
362
|
+
lines << line
|
|
363
|
+
end
|
|
364
|
+
lines.empty? ? nil : lines.join("\n")
|
|
365
|
+
rescue Interrupt
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
|
|
348
369
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
349
370
|
def dispatch_slash(cmd, input)
|
|
350
371
|
case cmd
|
|
@@ -416,6 +437,17 @@ module Legion
|
|
|
416
437
|
when '/reset' then handle_reset
|
|
417
438
|
when '/replace' then handle_replace(input)
|
|
418
439
|
when '/highlight' then handle_highlight(input)
|
|
440
|
+
when '/multiline' then handle_multiline
|
|
441
|
+
when '/annotate' then handle_annotate(input)
|
|
442
|
+
when '/annotations' then handle_annotations(input)
|
|
443
|
+
when '/filter' then handle_filter(input)
|
|
444
|
+
when '/truncate' then handle_truncate(input)
|
|
445
|
+
when '/tee' then handle_tee(input)
|
|
446
|
+
when '/pipe' then handle_pipe(input)
|
|
447
|
+
when '/archive' then handle_archive(input)
|
|
448
|
+
when '/archives' then handle_archives
|
|
449
|
+
when '/calc' then handle_calc(input)
|
|
450
|
+
when '/rand' then handle_rand(input)
|
|
419
451
|
else :handled
|
|
420
452
|
end
|
|
421
453
|
end
|
|
@@ -500,6 +532,7 @@ module Legion
|
|
|
500
532
|
"[DEBUG] msgs:#{@message_stream.messages.size} " \
|
|
501
533
|
"scroll:#{@message_stream.scroll_position&.dig(:current) || 0} " \
|
|
502
534
|
"plan:#{@plan_mode} " \
|
|
535
|
+
"multiline:#{@multiline_mode} " \
|
|
503
536
|
"personality:#{@personality || 'default'} " \
|
|
504
537
|
"aliases:#{@aliases.size} " \
|
|
505
538
|
"snippets:#{@snippets.size} " \
|
|
@@ -105,7 +105,7 @@ module Legion
|
|
|
105
105
|
@viewing_file = true
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
def edit_selected_key # rubocop:disable Metrics/AbcSize
|
|
108
|
+
def edit_selected_key # rubocop:disable Metrics/AbcSize
|
|
109
109
|
keys = @file_data.keys
|
|
110
110
|
return unless keys[@selected_key]
|
|
111
111
|
|
|
@@ -40,7 +40,7 @@ module Legion
|
|
|
40
40
|
|
|
41
41
|
# rubocop:enable Metrics/AbcSize
|
|
42
42
|
|
|
43
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
43
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
44
44
|
def handle_input(key)
|
|
45
45
|
case key
|
|
46
46
|
when 'r', :f5
|
|
@@ -66,7 +66,7 @@ module Legion
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
# rubocop:enable Metrics/CyclomaticComplexity
|
|
69
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
70
70
|
|
|
71
71
|
def selected_panel
|
|
72
72
|
PANELS[@selected_panel]
|
|
@@ -147,7 +147,7 @@ module Legion
|
|
|
147
147
|
|
|
148
148
|
# rubocop:enable Metrics/AbcSize
|
|
149
149
|
|
|
150
|
-
# rubocop:disable Metrics/AbcSize
|
|
150
|
+
# rubocop:disable Metrics/AbcSize
|
|
151
151
|
def render_system_panel(_width)
|
|
152
152
|
sys = @cached_data[:system] || {}
|
|
153
153
|
prefix = panel_prefix(:system)
|
|
@@ -161,7 +161,7 @@ module Legion
|
|
|
161
161
|
lines
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
# rubocop:enable Metrics/AbcSize
|
|
164
|
+
# rubocop:enable Metrics/AbcSize
|
|
165
165
|
|
|
166
166
|
def render_activity_panel(_width, max_lines)
|
|
167
167
|
activity = @cached_data[:activity] || []
|
|
@@ -219,7 +219,7 @@ module Legion
|
|
|
219
219
|
:pass
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
-
# rubocop:disable Metrics/AbcSize
|
|
222
|
+
# rubocop:disable Metrics/AbcSize
|
|
223
223
|
def llm_info
|
|
224
224
|
info = { provider: 'none', model: nil, started: false, daemon: false }
|
|
225
225
|
if defined?(Legion::LLM)
|
|
@@ -237,7 +237,7 @@ module Legion
|
|
|
237
237
|
info
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
-
# rubocop:enable Metrics/AbcSize
|
|
240
|
+
# rubocop:enable Metrics/AbcSize
|
|
241
241
|
|
|
242
242
|
def probe_services
|
|
243
243
|
require 'socket'
|
|
@@ -195,7 +195,7 @@ module Legion
|
|
|
195
195
|
end
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
# rubocop:disable Metrics/AbcSize
|
|
198
|
+
# rubocop:disable Metrics/AbcSize
|
|
199
199
|
def run_gaia_awakening
|
|
200
200
|
typed_output('Scanning for active cognition threads...')
|
|
201
201
|
sleep 1.2
|
|
@@ -225,7 +225,7 @@ module Legion
|
|
|
225
225
|
|
|
226
226
|
@output.puts
|
|
227
227
|
end
|
|
228
|
-
# rubocop:enable Metrics/AbcSize
|
|
228
|
+
# rubocop:enable Metrics/AbcSize
|
|
229
229
|
|
|
230
230
|
def collect_background_results
|
|
231
231
|
@log.log('collect', 'waiting for scanner results (10s timeout)')
|
|
@@ -438,7 +438,7 @@ module Legion
|
|
|
438
438
|
end
|
|
439
439
|
end
|
|
440
440
|
|
|
441
|
-
# rubocop:disable Metrics/AbcSize
|
|
441
|
+
# rubocop:disable Metrics/AbcSize
|
|
442
442
|
def run_intro_with_github
|
|
443
443
|
gh = @github_quick
|
|
444
444
|
name = gh[:name] || gh[:username]
|
|
@@ -462,7 +462,7 @@ module Legion
|
|
|
462
462
|
|
|
463
463
|
@output.puts
|
|
464
464
|
end
|
|
465
|
-
# rubocop:enable Metrics/AbcSize
|
|
465
|
+
# rubocop:enable Metrics/AbcSize
|
|
466
466
|
|
|
467
467
|
def collect_kerberos_identity
|
|
468
468
|
@log.log('kerberos', 'collecting identity (2s timeout)')
|
|
@@ -475,7 +475,7 @@ module Legion
|
|
|
475
475
|
end
|
|
476
476
|
end
|
|
477
477
|
|
|
478
|
-
# rubocop:disable Metrics/AbcSize
|
|
478
|
+
# rubocop:disable Metrics/AbcSize
|
|
479
479
|
def run_intro_with_identity
|
|
480
480
|
id = @kerberos_identity
|
|
481
481
|
typed_output("I see you, #{id[:first_name]}.")
|
|
@@ -505,7 +505,7 @@ module Legion
|
|
|
505
505
|
@output.puts
|
|
506
506
|
@output.puts
|
|
507
507
|
end
|
|
508
|
-
# rubocop:enable Metrics/AbcSize
|
|
508
|
+
# rubocop:enable Metrics/AbcSize
|
|
509
509
|
|
|
510
510
|
def ask_for_name
|
|
511
511
|
if @kerberos_identity
|
|
@@ -515,7 +515,7 @@ module Legion
|
|
|
515
515
|
end
|
|
516
516
|
end
|
|
517
517
|
|
|
518
|
-
# rubocop:disable Metrics/AbcSize
|
|
518
|
+
# rubocop:disable Metrics/AbcSize
|
|
519
519
|
def identity_summary_lines
|
|
520
520
|
return [] unless @kerberos_identity
|
|
521
521
|
|
|
@@ -529,7 +529,7 @@ module Legion
|
|
|
529
529
|
lines << " Email: #{id[:email]}" if id[:email]
|
|
530
530
|
lines
|
|
531
531
|
end
|
|
532
|
-
# rubocop:enable Metrics/AbcSize
|
|
532
|
+
# rubocop:enable Metrics/AbcSize
|
|
533
533
|
|
|
534
534
|
def scan_summary_lines(scan_data)
|
|
535
535
|
return [] unless scan_data.is_a?(Hash)
|
|
@@ -622,7 +622,7 @@ module Legion
|
|
|
622
622
|
['', "Terraform: #{dotfiles_tf[:hosts].join(', ')}"]
|
|
623
623
|
end
|
|
624
624
|
|
|
625
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
625
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
626
626
|
def github_summary_lines(github_data)
|
|
627
627
|
return [] unless github_data.is_a?(Hash)
|
|
628
628
|
|
|
@@ -655,9 +655,9 @@ module Legion
|
|
|
655
655
|
|
|
656
656
|
lines
|
|
657
657
|
end
|
|
658
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
658
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
659
659
|
|
|
660
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
660
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
661
661
|
def format_tenure(tenure)
|
|
662
662
|
return tenure.to_s unless tenure.is_a?(Hash)
|
|
663
663
|
|
|
@@ -670,7 +670,7 @@ module Legion
|
|
|
670
670
|
parts << "#{d} day#{'s' if d != 1}" if d&.positive?
|
|
671
671
|
parts.join(', ')
|
|
672
672
|
end
|
|
673
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
673
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
674
674
|
|
|
675
675
|
def typed_output(text, delay: TYPED_DELAY)
|
|
676
676
|
text.chars.each do |char|
|
data/lib/legion/tty/theme.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Legion
|
|
4
4
|
module TTY
|
|
5
|
-
# rubocop:disable Metrics/ModuleLength
|
|
6
5
|
module Theme
|
|
7
6
|
# rubocop:disable Naming/VariableNumber
|
|
8
7
|
THEMES = {
|
|
@@ -125,6 +124,5 @@ module Legion
|
|
|
125
124
|
end
|
|
126
125
|
end
|
|
127
126
|
end
|
|
128
|
-
# rubocop:enable Metrics/ModuleLength
|
|
129
127
|
end
|
|
130
128
|
end
|
data/lib/legion/tty/version.rb
CHANGED