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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3985cb489efcaf4eaf208514bee79ff9650b1f841a00cd76a32468a0734e4198
4
- data.tar.gz: fa7c612dac3248efe49762a4bd6b869b8348c4388ee569fdbd60c030501db918
3
+ metadata.gz: 8ea19b2f60e08522ddf09e24a577b745804f35189f764a9e8150a802f3dc33b5
4
+ data.tar.gz: a1ff57a93aba51bc23f0421fa8f35525251f0dcd73f858b004965d68aeaf0083
5
5
  SHA512:
6
- metadata.gz: 9909b4d373d5b22892611dd3c302d02cb4960bfec8524da602c117b62de5d8fbacb9f39a777b97e5eabe9c379e4918fb79aab313d2f67e5c4129ff1214153d32
7
- data.tar.gz: 3b8e072e3ad9cb3bd73b9e7e7bb5b6fb6baa86527e4da6e0045564c40851ec77e5e67a89cf7409d1280b59dc1a34831780859d56632e6e4c9a7b6095991a555f
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
- 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
@@ -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\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
@@ -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
- PALETTE = {
8
- purple_1: [30, 27, 46],
9
- purple_2: [41, 37, 63],
10
- purple_3: [52, 47, 80],
11
- purple_4: [63, 57, 97],
12
- purple_5: [74, 67, 114],
13
- purple_6: [85, 77, 131],
14
- purple_7: [96, 87, 148],
15
- purple_8: [107, 97, 165],
16
- purple_9: [118, 107, 182],
17
- purple_10: [129, 119, 199],
18
- purple_11: [140, 131, 210],
19
- purple_12: [157, 148, 221],
20
- purple_13: [174, 167, 230],
21
- purple_14: [191, 186, 239],
22
- purple_15: [208, 205, 245],
23
- purple_16: [225, 224, 250],
24
- purple_17: [242, 243, 255]
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
- SEMANTIC = {
28
- primary: :purple_9,
29
- secondary: :purple_6,
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
- if PALETTE.key?(name)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.2'
5
+ VERSION = '0.4.4'
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.2
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity