ollama_chat 0.0.134 → 0.0.136

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +121 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/chat.rb +14 -2
  5. data/lib/ollama_chat/commands.rb +14 -11
  6. data/lib/ollama_chat/database/models/session.rb +2 -2
  7. data/lib/ollama_chat/database/{session_locking.rb → row_locking.rb} +11 -16
  8. data/lib/ollama_chat/database.rb +1 -1
  9. data/lib/ollama_chat/follow_chat.rb +1 -135
  10. data/lib/ollama_chat/information.rb +59 -1
  11. data/lib/ollama_chat/model_handling/model_cache.rb +106 -0
  12. data/lib/ollama_chat/model_handling.rb +37 -25
  13. data/lib/ollama_chat/oc.rb +8 -0
  14. data/lib/ollama_chat/ollama_chat_config/default_config.yml +2 -1
  15. data/lib/ollama_chat/ollama_chat_config/fastfetch-profile.jsonc +13 -0
  16. data/lib/ollama_chat/personae_management.rb +20 -2
  17. data/lib/ollama_chat/prompt_management.rb +1 -1
  18. data/lib/ollama_chat/rag_handling.rb +2 -2
  19. data/lib/ollama_chat/session_management.rb +33 -26
  20. data/lib/ollama_chat/source_fetching.rb +1 -1
  21. data/lib/ollama_chat/tool_call_processor.rb +176 -0
  22. data/lib/ollama_chat/tool_calling.rb +0 -16
  23. data/lib/ollama_chat/tools/execute_shell.rb +72 -32
  24. data/lib/ollama_chat/version.rb +7 -2
  25. data/lib/ollama_chat.rb +1 -0
  26. data/ollama_chat.gemspec +7 -7
  27. data/spec/ollama_chat/commands_spec.rb +10 -2
  28. data/spec/ollama_chat/follow_chat_spec.rb +2 -1
  29. data/spec/ollama_chat/information_spec.rb +1 -1
  30. data/spec/ollama_chat/kramdown_ansi_spec.rb +1 -1
  31. data/spec/ollama_chat/model_handling/model_cache_spec.rb +181 -0
  32. data/spec/ollama_chat/personae_management_spec.rb +75 -0
  33. data/spec/ollama_chat/session_management_spec.rb +31 -6
  34. data/spec/ollama_chat/tool_call_processor_spec.rb +51 -0
  35. data/spec/ollama_chat/tool_calling_spec.rb +0 -26
  36. data/spec/ollama_chat/tools/execute_shell_spec.rb +68 -0
  37. metadata +18 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: affb84ab9e8755cad2b29f047efccc92a9483c155889075b4ee001cf58935f2c
4
- data.tar.gz: 6e50f2280a13eb4d43cc702b750e2b649c7481d8a49337ebc86b0a5c4a70c6e3
3
+ metadata.gz: e22822d0b02f1f0dad3fe7e9b2902fc727a6dc2d13ffcd192da3fda3980fdefc
4
+ data.tar.gz: 2bb58b895a48bceefb16600def20f1ea8bb16deeeabd4a307f7fea27749a9139
5
5
  SHA512:
6
- metadata.gz: 8e571cba1334e97fe44c0c102414d7fd61ab8ffe6ef3d1c68c73b18359e139a7fd143d140c6e291bb1bcc46adcebb0bc014f20469d8889519d3b2b0ac3ca6e75
7
- data.tar.gz: 74ac387728f8dbe3a7b176ebf578b9bd1fdcabdfc7557f0858c5a78021aa596145b59f104b047ceb5fb0c3b73eea69242fd6d16b5c6a87c476db21db33fdec08
6
+ metadata.gz: 41e74251f4318137993e985f97beba5626b9d200b460d827ce04bddfb6dc316df6dae2d78024dd1c7d4486f3b7f4f7febd4e38d666fdf1e5f00f0e3e246b7c70
7
+ data.tar.gz: 1cd96acf7bca275337aaaad85c41d65c2bd75101ccfa1f0924d72eda254f4fffe04c019c9c2ee12285df6ea2e91da96cf702753059c0a7e7d7aa4bde8c13b0f4
data/CHANGES.md CHANGED
@@ -1,5 +1,126 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-21 v0.0.136
4
+
5
+ ### Features
6
+
7
+ - Added a `machine` key to the static runtime information, injecting a compact
8
+ host description into the system prompt via a new `machine_profile` method in
9
+ `information.rb`.
10
+ - Introduced a new `OC::FASTFETCH` configuration option to resolve the
11
+ `fastfetch` executable path.
12
+ - Added a new `fastfetch-profile.jsonc` configuration file to define the
13
+ modules used for the machine profile.
14
+ - Added `always-allow` and `direct` modes to the `execute_shell` tool, allowing
15
+ users to remember command approvals for the session.
16
+ - Extended the `execute_shell` confirmation prompt to include `a` (always allow
17
+ with pager) and `d` (always allow direct to `STDOUT`) options.
18
+
19
+ ### Fixes
20
+
21
+ - Updated the `kramdown_ansi` table separator spec to use pipe-bounded syntax
22
+ (`| Name | Age |`) to align with `kramdown-ansi` **0.6** requirements,
23
+ preventing literal dashes from leaking into the output.
24
+ - Renamed the `/last` command option from `-p` to `-d` for consistency with the
25
+ `execute_shell` direct mode flag.
26
+
27
+ ### Improvements
28
+
29
+ - Refactored `execute_shell` output display to use a shared `write_output`
30
+ helper for both pager and direct output paths.
31
+ - Updated the `execute_shell` tool description to document the new
32
+ `always-allow` and `direct` options.
33
+
34
+ ### Build & CI
35
+
36
+ - Updated the `kramdown-ansi` dependency constraint from **0.5** to **0.6** in
37
+ `Rakefile` and `ollama_chat.gemspec`.
38
+ - Added `lib/ollama_chat/ollama_chat_config/fastfetch-profile.jsonc` to the
39
+ gemspec files list.
40
+ - Added `git config --global --add safe.directory /work` to the CI test script
41
+ in `.all_images.yml`.
42
+
43
+ ### Testing
44
+
45
+ - Added four new specs for `execute_shell` covering `always-allow` and `direct`
46
+ mode behaviors, including repeat execution and command differentiation.
47
+ - Updated `commands_spec.rb` to reflect the rename of the `/last` command
48
+ option from `-p` to `-d`.
49
+ - Added a `before` block in `execute_shell_spec.rb` to clear the
50
+ `always_allowed` hash and prevent cross-spec contamination.
51
+
52
+ ## 2026-10-19 v0.0.135
53
+
54
+ ### Features
55
+
56
+ - Introduced `OllamaChat::ModelHandling::ModelCache` to manage model metadata
57
+ and sizes with two-tier storage (names/sizes and full metadata), supporting
58
+ lazy registration and per-client isolation.
59
+ - Added `-r` flag to the `/model change` command to flush the model cache
60
+ before re-listing models.
61
+ - Added `OllamaChat::ToolCallProcessor` to encapsulate the tool call lifecycle,
62
+ including execution, result storage, and draining of results for the chat
63
+ loop.
64
+ - Enabled searching sessions by ID in the session chooser by including the
65
+ session ID in the search value.
66
+
67
+ ### Fixes
68
+
69
+ - Rescued `Ollama::Errors::BadRequestError` in `model_present?` to prevent
70
+ unhandled exceptions for model names rejected by the server with a 400
71
+ status.
72
+ - Fixed persona leakage in new sessions by using `config.persona&.default_name`
73
+ instead of the current session's persona in `Session.with_defaults`.
74
+ - Synchronized session triggers with the active persona via `sync_trigger` in
75
+ `PersonaeManagement`, ensuring triggers are rekeyed, cleared, or seeded
76
+ correctly on persona changes.
77
+
78
+ ### Improvements
79
+
80
+ - Refactored `model_present?` to use the new `ModelCache` instead of an inline
81
+ hash, improving cache management and reducing repeated API calls.
82
+ - Updated `choose_model` to skip the `all_models` API call if the requested
83
+ model is already confirmed present via `model_present?`.
84
+ - Renamed the prompt context for report templates from `session` to `report` in
85
+ `default_config.yml` and `SessionManagement` to better reflect its purpose.
86
+ - Renamed `OllamaChat::Database::SessionLocking` to
87
+ `OllamaChat::Database::RowLocking` to reflect its generic row-level locking
88
+ behavior, removing the `chat:` keyword argument from `lock?` and moving
89
+ feedback emission to the caller.
90
+ - Replaced `Kramdown::ANSI::Width.truncate` and `Kramdown::ANSI::Width.wrap`
91
+ with bare `truncate` and `wrap` in multiple modules where ANSI sequences are
92
+ not present at measurement time.
93
+ - Truncated session names in the `/sessions` list to fit terminal width,
94
+ preventing long names from wrapping or pushing metadata off-screen.
95
+ - Decoupled `model_with_size` from the `Ollama::Tag` shape, now accepting model
96
+ name and size directly.
97
+
98
+ ### Build & CI
99
+
100
+ - Updated `gem_hadar` development dependency to **2.20.1**.
101
+ - Regenerated gem metadata to include new files (`tool_call_processor.rb`,
102
+ `model_cache.rb`, `row_locking.rb`) and updated file lists.
103
+
104
+ ### Testing
105
+
106
+ - Added `spec/ollama_chat/model_handling/model_cache_spec.rb` with 18 examples
107
+ covering cache operations, lazy registration, per-instance isolation, and
108
+ flushing.
109
+ - Added `spec/ollama_chat/personae_management_spec.rb` with 5 examples covering
110
+ `sync_trigger` state transitions.
111
+ - Added `spec/ollama_chat/tool_call_processor_spec.rb` covering `drain`,
112
+ `clear!`, and result storage.
113
+ - Updated `spec/ollama_chat/commands_spec.rb` to test the `-r` flag for `/model
114
+ change`.
115
+ - Updated `spec/ollama_chat/session_management_spec.rb` to verify persona and
116
+ trigger behavior in new and duplicated sessions, and adjusted lock
117
+ expectations.
118
+ - Updated `spec/ollama_chat/information_spec.rb` to reflect the correct persona
119
+ display after initialization.
120
+ - Removed `#handle_tool_call_results?` tests from
121
+ `spec/ollama_chat/tool_calling_spec.rb` as the method was moved to
122
+ `ToolCallProcessor`.
123
+
3
124
  ## 2026-10-17 v0.0.134
4
125
 
5
126
  * Removed the local dependency on the `ollama-ruby` gem from the `Gemfile`.
data/Rakefile CHANGED
@@ -47,7 +47,7 @@ GemHadar do
47
47
  dependency 'redis', '~> 5.0'
48
48
  dependency 'mime-types', '~> 3.0'
49
49
  dependency 'reverse_markdown', '~> 3.0'
50
- dependency 'kramdown-ansi', '~> 0.5'
50
+ dependency 'kramdown-ansi', '~> 0.6'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
52
  dependency 'tins', '~> 1.61'
53
53
  dependency 'search_ui', '>= 0.2'
@@ -116,6 +116,7 @@ class OllamaChat::Chat
116
116
  setup_switches
117
117
  setup_session
118
118
  setup_state_selectors(config)
119
+ @model_cache = ModelCache.new
119
120
  connect_ollama
120
121
  connect_embedding_ollama
121
122
  @documents = setup_documents
@@ -123,7 +124,7 @@ class OllamaChat::Chat
123
124
  @cache = setup_cache
124
125
  @images = []
125
126
  @kramdown_ansi_styles = configure_kramdown_ansi_styles
126
- @tool_call_results = Hash.new { |h, name| h[name] = [] }
127
+ @tool_call_processor = OllamaChat::ToolCallProcessor.new(chat: self)
127
128
  setup_personae_directory
128
129
  @opts[?S] and init_server_socket
129
130
  rescue ComplexConfig::AttributeMissing, ComplexConfig::ConfigurationSyntaxError => e
@@ -166,6 +167,11 @@ class OllamaChat::Chat
166
167
  # @return [OllamaChat::RedisCache, nil] the cache instance
167
168
  attr_reader :cache
168
169
 
170
+ # The model_cache method provides read-only access to the model cache.
171
+ #
172
+ # @return [ ModelCache ] the model cache instance
173
+ attr_reader :model_cache
174
+
169
175
  # Returns the voice handler class based on the configuration.
170
176
  #
171
177
  # This method resolves the voice handler class specified in the config.
@@ -341,6 +347,12 @@ class OllamaChat::Chat
341
347
  "#<#{self.class}: #{base_url.to_s}>"
342
348
  end
343
349
 
350
+ # Returns the tool call processor instance.
351
+ #
352
+ # @return [OllamaChat::ToolCallProcessor] the processor that handles
353
+ # tool execution and result storage
354
+ attr_reader :tool_call_processor
355
+
344
356
  private
