legion-tty 0.4.8 → 0.4.9

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: 49d5dd5b69b7fa3412226bc38e93b0adb0df6be7c3b0317955bf7f9698a0759d
4
- data.tar.gz: ed73f018bbb85ee31fdf11aba24f5b6f332f28ef85970f9189ba5164402d8690
3
+ metadata.gz: 6a862cd998ed05b3eb9a633c41602683425e79ea59f9634f0c2826e24e981291
4
+ data.tar.gz: 7cda3afc03dfa47d640d7fd4eca2dce80c8701fea820e3077e7bf3f4a37fc30d
5
5
  SHA512:
6
- metadata.gz: 67a87dfd8538f4c42e5c9897d4460d8457071660e95df3fe019757d3e9e56ef95627d4a5a3576914611257f78fd766190909e4e95f8f4bee26b1c0328745c35f
7
- data.tar.gz: a6f9bd2210d04ad3d2200a4029e32141eb82b29043dee974cb5b37d5d6b927b1f2b284cac1d3e39c77c6105fa7c08f61a3da8a678e4d77e88de675cfe9cac2ea
6
+ metadata.gz: 9f65b4afeeed674a49e59a43e4116eee383db5bdc2083cd648342c9134ba2e971d8403d8d3fc57ebbcacacbf9dcec293d482cce818a97c84ce4485d843ad1490
7
+ data.tar.gz: cd51d5ede4592418fe9a25226c054a2cf26adec493f7efc94763c6165c4bef50b1fdfe74f039759fb4cabdf0fec36ab679b643f28273f8a8573011e8a063c3d8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.9] - 2026-03-19
4
+
5
+ ### Added
6
+ - StatusBar notifications: transient toast-style messages with TTL expiry (wired to save/load/export/theme)
7
+ - `/undo` command: remove last user+assistant message pair
8
+ - `/history` command: show last 20 input entries
9
+ - `/pin` and `/pins` commands: pin important messages, view pinned list
10
+ - `/rename <name>` command: rename current session (deletes old, saves new)
11
+
3
12
  ## [0.4.8] - 2026-03-19
4
13
 
5
14
  ### Added
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../theme'
4
+ require_relative 'notification'
4
5
 
5
6
  module Legion
6
7
  module TTY
@@ -8,6 +9,11 @@ module Legion
8
9
  class StatusBar
9
10
  def initialize
10
11
  @state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false }
12
+ @notifications = []
13
+ end
14
+
15
+ def notify(message:, level: :info, ttl: 5)
16
+ @notifications << Notification.new(message: message, level: level, ttl: ttl)
11
17
  end
12
18
 
13
19
  def update(**fields)
@@ -35,6 +41,7 @@ module Legion
35
41
  model_segment,
36
42
  plan_segment,
37
43
  thinking_segment,
44
+ notification_segment,
38
45
  tokens_segment,
39
46
  cost_segment,
40
47
  session_segment,
@@ -60,6 +67,13 @@ module Legion
60
67
  Theme.c(:warning, "#{frame} thinking...")
61
68
  end
62
69
 
70
+ def notification_segment
71
+ @notifications.reject!(&:expired?)
72
+ return nil if @notifications.empty?
73
+
74
+ @notifications.first.render
75
+ end
76
+
63
77
  def tokens_segment
64
78
  Theme.c(:secondary, "#{format_number(@state[:tokens])} tokens") if @state[:tokens].to_i.positive?
65
79
  end
@@ -14,7 +14,7 @@ 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 /stats /personality].freeze
17
+ /theme /search /stats /personality /undo /history /pin /pins /rename].freeze
18
18
 
19
19
  PERSONALITIES = {
20
20
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -38,6 +38,7 @@ module Legion
38
38
  @session_store = SessionStore.new
39
39
  @session_name = 'default'
40
40
  @plan_mode = false
41
+ @pinned_messages = []
41
42
  end
42
43
 
43
44
  def activate
@@ -292,6 +293,11 @@ module Legion
292
293
  when '/search' then handle_search(input)
293
294
  when '/stats' then handle_stats
294
295
  when '/personality' then handle_personality(input)
296
+ when '/undo' then handle_undo
297
+ when '/history' then handle_history
298
+ when '/pin' then handle_pin(input)
299
+ when '/pins' then handle_pins
300
+ when '/rename' then handle_rename(input)
295
301
  else :handled
296
302
  end
297
303
  end
@@ -309,7 +315,12 @@ module Legion
309
315
  "/copy -- copy last assistant message to clipboard\n " \
310
316
  "/diff -- show new messages since session was loaded\n " \
311
317
  "/stats -- show conversation statistics\n " \
312
- "/personality [name] -- switch assistant personality\n\n" \
318
+ "/personality [name] -- switch assistant personality\n " \
319
+ "/undo -- remove last user+assistant message pair\n " \
320
+ "/history -- show recent input history\n " \
321
+ "/pin [N] -- pin last assistant message (or message at index N)\n " \
322
+ "/pins -- show all pinned messages\n " \
323
+ "/rename <name> -- rename current session\n\n" \
313
324
  'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
314
325
  )
315
326
  :handled
@@ -403,6 +414,7 @@ module Legion
403
414
  @session_name = name
404
415
  @session_store.save(name, messages: @message_stream.messages)
405
416
  @status_bar.update(session: name)
417
+ @status_bar.notify(message: "Saved '#{name}'", level: :success, ttl: 3)
406
418
  @message_stream.add_message(role: :system, content: "Session saved as '#{name}'.")
407
419
  :handled
408
420
  end
@@ -422,6 +434,7 @@ module Legion
422
434
  @loaded_message_count = @message_stream.messages.size
