ollama_chat 0.0.130 โ†’ 0.0.131

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: 01d91ba996f30f84990c3d3eb60d0e65cc46b3eb0c593bd55eab6cc580e935e1
4
- data.tar.gz: 28563308f748187b5cf15dcad886a775dcb6d2abd3c4466ab66854a10dec63fd
3
+ metadata.gz: 8e7543207de9d23c4731b5c8b1e768ad28559f57a4d767bfcabaaf25dc7f4541
4
+ data.tar.gz: 7a3a03b47e077f2c0b6fa7d042a910d8dab32d30bcd0253c0e706f105d0a4e86
5
5
  SHA512:
6
- metadata.gz: edb330105098a15cb10bb0edaaca0131c1f339568c5639d2bc75b02adfaf1a4e0b94176e663e45784e9fb0d978732c854a680b937d135205bcb88c943c02c8b0
7
- data.tar.gz: 2dff67556aab12d5ad4a9a72239ffa34982804cfa311970a42bf35af33ad1e848baf94f82d74e9e65f21724019963b016c05823fa7e1ec40e01f7b511c1824fd
6
+ metadata.gz: d585ff38e8daeb72ddccedf4eb0ef1711c35f843fb3028f73c00bad1df84045ba5228b82916dec834bb4c2d518a15c198a5f009eb5bc541bd46eca0513458542
7
+ data.tar.gz: 291074810f529cf30873946a74e52a83de45461808ce0cb8aff4f366ef17c8cb70a5fccefc1b98c1b6e0a234dd2d67419788e59ad5da22ea64d1c1e04982b171
data/CHANGES.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-10 v0.0.131
4
+
5
+ * Extracted ANSI, emoji, and formatting logic from `Information` into a new
6
+ `OllamaChat::ContextUsage` class, providing a `colored`, `plain`, `filled`,
7
+ and `percent` API.
8
+ * Converted `context_usage_colored`, `context_filled`, and
9
+ `context_usage_plain` into one-line delegators on `Information`.
10
+ * Removed `context_percentage` and `context_gauge` methods, absorbing their
11
+ logic into `ContextUsage` private helpers.
12
+ * Updated `MessageList#compact!` to capture `context_usage_colored` for both
13
+ before and after states, ensuring the compaction report renders
14
+ threshold-colored values.
15
+ * Changed the plain format to be percent-first: `27.9% ยท 73.1 KT of 262.1 KT`.
16
+ * Updated `session_management.rb` log data to use `context_usage_plain`.
17
+ * Added `spec/ollama_chat/context_usage_spec.rb` to pin green/yellow/red
18
+ thresholds, plain format, nil-context fallback, and fill ratio.
19
+ * Added per-letter emojis to clear menu prompts in `rag_handling.rb` (๐Ÿ” view,
20
+ ๐Ÿงน clear, ๐Ÿ”™ other) to improve scannability of single-keypress `confirm?`
21
+ letter menus.
22
+ * Updated `asr.rb` to use `%w[]` for `Tempfile.new` prefix/suffix arguments and
23
+ replaced `File.size(wav.path)` with `wav.size`.
24
+ * Updated `asr_spec.rb` to stub the `Tempfile#size` instance method instead of
25
+ the `File.size` class method.
26
+ * Migrated spec temporary paths to the `asset_tmp_path` helper across 15 spec
27
+ files, replacing hardcoded `tmp/` paths, `Dir.tmpdir` + `$$` PID suffixes,
28
+ hand-rolled `Tins::Token.new(bits: 128)` strings, and `Time.now.to_i` epoch
29
+ hacks.
30
+ * Converted `File.write(pathname, ...)` calls to `pathname.write(...)` in
31
+ `delete_file_spec`, `parsing_spec`, `resolve_tag_spec`, and
32
+ `prompt_handling_spec`.
33
+ * Updated `prompt_handling_spec` to use `asset_pathname('prompt_file.txt')` for
34
+ the `Pathname#write` idiom.
35
+ * Removed `.txt` suffix from `include` assertions in `move_file_spec` as
36
+ tokenized basenames no longer match the original extension.
37
+ * Added a collection chooser to `memory_restore`, allowing users to select
38
+ which collections from a dump file to restore, with `[ALL]` / `[DONE]`
39
+ support.
40
+ * Updated `memory_dump` to use `Set#sort` directly instead of `Set#to_a.sort`.
41
+ * Added a new `asset_tmp_path` helper in `spec_helper.rb` that generates
42
+ collision-free temp paths via `Tins::Token` (128-bit).
43
+ * Updated restore specs to include chooser stubs, cancel and selective-restore
44
+ cases, and switched temp files to `asset_tmp_path`.
45
+
3
46
  ## 2026-10-08 v0.0.130
4
47
 
5
48
  * Replace `ask?` with `confirm?` in `rag_handling.rb` for the `[v]iew` /
@@ -26,8 +26,8 @@ module OllamaChat
26
26
  def transcribe(source_io, chat:, language: nil)
27
27
  asr_url = OC::OLLAMA::CHAT::ASR::URL
28
28
  asr_model = OC::OLLAMA::CHAT::ASR::MODEL
29
- input = Tempfile.new(['asr_in'])
30
- wav = Tempfile.new(['asr', '.wav'])
29
+ input = Tempfile.new(%w[ asr_in ])
30
+ wav = Tempfile.new(%w[ asr .wav ])
31
31
  begin
32
32
  IO.copy_stream(source_io, input)
33
33
  input.flush
@@ -41,7 +41,7 @@ module OllamaChat
41
41
  chat.feedback("ASR: #{executable} not found in PATH.", type: :warn)
42
42
  return
43
43
  end
44
- unless File.size(wav.path) > 0
44
+ unless wav.size > 0
45
45
  chat.feedback("ASR: #{executable} produced no output.", type: :warn)
46
46
  return
47
47
  end
@@ -0,0 +1,123 @@
1
+ require 'term/ansicolor'
2
+ require 'ollama_chat/utils/value_formatter'
3
+
4
+ module OllamaChat
5
+ # Renders the current context-window usage as colored or plain strings.
6
+ #
7
+ # Wraps an {OllamaChat::Chat} instance and concentrates the ANSI coloring,
8
+ # emoji, and token-formatting logic that was previously inlined in
9
+ # {OllamaChat::Information}. The color reflects how full the context is
10
+ # relative to the compaction `keep_recent` / `reserve` budgets:
11
+ #
12
+ # ๐ŸŸข green โ€” below `keep_recent` (plenty of room)
13
+ # ๐ŸŸก yellow โ€” between `keep_recent` and `reserve` (getting tight)
14
+ # ๐Ÿ”ด red โ€” above `reserve` (compaction imminent)
15
+ class ContextUsage
16
+ include Term::ANSIColor
17
+ include OllamaChat::Utils::ValueFormatter
18
+
19
+ EMOJI = { green: '๐ŸŸข', yellow: '๐ŸŸก', red: '๐Ÿ”ด' }.freeze
20
+
21
+ # @param chat [OllamaChat::Chat] the chat instance to read state from
22
+ def initialize(chat)
23
+ @chat = chat
24
+ end
25
+
26
+ # Colored usage string, e.g. `๐ŸŸข 73.1 KT of 262.1 KT (27.9%)`.
27
+ #
28
+ # Every value (used tokens, context limit, percentage) is painted with the
29
+ # threshold color; the surrounding text is bold. Returns a bold `n/a` when
30
+ # the context length cannot be determined.
31
+ #
32
+ # @return [String] the ANSI-colored usage string
33
+ def colored
34
+ ctx = @chat.current_context_length
35
+ return bold { 'n/a' } unless ctx
36
+
37
+ es = estimate
38
+ color = color_for(es.tokens, ctx)
39
+ percent = percent_for(es.tokens, ctx)
40
+
41
+ [
42
+ EMOJI[color], ' ',
43
+ bold { paint(color, percent) },
44
+ ' ยท ',
45
+ paint(color, es.tokens_formatted),
46
+ ' of ',
47
+ paint(color, format_tokens(ctx)),
48
+ ] * ''
49
+ end
50
+
51
+ # Plain (uncolored) usage string, e.g. `73.1 KT of 262.1 KT (27.9%)`.
52
+ #
53
+ # @return [String, nil] the usage string, or nil when the context length
54
+ # is unknown.
55
+ def plain
56
+ ctx = @chat.current_context_length
57
+ return nil unless ctx
58
+
59
+ es = estimate
60
+ '%s ยท %s of %s' % [
61
+ percent_for(es.tokens, ctx),
62
+ es.tokens_formatted,
63
+ format_tokens(ctx),
64
+ ]
65
+ end
66
+
67
+ # Fraction of the context window currently in use, clamped to 0.0โ€“1.0.
68
+ #
69
+ # @return [Float] the fill ratio (e.g. `0.279`)
70
+ def filled
71
+ es = estimate
72
+ (es.tokens.to_f / @chat.current_context_length).clamp(0..1).to_f
73
+ end
74
+
75
+ # Formatted percentage string, e.g. `27.9%`.
76
+ #
77
+ # @return [String] the percentage
78
+ def percent
79
+ format('%.1f%%', 100 * filled)
80
+ end
81
+
82
+ private
83
+
84
+ # @return [OllamaChat::TokenEstimator::Estimate] the compacted estimate
85
+ def estimate
86
+ @chat.messages.compacted_estimate_tokens
87
+ end
88
+
89
+ # @param tokens [Integer] used tokens
90
+ # @param ctx [Integer] context length
91
+ # @return [String] the formatted percentage
92
+ def percent_for(tokens, ctx)
93
+ format('%.1f%%', 100 * (tokens.to_f / ctx).clamp(0..1))
94
+ end
95
+
96
+ # Resolves the threshold color for a given fill level.
97
+ #
98
+ # @param tokens [Integer] used tokens
99
+ # @param ctx [Integer] context length
100
+ # @return [Symbol] one of :green, :yellow, :red
101
+ def color_for(tokens, ctx)
102
+ keep_recent = @chat.compact_ratio_tokens(:keep_recent, ctx)
103
+ reserve = @chat.compact_ratio_tokens(:reserve, ctx)
104
+ if tokens < keep_recent then :green
105
+ elsif tokens <= reserve then :yellow
106
+ else :red
107
+ end
108
+ end
109
+
110
+ # Paints a string with the given threshold color.
111
+ #
112
+ # @param color [Symbol] one of :green, :yellow, :red
113
+ # @param string [String] the text to colorize
114
+ # @return [String] the ANSI-colored string
115
+ def paint(color, string)
116
+ case color
117
+ when :green then green { string }
118
+ when :yellow then yellow { string }
119
+ else red { string }
120
+ end
121
+ end
122
+ end
123
+ end
@@ -124,18 +124,15 @@ module OllamaChat::Information
124
124
  end
125
125
  end
126
126
 
127
- # The context_usage_colored method computes a summary string of the current
128
- # context window's token usage. It formats the compacted message
129
- # estimate, the maximum context length, and a visual gauge indicating
130
- # the percentage of the context that is currently filled.
127
+ # The context_usage_colored method returns a summary string of the current
128
+ # context window's token usage, with the used, maximum, and percentage
129
+ # values all ANSI-colored according to the compaction thresholds.
131
130
  #
132
- # @return [ String ] the formatted string representing context usage
131
+ # Delegates the presentation (emoji + color) to {ContextUsage}.
132
+ #
133
+ # @return [ String ] the formatted, colored string representing context usage
133
134
  def context_usage_colored
134
- '%s of %s (%s)' % [
135
- messages.compacted_estimate_tokens.tokens_formatted,
136
- format_tokens(current_context_length),
137
- context_gauge(context_percentage),
138
- ]
135
+ context_usage.colored
139
136
  end
140
137
 
141
138
  # Displays a detailed view of the current chat session state, including the
@@ -397,7 +394,7 @@ module OllamaChat::Information
397
394
  tools_support: tools_support.on? ? 'enabled' : 'disabled',
398
395
  voice: voice.on? ? 'enabled' : 'disabled',
399
396
  weekday: now.strftime('%A'),