345
357
 
346
358
  # Wrapper for ollama.chat to provide consistent logging of all arguments.
@@ -462,7 +474,7 @@ class OllamaChat::Chat
462
474
  type = :terminal_input
463
475
  input_prompt = bold { color(172) { message_type(@images) + " user" } } + bold { "> " }
464
476
  begin
465
- tools_were_called = handle_tool_call_results? { |index, tool_name, content|
477
+ tools_were_called = tool_call_processor.drain { |index, tool_name, content|
466
478
  messages << OllamaChat::Message.new(
467
479
  role: 'user', # XXX this should be 'tool' but it doesn't currently seem to work in Ollama API
468
480
  sender_name: tool_name,
@@ -285,15 +285,15 @@ module OllamaChat::Commands
285
285
 
286
286
  command(
287
287
  name: :last,
288
- regexp: %r(^/last((?:\s+(?:-[ptsv]))*)(?:\s+(\d*))?$),
289
- options: '[-p|-t|-s|n=1]',
288
+ regexp: %r(^/last((?:\s+(?:-[dtsv]))*)(?:\s+(\d*))?$),
289
+ options: '[-d|-t|-s|n=1]',
290
290
  help: <<~EOT
291
291
  🔍 Show or speak last message(s)
292
- Options: -p (plain), -t (show thinking),
293
- -s (hide thinking) -v (voice output)
292
+ Options: -d (direct), -t (show thinking),
293
+ -s (hide thinking) -v (voice output)
294
294
  EOT
295
295
  ) do |opts,number|
296
- opts = go_command('ptsv', opts.to_s)
296
+ opts = go_command('dtsv', opts.to_s)
297
297
  n = number.to_i.clamp(1..)
298
298
  if opts[?v]
299
299
  messages.speak_last(n)
@@ -306,7 +306,7 @@ module OllamaChat::Commands
306
306
  else
307
307
  self.think_loud.on?
308
308
  end
309
- messages.show_last(n, think_loud:, pager: !opts[?p])
309
+ messages.show_last(n, think_loud:, pager: !opts[?d])
310
310
  end
311
311
  :next
312
312
  end
@@ -552,12 +552,12 @@ module OllamaChat::Commands
552
552
 
553
553
  command(
554
554
  name: :model,
555
- regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-p\s+[\S]+))*)$),
555
+ regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-r|-p\s+[\S]+))*)$),
556
556
  complete: [ 'model', %w[ change options options\ copy options\ delete options\ export options\ import options\ from\ session options\ to\ session ] ],
557
- options: '[-m|-p pattern]',
557
+ options: '[-m|-r|-p pattern]',
558
558
  help: <<~EOT
559
559
  🤖 Manage AI models & profiles:
560
- - change: Switch active model
560
+ - change: Switch active model (-r to refresh)
561
561
  - options: Edit saved profile config
562
562
  - options copy: Copy profile from another model
563
563
  - options delete: Delete a saved profile
@@ -565,11 +565,14 @@ module OllamaChat::Commands
565
565
  - options import: Selectively import profiles from JSON
566
566
  - options from session: Save live → Saved
567
567
  - options to session: Apply Saved → Live
568
- -m interactively choose a model
569
- -p PATTERN narrow file search for import
568
+ -m interactively choose a model
569
+ -r refresh model list (flush cache)
570
+ -p PATTERN narrow file search for import
570
571
  EOT
571
572
  ) do |subcommand, opts|
572
573
  if subcommand == 'change'
574
+ opts = go_command('mr', opts)
575
+ @model_cache.flush if opts[?r]
573
576
  begin
574
577
  model = choose_model('', @model)
575
578
  profile = choose_profile_for_model(model) || 'default'
@@ -5,7 +5,7 @@
5
5
  # the JSON serialization of complex attributes such as `tools_default_enabled`
6
6
  # and `model_options`.
7
7
  class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
8
- include OllamaChat::Database::SessionLocking
8
+ include OllamaChat::Database::RowLocking
9
9
  include OllamaChat::Database::Duplicatable
10
10
 
11
11
  plugin :timestamps, update_on_create: true
@@ -200,7 +200,7 @@ class OllamaChat::Database::Models::Session < Sequel::Model(OllamaChat::DB)
200
200
  name: ,
201
201
  current_model: ,
202
202
  current_collection: chat.initial_collection,
203
- default_persona_name: chat.initial_persona_name,
203
+ default_persona_name: chat.config.persona&.default_name,
204
204
  current_system_prompt: chat.initial_system_prompt,
205
205
  tools_enabled: chat.config.tools.enabled,
206
206
  tools_default_enabled: ,
@@ -1,15 +1,15 @@
1
- # The SessionLocking module provides a mechanism for exclusive session access,
1
+ # The RowLocking module provides a mechanism for exclusive row access,
2
2
  # similar to the swap-file locking used by Vim.
3
3
  #
4
4
  # It prevents multiple concurrent instances of the application from modifying
5
- # the same session, thereby avoiding race conditions and context collision.
5
+ # the same database row, thereby avoiding race conditions and state collision.
6
6
  # The lock is based on the Process ID (PID) and is validated by checking if
7
7
  # the owning process is still active on the system.
8
8
  #
9
- # This module is intended to be mixed into models that persist session state,
10
- # such as OllamaChat::Database::Models::Session.
11
- module OllamaChat::Database::SessionLocking
12
- # Checks if the session is currently locked by an active process.
9
+ # This module is intended to be mixed into any Sequel model that has a
10
+ # `locked_by_pid` column.
11
+ module OllamaChat::Database::RowLocking
12
+ # Checks if the row is currently locked by an active process.
13
13
  #
14
14
  # @return [Integer, nil] the PID of the locking process if active, or nil if unlocked/stale.
15
15
  def locked?
@@ -23,17 +23,12 @@ module OllamaChat::Database::SessionLocking
23
23
  end
24
24
  end
25
25
 
26
- # Attempts to acquire a lock on the session.
26
+ # Attempts to acquire a lock on the row.
27
27
  #
28
- # @param chat [OllamaChat] the chat instance that receives feedback
29
28
  # @return [Boolean] true if the lock was successfully acquired, false if already locked.
30
- def lock?(chat:)
31
- if running_pid = locked?
32
- chat.feedback("session #{id} locked by running process #{running_pid}", type: :warn)
33
- false
34
- else
35
- lock
36
- end
29
+ def lock?
30
+ return false if locked?
31
+ !!lock
37
32
  end
38
33
 
39
34
  # Sets the lock for the current process.
@@ -43,7 +38,7 @@ module OllamaChat::Database::SessionLocking
43
38
  update(locked_by_pid: $$)
44
39
  end
45
40
 
46
- # Releases the lock on the session.
41
+ # Releases the lock on the row.
47
42
  #
48
43
  # @return [Boolean] true if the update was successful.
49
44
  def unlock
@@ -51,4 +51,4 @@ module OllamaChat::Database
51
51
  end
52
52
  end
53
53
  require 'ollama_chat/database/duplicatable'
54
- require 'ollama_chat/database/session_locking'
54
+ require 'ollama_chat/database/row_locking'
@@ -89,147 +89,13 @@ class OllamaChat::FollowChat
89
89
 
90
90
  output_eval_stats(response)
91
91
 
92
- handle_tool_calls(response)
92
+ chat.tool_call_processor.handle(response)
93
93
 
94
94
  self
95
95
  end
96
96
 
97
97
  private
98
98
 
99
- # The handle_tool_calls method processes tool calls from a response and
100
- # executes them.
101
- #
102
- # This method checks if the response contains tool calls, and if so, iterates
103
- # through each tool call to execute the corresponding tool from the
104
- # registered tools. The results of the tool execution are stored in the
105
- # chat's tool_call_results hash using the tool name as the key.
106
- #
107
- # @param response [Object] the response object containing tool calls to
108
- # process
109
- def handle_tool_calls(response)
110
- return unless response.message.ask_and_send(:tool_calls)
111
-
112
- tools_used = {}
113
-
114
- response.message.tool_calls.each do |tool_call|
115
- name = tool_call.function.name
116
- if %r(.*[/:](?<new_name>.+)) =~ name
117
- msg = "Received namespaced tool call for #{name}, correcting to #{new_name}"
118
- chat.log(:warn, msg, data: { tool: name, new_name: })
119
- name = new_name
120
- end
121
- unless chat.tool_configured?(name)
122
- msg = "Error: Unconfigured tool named %s ignored => Skip.\n" % name
123
- chat.tool_call_results[name] << msg
124
- chat.log(:error, msg, data: { tool: name, reason: :unconfigured })
125
- next
126
- end
127
- unless chat.tool_registered?(name)
128
- msg = "Error: Unregistered tool named %s ignored => Skip.\n" % name
129
- chat.tool_call_results[name] << msg
130
- chat.log(:error, msg, data: { tool: name, reason: :unregistered })
131
- next
132
- end
133
- unless chat.tool_enabled?(name)
134
- msg = "Error: Disabled tool named %s ignored => Skip.\n" % name
135
- chat.tool_call_results[name] << msg
136
- chat.log(:error, msg, data: { tool: name, reason: :disabled })
137
- next
138
- end
139
- feedback(?\n, output:)
140
- confirmed = :implicit
141
- resolve = nil
142
- function = JSON.pretty_generate(tool_call.function)
143
- chat.log(:info, "Tool call received", data: { tool: name, function: })
144
- if chat.tool_function(name).require_confirmation?
145
- feedback(
146
- ("I want to execute tool %s\n%s\n" % [ bold { name }, italic { function }, ]),
147
- type: :alarm
148
- )
149
- prompt = '❓ Allow ✅[y]es / ⛔️[n]o / 📝[i]nstruct? '
150
- answer = chat.confirm?(prompt:)&.to_s&.downcase
151
- resolve = 'You **MUST** ask the user for instructions on how to proceed!!!'
152
- case answer
153
- when 'y'
154
- confirmed = :explicit
155
- when 'n'
156
- confirmed = :denied
157
- else
158
- confirmed = :denied
159
- instr = chat.ask?(prompt: '📝 Instructions: ')&.strip
160
- resolve = instr.full? || resolve
161
- end
162
- else
163
- feedback(
164
- "Executing tool %s\n%s" % [ bold { name }, italic { function }, ]
165
- )
166
- end
167
- start = Time.now
168
- result = nil
169
- case confirmed
170
- when :denied
171
- result = JSON(message: 'User denied confirmation!', resolve:)
172
- feedback(?\n)
173
- feedback(
174
- ("Execution of tool %s denied by user.\n\n" % [ bold { name } ]),
175
- type: :denied
176
- )
177
- chat.log(:warn, "Tool execution denied", data: { tool: name, reason: :denied })
178
- else
179
- symbol = confirmed == :implicit ? '☑️ ' : '✅'
180
- feedback(
181
- ("\n%s Execution of tool %s confirmed.\n\n" % [ symbol, bold { name } ])
182
- )
183
- result = OllamaChat::Tools.registered_tool(name, chat:).execute(tool_call)
184
- chat.log(:info, "Tool execution confirmed", data: { tool: name, confirmed: })
185
- end
186
-
187
- chat.tool_call_results[name] << result
188
- data = nil
189
- message = begin
190
- data = JSON.parse(result)
191
- chat.log(:info, "Tool result", data: { result: data })
192
- data.to_hash['message'] if data.respond_to?(:to_hash)
193
- rescue
194
- chat.log(:info, "Tool result", data: { result: })
195
- nil
196
- end
197
- warn =
198
- begin
199
- !!data.ask_and_send(:[], 'error') || data.ask_and_send(:[], 'success') == false
200
- rescue
201
- false
202
- end
203
-
204
- es = OllamaChat::TokenEstimator.estimate(result.to_s)
205
-
206
- tools_used[name] = {
207
- message:,
208
- warn: ,
209
- size: es.bytes_formatted,
210
- tokens: es.tokens_formatted,
211
- duration: Tins::Duration.new(Time.now - start).to_s,
212
- }
213
- end
214
-
215
- if tools_used.full?
216
- infobar.reset
217
- tools_used.each do |name, info|
218
- feedback_message =
219
- if info[:message]
220
- "\n%s %s\n\n" % [ info[:warn] ? '⚠️' : '💡', info[:message] ]
221
- end
222
- feedback(<<~EOT.strip)
223
- 🔧 Tool functions #{name} returned result (#{info[:size]}/#{info[:tokens]} in #{info[:duration]}).
224
- #{feedback_message}
225
- EOT
226
- feedback(?\n)
227
- timeout = chat.tool_function(name).result_display_timeout?
228
- chat.confirm?(prompt: '⏎ Press any key to continue (%s). ', timeout:)
229
- end
230
- end
231
- end
232
-
233
99
  # The truncate_for_terminal method processes text to fit within a specified
234
100
  # number of lines.
235
101
  #
@@ -74,7 +74,7 @@ module OllamaChat::Information
74
74
  def collection_stats(output: STDOUT)
75
75
  col = database_collection?(collection)
76
76
  length = (Tins::Terminal.cols - 10).clamp(0..)
77
- wrapped_tags = Kramdown::ANSI::Width.
77
+ wrapped_tags =
78
78
  wrap(@documents.tags.to_a.join(', '), length:).
79
79
  gsub(/(?<!\A)^/, ' ' * 4)
80
80
  output.puts <<~EOT
@@ -341,6 +341,7 @@ module OllamaChat::Information
341
341
  current_directory: Pathname.pwd.expand_path.to_path,
342
342
  languages: config.languages * ', ',
343
343
  location: location.on?.full? { location_description } || 'n/a',
344
+ machine: machine_profile || 'n/a',
344
345
  user: ,
345
346
  }
346
347
  end
@@ -474,6 +475,63 @@ module OllamaChat::Information
474
475
 
475
476
  private
476
477
 
478
+ # Builds and caches a compact, single-line description of the host
479
+ # machine, derived from a `fastfetch` run against the bundled profile
480
+ # (+OC::FASTFETCH+). The profile requests a focused set of modules
481
+ # (os, kernel, cpu, gpu, de, wm, terminal, memory, editor, packages,
482
+ # chassis, board, bios) so the result stays small enough to inject into
483
+ # the system prompt at zero per-turn cost.
484
+ #
485
+ # Gracefully degrades to `'n/a'` when +fastfetch+ is not installed or
486
+ # the config is missing, so a missing optional dependency never breaks
487
+ # session startup.
488
+ #
489
+ # @return [String] e.g.
490
+ # `C64 1541 (c64) · 6502@1MHz m64 · MOS Technology 6502 ·
491
+ # NVIDIA GeForce RTX 5090 (32GB) · VIC-II (no DE) · Home Computer ·
492
+ # PET2001N-Board · MOS 3.0 UEFI 4000 · 64 KiB · vim 1.0 · 1 pkgs`
493
+ def machine_profile
494
+ return @machine_profile if defined?(@machine_profile)
495
+ @machine_profile = nil
496
+ unless fastfetch = OC::FASTFETCH?
497
+ log(:warn, 'Fastfetch executable not found, skipping machine profile!')
498
+ return
499
+ end
500
+ config = Shellwords.escape(File.join(__dir__, 'ollama_chat_config', 'fastfetch-profile.jsonc'))
501
+ out = `#{fastfetch} -c #{config} --format json 2>/dev/null`.full? or return
502
+ mods = JSON.parse(out, object_class: JSON::GenericObject)
503
+ @machine_profile =
504
+ mods.map do |m|
505
+ r = m.result or next
506
+ case m.type
507
+ when 'OS'
508
+ "%s %s (%s)" % [ r.name, r.versionID, r.idLike ]
509
+ when 'Kernel'
510
+ "%s %s" % [ r.release, r.architecture ]
511
+ when 'CPU'
512
+ r.cpu
513
+ when 'GPU'
514
+ (r.find { _1.type == 'Discrete' } || r.first)&.name
515
+ when 'WM'
516
+ suffix = mods.any? { _1.type == 'DE' && _1.result } ? '' : ' (no DE)'
517
+ "%s/%s%s" % [ r.processName, r.protocolName, suffix ]
518
+ when 'Memory'
519
+ format_bytes(r.total)
520
+ when 'Editor'
521
+ "%s %s" % [ r.exe, r.version ]
522
+ when 'Packages'
523
+ "%s pkgs" % r.all
524
+ when 'Chassis'
525
+ r.type
526
+ when 'Board'
527
+ r.name
528
+ when 'BIOS'
529
+ "%s %s" % [ r.type, r.version ]
530
+ end
531
+ end.compact * ' · '
532
+ rescue JSON::ParserError
533
+ end
534
+
477
535
  # Lazily builds and caches the {ContextUsage} bound to this chat,
478
536
  # which owns the ANSI/emoji presentation logic for context usage.
479
537
  #
@@ -0,0 +1,106 @@
1
+ module OllamaChat::ModelHandling
2
+ # Caches model metadata for one or more Ollama instances.
3
+ #
4
+ # This class provides a simple in-memory cache to store and retrieve
5
+ # model metadata, reducing the need for repeated `ollama show` calls.
6
+ # It supports multiple Ollama instances by using the instance's
7
+ # object ID as part of the cache key.
8
+ class ModelCache
9
+ # Initializes a new model cache.
10
+ def initialize
11
+ @models = Hash.new { |h, k| h[k] = {} }
12
+ @sizes = Hash.new { |h, k| h[k] = {} }
13
+ @registered = Hash.new(false)
14
+ end
15
+
16
+ # Caches model metadata (and optionally size) for a given Ollama
17
+ # instance and model name.
18
+ #
19
+ # Existing metadata is never overwritten (`||=`); only missing
20
+ # entries are filled in. Size, when provided, overwrites unconditionally.
21
+ #
22
+ # @param ollama [Ollama::Client] the Ollama client instance
23
+ # @param name [String, Symbol] the model name
24
+ # @param value [ModelMetadata, nil] the metadata to cache (defaults to nil)
25
+ # @param size [Integer, nil] the model size in bytes (optional)
26
+ # @return [ModelMetadata, nil] the cached metadata
27
+ def cache(ollama, name, value = nil, size: nil)
28
+ name = name.to_s
29
+ @models[ollama.__id__][name] ||= value
30
+ size and @sizes[ollama.__id__][name] = size
31
+ @models[ollama.__id__][name]
32
+ end
33
+
34
+ # Returns the cached `ModelMetadata` for a given model, if it has
35
+ # been fetched. Note: this is a *metadata* predicate, not an
36
+ # existence check. A model can be registered (via `models`) and
37
+ # have a size, yet still return `nil` here until metadata is
38
+ # explicitly cached (e.g. by `model_present?`).
39
+ #
40
+ # @param ollama [Ollama::Client] the Ollama client instance
41
+ # @param name [String, Symbol] the model name
42
+ # @return [ModelMetadata, nil] the metadata if fetched, nil otherwise
43
+ def cached?(ollama, name)
44
+ name = name.to_s
45
+ @models[ollama.__id__][name]
46
+ end
47
+
48
+ # Returns the cached size in bytes for a given model.
49
+ #
50
+ # @param ollama [Ollama::Client] the Ollama client instance
51
+ # @param name [String, Symbol] the model name
52
+ # @return [Integer] the size in bytes, 0 if not cached
53
+ def model_size(ollama, name)
54
+ @sizes[ollama.__id__][name].to_i
55
+ end
56
+
57
+ # Returns a list of cached model names for the given Ollama instance.
58
+ #
59
+ # @param ollama [Ollama::Client] the Ollama client instance
60
+ # @return [Array<String>] the cached model names
61
+ def models(ollama)
62
+ registered?(ollama) or register_all(ollama)
63
+ @models[ollama.__id__].keys
64
+ end
65
+
66
+ # Flushes cached metadata and sizes for one or all Ollama instances.
67
+ #
68
+ # @param ollama [Ollama::Client, Integer, nil] an Ollama client instance,
69
+ # a raw object ID, or `nil` to flush all instances (default: nil)
70
+ # @return [true]
71
+ def flush(ollama = nil)
72
+ if ollama
73
+ ollama_id = ollama.ask_and_send(:to_int) || ollama.__id__
74
+ @models[ollama_id].clear
75
+ @sizes[ollama_id].clear
76
+ @registered[ollama_id] = false
77
+ else
78
+ @models.each_key { |ollama_id| flush(ollama_id) }
79
+ end
80
+ true
81
+ end
82
+
83
+ private
84
+
85
+ # Checks whether model registration has been completed for the instance.
86
+ #
87
+ # @param ollama [Ollama::Client] the Ollama client instance
88
+ # @return [Boolean]
89
+ def registered?(ollama)
90
+ @registered[ollama.__id__]
91
+ end
92
+
93
+ # Registers multiple model names as cached for the given Ollama instance.
94
+ #
95
+ # @param ollama [Ollama::Client] the Ollama client instance
96
+ #
97
+ # @return [ ModelCache ] this cache instance
98
+ def register_all(ollama)
99
+ ollama.tags.models.each do |model|
100
+ cache(ollama, model.name, size: model.size)
101
+ end
102
+ @registered[ollama.__id__] = true
103
+ self
104
+ end
105
+ end
106
+ end