423
435
  @session_name = name
424
436
  @status_bar.update(session: name)
437
+ @status_bar.notify(message: "Loaded '#{name}'", level: :info, ttl: 3)
425
438
  @message_stream.add_message(role: :system,
426
439
  content: "Session '#{name}' loaded (#{data[:messages].size} messages).")
427
440
  :handled
@@ -454,16 +467,29 @@ module Legion
454
467
  :handled
455
468
  end
456
469
 
457
- # rubocop:disable Metrics/AbcSize
458
470
  def handle_export(input)
459
471
  require 'fileutils'
472
+ path = build_export_path(input)
473
+ dispatch_export(path, input.split[1]&.downcase)
474
+ @status_bar.notify(message: 'Exported', level: :success, ttl: 3)
475
+ @message_stream.add_message(role: :system, content: "Exported to: #{path}")
476
+ :handled
477
+ rescue StandardError => e
478
+ @message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
479
+ :handled
480
+ end
481
+
482
+ def build_export_path(input)
460
483
  format = input.split[1]&.downcase
461
484
  format = 'md' unless %w[json md html].include?(format)
462
485
  exports_dir = File.expand_path('~/.legionio/exports')
463
486
  FileUtils.mkdir_p(exports_dir)
464
487
  timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
465
488
  ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
466
- path = File.join(exports_dir, "chat-#{timestamp}.#{ext}")
489
+ File.join(exports_dir, "chat-#{timestamp}.#{ext}")
490
+ end
491
+
492
+ def dispatch_export(path, format)
467
493
  if format == 'json'
468
494
  export_json(path)
469
495
  elsif format == 'html'
@@ -471,15 +497,8 @@ module Legion
471
497
  else
472
498
  export_markdown(path)
473
499
  end
474
- @message_stream.add_message(role: :system, content: "Exported to: #{path}")
475
- :handled
476
- rescue StandardError => e
477
- @message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
478
- :handled
479
500
  end
480
501
 
481
- # rubocop:enable Metrics/AbcSize
482
-
483
502
  # rubocop:disable Metrics/AbcSize
484
503
  def handle_tools
485
504
  lex_gems = Gem::Specification.select { |s| s.name.start_with?('lex-') }
@@ -605,6 +624,7 @@ module Legion
605
624
  name = input.split(nil, 2)[1]
606
625
  if name
607
626
  if Theme.switch(name)
627
+ @status_bar.notify(message: "Theme: #{name}", level: :info, ttl: 2)
608
628
  @message_stream.add_message(role: :system, content: "Theme switched to: #{name}")
609
629
  else
610
630
  available = Theme.available_themes.join(', ')
@@ -853,6 +873,78 @@ module Legion
853
873
  text.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
854
874
  end
855
875
 
876
+ def handle_undo
877
+ msgs = @message_stream.messages
878
+ last_user_idx = msgs.rindex { |m| m[:role] == :user }
879
+ unless last_user_idx
880
+ @message_stream.add_message(role: :system, content: 'Nothing to undo.')
881
+ return :handled
882
+ end
883
+
884
+ msgs.slice!(last_user_idx..)
885
+ :handled
886
+ end
887
+
888
+ def handle_history
889
+ entries = @input_bar.history
890
+ if entries.empty?
891
+ @message_stream.add_message(role: :system, content: 'No input history.')
892
+ else
893
+ recent = entries.last(20)
894
+ lines = recent.each_with_index.map { |entry, i| " #{i + 1}. #{entry}" }
895
+ @message_stream.add_message(role: :system,
896
+ content: "Input history (last #{recent.size}):\n#{lines.join("\n")}")
897
+ end
898
+ :handled
899
+ end
900
+
901
+ def handle_pin(input)
902
+ idx_str = input.split(nil, 2)[1]
903
+ msg = if idx_str
904
+ @message_stream.messages[idx_str.to_i]
905
+ else
906
+ @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
907
+ end
908
+ unless msg
909
+ @message_stream.add_message(role: :system, content: 'No message to pin.')
910
+ return :handled
911
+ end
912
+
913
+ @pinned_messages << msg
914
+ preview = truncate_text(msg[:content].to_s, 60)
915
+ @message_stream.add_message(role: :system, content: "Pinned: #{preview}")
916
+ :handled
917
+ end
918
+
919
+ def handle_pins
920
+ if @pinned_messages.empty?
921
+ @message_stream.add_message(role: :system, content: 'No pinned messages.')
922
+ else
923
+ lines = @pinned_messages.each_with_index.map do |msg, i|
924
+ " #{i + 1}. [#{msg[:role]}] #{truncate_text(msg[:content].to_s, 70)}"
925
+ end
926
+ @message_stream.add_message(role: :system,
927
+ content: "Pinned messages (#{@pinned_messages.size}):\n#{lines.join("\n")}")
928
+ end
929
+ :handled
930
+ end
931
+
932
+ def handle_rename(input)
933
+ name = input.split(nil, 2)[1]
934
+ unless name
935
+ @message_stream.add_message(role: :system, content: 'Usage: /rename <new-name>')
936
+ return :handled
937
+ end
938
+
939
+ old_name = @session_name
940
+ @session_store.delete(old_name) if old_name != 'default'
941
+ @session_name = name
942
+ @status_bar.update(session: name)
943
+ @session_store.save(name, messages: @message_stream.messages)
944
+ @message_stream.add_message(role: :system, content: "Session renamed to '#{name}'.")
945
+ :handled
946
+ end
947
+
856
948
  def build_default_input_bar
857
949
  cfg = safe_config
858
950
  name = cfg[:name] || 'User'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.8'
5
+ VERSION = '0.4.9'
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.8
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity