legion-tty 0.4.4 → 0.4.6
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 +15 -0
- data/lib/legion/tty/components/notification.rb +42 -0
- data/lib/legion/tty/screens/chat.rb +148 -3
- 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: 750c41521ce23860a6e8613b4e84471e4c2813657ec68f23b24fcf0aa7015ecd
|
|
4
|
+
data.tar.gz: b203df2393abcc0afa085b7ec30c73fbbe3815d2f9886d1b3744fdbbf1fddc62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06d4eab7afdfbdb8933c3166c3b5dc3cd028c7462f29d4601364ba759ae9b648bbdd1a7e4f304587dcc1148af166840815bba87d5c81044cd622355964cb9cc1
|
|
7
|
+
data.tar.gz: e331268056bf882144790fe0c88f95454050a6b16a017b033f00e29785f71a027107784fc8a4e25906a3d3cd733c0d20454577423d2e5a038b21b6d9fb490c29
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.6] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/stats` command: show conversation statistics (message counts, characters, token summary)
|
|
7
|
+
- `/personality <style>` command: switch between default/concise/detailed/friendly/technical personas
|
|
8
|
+
- Notification component: transient message display with TTL expiry and level-based icons/colors
|
|
9
|
+
|
|
10
|
+
## [0.4.5] - 2026-03-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `/compact [N]` command: remove older messages, keep last N pairs (default 5)
|
|
14
|
+
- `/copy` command: copy last assistant response to clipboard (macOS pbcopy, Linux xclip)
|
|
15
|
+
- `/diff` command: show new messages since last session load
|
|
16
|
+
- Session load tracking: `@loaded_message_count` for diff comparison
|
|
17
|
+
|
|
3
18
|
## [0.4.4] - 2026-03-19
|
|
4
19
|
|
|
5
20
|
### Added
|
|
@@ -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
|
|
@@ -12,8 +12,17 @@ module Legion
|
|
|
12
12
|
module Screens
|
|
13
13
|
# rubocop:disable Metrics/ClassLength
|
|
14
14
|
class Chat < Base
|
|
15
|
-
SLASH_COMMANDS = %w[/help /quit /clear /model /session /cost /export /tools /dashboard
|
|
16
|
-
/sessions /system /delete /plan /palette /extensions /config
|
|
15
|
+
SLASH_COMMANDS = %w[/help /quit /clear /compact /copy /diff /model /session /cost /export /tools /dashboard
|
|
16
|
+
/hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
|
|
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
|
|
17
26
|
|
|
18
27
|
attr_reader :message_stream, :status_bar
|
|
19
28
|
|
|
@@ -259,6 +268,9 @@ module Legion
|
|
|
259
268
|
when '/quit' then :quit
|
|
260
269
|
when '/help' then handle_help
|
|
261
270
|
when '/clear' then handle_clear
|
|
271
|
+
when '/compact' then handle_compact(input)
|
|
272
|
+
when '/copy' then handle_copy(input)
|
|
273
|
+
when '/diff' then handle_diff(input)
|
|
262
274
|
when '/model' then handle_model(input)
|
|
263
275
|
when '/session' then handle_session(input)
|
|
264
276
|
when '/cost' then handle_cost
|
|
@@ -277,6 +289,8 @@ module Legion
|
|
|
277
289
|
when '/config' then handle_config_screen
|
|
278
290
|
when '/theme' then handle_theme(input)
|
|
279
291
|
when '/search' then handle_search(input)
|
|
292
|
+
when '/stats' then handle_stats
|
|
293
|
+
when '/personality' then handle_personality(input)
|
|
280
294
|
else :handled
|
|
281
295
|
end
|
|
282
296
|
end
|
|
@@ -289,7 +303,12 @@ module Legion
|
|
|
289
303
|
"/export [md|json] /tools /dashboard /hotkeys /save /load /sessions\n " \
|
|
290
304
|
"/system <prompt> /delete <session> /plan /palette /extensions /config\n " \
|
|
291
305
|
"/theme [name] -- switch color theme (purple, green, blue, amber)\n " \
|
|
292
|
-
"/search <text> -- search message history\n
|
|
306
|
+
"/search <text> -- search message history\n " \
|
|
307
|
+
"/compact [n] -- keep last n message pairs (default 5)\n " \
|
|
308
|
+
"/copy -- copy last assistant message to clipboard\n " \
|
|
309
|
+
"/diff -- show new messages since session was loaded\n " \
|
|
310
|
+
"/stats -- show conversation statistics\n " \
|
|
311
|
+
"/personality [name] -- switch assistant personality\n\n" \
|
|
293
312
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
294
313
|
)
|
|
295
314
|
:handled
|
|
@@ -399,6 +418,7 @@ module Legion
|
|
|
399
418
|
return :handled
|
|
400
419
|
end
|
|
401
420
|
@message_stream.messages.replace(data[:messages])
|
|
421
|
+
@loaded_message_count = @message_stream.messages.size
|
|
402
422
|
@session_name = name
|
|
403
423
|
@status_bar.update(session: name)
|
|
404
424
|
@message_stream.add_message(role: :system,
|
|
@@ -611,6 +631,131 @@ module Legion
|
|
|
611
631
|
:handled
|
|
612
632
|
end
|
|
613
633
|
|
|
634
|
+
def handle_stats
|
|
635
|
+
@message_stream.add_message(role: :system, content: build_stats_lines.join("\n"))
|
|
636
|
+
:handled
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def build_stats_lines
|
|
640
|
+
msgs = @message_stream.messages
|
|
641
|
+
counts = count_by_role(msgs)
|
|
642
|
+
total_chars = msgs.sum { |m| m[:content].to_s.length }
|
|
643
|
+
lines = stats_header_lines(msgs, counts, total_chars)
|
|
644
|
+
lines << " Tool calls: #{counts[:tool]}" if counts[:tool].positive?
|
|
645
|
+
lines
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def count_by_role(msgs)
|
|
649
|
+
%i[user assistant system tool].to_h { |role| [role, msgs.count { |m| m[:role] == role }] }
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def stats_header_lines(msgs, counts, total_chars)
|
|
653
|
+
[
|
|
654
|
+
"Messages: #{msgs.size} total",
|
|
655
|
+
" User: #{counts[:user]}, Assistant: #{counts[:assistant]}, System: #{counts[:system]}",
|
|
656
|
+
"Characters: #{format_stat_number(total_chars)}",
|
|
657
|
+
"Session: #{@session_name}",
|
|
658
|
+
"Tokens: #{@token_tracker.summary}"
|
|
659
|
+
]
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def format_stat_number(num)
|
|
663
|
+
num.to_s.chars.reverse.each_slice(3).map(&:join).join(',').reverse
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def handle_personality(input)
|
|
667
|
+
name = input.split(nil, 2)[1]
|
|
668
|
+
if name && PERSONALITIES.key?(name)
|
|
669
|
+
apply_personality(name)
|
|
670
|
+
elsif name
|
|
671
|
+
available = PERSONALITIES.keys.join(', ')
|
|
672
|
+
@message_stream.add_message(role: :system,
|
|
673
|
+
content: "Unknown personality '#{name}'. Available: #{available}")
|
|
674
|
+
else
|
|
675
|
+
current = @personality || 'default'
|
|
676
|
+
available = PERSONALITIES.keys.join(', ')
|
|
677
|
+
@message_stream.add_message(role: :system, content: "Current: #{current}\nAvailable: #{available}")
|
|
678
|
+
end
|
|
679
|
+
:handled
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def apply_personality(name)
|
|
683
|
+
@personality = name
|
|
684
|
+
if @llm_chat.respond_to?(:with_instructions)
|
|
685
|
+
@llm_chat.with_instructions(PERSONALITIES[name])
|
|
686
|
+
@message_stream.add_message(role: :system, content: "Personality switched to: #{name}")
|
|
687
|
+
else
|
|
688
|
+
@message_stream.add_message(role: :system, content: "Personality set to: #{name} (no active LLM)")
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# rubocop:disable Metrics/AbcSize
|
|
693
|
+
def handle_compact(input)
|
|
694
|
+
keep = (input.split(nil, 2)[1] || '5').to_i.clamp(1, 50)
|
|
695
|
+
msgs = @message_stream.messages
|
|
696
|
+
if msgs.size <= keep * 2
|
|
697
|
+
@message_stream.add_message(role: :system, content: 'Conversation is already compact.')
|
|
698
|
+
return :handled
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
system_msgs = msgs.select { |m| m[:role] == :system }
|
|
702
|
+
recent = msgs.reject { |m| m[:role] == :system }.last(keep * 2)
|
|
703
|
+
removed_count = msgs.size - system_msgs.size - recent.size
|
|
704
|
+
@message_stream.messages.replace(system_msgs + recent)
|
|
705
|
+
@message_stream.add_message(
|
|
706
|
+
role: :system,
|
|
707
|
+
content: "Compacted: removed #{removed_count} older messages, kept #{recent.size} recent."
|
|
708
|
+
)
|
|
709
|
+
:handled
|
|
710
|
+
end
|
|
711
|
+
# rubocop:enable Metrics/AbcSize
|
|
712
|
+
|
|
713
|
+
def handle_copy(_input)
|
|
714
|
+
last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
715
|
+
unless last_assistant
|
|
716
|
+
@message_stream.add_message(role: :system, content: 'No assistant message to copy.')
|
|
717
|
+
return :handled
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
content = last_assistant[:content].to_s
|
|
721
|
+
copy_to_clipboard(content)
|
|
722
|
+
@message_stream.add_message(
|
|
723
|
+
role: :system,
|
|
724
|
+
content: "Copied #{content.length} characters to clipboard."
|
|
725
|
+
)
|
|
726
|
+
:handled
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def copy_to_clipboard(text)
|
|
730
|
+
IO.popen('pbcopy', 'w') { |io| io.write(text) }
|
|
731
|
+
rescue Errno::ENOENT
|
|
732
|
+
begin
|
|
733
|
+
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) }
|
|
734
|
+
rescue Errno::ENOENT
|
|
735
|
+
nil
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def handle_diff(_input)
|
|
740
|
+
if @loaded_message_count.nil?
|
|
741
|
+
@message_stream.add_message(role: :system, content: 'No session was loaded. Nothing to diff against.')
|
|
742
|
+
return :handled
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
new_count = @message_stream.messages.size - @loaded_message_count
|
|
746
|
+
if new_count <= 0
|
|
747
|
+
@message_stream.add_message(role: :system, content: 'No new messages since session was loaded.')
|
|
748
|
+
else
|
|
749
|
+
new_msgs = @message_stream.messages.last(new_count)
|
|
750
|
+
lines = new_msgs.map { |m| " + [#{m[:role]}] #{truncate_text(m[:content].to_s, 60)}" }
|
|
751
|
+
@message_stream.add_message(
|
|
752
|
+
role: :system,
|
|
753
|
+
content: "#{new_count} new message(s) since load:\n#{lines.join("\n")}"
|
|
754
|
+
)
|
|
755
|
+
end
|
|
756
|
+
:handled
|
|
757
|
+
end
|
|
758
|
+
|
|
614
759
|
def search_messages(query)
|
|
615
760
|
pattern = query.downcase
|
|
616
761
|
@message_stream.messages.select do |msg|
|
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.6
|
|
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
|