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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e82df7132986b553218c083f5b414f549a4aa72b1aa7cdf3a62bec7e944ff43d
4
- data.tar.gz: d05fe8cc4cf17d78301d2a321fee9c9ebd1d9030973322973bd59b404f502f21
3
+ metadata.gz: 5b54f067b654097c2ea13e9a36aed12ac6e79f01a7e737494fa57da489d9ada9
4
+ data.tar.gz: 767c21b9ed0f1ac37f92db12bbef79432d42de65695d07736c9e1478b48ef96d
5
5
  SHA512:
6
- metadata.gz: 357be1595f03767084c835e496c2609714bbe558ee486ec111d7d67ae6c43f44fbc09fb81518f64e0828bc3b582a658ed1c728b07bae5000a7b3107bd190d99a
7
- data.tar.gz: 6ae28cd4f18f1a1e10f20ccfac53bface69bf65d7d5221b33f6f33b9ca3315dc2913da69abfa4758bf18c2caa84ef36e3f678ac5aea06ead5da68494f94f61ec
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.1
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 19 slash commands, tab completion, markdown rendering, and tool panels
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 # 598 examples, 0 failures
158
- bundle exec rubocop # 68 files, 0 offenses
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
- prefix = Theme.c(:accent, 'You')
85
- ['', "#{prefix}: #{msg[:content]}"]
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\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
- "auto-#{Time.now.strftime('%Y%m%d-%H%M%S')}"
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.5'
5
+ VERSION = '0.4.7'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
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