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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/legion/tty/components/status_bar.rb +14 -0
- data/lib/legion/tty/screens/chat.rb +103 -11
- data/lib/legion/tty/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a862cd998ed05b3eb9a633c41602683425e79ea59f9634f0c2826e24e981291
|
|
4
|
+
data.tar.gz: 7cda3afc03dfa47d640d7fd4eca2dce80c8701fea820e3077e7bf3f4a37fc30d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
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'
|
data/lib/legion/tty/version.rb
CHANGED