legion-tty 0.4.6 → 0.4.8
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 +14 -0
- data/README.md +18 -4
- data/lib/legion/tty/components/message_stream.rb +19 -6
- data/lib/legion/tty/components/status_bar.rb +9 -1
- data/lib/legion/tty/screens/chat.rb +44 -2
- data/lib/legion/tty/screens/config.rb +11 -1
- data/lib/legion/tty/screens/extensions.rb +29 -1
- data/lib/legion/tty/session_store.rb +8 -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: 49d5dd5b69b7fa3412226bc38e93b0adb0df6be7c3b0317955bf7f9698a0759d
|
|
4
|
+
data.tar.gz: ed73f018bbb85ee31fdf11aba24f5b6f332f28ef85970f9189ba5164402d8690
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67a87dfd8538f4c42e5c9897d4460d8457071660e95df3fe019757d3e9e56ef95627d4a5a3576914611257f78fd766190909e4e95f8f4bee26b1c0328745c35f
|
|
7
|
+
data.tar.gz: a6f9bd2210d04ad3d2200a4029e32141eb82b29043dee974cb5b37d5d6b927b1f2b284cac1d3e39c77c6105fa7c08f61a3da8a678e4d77e88de675cfe9cac2ea
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.8] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/export html` format: dark-theme HTML export with XSS-safe content escaping
|
|
7
|
+
- Extension homepage opener: press 'o' in extensions browser to open gem homepage in browser
|
|
8
|
+
- Config JSON validation: validates data before saving to prevent corrupt config files
|
|
9
|
+
|
|
10
|
+
## [0.4.7] - 2026-03-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Smart session auto-naming: generates slug from first user message instead of "default"
|
|
14
|
+
- Message timestamps: each message records creation time, displayed in user message headers
|
|
15
|
+
- Scroll position indicator in status bar (shows current/total when content is scrollable)
|
|
16
|
+
|
|
3
17
|
## [0.4.6] - 2026-03-19
|
|
4
18
|
|
|
5
19
|
### Added
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Rich terminal UI for the LegionIO async cognition engine.
|
|
4
4
|
|
|
5
|
-
**Version**: 0.4.
|
|
5
|
+
**Version**: 0.4.8
|
|
6
6
|
|
|
7
7
|
Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell, operational dashboard, extensions browser, config editor, and session persistence - all rendered with the [tty-ruby](https://ttytoolkit.org/) gem ecosystem.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with iden
|
|
|
10
10
|
|
|
11
11
|
- **Onboarding wizard** - First-run setup with Kerberos identity detection, GitHub profile probing, environment scanning, and LLM provider selection
|
|
12
12
|
- **Digital rain intro** - Matrix-style rain using discovered LEX extension names
|
|
13
|
-
- **AI chat shell** - Streaming LLM chat with
|
|
13
|
+
- **AI chat shell** - Streaming LLM chat with 27 slash commands, tab completion, markdown rendering, and tool panels
|
|
14
14
|
- **Operational dashboard** - Service status, extension inventory, system info, recent activity (Ctrl+D or `/dashboard`)
|
|
15
15
|
- **Extensions browser** - Browse installed LEX gems by category (core, agentic, service, AI, other) with detail view
|
|
16
16
|
- **Config viewer/editor** - View and edit `~/.legionio/settings/*.json` with vault:// masking
|
|
@@ -19,9 +19,15 @@ Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with iden
|
|
|
19
19
|
- **Session management** - Auto-save on quit, `/save`, `/load`, `/sessions`, session picker (Ctrl+S)
|
|
20
20
|
- **Token tracking** - Per-model pricing for 9 models across 8 providers via `/cost`
|
|
21
21
|
- **Plan mode** - Bookmark messages without sending to LLM (`/plan`)
|
|
22
|
+
- **Personality styles** - Switch between default, concise, detailed, friendly, technical (`/personality`)
|
|
23
|
+
- **Theme selection** - Four built-in themes: purple (default), green, blue, amber (`/theme`)
|
|
24
|
+
- **Conversation tools** - `/compact`, `/copy`, `/diff`, `/search`, `/stats`
|
|
22
25
|
- **Hotkey navigation** - Ctrl+D (dashboard), Ctrl+K (palette), Ctrl+S (sessions), Escape (back)
|
|
23
26
|
- **Tab completion** - Type `/` and Tab to auto-complete slash commands
|
|
27
|
+
- **Input history** - Up/down arrow to navigate previous inputs
|
|
24
28
|
- **Progress panel** - Visual progress bars for long operations (extension scanning, gem ops)
|
|
29
|
+
- **Animated spinner** - Status bar spinner during LLM thinking
|
|
30
|
+
- **Daemon routing** - Routes through LegionIO daemon when available, falls back to direct
|
|
25
31
|
- **Second-run flow** - Skips onboarding, re-scans environment, drops into chat
|
|
26
32
|
|
|
27
33
|
## Installation
|
|
@@ -83,6 +89,13 @@ legion chat prompt "explain async cognition"
|
|
|
83
89
|
| `/palette` | Open command palette (fuzzy search) |
|
|
84
90
|
| `/extensions` | Browse installed LEX extensions |
|
|
85
91
|
| `/config` | View and edit settings files |
|
|
92
|
+
| `/theme [name]` | Switch color theme (purple/green/blue/amber) |
|
|
93
|
+
| `/search <text>` | Search message history |
|
|
94
|
+
| `/compact [N]` | Keep last N message pairs, remove older |
|
|
95
|
+
| `/copy` | Copy last assistant response to clipboard |
|
|
96
|
+
| `/diff` | Show new messages since last session load |
|
|
97
|
+
| `/stats` | Show conversation statistics |
|
|
98
|
+
| `/personality [style]` | Switch personality (default/concise/detailed/friendly/technical) |
|
|
86
99
|
|
|
87
100
|
## Hotkeys
|
|
88
101
|
|
|
@@ -126,6 +139,7 @@ legion-tty
|
|
|
126
139
|
SessionPicker # Session list and selection
|
|
127
140
|
TableView # TTY::Table wrapper
|
|
128
141
|
ProgressPanel # TTY::ProgressBar wrapper
|
|
142
|
+
Notification # Transient notifications with TTL and levels
|
|
129
143
|
|
|
130
144
|
Background/
|
|
131
145
|
Scanner # Service port probing, git repo discovery, shell history
|
|
@@ -154,8 +168,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
|
|
|
154
168
|
|
|
155
169
|
```bash
|
|
156
170
|
bundle install
|
|
157
|
-
bundle exec rspec #
|
|
158
|
-
bundle exec rubocop #
|
|
171
|
+
bundle exec rspec # 653 examples, 0 failures
|
|
172
|
+
bundle exec rubocop # 77 files, 0 offenses
|
|
159
173
|
```
|
|
160
174
|
|
|
161
175
|
## License
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def add_message(role:, content:)
|
|
17
|
-
@messages << { role: role, content: content, tool_panels: [] }
|
|
17
|
+
@messages << { role: role, content: content, tool_panels: [], timestamp: Time.now }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def append_streaming(text)
|
|
@@ -57,7 +57,13 @@ module Legion
|
|
|
57
57
|
total = all_lines.size
|
|
58
58
|
start_idx = [total - height - @scroll_offset, 0].max
|
|
59
59
|
start_idx = [start_idx, total].min
|
|
60
|
-
all_lines[start_idx, height] || []
|
|
60
|
+
result = all_lines[start_idx, height] || []
|
|
61
|
+
@last_visible_count = result.size
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def scroll_position
|
|
66
|
+
{ current: @scroll_offset, total: @messages.size, visible: @last_visible_count || 0 }
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
private
|
|
@@ -72,7 +78,7 @@ module Legion
|
|
|
72
78
|
|
|
73
79
|
def role_lines(msg, width)
|
|
74
80
|
case msg[:role]
|
|
75
|
-
when :user then user_lines(msg)
|
|
81
|
+
when :user then user_lines(msg, width)
|
|
76
82
|
when :assistant then assistant_lines(msg, width)
|
|
77
83
|
when :system then system_lines(msg)
|
|
78
84
|
when :tool then tool_call_lines(msg, width)
|
|
@@ -80,9 +86,16 @@ module Legion
|
|
|
80
86
|
end
|
|
81
87
|
end
|
|
82
88
|
|
|
83
|
-
def user_lines(msg)
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
def user_lines(msg, _width)
|
|
90
|
+
ts = format_timestamp(msg[:timestamp])
|
|
91
|
+
header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
|
|
92
|
+
['', "#{header}: #{msg[:content]}"]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_timestamp(time)
|
|
96
|
+
return '' unless time
|
|
97
|
+
|
|
98
|
+
time.strftime('%H:%M')
|
|
86
99
|
end
|
|
87
100
|
|
|
88
101
|
def assistant_lines(msg, width)
|
|
@@ -37,7 +37,8 @@ module Legion
|
|
|
37
37
|
thinking_segment,
|
|
38
38
|
tokens_segment,
|
|
39
39
|
cost_segment,
|
|
40
|
-
session_segment
|
|
40
|
+
session_segment,
|
|
41
|
+
scroll_segment
|
|
41
42
|
].compact
|
|
42
43
|
end
|
|
43
44
|
|
|
@@ -71,6 +72,13 @@ module Legion
|
|
|
71
72
|
Theme.c(:muted, @state[:session]) if @state[:session]
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
def scroll_segment
|
|
76
|
+
scroll = @state[:scroll]
|
|
77
|
+
return nil unless scroll.is_a?(Hash) && scroll[:total].to_i > scroll[:visible].to_i
|
|
78
|
+
|
|
79
|
+
Theme.c(:muted, "#{scroll[:current]}/#{scroll[:total]}")
|
|
80
|
+
end
|
|
81
|
+
|
|
74
82
|
def format_number(num)
|
|
75
83
|
num.to_s.chars.reverse.each_slice(3).map(&:join).join(',').reverse
|
|
76
84
|
end
|
|
@@ -121,6 +121,7 @@ module Legion
|
|
|
121
121
|
divider = Theme.c(:muted, '-' * width)
|
|
122
122
|
stream_height = [height - 2, 1].max
|
|
123
123
|
stream_lines = @message_stream.render(width: width, height: stream_height)
|
|
124
|
+
@status_bar.update(scroll: @message_stream.scroll_position)
|
|
124
125
|
stream_lines + [divider, bar_line]
|
|
125
126
|
end
|
|
126
127
|
|
|
@@ -440,6 +441,9 @@ module Legion
|
|
|
440
441
|
def auto_save_session
|
|
441
442
|
return if @message_stream.messages.empty?
|
|
442
443
|
|
|
444
|
+
if @session_name == 'default'
|
|
445
|
+
@session_name = @session_store.auto_session_name(messages: @message_stream.messages)
|
|
446
|
+
end
|
|
443
447
|
@session_store.save(@session_name, messages: @message_stream.messages)
|
|
444
448
|
rescue StandardError
|
|
445
449
|
nil
|
|
@@ -454,13 +458,16 @@ module Legion
|
|
|
454
458
|
def handle_export(input)
|
|
455
459
|
require 'fileutils'
|
|
456
460
|
format = input.split[1]&.downcase
|
|
457
|
-
format = 'md' unless %w[json md].include?(format)
|
|
461
|
+
format = 'md' unless %w[json md html].include?(format)
|
|
458
462
|
exports_dir = File.expand_path('~/.legionio/exports')
|
|
459
463
|
FileUtils.mkdir_p(exports_dir)
|
|
460
464
|
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
461
|
-
|
|
465
|
+
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
|
|
466
|
+
path = File.join(exports_dir, "chat-#{timestamp}.#{ext}")
|
|
462
467
|
if format == 'json'
|
|
463
468
|
export_json(path)
|
|
469
|
+
elsif format == 'html'
|
|
470
|
+
export_html(path)
|
|
464
471
|
else
|
|
465
472
|
export_markdown(path)
|
|
466
473
|
end
|
|
@@ -811,6 +818,41 @@ module Legion
|
|
|
811
818
|
File.write(path, ::JSON.pretty_generate(data))
|
|
812
819
|
end
|
|
813
820
|
|
|
821
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
822
|
+
def export_html(path)
|
|
823
|
+
lines = [
|
|
824
|
+
'<!DOCTYPE html><html><head>',
|
|
825
|
+
'<meta charset="utf-8">',
|
|
826
|
+
'<title>Chat Export</title>',
|
|
827
|
+
'<style>',
|
|
828
|
+
'body { font-family: system-ui; max-width: 800px; margin: 0 auto; ' \
|
|
829
|
+
'padding: 20px; background: #1e1b2e; color: #d0cce6; }',
|
|
830
|
+
'.msg { margin: 12px 0; padding: 8px 12px; border-radius: 8px; }',
|
|
831
|
+
'.user { background: #2a2640; }',
|
|
832
|
+
'.assistant { background: #1a1730; }',
|
|
833
|
+
'.system { background: #25223a; color: #8b85a8; font-style: italic; }',
|
|
834
|
+
'.role { font-weight: bold; color: #9d91e6; font-size: 0.85em; }',
|
|
835
|
+
'</style></head><body>',
|
|
836
|
+
'<h1>Chat Export</h1>',
|
|
837
|
+
"<p>Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</p>"
|
|
838
|
+
]
|
|
839
|
+
@message_stream.messages.each do |msg|
|
|
840
|
+
role = msg[:role].to_s
|
|
841
|
+
content = escape_html(msg[:content].to_s).gsub("\n", '<br>')
|
|
842
|
+
lines << "<div class='msg #{role}'>"
|
|
843
|
+
lines << "<span class='role'>#{role.capitalize}</span>"
|
|
844
|
+
lines << "<p>#{content}</p>"
|
|
845
|
+
lines << '</div>'
|
|
846
|
+
end
|
|
847
|
+
lines << '</body></html>'
|
|
848
|
+
File.write(path, lines.join("\n"))
|
|
849
|
+
end
|
|
850
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
851
|
+
|
|
852
|
+
def escape_html(text)
|
|
853
|
+
text.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
854
|
+
end
|
|
855
|
+
|
|
814
856
|
def build_default_input_bar
|
|
815
857
|
cfg = safe_config
|
|
816
858
|
name = cfg[:name] || 'User'
|
|
@@ -100,7 +100,7 @@ module Legion
|
|
|
100
100
|
@viewing_file = true
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
def edit_selected_key # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
103
|
+
def edit_selected_key # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
104
104
|
keys = @file_data.keys
|
|
105
105
|
return unless keys[@selected_key]
|
|
106
106
|
|
|
@@ -115,11 +115,21 @@ module Legion
|
|
|
115
115
|
return if new_val.nil? || new_val == '********'
|
|
116
116
|
|
|
117
117
|
@file_data[key] = new_val
|
|
118
|
+
return unless validate_config(@file_data)
|
|
119
|
+
|
|
118
120
|
save_current_file
|
|
119
121
|
rescue ::TTY::Reader::InputInterrupt, Interrupt
|
|
120
122
|
nil
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
def validate_config(data)
|
|
126
|
+
::JSON.generate(data)
|
|
127
|
+
true
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
@messages = ["Invalid JSON: #{e.message}"]
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
123
133
|
def save_current_file
|
|
124
134
|
return unless @files[@selected_file]
|
|
125
135
|
|
|
@@ -42,10 +42,11 @@ module Legion
|
|
|
42
42
|
else
|
|
43
43
|
list_lines(height - 4)
|
|
44
44
|
end
|
|
45
|
-
lines += ['', Theme.c(:muted, ' Enter=detail q=back')]
|
|
45
|
+
lines += ['', Theme.c(:muted, ' Enter=detail o=open q=back')]
|
|
46
46
|
pad_lines(lines, height)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# rubocop:disable Metrics/MethodLength
|
|
49
50
|
def handle_input(key)
|
|
50
51
|
case key
|
|
51
52
|
when :up
|
|
@@ -57,6 +58,9 @@ module Legion
|
|
|
57
58
|
when :enter
|
|
58
59
|
@detail = !@detail
|
|
59
60
|
:handled
|
|
61
|
+
when 'o'
|
|
62
|
+
open_homepage
|
|
63
|
+
:handled
|
|
60
64
|
when 'q', :escape
|
|
61
65
|
if @detail
|
|
62
66
|
@detail = false
|
|
@@ -68,6 +72,7 @@ module Legion
|
|
|
68
72
|
:pass
|
|
69
73
|
end
|
|
70
74
|
end
|
|
75
|
+
# rubocop:enable Metrics/MethodLength
|
|
71
76
|
|
|
72
77
|
private
|
|
73
78
|
|
|
@@ -127,6 +132,29 @@ module Legion
|
|
|
127
132
|
]
|
|
128
133
|
end
|
|
129
134
|
|
|
135
|
+
def open_homepage
|
|
136
|
+
entry = current_gem
|
|
137
|
+
return unless entry && entry[:homepage]
|
|
138
|
+
|
|
139
|
+
system_open(entry[:homepage])
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def system_open(url)
|
|
145
|
+
case RUBY_PLATFORM
|
|
146
|
+
when /darwin/ then system('open', url)
|
|
147
|
+
when /linux/ then system('xdg-open', url)
|
|
148
|
+
when /mingw|mswin/ then system('start', url)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def current_gem
|
|
153
|
+
return nil if @gems.empty?
|
|
154
|
+
|
|
155
|
+
@gems[@selected]
|
|
156
|
+
end
|
|
157
|
+
|
|
130
158
|
def pad_lines(lines, height)
|
|
131
159
|
lines + Array.new([height - lines.size, 0].max, '')
|
|
132
160
|
end
|
|
@@ -51,8 +51,14 @@ module Legion
|
|
|
51
51
|
FileUtils.rm_f(path)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def auto_session_name
|
|
55
|
-
|
|
54
|
+
def auto_session_name(messages: [])
|
|
55
|
+
first_user = messages.find { |m| m[:role] == :user }
|
|
56
|
+
return "session-#{Time.now.strftime('%H%M%S')}" unless first_user
|
|
57
|
+
|
|
58
|
+
words = first_user[:content].to_s.downcase.gsub(/[^a-z0-9\s]/, '').split
|
|
59
|
+
slug = words.first(4).join('-')
|
|
60
|
+
slug = "session-#{Time.now.strftime('%H%M%S')}" if slug.empty?
|
|
61
|
+
slug
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
private
|
data/lib/legion/tty/version.rb
CHANGED