ollama_chat 0.0.137 → 0.0.139

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +110 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/chat.rb +1 -1
  5. data/lib/ollama_chat/command_concern.rb +7 -4
  6. data/lib/ollama_chat/compaction.rb +1 -1
  7. data/lib/ollama_chat/conversation.rb +1 -1
  8. data/lib/ollama_chat/database/models/collection.rb +1 -1
  9. data/lib/ollama_chat/dialog.rb +1 -1
  10. data/lib/ollama_chat/http_handling.rb +1 -1
  11. data/lib/ollama_chat/information.rb +3 -2
  12. data/lib/ollama_chat/kramdown_ansi.rb +107 -21
  13. data/lib/ollama_chat/links_set.rb +1 -0
  14. data/lib/ollama_chat/model_handling.rb +15 -7
  15. data/lib/ollama_chat/rag_handling.rb +24 -12
  16. data/lib/ollama_chat/session_management.rb +17 -15
  17. data/lib/ollama_chat/source_fetching.rb +16 -1
  18. data/lib/ollama_chat/tools/browse.rb +4 -4
  19. data/lib/ollama_chat/tools/concern.rb +1 -0
  20. data/lib/ollama_chat/tools/execute_grep.rb +3 -3
  21. data/lib/ollama_chat/tools/generate_image.rb +2 -2
  22. data/lib/ollama_chat/tools/get_url.rb +5 -0
  23. data/lib/ollama_chat/utils/analyze_directory.rb +19 -19
  24. data/lib/ollama_chat/utils/fetcher.rb +2 -1
  25. data/lib/ollama_chat/utils/redact.rb +32 -0
  26. data/lib/ollama_chat/utils.rb +1 -0
  27. data/lib/ollama_chat/version.rb +1 -1
  28. data/lib/ollama_chat/web_searching.rb +1 -1
  29. data/ollama_chat.gemspec +5 -5
  30. data/spec/ollama_chat/compaction_spec.rb +1 -1
  31. data/spec/ollama_chat/information_spec.rb +23 -0
  32. data/spec/ollama_chat/kramdown_ansi_spec.rb +54 -2
  33. data/spec/ollama_chat/parsing_spec.rb +5 -4
  34. data/spec/ollama_chat/rag_handling_spec.rb +62 -3
  35. data/spec/ollama_chat/tools/get_url_spec.rb +24 -5
  36. data/spec/ollama_chat/tools/list_directory_spec.rb +10 -3
  37. data/spec/ollama_chat/utils/analyze_directory_spec.rb +33 -21
  38. data/spec/ollama_chat/web_searching_spec.rb +1 -1
  39. metadata +5 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '00825774cef8e349ad7666a4e3046995bfa4803216d75fe688c8479a3bfce6c2'
4
- data.tar.gz: 45f95f47c73c711686b479c49ac6fec3309fe3a68c385fb9d780e28f8f10dd13
3
+ metadata.gz: e1a0cfa8c0914331039a424abe408286dce18f50b3b593bc3cac04c551c9b078
4
+ data.tar.gz: ab96f2413e6ab996329d654d1f7dc60b75d99cf6cd2bb93f5c0b1f5bc09627a7
5
5
  SHA512:
6
- metadata.gz: 157e6951f84674cae4cc74e3423f9569f6cb30efc92109d7b27538ed7424e4d938e03513b9d3d0044b233be9f74b06572218c4ab128b5881d4361424585bf285
7
- data.tar.gz: 98eed217eca3fc4e396110ec20729e9e10bad64e5bf40539df1768ccd70c21ede9896855ba94a4b551ab3133c3e6020f45383de2ef59e7e9e75206c168f685d7
6
+ metadata.gz: 987632cebff57817b71ad6f56326c85edf921a4f54c1a42fff906e93603ec7c45113be2eb0b57b372987f9050d62213f54985359fdaf84f4bbc5bcd4bcdf8392
7
+ data.tar.gz: 9d9cee9148bc2c3542c9eb543015fa5b16393832f2741b56926f8d6b37a0d0633abea5252d32a5d3b931e350d4e44d874e857cdf84ae72d0c70ab79d8b747183
data/CHANGES.md CHANGED
@@ -1,5 +1,115 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-30 v0.0.139
4
+
5
+ ### Features
6
+
7
+ - Added terminal table wrapping for `/help` and `/session list` output. New
8
+ private methods `wrap_table_rows` and `table_column_budgets` in
9
+ `OllamaChat::KramdownANSI` compute per-column width budgets and wrap cell
10
+ content to fit within a configurable percentage of the terminal width.
11
+ - Updated `help_message` and `list_sessions` to collect rows into arrays and
12
+ route them through `wrap_table_rows`, removing manual truncation and heading
13
+ assignment.
14
+
15
+ ### Fixes
16
+
17
+ - Fixed a `TypeError` in `Collection.sync` when `chat.documents.collections`
18
+ yielded no collections (e.g., fresh database) by using `.to_a` instead of
19
+ relying on `Tins::NullClass#to_ary`.
20
+ - Improved error handling in `fetch_source` for `OllamaChat::HTTPError` by
21
+ adding a dedicated rescue clause that logs a warning without a backtrace and
22
+ stamps `response_status` and `response_reason` onto the
23
+ `ResponseMetadata.failed` IO.
24
+ - Added a non-2xx HTTP status guard in the `get_url` tool to short-circuit
25
+ processing before document policy dispatch, returning the raw body and a
26
+ status message instead of feeding error pages to the summarizer.
27
+
28
+ ### Improvements
29
+
30
+ - Updated the compaction voice report to format context fill percentages as
31
+ rounded integers with the unit "percent" for grammatical completeness
32
+ (e.g., "from 38 percent to 19 percent").
33
+ - Moved `configure_kramdown_ansi_styles` to a private method in
34
+ `OllamaChat::KramdownANSI`.
35
+ - Expanded YARD documentation for `OllamaChat::KramdownANSI` to describe
36
+ styling, parsing, and table wrapping concerns.
37
+
38
+ ### Build & CI
39
+
40
+ - Bumped `kramdown-ansi` dependency from `~> 0.7` to `~> 0.9` in `Rakefile`
41
+ and `ollama_chat.gemspec`.
42
+
43
+ ### Testing
44
+
45
+ - Updated `compaction_spec.rb` to expect rounded integer percentages in the
46
+ spoken compaction report.
47
+ - Updated `kramdown_ansi_spec.rb` to use a real `OllamaChat::Chat` instance
48
+ instead of a double and added examples for `wrap_table_rows` and
49
+ `table_column_budgets`.
50
+ - Updated `get_url_spec.rb` to stub `response_status: nil` on `SourceIO`
51
+ doubles and added a spec verifying that non-200 statuses skip summarization
52
+ and import.
53
+
54
+ ## 2026-10-28 v0.0.138
55
+
56
+ ### Features
57
+
58
+ - Added `OllamaChat::Utils::Redact` module with `redact_password` to mask
59
+ credentials in URLs for user-facing display.
60
+ - `memory_restore` now deduplicates entries by checking existing tags before
61
+ adding records, reporting the number of skipped duplicates.
62
+ - `OllamaChat::Utils::AnalyzeDirectory` now returns a `Hash` with `:height` and
63
+ `:entries` keys instead of a bare `Array`. Entry hashes no longer include
64
+ `:type` or `:name`; directories are identified by a trailing `/` in `:path`.
65
+
66
+ ### Fixes
67
+
68
+ - `use_model` now raises `OllamaChat::UnknownModelError` if the previous model
69
+ is missing or blank after a cancelled pull, preventing stale metadata from
70
+ being returned.
71
+ - Fixed `update_collection` to use `Set` for the `seen` variable, resolving a
72
+ latent `NoMethodError` when upstream yields a `Set`.
73
+ - Corrected the error message in `pull_model_unless_present` to reference the
74
+ `model` parameter instead of the potentially stale `@model` instance
75
+ variable.
76
+
77
+ ### Improvements
78
+
79
+ - Applied password redaction to all user-visible URL displays, including
80
+ `Chat#inspect`, `Dialog#connect_message`, `Information#info_rag`,
81
+ `Information#print_welcome`, `Tools::Browse#execute`,
82
+ `Tools::GenerateImage#execute`, and `Fetcher.get`.
83
+ - Standardized `Set` insertion to use the `<<` idiom across `conversation.rb`,
84
+ `http_handling.rb`, `rag_handling.rb`, `generate_image.rb`, and
85
+ `web_searching.rb`.
86
+ - Added `<<` alias to `LinksSet` to support the standard `Set` insertion idiom.
87
+ - `AnalyzeDirectory` now omits the `:children` key from directory entries when
88
+ the array is empty, both during build and pruning.
89
+
90
+ ### Build & CI
91
+
92
+ - Added `lib/ollama_chat/utils/redact.rb` to the gemspec file list.
93
+
94
+ ### Testing
95
+
96
+ - Updated `analyze_directory_spec.rb`, `parsing_spec.rb`, and
97
+ `list_directory_spec.rb` to assert the new `AnalyzeDirectory` return
98
+ structure.
99
+ - Added specs for `redact_password` covering URI/String inputs, passthrough for
100
+ URLs without passwords, and non-mutation of the original object.
101
+ - Added specs for `memory_restore` deduplication logic, covering partial and
102
+ full skip scenarios.
103
+ - Updated `web_searching_spec.rb` to expect `<<` instead of `add` for link
104
+ insertion.
105
+
106
+ ### Documentation
107
+
108
+ - Updated `execute_grep` tool description to explicitly state that the regex
109
+ runs against file contents.
110
+ - Updated YARD documentation for `AnalyzeDirectory` to reflect the new return
111
+ value structure.
112
+
3
113
  ## 2026-10-23 v0.0.137
