legion-tty 0.4.2 → 0.4.4
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/input_bar.rb +7 -1
- data/lib/legion/tty/components/status_bar.rb +5 -1
- data/lib/legion/tty/screens/chat.rb +56 -2
- data/lib/legion/tty/theme.rb +102 -35
- 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: 8ea19b2f60e08522ddf09e24a577b745804f35189f764a9e8150a802f3dc33b5
|
|
4
|
+
data.tar.gz: a1ff57a93aba51bc23f0421fa8f35525251f0dcd73f858b004965d68aeaf0083
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '00195e25f48241ad47b602d4d37b4d19bb1c1dc817d63ecc144ffa145e198974e37020065b2937c6864d5917ad13fd98870c9396d5bd0e2aafaf1e6d4f58e5e7'
|
|
7
|
+
data.tar.gz: 4b0c13ed2eaee447e559fd030f6e096c87ffac1b60316d525151a157820faf7510abda2fe40938916fffc67a1a385192511b3693b4005c43c89a62189bbcb231
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.4] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Animated spinner in status bar thinking indicator (cycles through frames on each render)
|
|
7
|
+
- `/search <text>` command: case-insensitive search across chat message history
|
|
8
|
+
- `/theme <name>` command: switch between purple, green, blue, amber themes at runtime
|
|
9
|
+
- Chat input history: up/down arrow navigation through previous inputs (via TTY::Reader history_cycle)
|
|
10
|
+
|
|
11
|
+
## [0.4.3] - 2026-03-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Daemon-first chat routing: chat screen routes through LegionIO daemon when available
|
|
15
|
+
- `send_via_daemon` and `send_via_direct` methods with automatic fallback
|
|
16
|
+
- `daemon_available?` guard for `Legion::LLM::DaemonClient` presence
|
|
17
|
+
|
|
3
18
|
## [0.4.2] - 2026-03-19
|
|
4
19
|
|
|
5
20
|
### Added
|
|
@@ -41,11 +41,17 @@ module Legion
|
|
|
41
41
|
@completions.select { |c| c.start_with?(partial) }.sort
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
def history
|
|
45
|
+
return [] unless @reader.respond_to?(:history)
|
|
46
|
+
|
|
47
|
+
@reader.history.to_a
|
|
48
|
+
end
|
|
49
|
+
|
|
44
50
|
private
|
|
45
51
|
|
|
46
52
|
def build_default_reader
|
|
47
53
|
require 'tty-reader'
|
|
48
|
-
reader = ::TTY::Reader.new
|
|
54
|
+
reader = ::TTY::Reader.new(history_cycle: true)
|
|
49
55
|
register_tab_completion(reader)
|
|
50
56
|
reader
|
|
51
57
|
rescue LoadError
|
|
@@ -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
|
-
|
|
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
|
|
@@ -13,7 +13,7 @@ module Legion
|
|
|
13
13
|
# rubocop:disable Metrics/ClassLength
|
|
14
14
|
class Chat < Base
|
|
15
15
|
SLASH_COMMANDS = %w[/help /quit /clear /model /session /cost /export /tools /dashboard /hotkeys /save /load
|
|
16
|
-
/sessions /system /delete /plan /palette /extensions /config].freeze
|
|
16
|
+
/sessions /system /delete /plan /palette /extensions /config /theme /search].freeze
|
|
17
17
|
|
|
18
18
|
attr_reader :message_stream, :status_bar
|
|
19
19
|
|
|
@@ -275,6 +275,8 @@ module Legion
|
|
|
275
275
|
when '/palette' then handle_palette
|
|
276
276
|
when '/extensions' then handle_extensions_screen
|
|
277
277
|
when '/config' then handle_config_screen
|
|
278
|
+
when '/theme' then handle_theme(input)
|
|
279
|
+
when '/search' then handle_search(input)
|
|
278
280
|
else :handled
|
|
279
281
|
end
|
|
280
282
|
end
|
|
@@ -285,7 +287,9 @@ module Legion
|
|
|
285
287
|
role: :system,
|
|
286
288
|
content: "Commands:\n /help /quit /clear /model <name> /session <name> /cost\n " \
|
|
287
289
|
"/export [md|json] /tools /dashboard /hotkeys /save /load /sessions\n " \
|
|
288
|
-
"/system <prompt> /delete <session> /plan /palette /extensions /config\n
|
|
290
|
+
"/system <prompt> /delete <session> /plan /palette /extensions /config\n " \
|
|
291
|
+
"/theme [name] -- switch color theme (purple, green, blue, amber)\n " \
|
|
292
|
+
"/search <text> -- search message history\n\n" \
|
|
289
293
|
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
290
294
|
)
|
|
291
295
|
:handled
|
|
@@ -570,6 +574,56 @@ module Legion
|
|
|
570
574
|
:handled
|
|
571
575
|
end
|
|
572
576
|
|
|
577
|
+
def handle_theme(input)
|
|
578
|
+
name = input.split(nil, 2)[1]
|
|
579
|
+
if name
|
|
580
|
+
if Theme.switch(name)
|
|
581
|
+
@message_stream.add_message(role: :system, content: "Theme switched to: #{name}")
|
|
582
|
+
else
|
|
583
|
+
available = Theme.available_themes.join(', ')
|
|
584
|
+
@message_stream.add_message(role: :system, content: "Unknown theme '#{name}'. Available: #{available}")
|
|
585
|
+
end
|
|
586
|
+
else
|
|
587
|
+
current = Theme.current_theme
|
|
588
|
+
available = Theme.available_themes.join(', ')
|
|
589
|
+
@message_stream.add_message(role: :system, content: "Current theme: #{current}\nAvailable: #{available}")
|
|
590
|
+
end
|
|
591
|
+
:handled
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def handle_search(input)
|
|
595
|
+
query = input.split(nil, 2)[1]
|
|
596
|
+
unless query
|
|
597
|
+
@message_stream.add_message(role: :system, content: 'Usage: /search <text>')
|
|
598
|
+
return :handled
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
results = search_messages(query)
|
|
602
|
+
if results.empty?
|
|
603
|
+
@message_stream.add_message(role: :system, content: "No messages matching '#{query}'.")
|
|
604
|
+
else
|
|
605
|
+
lines = results.map { |r| " [#{r[:role]}] #{truncate_text(r[:content], 80)}" }
|
|
606
|
+
@message_stream.add_message(
|
|
607
|
+
role: :system,
|
|
608
|
+
content: "Found #{results.size} message(s) matching '#{query}':\n#{lines.join("\n")}"
|
|
609
|
+
)
|
|
610
|
+
end
|
|
611
|
+
:handled
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def search_messages(query)
|
|
615
|
+
pattern = query.downcase
|
|
616
|
+
@message_stream.messages.select do |msg|
|
|
617
|
+
msg[:content].is_a?(::String) && msg[:content].downcase.include?(pattern)
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def truncate_text(text, max_length)
|
|
622
|
+
return text if text.length <= max_length
|
|
623
|
+
|
|
624
|
+
"#{text[0...max_length]}..."
|
|
625
|
+
end
|
|
626
|
+
|
|
573
627
|
def detect_provider
|
|
574
628
|
cfg = safe_config
|
|
575
629
|
provider = cfg[:provider].to_s.downcase
|
data/lib/legion/tty/theme.rb
CHANGED
|
@@ -2,44 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
module Legion
|
|
4
4
|
module TTY
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
6
|
module Theme
|
|
6
7
|
# rubocop:disable Naming/VariableNumber
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
THEMES = {
|
|
9
|
+
purple: {
|
|
10
|
+
palette: {
|
|
11
|
+
shade_1: [30, 27, 46], shade_2: [41, 37, 63], shade_3: [52, 47, 80],
|
|
12
|
+
shade_4: [63, 57, 97], shade_5: [74, 67, 114], shade_6: [85, 77, 131],
|
|
13
|
+
shade_7: [96, 87, 148], shade_8: [107, 97, 165], shade_9: [118, 107, 182],
|
|
14
|
+
shade_10: [129, 119, 199], shade_11: [140, 131, 210], shade_12: [157, 148, 221],
|
|
15
|
+
shade_13: [174, 167, 230], shade_14: [191, 186, 239], shade_15: [208, 205, 245],
|
|
16
|
+
shade_16: [225, 224, 250], shade_17: [242, 243, 255]
|
|
17
|
+
},
|
|
18
|
+
semantic: {
|
|
19
|
+
primary: :shade_9, secondary: :shade_6, accent: :shade_12,
|
|
20
|
+
success: [0, 200, 83], warning: [255, 191, 0], error: [255, 69, 58],
|
|
21
|
+
info: :shade_7, surface: :shade_1, muted: :shade_4,
|
|
22
|
+
rain: :shade_11, rain_fade: :shade_3
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
green: {
|
|
26
|
+
palette: {
|
|
27
|
+
shade_1: [15, 30, 15], shade_2: [20, 45, 20], shade_3: [25, 60, 25],
|
|
28
|
+
shade_4: [30, 75, 30], shade_5: [35, 90, 35], shade_6: [40, 110, 40],
|
|
29
|
+
shade_7: [50, 130, 50], shade_8: [60, 150, 60], shade_9: [75, 170, 75],
|
|
30
|
+
shade_10: [90, 190, 90], shade_11: [110, 210, 110], shade_12: [140, 225, 140],
|
|
31
|
+
shade_13: [170, 235, 170], shade_14: [195, 242, 195], shade_15: [215, 248, 215],
|
|
32
|
+
shade_16: [230, 252, 230], shade_17: [245, 255, 245]
|
|
33
|
+
},
|
|
34
|
+
semantic: {
|
|
35
|
+
primary: :shade_9, secondary: :shade_6, accent: :shade_12,
|
|
36
|
+
success: [0, 200, 83], warning: [255, 191, 0], error: [255, 69, 58],
|
|
37
|
+
info: :shade_7, surface: :shade_1, muted: :shade_4,
|
|
38
|
+
rain: :shade_11, rain_fade: :shade_3
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
blue: {
|
|
42
|
+
palette: {
|
|
43
|
+
shade_1: [15, 20, 40], shade_2: [20, 30, 60], shade_3: [25, 40, 80],
|
|
44
|
+
shade_4: [30, 50, 100], shade_5: [40, 65, 120], shade_6: [50, 80, 140],
|
|
45
|
+
shade_7: [65, 100, 160], shade_8: [80, 120, 180], shade_9: [100, 140, 200],
|
|
46
|
+
shade_10: [120, 160, 215], shade_11: [145, 185, 225], shade_12: [170, 205, 235],
|
|
47
|
+
shade_13: [195, 220, 242], shade_14: [210, 230, 248], shade_15: [225, 240, 252],
|
|
48
|
+
shade_16: [238, 248, 255], shade_17: [248, 252, 255]
|
|
49
|
+
},
|
|
50
|
+
semantic: {
|
|
51
|
+
primary: :shade_9, secondary: :shade_6, accent: :shade_12,
|
|
52
|
+
success: [0, 200, 83], warning: [255, 191, 0], error: [255, 69, 58],
|
|
53
|
+
info: :shade_7, surface: :shade_1, muted: :shade_4,
|
|
54
|
+
rain: :shade_11, rain_fade: :shade_3
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
amber: {
|
|
58
|
+
palette: {
|
|
59
|
+
shade_1: [35, 25, 10], shade_2: [50, 35, 15], shade_3: [65, 45, 20],
|
|
60
|
+
shade_4: [80, 55, 25], shade_5: [100, 70, 30], shade_6: [120, 85, 35],
|
|
61
|
+
shade_7: [140, 100, 40], shade_8: [165, 120, 50], shade_9: [190, 140, 60],
|
|
62
|
+
shade_10: [210, 160, 70], shade_11: [225, 180, 85], shade_12: [235, 200, 110],
|
|
63
|
+
shade_13: [242, 215, 140], shade_14: [248, 230, 170], shade_15: [252, 240, 200],
|
|
64
|
+
shade_16: [255, 248, 225], shade_17: [255, 252, 245]
|
|
65
|
+
},
|
|
66
|
+
semantic: {
|
|
67
|
+
primary: :shade_9, secondary: :shade_6, accent: :shade_12,
|
|
68
|
+
success: [0, 200, 83], warning: [255, 191, 0], error: [255, 69, 58],
|
|
69
|
+
info: :shade_7, surface: :shade_1, muted: :shade_4,
|
|
70
|
+
rain: :shade_11, rain_fade: :shade_3
|
|
71
|
+
}
|
|
72
|
+
}
|
|
25
73
|
}.freeze
|
|
26
74
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
accent: :purple_12,
|
|
31
|
-
success: [0, 200, 83],
|
|
32
|
-
warning: [255, 191, 0],
|
|
33
|
-
error: [255, 69, 58],
|
|
34
|
-
info: :purple_7,
|
|
35
|
-
surface: :purple_1,
|
|
36
|
-
muted: :purple_4,
|
|
37
|
-
rain: :purple_11,
|
|
38
|
-
rain_fade: :purple_3
|
|
39
|
-
}.freeze
|
|
75
|
+
# Legacy aliases for backward compatibility
|
|
76
|
+
PALETTE = THEMES[:purple][:palette].transform_keys { |k| k.to_s.sub('shade_', 'purple_').to_sym }.freeze
|
|
77
|
+
SEMANTIC = THEMES[:purple][:semantic].freeze
|
|
40
78
|
# rubocop:enable Naming/VariableNumber
|
|
41
79
|
|
|
42
80
|
class << self
|
|
81
|
+
def current_theme
|
|
82
|
+
@current_theme || :purple
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# rubocop:disable Naming/PredicateMethod
|
|
86
|
+
def switch(name)
|
|
87
|
+
name = name.to_sym
|
|
88
|
+
return false unless THEMES.key?(name)
|
|
89
|
+
|
|
90
|
+
@current_theme = name
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
# rubocop:enable Naming/PredicateMethod
|
|
94
|
+
|
|
95
|
+
def available_themes
|
|
96
|
+
THEMES.keys
|
|
97
|
+
end
|
|
98
|
+
|
|
43
99
|
def c(name, text)
|
|
44
100
|
rgb = resolve_rgb(name)
|
|
45
101
|
return text unless rgb
|
|
@@ -47,17 +103,28 @@ module Legion
|
|
|
47
103
|
"\e[38;2;#{rgb[0]};#{rgb[1]};#{rgb[2]}m#{text}\e[0m"
|
|
48
104
|
end
|
|
49
105
|
|
|
106
|
+
def reset_theme
|
|
107
|
+
@current_theme = :purple
|
|
108
|
+
end
|
|
109
|
+
|
|
50
110
|
private
|
|
51
111
|
|
|
52
112
|
def resolve_rgb(name)
|
|
53
|
-
|
|
113
|
+
theme = THEMES[current_theme]
|
|
114
|
+
palette = theme[:palette]
|
|
115
|
+
semantic = theme[:semantic]
|
|
116
|
+
|
|
117
|
+
if palette.key?(name)
|
|
118
|
+
palette[name]
|
|
119
|
+
elsif semantic.key?(name)
|
|
120
|
+
ref = semantic[name]
|
|
121
|
+
ref.is_a?(Symbol) ? palette[ref] : ref
|
|
122
|
+
elsif PALETTE.key?(name)
|
|
54
123
|
PALETTE[name]
|
|
55
|
-
elsif SEMANTIC.key?(name)
|
|
56
|
-
ref = SEMANTIC[name]
|
|
57
|
-
ref.is_a?(Symbol) ? PALETTE[ref] : ref
|
|
58
124
|
end
|
|
59
125
|
end
|
|
60
126
|
end
|
|
61
127
|
end
|
|
128
|
+
# rubocop:enable Metrics/ModuleLength
|
|
62
129
|
end
|
|
63
130
|
end
|
data/lib/legion/tty/version.rb
CHANGED