legion-tty 0.4.3 → 0.4.5

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: 93f9cd78860072bfa0df27f33574552a7d5ac749fbb9440f61017ed76f7f9f4d
4
- data.tar.gz: c590bb6fb1cbe8639b5df4e46e0aa255f5d23f01499c8d2b1137ad86700412b7
3
+ metadata.gz: e82df7132986b553218c083f5b414f549a4aa72b1aa7cdf3a62bec7e944ff43d
4
+ data.tar.gz: d05fe8cc4cf17d78301d2a321fee9c9ebd1d9030973322973bd59b404f502f21
5
5
  SHA512:
6
- metadata.gz: 8880c1013b5c36a3d9a7d378280b6a1212ba82f527a37dd7f5f81b5b0fc591435ea10fec4a565b5e95a714be1b6340b587214ce9ccde08ee1dfbc63572e0d2f9
7
- data.tar.gz: d99de6e491d811a3e891833760c4f25fe45c4034494bd200322b41145fc39a16929b8ea3b5295c2044cc1e123e45f30022f310ca97aaf1643f842696a424db77
6
+ metadata.gz: 357be1595f03767084c835e496c2609714bbe558ee486ec111d7d67ae6c43f44fbc09fb81518f64e0828bc3b582a658ed1c728b07bae5000a7b3107bd190d99a
7
+ data.tar.gz: 6ae28cd4f18f1a1e10f20ccfac53bface69bf65d7d5221b33f6f33b9ca3315dc2913da69abfa4758bf18c2caa84ef36e3f678ac5aea06ead5da68494f94f61ec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.5] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/compact [N]` command: remove older messages, keep last N pairs (default 5)
7
+ - `/copy` command: copy last assistant response to clipboard (macOS pbcopy, Linux xclip)
8
+ - `/diff` command: show new messages since last session load
9
+ - Session load tracking: `@loaded_message_count` for diff comparison
10
+
11
+ ## [0.4.4] - 2026-03-19
12
+
13
+ ### Added
14
+ - Animated spinner in status bar thinking indicator (cycles through frames on each render)
15
+ - `/search <text>` command: case-insensitive search across chat message history
16
+ - `/theme <name>` command: switch between purple, green, blue, amber themes at runtime
17
+ - Chat input history: up/down arrow navigation through previous inputs (via TTY::Reader history_cycle)
18
+
3
19
  ## [0.4.3] - 2026-03-19
4
20
 
5
21
  ### Added
@@ -26,6 +26,8 @@ module Legion
26
26
  end
27
27
  end
28
28
 
29
+ SPINNER_FRAMES = %w[| / - \\].freeze
30
+
29
31
  private
30
32
 
31
33
  def build_segments
@@ -52,7 +54,9 @@ module Legion
52
54
  def thinking_segment
53
55
  return nil unless @state[:thinking]
54
56
 
55
- Theme.c(:warning, 'thinking...')
57
+ @spinner_index = ((@spinner_index || 0) + 1) % SPINNER_FRAMES.size
58
+ frame = SPINNER_FRAMES[@spinner_index]
59
+ Theme.c(:warning, "#{frame} thinking...")
56
60
  end
57
61
 
58
62
  def tokens_segment
@@ -12,8 +12,9 @@ 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].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].freeze
17
18
 
18
19
  attr_reader :message_stream, :status_bar
19
20
 
@@ -259,6 +260,9 @@ module Legion
259
260
  when '/quit' then :quit
260
261
  when '/help' then handle_help
261
262
  when '/clear' then handle_clear
263
+ when '/compact' then handle_compact(input)
264
+ when '/copy' then handle_copy(input)
265
+ when '/diff' then handle_diff(input)
262
266
  when '/model' then handle_model(input)
263
267
  when '/session' then handle_session(input)
264
268
  when '/cost' then handle_cost
@@ -276,6 +280,7 @@ module Legion
276
280
  when '/extensions' then handle_extensions_screen
277
281
  when '/config' then handle_config_screen
278
282
  when '/theme' then handle_theme(input)
283
+ when '/search' then handle_search(input)
279
284
  else :handled
280
285
  end
281
286
  end
@@ -287,7 +292,11 @@ module Legion
287
292
  content: "Commands:\n /help /quit /clear /model <name> /session <name> /cost\n " \
288
293
  "/export [md|json] /tools /dashboard /hotkeys /save /load /sessions\n " \
289
294
  "/system <prompt> /delete <session> /plan /palette /extensions /config\n " \
290
- "/theme [name] -- switch color theme (purple, green, blue, amber)\n\n" \
295
+ "/theme [name] -- switch color theme (purple, green, blue, amber)\n " \
296
+ "/search <text> -- search message history\n " \
297
+ "/compact [n] -- keep last n message pairs (default 5)\n " \
298
+ "/copy -- copy last assistant message to clipboard\n " \
299
+ "/diff -- show new messages since session was loaded\n\n" \
291
300
  'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
292
301
  )
293
302
  :handled
@@ -397,6 +406,7 @@ module Legion
397
406
  return :handled
398
407
  end
399
408
  @message_stream.messages.replace(data[:messages])
409
+ @loaded_message_count = @message_stream.messages.size
400
410
  @session_name = name
401
411
  @status_bar.update(session: name)
402
412
  @message_stream.add_message(role: :system,
@@ -589,6 +599,106 @@ module Legion
589
599
  :handled
590
600
  end
591
601
 
602
+ def handle_search(input)
603
+ query = input.split(nil, 2)[1]
604
+ unless query
605
+ @message_stream.add_message(role: :system, content: 'Usage: /search <text>')
606
+ return :handled
607
+ end
608
+
609
+ results = search_messages(query)
610
+ if results.empty?
611
+ @message_stream.add_message(role: :system, content: "No messages matching '#{query}'.")
612
+ else
613
+ lines = results.map { |r| " [#{r[:role]}] #{truncate_text(r[:content], 80)}" }
614
+ @message_stream.add_message(
615
+ role: :system,
616
+ content: "Found #{results.size} message(s) matching '#{query}':\n#{lines.join("\n")}"
617
+ )
618
+ end
619
+ :handled
620
+ end
621
+
622
+ # rubocop:disable Metrics/AbcSize
623
+ def handle_compact(input)
624
+ keep = (input.split(nil, 2)[1] || '5').to_i.clamp(1, 50)
625
+ msgs = @message_stream.messages
626
+ if msgs.size <= keep * 2
627
+ @message_stream.add_message(role: :system, content: 'Conversation is already compact.')
628
+ return :handled
629
+ end
630
+
631
+ system_msgs = msgs.select { |m| m[:role] == :system }
632
+ recent = msgs.reject { |m| m[:role] == :system }.last(keep * 2)
633
+ removed_count = msgs.size - system_msgs.size - recent.size
634
+ @message_stream.messages.replace(system_msgs + recent)
635
+ @message_stream.add_message(
636
+ role: :system,
637
+ content: "Compacted: removed #{removed_count} older messages, kept #{recent.size} recent."
638
+ )
639
+ :handled
640
+ end
641
+ # rubocop:enable Metrics/AbcSize
642
+
643
+ def handle_copy(_input)
644
+ last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
645
+ unless last_assistant
646
+ @message_stream.add_message(role: :system, content: 'No assistant message to copy.')
647
+ return :handled
648
+ end
649
+
650
+ content = last_assistant[:content].to_s
651
+ copy_to_clipboard(content)
652
+ @message_stream.add_message(
653
+ role: :system,
654
+ content: "Copied #{content.length} characters to clipboard."
655
+ )
656
+ :handled
657
+ end
658
+
659
+ def copy_to_clipboard(text)
660
+ IO.popen('pbcopy', 'w') { |io| io.write(text) }
661
+ rescue Errno::ENOENT
662
+ begin
663
+ IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) }
664
+ rescue Errno::ENOENT
665
+ nil
666
+ end
667
+ end
668
+
669
+ def handle_diff(_input)
670
+ if @loaded_message_count.nil?
671
+ @message_stream.add_message(role: :system, content: 'No session was loaded. Nothing to diff against.')
672
+ return :handled
673
+ end
674
+
675
+ new_count = @message_stream.messages.size - @loaded_message_count
676
+ if new_count <= 0
677
+ @message_stream.add_message(role: :system, content: 'No new messages since session was loaded.')
678
+ else
679
+ new_msgs = @message_stream.messages.last(new_count)
680
+ lines = new_msgs.map { |m| " + [#{m[:role]}] #{truncate_text(m[:content].to_s, 60)}" }
681
+ @message_stream.add_message(
682
+ role: :system,
683
+ content: "#{new_count} new message(s) since load:\n#{lines.join("\n")}"
684
+ )
685
+ end
686
+ :handled
687
+ end
688
+
689
+ def search_messages(query)
690
+ pattern = query.downcase
691
+ @message_stream.messages.select do |msg|
692
+ msg[:content].is_a?(::String) && msg[:content].downcase.include?(pattern)
693
+ end
694
+ end
695
+
696
+ def truncate_text(text, max_length)
697
+ return text if text.length <= max_length
698
+
699
+ "#{text[0...max_length]}..."
700
+ end
701
+
592
702
  def detect_provider
593
703
  cfg = safe_config
594
704
  provider = cfg[:provider].to_s.downcase
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.3'
5
+ VERSION = '0.4.5'
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.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity