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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc958211b7004e0392a9e96cf2a874d6c54d6f0c6f183d87d80f428e16d3b014
4
- data.tar.gz: 13264f1761391724054983f250767d9308aab1a397d0dc1577292268a0f7e031
3
+ metadata.gz: bce8d6f696692fc8e9173eee8cb0afd87f37dc48b12325a149a7de1748f13447
4
+ data.tar.gz: d0e115df860ddd59c02e92b27d65a74a3a1413bafd25c7484f8dcc8c631832fb
5
5
  SHA512:
6
- metadata.gz: fd92069e863ebb5bd1185068adf3f50323fb276a2ae0479a8253e0220c1e4bc22a76abb7b8eb73a4c4a22c84be9f22cddf6eea22d510d7772fd459d3d74a1ff9
7
- data.tar.gz: afcce634192c2e16f133b0d237cd3720536dfce07e2cae17089f208867546d82d75a58bb352887da1d0d29c0e651d047b79596fea0428d35f152e48f17707069
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 { |msg| render_message(msg, width) }
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].freeze
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.13'
5
+ VERSION = '0.4.14'
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.13
4
+ version: 0.4.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity