legion-tty 0.4.1 → 0.4.2

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: 4e5ec362f89174ce0ccc182a50343da7a6ce2846f3c01ad51ad14afe8a7f80f8
4
- data.tar.gz: 763d695f1c7a2e8c13e3a518b36d2e90ae25713954a2127f5ed71869eaa31908
3
+ metadata.gz: 3985cb489efcaf4eaf208514bee79ff9650b1f841a00cd76a32468a0734e4198
4
+ data.tar.gz: fa7c612dac3248efe49762a4bd6b869b8348c4388ee569fdbd60c030501db918
5
5
  SHA512:
6
- metadata.gz: 4cde6c72b3a66dd7b5daffce9c3407c0d82275a41bb1ed564579ccd013d31bd2f42f4e0366426446adf19f382d5f4911de6cc04622c86fd2e7090663ba1b7177
7
- data.tar.gz: e4350ae0a6145f62758118c141f809de74d90ba5db873cbae1bd499c0c6b958325fa2196f91edd3df7aa3ddeb933f0fc818e3b7a372fab9ff8feb157f68ac680
6
+ metadata.gz: 9909b4d373d5b22892611dd3c302d02cb4960bfec8524da602c117b62de5d8fbacb9f39a777b97e5eabe9c379e4918fb79aab313d2f67e5c4129ff1214153d32
7
+ data.tar.gz: 3b8e072e3ad9cb3bd73b9e7e7bb5b6fb6baa86527e4da6e0045564c40851ec77e5e67a89cf7409d1280b59dc1a34831780859d56632e6e4c9a7b6095991a555f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.2] - 2026-03-19
4
+
5
+ ### Added
6
+ - Multi-provider model switching: `/model <provider>` creates new Legion::LLM.chat instance
7
+ - Model picker integration: `open_model_picker` for interactive provider/model selection
8
+ - ToolPanel wiring in MessageStream: `add_tool_call`, `update_tool_call` methods
9
+ - Tool call rendering in chat messages (`:tool` role with panel display)
10
+
11
+ ### Fixed
12
+ - Flaky table_view_spec: added explicit `require 'tty-table'` to prevent test ordering failures
13
+
3
14
  ## [0.4.1] - 2026-03-19
4
15
 
5
16
  ### Added
@@ -29,6 +29,21 @@ module Legion
29
29
  @messages.last[:tool_panels] << panel
30
30
  end
31
31
 
32
+ def add_tool_call(name:, args: {}, status: :running)
33
+ require_relative 'tool_panel'
34
+ panel = ToolPanel.new(name: name, args: args, status: status)
35
+ @messages << { role: :tool, content: panel, tool_panel: true }
36
+ end
37
+
38
+ def update_tool_call(name:, status:, duration: nil, result: nil, error: nil)
39
+ tool_msg = @messages.reverse.find do |m|
40
+ m[:tool_panel] && m[:content].is_a?(ToolPanel) && m[:content].instance_variable_get(:@name) == name
41
+ end
42
+ return unless tool_msg
43
+
44
+ apply_tool_panel_update(tool_msg[:content], status: status, duration: duration, result: result, error: error)
45
+ end
46
+
32
47
  def scroll_up(lines = 1)
33
48
  @scroll_offset += lines
34
49
  end
@@ -60,6 +75,7 @@ module Legion
60
75
  when :user then user_lines(msg)
61
76
  when :assistant then assistant_lines(msg, width)
62
77
  when :system then system_lines(msg)
78
+ when :tool then tool_call_lines(msg, width)
63
79
  else []
64
80
  end
65
81
  end
@@ -85,9 +101,22 @@ module Legion
85
101
  msg[:content].split("\n").map { |l| " #{Theme.c(:muted, l)}" }
86
102
  end
87
103
 
104
+ def tool_call_lines(msg, width)
105
+ return [] unless msg[:tool_panel] && msg[:content].respond_to?(:render)
106
+
107
+ msg[:content].render(width: width).split("\n")
108
+ end
109
+
88
110
  def panel_lines(msg, width)
89
111
  msg[:tool_panels].flat_map { |panel| panel.render(width: width).split("\n") }
90
112
  end
113
+
114
+ def apply_tool_panel_update(panel, status:, duration:, result:, error:)
115
+ panel.instance_variable_set(:@status, status)
116
+ panel.instance_variable_set(:@duration, duration) if duration
117
+ panel.instance_variable_set(:@result, result) if result
118
+ panel.instance_variable_set(:@error, error) if error
119
+ end
91
120
  end
92
121
  end
93
122
  end
@@ -92,20 +92,16 @@ module Legion
92
92
  end
93
93
 
94
94
  def send_to_llm(message)