400
- context_usage: ,
397
+ context_usage: context_usage_plain,
401
398
  conversation_length: ,
402
399
  }
403
400
  end
@@ -455,66 +452,32 @@ module OllamaChat::Information
455
452
 
456
453
  # Computes the fraction of the context window currently in use.
457
454
  #
458
- # Divides the estimated token count of the session's messages by the
459
- # effective context length, clamped to the range 0.0โ€“1.0.
455
+ # Delegates to {ContextUsage#filled}; clamped to 0.0โ€“1.0.
460
456
  #
461
457
  # @return [Float] the ratio of used context (e.g. `0.73` for 73%)
462
458
  def context_filled
463
- es = messages.compacted_estimate_tokens
464
- (es.tokens.to_f / current_context_length).clamp(0..1).to_f
465
- end
466
-
467
- # Formats the current context fill ratio as a percentage string.
468
- #
469
- # @return [String] the formatted percentage (e.g. `"64.0%"`)
470
- def context_percentage
471
- format('%.1f%%', 100 * context_filled)
459
+ context_usage.filled
472
460
  end
473
461
 
474
462
  # Formats the current context usage as a human-readable string.
475
463
  #
476
464
  # Returns a string like `"167.7 KT of 262.1 KT (64.0%)"` combining
477
465
  # the estimated tokens in use, the effective context length, and
478
- # the percentage (via +context_filled+).
466
+ # the percentage. Delegates to {ContextUsage#plain}.
479
467
  #
480
468
  # @return [String, nil] the formatted usage string, or +nil+ if
481
469
  # the context length cannot be determined.
482
- def context_usage
483
- if cl = current_context_length
484
- '%s of %s (%s)' % [
485
- messages.compacted_estimate_tokens.tokens_formatted,
486
- format_tokens(current_context_length),
487
- context_percentage,
488
- ]
489
- end
470
+ def context_usage_plain
471
+ context_usage.plain
490
472
  end
491
473
 
492
- # Wraps a percentage string in an ANSI color based on context usage
493
- # relative to compaction thresholds.
494
- #
495
- # Green: below `keep_recent` budget โ€” plenty of room.
496
- # Yellow: between `keep_recent` and `reserve` โ€” getting tight.
497
- # Red: above `reserve` โ€” compaction imminent.
498
- #
499
- # Falls back to bold if context length is unknown.
500
- #
501
- # @param percent_string [String] the formatted percentage to colorize
502
- # @return [String] the ANSI-colored percentage string
503
- def context_gauge(percent_string)
504
- tokens = messages.compacted_estimate_tokens.tokens
505
- ctx = current_context_length
506
- return bold { percent_string } unless ctx
507
-
508
- keep_recent = compact_ratio_tokens(:keep_recent, ctx)
509
- reserve = compact_ratio_tokens(:reserve, ctx)
510
- bold do
511
- if tokens < keep_recent
512
- '๐ŸŸข ' + green { percent_string }
513
- elsif tokens <= reserve
514
- '๐ŸŸก ' + yellow { percent_string }
515
- else
516
- '๐Ÿ”ด ' + red { percent_string }
517
- end
518
- end
474
+ private
475
+
476
+ # Lazily builds and caches the {ContextUsage} bound to this chat,
477
+ # which owns the ANSI/emoji presentation logic for context usage.
478
+ #
479
+ # @return [ContextUsage]
480
+ def context_usage
481
+ @context_usage ||= OllamaChat::ContextUsage.new(self)
519
482
  end
520
483
  end
@@ -164,7 +164,7 @@ class OllamaChat::MessageList
164
164
  cand_es = OllamaChat::TokenEstimator::Crude.new(
165
165
  candidates.sum { |m| m.content.to_s.bytesize }
166
166
  ).perform
167
- context_before = @chat.context_usage
167
+ context_before = @chat.context_usage_colored
168
168
  context_filled_before = @chat.context_filled
169
169
 
170
170
  @chat.log(:info, 'Compaction: starting', data: {
@@ -211,7 +211,7 @@ class OllamaChat::MessageList
211
211
 
212
212
  OllamaChat::Compaction::Result.new(
213
213
  context_before:,
214
- context_after: @chat.context_usage,
214
+ context_after: @chat.context_usage_colored,
215
215
  context_filled_before: ,
216
216
  context_filled_after: @chat.context_filled,
217
217
  candidates: candidates.size,
@@ -137,10 +137,10 @@ module OllamaChat::RAGHandling
137
137
  filename = Pathname.new(filename).expand_path
138
138
  should_overwrite?(filename) or return
139
139
 
140
- feedback("Dumping #{selected.size} collection(s): #{selected.to_a.sort.join(', ')}")
140
+ feedback("Dumping #{selected.size} collection(s): #{selected.sort.join(', ')}")
141
141
 
142
142
  records = []
143
- selected.to_a.sort.each do |col|
143
+ selected.sort.each do |col|
144
144
  switch_collection(col) do
145
145
  @documents.each_record do |r|
146
146
  records << {
@@ -155,7 +155,7 @@ module OllamaChat::RAGHandling
155
155
 
156
156
  OllamaChat::Utils::JSONJSONLIO.new(filename).write(collection: records)
157
157
  log(:info, 'Memory dumped',
158
- data: { collections: selected.to_a.sort, records: records.size, file: filename.to_s })
158
+ data: { collections: selected.sort, records: records.size, file: filename.to_s })
159
159
  feedback(
160
160
  "Dumped #{records.size} record(s) across #{selected.size} collection(s) to #{filename.to_s.inspect}.",
161
161
  type: :success
@@ -164,8 +164,10 @@ module OllamaChat::RAGHandling
164
164
 
165
165
  # Restores `memory-<persona>` collections from a JSONL dump file.
166
166
  #
167
- # Each line's `collection` field determines the target collection;
168
- # records are added with their original tag and source so timestamps
167
+ # Presents an accumulating chooser of all collections found in the file:
168
+ # pick individual collections (they drop out of the list once selected),
169
+ # `[ALL]` to grab everything remaining, or `[DONE]` to commit the restore.
170
+ # Records are added with their original tag and source so timestamps
169
171
  # and file associations survive.
170
172
  #
171
173
  # @param filename [String, nil] the input path (must end in .json/.jsonl);
@@ -184,19 +186,55 @@ module OllamaChat::RAGHandling
184
186
  entries = OllamaChat::Utils::JSONJSONLIO.new(filename).read.to_a
185
187
  return feedback("No records found in #{filename.to_s.inspect}.", type: :warn) if entries.empty?
186
188
 
189
+ collections = entries.group_by { |e| e['collection'] }.reject { _1.nil? }
190
+ return feedback('No collection field found in entries.', type: :warn) if collections.empty?
191
+
192
+ selected = Set.new
193
+ choose_with_state do
194
+ loop do
195
+ remaining = collections.keys - selected.to_a
196
+ choices = (remaining.empty? ? [] : ['[ALL]'] + remaining) + ['[DONE]']
197
+ choice = choose_entry(choices, prompt: 'Which memory collections to restore? %s')
198
+ case choice
199
+ when nil, '[DONE]'
200
+ break
201
+ when '[ALL]'
202
+ selected.merge(remaining)
203
+ break
204
+ else
205
+ selected.add(choice)
206
+ end
207
+ end
208
+ end
209
+
210
+ return feedback('Cancelled, no collections selected.', type: :cancel) if selected.empty?
211
+
212
+ selected = selected.sort
213
+
214
+ feedback("Restoring #{selected.size} collection(s): #{selected.join(', ')}")
215
+
187
216
  total = 0
188
- entries.group_by { |e| e['collection'] }.each do |col, group|
217
+ collections.slice(*selected).sort.each do |col, group|
189
218
  create_memory_collection(col)
190
219
  switch_collection(col) do
191
220
  group.each do |entry|
192
- @documents.add([entry['text']], tags: Array(entry['tags']), batch_size: 1, source: entry['source'])
221
+ @documents.add(
222
+ [entry['text']],
223
+ tags: Array(entry['tags']),
224
+ batch_size: 1,
225
+ source: entry['source']
226
+ )
193
227
  total += 1
194
228
  end
195
229
  end
196
230
  end
197
231
 
198
- log(:info, 'Memory restored', data: { file: filename.to_s, records: total })
199
- feedback("Restored #{total} record(s) from #{filename.to_s.inspect}.", type: :success)
232
+ log(:info, 'Memory restored',
233
+ data: { file: filename.to_s, collections: selected, records: total })
234
+ feedback(
235
+ "Restored #{total} record(s) across #{selected.size} collection(s) from #{filename.to_s.inspect}.",
236
+ type: :success
237
+ )
200
238
  end
201
239
 
202
240
  private
@@ -248,7 +286,7 @@ module OllamaChat::RAGHandling
248
286
  label = source ? "#{source} #{snippet}" : snippet
249
287
  feedback(label, type: :info)
250
288
  view_tag_records(tag, [record])
251
- confirm?(prompt: " [c]lear / other to go back: ") =~ /\Ac/i or next
289
+ confirm?(prompt: " ๐Ÿงน[c]lear / ๐Ÿ”™other to go back: ") =~ /\Ac/i or next
252
290
  clear_tag_records(tag, source)
253
291
  else
254
292
  group_lines = []
@@ -270,7 +308,7 @@ module OllamaChat::RAGHandling
270
308
  )
271
309
  next if chosen.nil? || chosen == '[back]'
272
310
  src, recs = group_map[chosen]
273
- action = confirm?(prompt: " [v]iew / [c]lear / other to go back: ")
311
+ action = confirm?(prompt: " ๐Ÿ”[v]iew / ๐Ÿงน[c]lear / ๐Ÿ”™other to go back: ")
274
312
  case action
275
313
  when /\Av/i
276
314
  view_tag_records(tag, recs)
@@ -350,7 +388,7 @@ module OllamaChat::RAGHandling
350
388
  break
351
389
  else
352
390
  source = display_map[chosen]
353
- action = confirm?(prompt: " [v]iew / [c]lear / other to go back: ")
391
+ action = confirm?(prompt: " ๐Ÿ”[v]iew / ๐Ÿงน[c]lear / ๐Ÿ”™other to go back: ")
354
392
  case action
355
393
  when /\Av/i
356
394
  records = @documents.records(sources: [ source ])
@@ -17,7 +17,7 @@ module OllamaChat::SessionManagement
17
17
  log(:info, "Messages stored in session", data: {
18
18
  session_id: session.id,
19
19
  size: es.bytes_formatted,
20
- context_usage: ,
20
+ context_usage: context_usage_plain,
21
21
  messages: session.count_messages
22
22
  })
23
23
  self
@@ -1,6 +1,6 @@
1
1
  module OllamaChat
2
2
  # OllamaChat version
3
- VERSION = '0.0.130'
3
+ VERSION = '0.0.131'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/lib/ollama_chat.rb CHANGED
@@ -128,6 +128,7 @@ require 'ollama_chat/source_fetching'
128
128
  require 'ollama_chat/web_searching'
129
129
  require 'ollama_chat/dialog'
130
130
  require 'ollama_chat/think_control'
131
+ require 'ollama_chat/context_usage'
131
132
  require 'ollama_chat/information'
132
133
  require 'ollama_chat/message_output'
133
134
  require 'ollama_chat/clipboard'