legion-tty 0.4.5 → 0.4.7
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/notification.rb +42 -0
- data/lib/legion/tty/components/status_bar.rb +9 -1
- data/lib/legion/tty/screens/chat.rb +76 -2
- data/lib/legion/tty/session_store.rb +8 -2
- data/lib/legion/tty/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b54f067b654097c2ea13e9a36aed12ac6e79f01a7e737494fa57da489d9ada9
|
|
4
|
+
data.tar.gz: 767c21b9ed0f1ac37f92db12bbef79432d42de65695d07736c9e1478b48ef96d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cec6ebd2ee78648a6f8d00e5db9c3004d1b9e1787b914a31135b1ed75137d467c9982363c83dc40f8697b9ef930869e9ab719b066297e5dc61011361a8f5f22
|
|
7
|
+
data.tar.gz: 3b1f39208ef1198f3db8620aee08f56d65146f3d761d5356b04992aadd79514aae957e044384878181b390cf738e2258ea08fadf6c4299c3c1aaf1bba0e4b7f4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.7] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Smart session auto-naming: generates slug from first user message instead of "default"
|
|
7
|
+
- Message timestamps: each message records creation time, displayed in user message headers
|
|
8
|
+
- Scroll position indicator in status bar (shows current/total when content is scrollable)
|
|
9
|
+
|
|
10
|
+
## [0.4.6] - 2026-03-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `/stats` command: show conversation statistics (message counts, characters, token summary)
|
|
14
|
+
- `/personality <style>` command: switch between default/concise/detailed/friendly/technical personas
|
|
15
|
+
- Notification component: transient message display with TTL expiry and level-based icons/colors
|
|
16
|
+
|
|
3
17
|
## [0.4.5] - 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.6
|
|
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)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../theme'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module TTY
|
|
7
|
+
module Components
|
|
8
|
+
class Notification
|
|
9
|
+
LEVELS = %i[info success warning error].freeze
|
|
10
|
+
ICONS = { info: 'i', success: '+', warning: '!', error: 'x' }.freeze
|
|
11
|
+
COLORS = { info: :info, success: :success, warning: :warning, error: :error }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :message, :level, :created_at
|
|
14
|
+
|
|
15
|
+
def initialize(message:, level: :info, ttl: 5)
|
|
16
|
+
@message = message
|
|
17
|
+
@level = LEVELS.include?(level) ? level : :info
|
|
18
|
+
@ttl = ttl
|
|
19
|
+
@created_at = Time.now
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def expired?
|
|
23
|
+
Time.now - @created_at > @ttl
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(width: 80)
|
|
27
|
+
icon = Theme.c(COLORS[@level], ICONS[@level])
|
|
28
|
+
text = Theme.c(COLORS[@level], @message)
|
|
29
|
+
line = "#{icon} #{text}"
|
|
30
|
+
plain_len = strip_ansi(line).length
|
|
31
|
+
line + (' ' * [width - plain_len, 0].max)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def strip_ansi(str)
|
|
37
|
+
str.gsub(/\e\[[0-9;]*m/, '')
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -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
|
|
@@ -14,7 +14,15 @@ 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].freeze
|
|
17
|
+
/theme /search /stats /personality].freeze
|
|
18
|
+
|
|
19
|
+
PERSONALITIES = {
|
|
20
|
+
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
21
|
+
'concise' => 'You are Legion. Respond in as few words as possible. No filler.',
|
|
22
|
+
'detailed' => 'You are Legion. Provide thorough, detailed explanations with examples when helpful.',
|
|
23
|
+
'friendly' => 'You are Legion, a friendly AI companion. Use a warm, conversational tone.',
|
|
24
|
+
'technical' => 'You are Legion, a senior engineer. Use precise technical language. Include code examples.'
|
|
25
|
+
}.freeze
|
|
18
26
|
|
|
19
27
|
attr_reader :message_stream, :status_bar
|
|
20
28
|
|
|
@@ -113,6 +121,7 @@ module Legion
|
|
|
113
121
|
divider = Theme.c(:muted, '-' * width)
|
|
114
122
|
stream_height = [height - 2, 1].max
|
|
115
123
|
stream_lines = @message_stream.render(width: width, height: stream_height)
|
|
124
|
+
@status_bar.update(scroll: @message_stream.scroll_position)
|
|
116
125
|
stream_lines + [divider, bar_line]
|
|
117
126
|
end
|
|
118
127
|
|
|
@@ -281,6 +290,8 @@ module Legion
|
|
|
281
290
|
when '/config' then handle_config_screen
|
|
282
291
|
when '/theme' then handle_theme(input)
|
|
283
292
|
when '/search' then handle_search(input)
|
|
293
|
+
when '/stats' then handle_stats
|
|
294
|
+
when '/personality' then handle_personality(input)
|
|
284
295
|
else :handled
|
|
285
296
|
end
|
|
286
297
|
end
|
|
@@ -296,7 +307,9 @@ module Legion
|
|
|
296
307
|
"/search <text> -- search message history\n " \
|
|
297
308
|
"/compact [n] -- keep last n message pairs (default 5)\n " \
|
|
298
309
|
"/copy -- copy last assistant message to clipboard\n " \
|
|
299
|
-
"/diff -- show new messages since session was loaded\n
|
|
310
|
+
"/diff -- show new messages since session was loaded\n " \
|
|
311
|
+
"/stats -- show conversation statistics\n " \
|
|
312
|
+
"/personality [name] -- switch assistant personality\n\n" \
|
|
300
313
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
301
314
|
)
|
|
302
315
|
:handled
|
|
@@ -428,6 +441,9 @@ module Legion
|
|
|
428
441
|
def auto_save_session
|
|
429
442
|
return if @message_stream.messages.empty?
|
|
430
443
|
|
|
444
|
+
if @session_name == 'default'
|
|
445
|
+
@session_name = @session_store.auto_session_name(messages: @message_stream.messages)
|
|
446
|
+
end
|
|
431
447
|
@session_store.save(@session_name, messages: @message_stream.messages)
|
|
432
448
|
rescue StandardError
|
|
433
449
|
nil
|
|
@@ -619,6 +635,64 @@ module Legion
|
|
|
619
635
|
:handled
|
|
620
636
|
end
|
|
621
637
|
|
|
638
|
+
def handle_stats
|
|
639
|
+
@message_stream.add_message(role: :system, content: build_stats_lines.join("\n"))
|
|
640
|
+
:handled
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def build_stats_lines
|
|
644
|
+
msgs = @message_stream.messages
|
|
645
|
+
counts = count_by_role(msgs)
|
|
646
|
+
total_chars = msgs.sum { |m| m[:content].to_s.length }
|
|
647
|
+
lines = stats_header_lines(msgs, counts, total_chars)
|
|
648
|
+
lines << " Tool calls: #{counts[:tool]}" if counts[:tool].positive?
|
|
649
|
+
lines
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def count_by_role(msgs)
|
|
653
|
+
%i[user assistant system tool].to_h { |role| [role, msgs.count { |m| m[:role] == role }] }
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def stats_header_lines(msgs, counts, total_chars)
|
|
657
|
+
[
|
|
658
|
+
"Messages: #{msgs.size} total",
|
|
659
|
+
" User: #{counts[:user]}, Assistant: #{counts[:assistant]}, System: #{counts[:system]}",
|
|
660
|
+
"Characters: #{format_stat_number(total_chars)}",
|
|
661
|
+
"Session: #{@session_name}",
|
|
662
|
+
"Tokens: #{@token_tracker.summary}"
|
|
663
|
+
]
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def format_stat_number(num)
|
|
667
|
+
num.to_s.chars.reverse.each_slice(3).map(&:join).join(',').reverse
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def handle_personality(input)
|
|
671
|
+
name = input.split(nil, 2)[1]
|
|
672
|
+
if name && PERSONALITIES.key?(name)
|
|
673
|
+
apply_personality(name)
|
|
674
|
+
elsif name
|
|
675
|
+
available = PERSONALITIES.keys.join(', ')
|
|
676
|
+
@message_stream.add_message(role: :system,
|
|
677
|
+
content: "Unknown personality '#{name}'. Available: #{available}")
|
|
678
|
+
else
|
|
679
|
+
current = @personality || 'default'
|
|
680
|
+
available = PERSONALITIES.keys.join(', ')
|
|
681
|
+
@message_stream.add_message(role: :system, content: "Current: #{current}\nAvailable: #{available}")
|
|
682
|
+
end
|
|
683
|
+
:handled
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def apply_personality(name)
|
|
687
|
+
@personality = name
|
|
688
|
+
if @llm_chat.respond_to?(:with_instructions)
|
|
689
|
+
@llm_chat.with_instructions(PERSONALITIES[name])
|
|
690
|
+
@message_stream.add_message(role: :system, content: "Personality switched to: #{name}")
|
|
691
|
+
else
|
|
692
|
+
@message_stream.add_message(role: :system, content: "Personality set to: #{name} (no active LLM)")
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
622
696
|
# rubocop:disable Metrics/AbcSize
|
|
623
697
|
def handle_compact(input)
|
|
624
698
|
keep = (input.split(nil, 2)[1] || '5').to_i.clamp(1, 50)
|
|
@@ -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
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.
|
|
4
|
+
version: 0.4.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -207,6 +207,7 @@ files:
|
|
|
207
207
|
- lib/legion/tty/components/markdown_view.rb
|
|
208
208
|
- lib/legion/tty/components/message_stream.rb
|
|
209
209
|
- lib/legion/tty/components/model_picker.rb
|
|
210
|
+
- lib/legion/tty/components/notification.rb
|
|
210
211
|
- lib/legion/tty/components/progress_panel.rb
|
|
211
212
|
- lib/legion/tty/components/session_picker.rb
|
|
212
213
|
- lib/legion/tty/components/status_bar.rb
|