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 +4 -4
- data/CHANGELOG.md +11 -0
- data/lib/legion/tty/components/message_stream.rb +29 -0
- data/lib/legion/tty/screens/chat.rb +76 -12
- 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: 3985cb489efcaf4eaf208514bee79ff9650b1f841a00cd76a32468a0734e4198
|
|
4
|
+
data.tar.gz: fa7c612dac3248efe49762a4bd6b869b8348c4388ee569fdbd60c030501db918
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
data/lib/legion/tty/version.rb
CHANGED