4
114
 
5
115
  ### Fixes
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.7'
50
+ dependency 'kramdown-ansi', '~> 0.9'
51
51
  dependency 'complex_config', '~> 0.22', '>= 0.22.2'
52
52
  dependency 'tins', '~> 1.61'
53
53
  dependency 'search_ui', '>= 0.2'
@@ -344,7 +344,7 @@ class OllamaChat::Chat
344
344
  #
345
345
  # @return [String] the inspected string
346
346
  def inspect
347
- "#<#{self.class}: #{base_url.to_s}>"
347
+ "#<#{self.class}: #{redact_password(base_url)}>"
348
348
  end
349
349
 
350
350
  # Returns the tool call processor instance.
@@ -6,6 +6,8 @@ module OllamaChat::CommandConcern
6
6
  extend Tins::Concern
7
7
 
8
8
  class_methods do
9
+ include OllamaChat::KramdownANSI
10
+
9
11
  # @!attribute [rw] commands
10
12
  # @return [Hash<Symbol, Command>] A hash mapping command names to
11
13
  # `Command` instances.
@@ -76,8 +78,8 @@ module OllamaChat::CommandConcern
76
78
  all_separators: true,
77
79
  border: :unicode_round,
78
80
  }
79
- table.headings = %w[ CMD SUBCMD OPTS HELP ]
80
81
  sorted_commands = self.commands.sort_by { [ _2.category, _2.name ] }.map(&:last)
82
+ rows = []
81
83
  cat = nil
82
84
  sorted_commands.each do |command|
83
85
  command.help or next
@@ -93,15 +95,16 @@ module OllamaChat::CommandConcern
93
95
  }
94
96
  if cat != command.category
95
97
  cat = command.category
96
- table << [ { value: bold{cat}, colspan: 4 } ]
98
+ rows << [ { value: bold{cat}, colspan: 4 } ]
97
99
  end
98
- table << [
99
- "%s" % command.command_names.map { ?/ + _1 }.join(?\n),
100
+ rows << [
101
+ '%s' % command.command_names.map { ?/ + _1 }.join(?\n),
100
102
  '%s' % subcommands,
101
103
  command.options,
102
104
  command.help,
103
105
  ]
104
106
  end
107
+ wrap_table_rows(rows, 4, headings: %w[ CMD SUBCMD OPTS HELP ], percentage: 95).each { |row| table << row }
105
108
  table
106
109
  end
107
110
  end
@@ -327,7 +327,7 @@ module OllamaChat::Compaction
327
327
  duration.minutes? ? '%M minutes and %u seconds' : '%u seconds'
328
328
  )
329
329
  speak(
330
- 'Compressed conversation context from %.f to %.f in %s.' % [
330
+ 'Compressed conversation context from %.0f percent to %.0f percent in %s.' % [
331
331
  100 * result.context_filled_before,
332
332
  100 * result.context_filled_after,
333
333
  duration_string,
@@ -86,7 +86,7 @@ module OllamaChat::Conversation
86
86
  selected.merge(remaining)
87
87
  break
88
88
  else
89
- selected.add(choice)
89
+ selected << choice
90
90
  end
91
91
  end
92
92
  end
@@ -51,7 +51,7 @@ class OllamaChat::Database::Models::Collection < Sequel::Model(OllamaChat::DB)
51
51
  #
52
52
  # @param chat [OllamaChat::Chat] the chat instance providing the configuration
53
53
  def self.sync(chat)
54
- all_collections = (%w[ default ] + chat.documents.collections).to_set
54
+ all_collections = (%w[ default ] + chat.documents.collections.to_a).to_set
55
55
  all_collections.each do |name|
56
56
  where(name: name.to_s).first and next
57
57
  create(
@@ -142,7 +142,7 @@ module OllamaChat::Dialog
142
142
  # distinguish the connection type, e.g. `"embedding "` for a dedicated
143
143
  # embedding host (default: `""`)
144
144
  def connect_message(base_url:, prefix: '')
145
- feedback green { "Connecting to #{prefix}ollama #{base_url.to_s.inspect}…" }, type: :info
145
+ feedback green { "Connecting to #{prefix}ollama #{redact_password(base_url).inspect}…" }, type: :info
146
146
  end
147
147
 
148
148
  # Displays a success-level feedback message confirming that a
@@ -61,7 +61,7 @@ module OllamaChat::HTTPHandling
61
61
  # @yield [IO] yields a temporary file handle for the caller to consume.
62
62
  # @return [Object] the result of the block or the fetcher.
63
63
  def get_url(url, headers: nil, cache: nil, remember: true, &block)
64
- remember and links.add(url.to_s)
64
+ remember and links << url.to_s
65
65
  headers = config.request_headers?.to_h | headers
66
66
  OllamaChat::Utils::Fetcher.get(
67
67
  url,
@@ -16,6 +16,7 @@
16
16
  # chat.usage
17
17
  module OllamaChat::Information
18
18
  extend Tins::Concern
19
+ include OllamaChat::Utils::Redact
19
20
 
20
21
  included do
21
22
  include UserAgent
@@ -190,7 +191,7 @@ module OllamaChat::Information
190
191
  def info_rag(output: STDOUT)
191
192
  if embedding.on?
192
193
  if base_url = OC::OLLAMA::EMBEDDING_URL?
193
- output.puts "šŸ”Œ Connected to embedding ollama server on: #{base_url.to_s.inspect}"
194
+ output.puts "šŸ”Œ Connected to embedding ollama server on: #{redact_password(base_url)}"
194
195
  end
195
196
  output.puts "šŸ—„ļø Current RAG model is #{bold{@embedding_model}}"
196
197
  if @embedding_model_options.present?
@@ -220,7 +221,7 @@ module OllamaChat::Information
220
221
  def print_welcome(output: STDOUT)
221
222
  feedback(<<~EOT, output:)
222
223
  šŸ’Ž Running ollama_chat version: #{bold{OllamaChat::VERSION}}
223
- šŸ”Œ Connected to ollama server version: #{bold{server_version}} on: #{bold{server_url}}
224
+ šŸ”Œ Connected to ollama server version: #{bold{server_version}} on: #{bold{redact_password(server_url)}}
224
225
  EOT
225
226
  end
226
227
 
@@ -1,30 +1,22 @@
1
- # A module that provides Kramdown::ANSI styling configuration and parsing
2
- # functionality for OllamaChat.
1
+ # A module that provides Kramdown::ANSI styling, parsing, and
2
+ # terminal table wrapping for OllamaChat.
3
3
  #
4
- # This module handles the setup and application of ANSI styles for markdown
5
- # rendering, allowing for customizable terminal output formatting. It manages
6
- # the configuration of ANSI styles either from environment variables or falls
7
- # back to default settings, and provides methods to parse content with the
8
- # configured styling.
4
+ # This module handles three concerns:
5
+ #
6
+ # * **Styling configuration** – sets up ANSI styles either from the
7
+ # `KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES` environment variable or from
8
+ # built-in defaults.
9
+ # * **Markdown parsing** – converts raw markdown into ANSI-formatted
10
+ # terminal output using the configured styles, with a fallback for
11
+ # invalid encodings.
12
+ # * **Table wrapping** – computes per-column width budgets and wraps
13
+ # cell content so that `Terminal::Table` output stays within a
14
+ # configurable percentage of the terminal width.
9
15
  #
10
16
  # @example Configuring custom ANSI styles via environment variable
11
17
  # Set KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES to a JSON object containing style
12
18
  # definitions for customizing markdown output formatting in the terminal.
13
19
  module OllamaChat::KramdownANSI
14
- # The configure_kramdown_ansi_styles method sets up ANSI styling for
15
- # Kramdown::ANSI output by checking for specific environment variables and
16
- # falling back to default styles.
17
- #
18
- # @return [ Hash ] a hash of ANSI styles configured either from environment
19
- # variables or using default settings
20
- def configure_kramdown_ansi_styles
21
- if json = OC::KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES?
22
- Kramdown::ANSI::Styles.from_json(json).ansi_styles
23
- else
24
- Kramdown::ANSI::Styles.new.ansi_styles
25
- end
26
- end
27
-
28
20
  # The kramdown_ansi_parse method processes content using Kramdown::ANSI with
29
21
  # custom ANSI styles.
30
22
  #
@@ -49,6 +41,22 @@ module OllamaChat::KramdownANSI
49
41
  retry
50
42
  end
51
43
 
44
+ private
45
+
46
+ # The configure_kramdown_ansi_styles method sets up ANSI styling for
47
+ # Kramdown::ANSI output by checking for specific environment variables and
48
+ # falling back to default styles.
49
+ #
50
+ # @return [ Hash ] a hash of ANSI styles configured either from environment
51
+ # variables or using default settings
52
+ def configure_kramdown_ansi_styles
53
+ if json = OC::KRAMDOWN_ANSI_OLLAMA_CHAT_STYLES?
54
+ Kramdown::ANSI::Styles.from_json(json).ansi_styles
55
+ else
56
+ Kramdown::ANSI::Styles.new.ansi_styles
57
+ end
58
+ end
59
+
52
60
  # The kramdown_markdown_remove method strips markdown formatting, emoji,
53
61
  # ANSI escape sequences, and box-drawing characters from content, yielding
54
62
  # plain text suitable for TTS playback.
@@ -74,4 +82,82 @@ module OllamaChat::KramdownANSI
74
82
  .gsub(/\s{2,}/, ' ')
75
83
  .strip
76
84
  end
85
+
86
+ # Wraps the cells of a terminal table to fit within a terminal-width
87
+ # budget.
88
+ #
89
+ # Terminal::Table sizes each column to its widest cell, so a single long
90
+ # cell forces the whole table wider than the terminal. This computes a
91
+ # per-column budget from the widest cell in each column across all rows,
92
+ # then wraps each cell to it. Cells expressed as Hashes (e.g. colspan
93
+ # rows) are passed through untouched.
94
+ #
95
+ # @param rows [Array<Array<String, Hash>>] the data rows to wrap
96
+ # @param cols [Integer] the number of columns in the table
97
+ # @param headings [Array<String>, nil] the heading row; when given it is
98
+ # measured for the budget and returned first, right-padded to each
99
+ # column's allocated width so Terminal::Table sizes columns to the
100
+ # full budget even when no data cell reaches it
101
+ # @param percentage [Numeric] the percentage of terminal width to use for
102
+ # the total budget (default: 90)
103
+ # @return [Array<Array<String, Hash>>] wrapped rows; the padded heading
104
+ # row is returned first when `headings` was supplied
105
+ def wrap_table_rows(rows, cols, headings: nil, percentage: 90)
106
+ all = rows + (headings ? [headings] : [])
107
+ budget = table_column_budgets(all, cols, percentage:)
108
+ wrap_row = ->(row) {
109
+ row.each_with_index.map { |cell, i|
110
+ cell.is_a?(Hash) and next cell
111
+ Kramdown::ANSI::Width.wrap(cell.to_s, length: budget[i], reflow: false)
112
+ }
113
+ }
114
+ result = rows.map(&wrap_row)
115
+ if headings
116
+ padded = headings.each_with_index.map { |h, i|
117
+ w = Kramdown::ANSI::Width.display_width(h)
118
+ w < budget[i] ? h + ' ' * (budget[i] - w) : h
119
+ }
120
+ result.unshift(padded)
121
+ end
122
+ result
123
+ end
124
+
125
+ # Computes a per-column display-width budget for a terminal table.
126
+ #
127
+ # @param rows [Array<Array<String, Hash>>] the rows to measure (Hash cells
128
+ # are ignored)
129
+ # @param cols [Integer] the number of columns
130
+ # @param percentage [Numeric] the percentage of terminal width to use for
131
+ # the total budget (default: 90)
132
+ # @return [Array<Integer>] the per-column width budget
133
+ def table_column_budgets(rows, cols, percentage: 90)
134
+ max_w = Array.new(cols, 0)
135
+ rows.each do |row|
136
+ row.each_with_index do |cell, i|
137
+ cell.is_a?(Hash) and next
138
+ w = cell.to_s.lines.map { |l| Kramdown::ANSI::Width.display_width(l) }.max || 0
139
+ max_w[i] = [max_w[i], w].max
140
+ end
141
+ end
142
+ total = max_w.sum
143
+ budget = Kramdown::ANSI::Width.width(percentage:) - (3 * cols + 1)
144
+ return Array.new(cols, (budget.to_f / cols).to_i) if total.zero?
145
+ if total <= budget
146
+ extra = budget - total
147
+ max_w.map { |w| w + ((w.to_f / total) * extra).floor }
148
+ else
149
+ share = (budget.to_f / cols).to_i
150
+ alloc = max_w.map { |w| [w, share].min }
151
+ unused = budget - alloc.sum
152
+ return alloc if unused.zero?
153
+ capped = (0...cols).to_a.select { |i| alloc[i] < max_w[i] }
154
+ need = capped.sum { |i| max_w[i] - alloc[i] }
155
+ return alloc if need.zero?
156
+ capped.each do |i|
157
+ extra = ((max_w[i] - alloc[i]).to_f / need * unused).floor
158
+ alloc[i] = [max_w[i], alloc[i] + extra].min
159
+ end
160
+ alloc
161
+ end
162
+ end
77
163
  end
@@ -23,6 +23,7 @@ class OllamaChat::LinksSet
23
23
  @set.add(x)
24
24
  sync
25
25
  end
26
+ alias << add
26
27
 
27
28
  # Removes all links from the set and synchronizes it with the session.
28
29
  #
@@ -465,7 +465,7 @@ module OllamaChat::ModelHandling
465
465
  if model_metadata = model_present?(model, ollama:)
466
466
  return model_metadata
467
467
  end
468
- raise OllamaChat::UnknownModelError, "unknown model named #{@model.inspect}"
468
+ raise OllamaChat::UnknownModelError, "unknown model named #{model.inspect}"
469
469
  end
470
470
  end
471
471
 
@@ -587,17 +587,25 @@ module OllamaChat::ModelHandling
587
587
  old_model = @model
588
588
 
589
589
  if model.blank?
590
- @model = choose_model('', @model)
590
+ new_model = choose_model('', old_model)
591
591
  else
592
- @model = choose_model(model, config.model.name)
592
+ new_model = choose_model(model, config.model.name)
593
593
  end
594
594
 
595
- if @model_metadata = model_present?(@model, ollama:)
596
- session.update(current_model: @model)
597
- else
598
- session.update(current_model: nil)
595
+ unless model_metadata = model_present?(new_model, ollama:)
596
+ feedback("Model #{bold{new_model}} not found locally.", type: :warn)
597
+ unless confirm?(prompt: "Pull it from remote? (y/n) ", yes: /\Ay/i)
598
+ feedback("Cancelled.", type: :cancel)
599
+ old_model.full? && model_present?(old_model, ollama:) or
600
+ raise OllamaChat::UnknownModelError, "no usable model after cancel"
601
+ return @model_metadata
602
+ end
603
+ model_metadata = pull_model_unless_present(new_model, ollama:)
599
604
  end
600
605
 
606
+ @model, @model_metadata = new_model, model_metadata
607
+ session.update(current_model: @model)
608
+
601
609
  if old_model != @model
602
610
  reconfigure_model_options(profile:, keep_options:)
603
611
  if @model_metadata&.can?('thinking')
@@ -122,7 +122,7 @@ module OllamaChat::RAGHandling
122
122
  selected.merge(remaining)
123
123
  break
124
124
  else
125
- selected.add(choice)
125
+ selected << choice
126
126
  end
127
127
  end
128
128
  end
@@ -202,7 +202,7 @@ module OllamaChat::RAGHandling
202
202
  selected.merge(remaining)
203
203
  break
204
204
  else
205
- selected.add(choice)
205
+ selected << choice
206
206
  end
207
207
  end
208
208
  end
@@ -213,14 +213,25 @@ module OllamaChat::RAGHandling
213
213
 
214
214
  feedback("Restoring #{selected.size} collection(s): #{selected.join(', ')}")
215
215
 
216
- total = 0
216
+ total = 0
217
+ skipped = 0
217
218
  collections.slice(*selected).sort.each do |col, group|
218
219
  create_memory_collection(col)
219
220
  switch_collection(col) do
221
+ existing_tags = Set.new
222
+ @documents.each_record do |r|
223
+ r.tags.each { |t| existing_tags << t.to_s(link: false) }
224
+ end
225
+
220
226
  group.each do |entry|
227
+ tags = Array(entry['tags'])
228
+ if tags.any? { |t| existing_tags.include?(t) }
229
+ skipped += 1
230
+ next
231
+ end
221
232
  @documents.add(
222
233
  [entry['text']],
223
- tags: Array(entry['tags']),
234
+ tags:,
224
235
  batch_size: 1,
225
236
  source: entry['source']
226
237
  )
@@ -230,9 +241,10 @@ module OllamaChat::RAGHandling
230
241
  end
231
242
 
232
243
  log(:info, 'Memory restored',
233
- data: { file: filename.to_s, collections: selected, records: total })
244
+ data: { file: filename.to_s, collections: selected, records: total, skipped: })
234
245
  feedback(
235
- "Restored #{total} record(s) across #{selected.size} collection(s) from #{filename.to_s.inspect}.",
246
+ "Restored #{total} record(s) across #{selected.size} collection(s) from #{filename.to_s.inspect}." +
247
+ (skipped.zero? ? '' : " (#{skipped} duplicate(s) skipped)."),
236
248
  type: :success
237
249
  )
238
250
  end
@@ -374,7 +386,7 @@ module OllamaChat::RAGHandling
374
386
  source_tags = Hash.new { |h, k| h[k] = Set.new }
375
387
  @documents.records.each do |record|
376
388
  source = record.source.full? or next
377
- sources.add(source)
389
+ sources << source
378
390
  source_tags[source].merge(record.tags)
379
391
  end
380
392
  display_map = {}
@@ -599,12 +611,12 @@ module OllamaChat::RAGHandling
599
611
  end
600
612
 
601
613
  sources = {}
602
- seen = {}
614
+ seen = Set.new
603
615
  @documents.each_record do |record|
604
616
  source = @documents.normalize_source(record.source) or next
605
617
  if matched_sources.nil? || matched_sources.member?(source)
606
- seen.key?(source) and next
607
- seen[source] = true
618
+ seen.member?(source) and next
619
+ seen << source
608
620
  unless @documents.source_modified?(source)
609
621
  infobar.puts "Source #{source.to_s.inspect} is unmodified. => Skipping."
610
622
  next
@@ -615,8 +627,8 @@ module OllamaChat::RAGHandling
615
627
  end
616
628
 
617
629
  if patterns = col.patterns.full?
618
- new_sources = all_file_set(patterns).map(&:to_s).reject { seen.key?(_1) }.to_set
619
- new_sources.each { seen[_1] = true }
630
+ new_sources = all_file_set(patterns).map(&:to_s).reject { seen.member?(_1) }.to_set
631
+ new_sources.each { seen << _1 }
620
632
  new_sources.each { sources[_1] = [] }
621
633
  end
622
634
 
@@ -107,22 +107,23 @@ module OllamaChat::SessionManagement
107
107
  all_separators: true,
108
108
  border: :unicode_round,
109
109
  }
110
- table.headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |header| bold { header } }
111
- now = Time.now
110
+ headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |h| bold { h } }
111
+ now = Time.now
112
+ rows = []
112
113
  models::Session.order(Sequel.desc(:updated_at)).each do |s|
113
- name = truncate(s.name, length: 32)
114
- name = session.id == s.id ? bold { name } : name
115
- name = if pid = s.locked?
116
- if pid == $$
117
- "#{name} šŸ”“"
118
- else
119
- "#{name} šŸ”"
120
- end
121
- else
122
- name
123
- end
124
- es = s.estimate_tokens
125
- table << [
114
+ name = s.name
115
+ name = session.id == s.id ? bold { name } : name
116
+ name = if pid = s.locked?
117
+ if pid == $$
118
+ "#{name} šŸ”“"
119
+ else
120
+ "#{name} šŸ”"
121
+ end
122
+ else
123
+ name
124
+ end
125
+ es = s.estimate_tokens
126
+ rows << [
126
127
  s.id.to_s,
127
128
  name,
128
129
  es.bytes_formatted,
@@ -131,6 +132,7 @@ module OllamaChat::SessionManagement
131
132
  s.age(now:),
132
133
  ]
133
134
  end
135
+ wrap_table_rows(rows, 6, headings:).each { |row| table << row }
134
136
  table.align_column 0, :right
135
137
  table.align_column 2, :left
136
138
  table.align_column 3, :right
@@ -33,6 +33,7 @@ module OllamaChat::SourceFetching
33
33
  #
34
34
  # @yield [ tmp ]
35
35
  def fetch_source(source, check_exist: false, &block)
36
+ confirm_notification = false
36
37
  source = source.ask_and_send_or_self(:to_path).to_s
37
38
  case source
38
39
  when %r{\A!(.*)}
@@ -72,14 +73,28 @@ module OllamaChat::SourceFetching
72
73
  else
73
74
  raise "invalid source #{source.inspect}"
74
75
  end
76
+ rescue OllamaChat::HTTPError => e
77
+ msg = "Fetching source #{source.to_s.inspect}: #{e.class} #{e}"
78
+ feedback(msg, type: :warn)
79
+ log(:warn, msg, data: { source: source.to_s, status: e.status })
80
+ confirm_notification = true
81
+ msg = OllamaChat::Utils::Fetcher::ResponseMetadata.failed(msg)
82
+ msg.response_status = e.status
83
+ msg.response_reason = e.message
84
+ block.(msg)
85
+ msg
75
86
  rescue => e
76
87
  msg = "Fetching source #{source.to_s.inspect}: #{e.class} #{e}"
77
88
  feedback("#{msg}\n#{e.backtrace * ?\n}", type: :warn)
78
89
  log(:error, msg, data: { source: source.to_s, error: e.class })
79
- confirm?(prompt: 'āŽ Press any key to continue (%s). ', output: STDERR, timeout: 3)
90
+ confirm_notification = true
80
91
  msg = OllamaChat::Utils::Fetcher::ResponseMetadata.failed(msg)
81
92
  block.(msg)
82
93
  msg
94
+ ensure
95
+ if confirm_notification
96
+ confirm?(prompt: "\nāŽ Press any key to continue (%s). ", output: STDERR, timeout: 3)
97
+ end
83
98
  end
84
99
 
85
100
  # Adds an image to the images collection from the given source IO and source
@@ -60,10 +60,10 @@ class OllamaChat::Tools::Browse
60
60
  result = browse_url(url)
61
61
 
62
62
  message = if result.success?
63
- "Opened #{url.to_s.inspect} in browser."
64
- else
65
- "Could not open #{url.to_s.inspect} in browser."
66
- end
63
+ "Opened #{redact_password(url).inspect} in browser."
64
+ else
65
+ "Could not open #{redact_password(url).inspect} in browser."
66
+ end
67
67
 
68
68
  {
69
69
  success: result.success?,
@@ -7,6 +7,7 @@
7
7
  module OllamaChat::Tools::Concern
8
8
  extend Tins::Concern
9
9
  include OllamaChat::Utils::ValueFormatter
10
+ include OllamaChat::Utils::Redact
10
11
  include OllamaChat::Utils::Backup
11
12
  include OllamaChat::Feedback
12
13