legion-tty 0.4.13 → 0.4.14
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 +8 -0
- data/lib/legion/tty/components/message_stream.rb +9 -1
- data/lib/legion/tty/screens/chat/session_commands.rb +38 -0
- data/lib/legion/tty/screens/chat/ui_commands.rb +47 -0
- data/lib/legion/tty/screens/chat.rb +14 -1
- 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: bce8d6f696692fc8e9173eee8cb0afd87f37dc48b12325a149a7de1748f13447
|
|
4
|
+
data.tar.gz: d0e115df860ddd59c02e92b27d65a74a3a1413bafd25c7484f8dcc8c631832fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c6bc9b9eb07cf1838830fddd0bf626c4d9a0404db3330f7f6a658c5dd6425f9293d316a3a6f013858262f47ddb2875421c768a849a14213432028d66f56d94c7
|
|
7
|
+
data.tar.gz: 4eddcc425c258ec26d3c74e9ec3a9ce1ac470f8b25e985bed26db41f504e3825a61f21113587ccb70c5e7ce35789e06325affba7b704e0580687165687d4dbf5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.14] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- LLM response timing: tracks elapsed time per response, shows notification, includes avg in `/stats`
|
|
7
|
+
- `/wc` command: word count statistics per role (user/assistant/system) with averages
|
|
8
|
+
- `/import <path>` command: import session from any JSON file path with validation
|
|
9
|
+
- `/mute` command: toggle system message display in chat (messages still tracked, just hidden)
|
|
10
|
+
|
|
3
11
|
## [0.4.13] - 2026-03-19
|
|
4
12
|
|
|
5
13
|
### Added
|
|
@@ -5,12 +5,15 @@ require_relative '../theme'
|
|
|
5
5
|
module Legion
|
|
6
6
|
module TTY
|
|
7
7
|
module Components
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
9
|
class MessageStream
|
|
9
10
|
attr_reader :messages, :scroll_offset
|
|
11
|
+
attr_accessor :mute_system
|
|
10
12
|
|
|
11
13
|
def initialize
|
|
12
14
|
@messages = []
|
|
13
15
|
@scroll_offset = 0
|
|
16
|
+
@mute_system = false
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def add_message(role:, content:)
|
|
@@ -69,7 +72,11 @@ module Legion
|
|
|
69
72
|
private
|
|
70
73
|
|
|
71
74
|
def build_all_lines(width)
|
|
72
|
-
@messages.flat_map
|
|
75
|
+
@messages.flat_map do |msg|
|
|
76
|
+
next [] if @mute_system && msg[:role] == :system
|
|
77
|
+
|
|
78
|
+
render_message(msg, width)
|
|
79
|
+
end
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
def render_message(msg, width)
|
|
@@ -131,6 +138,7 @@ module Legion
|
|
|
131
138
|
panel.instance_variable_set(:@error, error) if error
|
|
132
139
|
end
|
|
133
140
|
end
|
|
141
|
+
# rubocop:enable Metrics/ClassLength
|
|
134
142
|
end
|
|
135
143
|
end
|
|
136
144
|
end
|
|
@@ -4,6 +4,7 @@ module Legion
|
|
|
4
4
|
module TTY
|
|
5
5
|
module Screens
|
|
6
6
|
class Chat < Base
|
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
|
7
8
|
module SessionCommands
|
|
8
9
|
private
|
|
9
10
|
|
|
@@ -76,6 +77,42 @@ module Legion
|
|
|
76
77
|
:handled
|
|
77
78
|
end
|
|
78
79
|
|
|
80
|
+
def handle_import(input)
|
|
81
|
+
path = input.split(nil, 2)[1]
|
|
82
|
+
unless path
|
|
83
|
+
@message_stream.add_message(role: :system, content: 'Usage: /import <path>')
|
|
84
|
+
return :handled
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
load_import_file(File.expand_path(path))
|
|
88
|
+
rescue ::JSON::ParserError => e
|
|
89
|
+
@message_stream.add_message(role: :system, content: "Invalid JSON: #{e.message}")
|
|
90
|
+
:handled
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def load_import_file(path)
|
|
94
|
+
unless File.exist?(path)
|
|
95
|
+
@message_stream.add_message(role: :system, content: "File not found: #{path}")
|
|
96
|
+
return :handled
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
data = ::JSON.parse(File.read(path), symbolize_names: true)
|
|
100
|
+
unless data.is_a?(Hash) && data[:messages].is_a?(Array)
|
|
101
|
+
@message_stream.add_message(role: :system, content: 'Invalid session file: missing messages array.')
|
|
102
|
+
return :handled
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
apply_imported_messages(data[:messages], path)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def apply_imported_messages(messages, path)
|
|
109
|
+
imported = messages.map { |m| { role: m[:role].to_sym, content: m[:content].to_s } }
|
|
110
|
+
@message_stream.messages.replace(imported)
|
|
111
|
+
@status_bar.notify(message: "Imported #{imported.size} messages", level: :success, ttl: 3)
|
|
112
|
+
@message_stream.add_message(role: :system, content: "Imported #{imported.size} messages from #{path}.")
|
|
113
|
+
:handled
|
|
114
|
+
end
|
|
115
|
+
|
|
79
116
|
def auto_save_session
|
|
80
117
|
return if @message_stream.messages.empty?
|
|
81
118
|
|
|
@@ -87,6 +124,7 @@ module Legion
|
|
|
87
124
|
nil
|
|
88
125
|
end
|
|
89
126
|
end
|
|
127
|
+
# rubocop:enable Metrics/ModuleLength
|
|
90
128
|
end
|
|
91
129
|
end
|
|
92
130
|
end
|
|
@@ -204,15 +204,62 @@ module Legion
|
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
+
def handle_wc
|
|
208
|
+
msgs = @message_stream.messages
|
|
209
|
+
by_role = word_counts_by_role(msgs)
|
|
210
|
+
total = by_role.values.sum
|
|
211
|
+
avg = (total.to_f / [msgs.size, 1].max).round
|
|
212
|
+
@message_stream.add_message(role: :system, content: build_wc_lines(by_role, total, avg).join("\n"))
|
|
213
|
+
:handled
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def word_counts_by_role(msgs)
|
|
217
|
+
%i[user assistant system].to_h do |role|
|
|
218
|
+
words = msgs.select { |m| m[:role] == role }.sum { |m| m[:content].to_s.split.size }
|
|
219
|
+
[role, words]
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_wc_lines(by_role, total, avg)
|
|
224
|
+
[
|
|
225
|
+
'Word count:',
|
|
226
|
+
" Total: #{format_stat_number(total)}",
|
|
227
|
+
" User: #{format_stat_number(by_role[:user])}",
|
|
228
|
+
" Assistant: #{format_stat_number(by_role[:assistant])}",
|
|
229
|
+
" System: #{format_stat_number(by_role[:system])}",
|
|
230
|
+
" Avg words/message: #{avg}"
|
|
231
|
+
]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def handle_mute
|
|
235
|
+
@muted_system = !@muted_system
|
|
236
|
+
@message_stream.mute_system = @muted_system
|
|
237
|
+
if @muted_system
|
|
238
|
+
@status_bar.notify(message: 'System messages hidden', level: :info, ttl: 3)
|
|
239
|
+
else
|
|
240
|
+
@status_bar.notify(message: 'System messages visible', level: :info, ttl: 3)
|
|
241
|
+
end
|
|
242
|
+
:handled
|
|
243
|
+
end
|
|
244
|
+
|
|
207
245
|
def build_stats_lines
|
|
208
246
|
msgs = @message_stream.messages
|
|
209
247
|
counts = count_by_role(msgs)
|
|
210
248
|
total_chars = msgs.sum { |m| m[:content].to_s.length }
|
|
211
249
|
lines = stats_header_lines(msgs, counts, total_chars)
|
|
212
250
|
lines << " Tool calls: #{counts[:tool]}" if counts[:tool].positive?
|
|
251
|
+
append_response_time_stat(lines, msgs)
|
|
213
252
|
lines
|
|
214
253
|
end
|
|
215
254
|
|
|
255
|
+
def append_response_time_stat(lines, msgs)
|
|
256
|
+
timed = msgs.select { |m| m[:response_time] }
|
|
257
|
+
return unless timed.any?
|
|
258
|
+
|
|
259
|
+
avg_rt = timed.sum { |m| m[:response_time] }.to_f / timed.size
|
|
260
|
+
lines << " Avg response time: #{avg_rt.round(2)}s (#{timed.size} responses)"
|
|
261
|
+
end
|
|
262
|
+
|
|
216
263
|
def count_by_role(msgs)
|
|
217
264
|
%i[user assistant system tool].to_h { |role| [role, msgs.count { |m| m[:role] == role }] }
|
|
218
265
|
end
|
|
@@ -28,7 +28,8 @@ module Legion
|
|
|
28
28
|
SLASH_COMMANDS = %w[/help /quit /clear /compact /copy /diff /model /session /cost /export /tools /dashboard
|
|
29
29
|
/hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
|
|
30
30
|
/theme /search /grep /stats /personality /undo /history /pin /pins /rename
|
|
31
|
-
/context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
|
|
31
|
+
/context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
|
|
32
|
+
/wc /import /mute].freeze
|
|
32
33
|
|
|
33
34
|
PERSONALITIES = {
|
|
34
35
|
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
@@ -58,6 +59,7 @@ module Legion
|
|
|
58
59
|
@snippets = {}
|
|
59
60
|
@debug_mode = false
|
|
60
61
|
@session_start = Time.now
|
|
62
|
+
@muted_system = false
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -202,15 +204,23 @@ module Legion
|
|
|
202
204
|
|
|
203
205
|
@status_bar.update(thinking: true)
|
|
204
206
|
render_screen
|
|
207
|
+
start_time = Time.now
|
|
205
208
|
response = @llm_chat.ask(message) do |chunk|
|
|
206
209
|
@status_bar.update(thinking: false)
|
|
207
210
|
@message_stream.append_streaming(chunk.content) if chunk.content
|
|
208
211
|
render_screen
|
|
209
212
|
end
|
|
213
|
+
record_response_time(Time.now - start_time)
|
|
210
214
|
@status_bar.update(thinking: false)
|
|
211
215
|
track_response_tokens(response)
|
|
212
216
|
end
|
|
213
217
|
|
|
218
|
+
def record_response_time(elapsed)
|
|
219
|
+
@last_response_time = elapsed
|
|
220
|
+
@message_stream.messages.last[:response_time] = elapsed if @message_stream.messages.last
|
|
221
|
+
@status_bar.notify(message: "Response: #{elapsed.round(1)}s", level: :info, ttl: 4)
|
|
222
|
+
end
|
|
223
|
+
|
|
214
224
|
def daemon_available?
|
|
215
225
|
!!(defined?(Legion::LLM::DaemonClient) && Legion::LLM::DaemonClient.available?)
|
|
216
226
|
end
|
|
@@ -340,6 +350,9 @@ module Legion
|
|
|
340
350
|
when '/bookmark' then handle_bookmark
|
|
341
351
|
when '/welcome' then handle_welcome
|
|
342
352
|
when '/tips' then handle_tips
|
|
353
|
+
when '/wc' then handle_wc
|
|
354
|
+
when '/import' then handle_import(input)
|
|
355
|
+
when '/mute' then handle_mute
|
|
343
356
|
else :handled
|
|
344
357
|
end
|
|
345
358
|
end
|
data/lib/legion/tty/version.rb
CHANGED