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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ea19b2f60e08522ddf09e24a577b745804f35189f764a9e8150a802f3dc33b5
4
- data.tar.gz: a1ff57a93aba51bc23f0421fa8f35525251f0dcd73f858b004965d68aeaf0083
3
+ metadata.gz: 750c41521ce23860a6e8613b4e84471e4c2813657ec68f23b24fcf0aa7015ecd
4
+ data.tar.gz: b203df2393abcc0afa085b7ec30c73fbbe3815d2f9886d1b3744fdbbf1fddc62
5
5
  SHA512:
6
- metadata.gz: '00195e25f48241ad47b602d4d37b4d19bb1c1dc817d63ecc144ffa145e198974e37020065b2937c6864d5917ad13fd98870c9396d5bd0e2aafaf1e6d4f58e5e7'
7
- data.tar.gz: 4b0c13ed2eaee447e559fd030f6e096c87ffac1b60316d525151a157820faf7510abda2fe40938916fffc67a1a385192511b3693b4005c43c89a62189bbcb231
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 /hotkeys /save /load
16
- /sessions /system /delete /plan /palette /extensions /config /theme /search].freeze
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\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|
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.4'
5
+ VERSION = '0.4.6'
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.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