95
- unless @llm_chat
95
+ unless @llm_chat || daemon_available?
96
96
  @message_stream.append_streaming('LLM not configured. Use /help for commands.')
97
97
  return
98
98
  end
99
99
 
100
- @status_bar.update(thinking: true)
101
- render_screen
102
- response = @llm_chat.ask(message) do |chunk|
103
- @status_bar.update(thinking: false)
104
- @message_stream.append_streaming(chunk.content) if chunk.content
105
- render_screen
100
+ if daemon_available?
101
+ send_via_daemon(message)
102
+ else
103
+ send_via_direct(message)
106
104
  end
107
- @status_bar.update(thinking: false)
108
- track_response_tokens(response)
109
105
  rescue StandardError => e
110
106
  @status_bar.update(thinking: false)
111
107
  @message_stream.append_streaming("\n[Error: #{e.message}]")
@@ -142,6 +138,40 @@ module Legion
142
138
  @llm_chat.with_instructions(prompt) if @llm_chat.respond_to?(:with_instructions)
143
139
  end
144
140
 
141
+ def send_via_daemon(message)
142
+ result = Legion::LLM.ask(message: message)
143
+
144
+ case result&.dig(:status)
145
+ when :done
146
+ @message_stream.append_streaming(result[:response])
147
+ when :error
148
+ err = result.dig(:error, :message) || 'Unknown error'
149
+ @message_stream.append_streaming("\n[Daemon error: #{err}]")
150
+ else
151
+ send_via_direct(message)
152
+ end
153
+ rescue StandardError
154
+ send_via_direct(message)
155
+ end
156
+
157
+ def send_via_direct(message)
158
+ return unless @llm_chat
159
+
160
+ @status_bar.update(thinking: true)
161
+ render_screen
162
+ response = @llm_chat.ask(message) do |chunk|
163
+ @status_bar.update(thinking: false)
164
+ @message_stream.append_streaming(chunk.content) if chunk.content
165
+ render_screen
166
+ end
167
+ @status_bar.update(thinking: false)
168
+ track_response_tokens(response)
169
+ end
170
+
171
+ def daemon_available?
172
+ !!(defined?(Legion::LLM::DaemonClient) && Legion::LLM::DaemonClient.available?)
173
+ end
174
+
145
175
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
146
176
  def build_system_prompt(cfg)
147
177
  lines = ['You are Legion, an async cognition engine and AI assistant.']
@@ -282,16 +312,50 @@ module Legion
282
312
  return
283
313
  end
284
314
 
285
- if @llm_chat.respond_to?(:with_model)
315
+ apply_model_switch(name)
316
+ rescue StandardError => e
317
+ @message_stream.add_message(role: :system, content: "Failed to switch model: #{e.message}")
318
+ end
319
+
320
+ def apply_model_switch(name)
321
+ new_chat = try_provider_switch(name)
322
+ if new_chat
323
+ @llm_chat = new_chat
324
+ @status_bar.update(model: name)
325
+ @token_tracker.update_model(name)
326
+ @message_stream.add_message(role: :system, content: "Switched to provider: #{name}")
327
+ elsif @llm_chat.respond_to?(:with_model)
286
328
  @llm_chat.with_model(name)
287
329
  @status_bar.update(model: name)
330
+ @token_tracker.update_model(name)
288
331
  @message_stream.add_message(role: :system, content: "Model switched to: #{name}")
289
332
  else
290
333
  @status_bar.update(model: name)
291
334
  @message_stream.add_message(role: :system, content: "Model set to: #{name}")
292
335
  end
293
- rescue StandardError => e
294
- @message_stream.add_message(role: :system, content: "Failed to switch model: #{e.message}")
336
+ end
337
+
338
+ def try_provider_switch(name)
339
+ return nil unless defined?(Legion::LLM)
340
+
341
+ providers = Legion::LLM.settings[:providers]
342
+ return nil unless providers.is_a?(Hash) && providers.key?(name.to_sym)
343
+
344
+ Legion::LLM.chat(provider: name)
345
+ rescue StandardError
346
+ nil
347
+ end
348
+
349
+ def open_model_picker
350
+ require_relative '../components/model_picker'
351
+ picker = Components::ModelPicker.new(
352
+ current_provider: safe_config[:provider],
353
+ current_model: @llm_chat.respond_to?(:model) ? @llm_chat.model.to_s : nil
354
+ )
355
+ selection = picker.select_with_prompt(output: @output)
356
+ return unless selection
357
+
358
+ switch_model(selection[:provider])
295
359
  end
296
360
 
297
361
  def show_current_model
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.1'
5
+ VERSION = '0.4.2'
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.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity