ollama_chat 0.0.107 → 0.0.109

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +118 -0
  3. data/Rakefile +1 -1
  4. data/lib/ollama_chat/chat.rb +7 -4
  5. data/lib/ollama_chat/clipboard.rb +11 -0
  6. data/lib/ollama_chat/commands.rb +32 -11
  7. data/lib/ollama_chat/database/migrations/008_drop_think_mode_constraint.rb +130 -0
  8. data/lib/ollama_chat/dialog.rb +24 -17
  9. data/lib/ollama_chat/file_editing.rb +15 -13
  10. data/lib/ollama_chat/follow_chat.rb +34 -15
  11. data/lib/ollama_chat/kramdown_ansi.rb +1 -1
  12. data/lib/ollama_chat/model_handling.rb +119 -1
  13. data/lib/ollama_chat/ollama_chat_config/default_config.yml +11 -5
  14. data/lib/ollama_chat/personae_management.rb +22 -18
  15. data/lib/ollama_chat/prompt_handling.rb +9 -9
  16. data/lib/ollama_chat/prompt_management.rb +7 -5
  17. data/lib/ollama_chat/rag_handling.rb +27 -23
  18. data/lib/ollama_chat/session_management.rb +21 -5
  19. data/lib/ollama_chat/think_control.rb +4 -1
  20. data/lib/ollama_chat/tools/patch_file.rb +5 -1
  21. data/lib/ollama_chat/tools/search_knowledge.rb +3 -0
  22. data/lib/ollama_chat/version.rb +1 -1
  23. data/lib/ollama_chat/web_searching.rb +6 -4
  24. data/lib/ollama_chat.rb +9 -0
  25. data/ollama_chat.gemspec +5 -5
  26. data/spec/ollama_chat/chat_spec.rb +24 -0
  27. data/spec/ollama_chat/commands_spec.rb +24 -0
  28. data/spec/ollama_chat/config_handling_spec.rb +150 -0
  29. data/spec/ollama_chat/conversation_spec.rb +107 -0
  30. data/spec/ollama_chat/favourites_management_spec.rb +143 -0
  31. data/spec/ollama_chat/follow_chat_spec.rb +1 -2
  32. data/spec/ollama_chat/information_spec.rb +36 -2
  33. data/spec/ollama_chat/model_handling_spec.rb +139 -0
  34. data/spec/ollama_chat/prompt_handling_spec.rb +183 -0
  35. data/spec/ollama_chat/prompt_management_spec.rb +464 -0
  36. data/spec/ollama_chat/rag_handling_spec.rb +498 -0
  37. data/spec/ollama_chat/session_management_spec.rb +5 -5
  38. data/spec/ollama_chat/system_prompt_management_spec.rb +200 -0
  39. data/spec/ollama_chat/think_control_spec.rb +2 -2
  40. data/spec/ollama_chat/tools/execute_ri_spec.rb +2 -2
  41. data/spec/ollama_chat/tools/generate_image_spec.rb +6 -6
  42. data/spec/ollama_chat/tools/get_url_spec.rb +3 -3
  43. data/spec/ollama_chat/tools/move_file_spec.rb +1 -1
  44. data/spec/ollama_chat/tools/patch_file_spec.rb +3 -3
  45. data/spec/ollama_chat/tools/run_tests_spec.rb +3 -3
  46. data/spec/ollama_chat/tools/search_knowledge_spec.rb +28 -5
  47. data/spec/ollama_chat/utils/path_completer_spec.rb +1 -1
  48. data/spec/ollama_chat/utils/png_metadata_extractor_spec.rb +3 -3
  49. data/spec/ollama_chat/web_searching_spec.rb +99 -21
  50. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2686defd34fe5cf4e0f61c980480578f6b2589bc429d7b724ed4f3cb478d7b8
4
- data.tar.gz: 8bd9136e0e6f3e202c93f8c4aec0a4b482a447bfd1e29163b0dff1973e64f2fa
3
+ metadata.gz: e6c85b95f4c5051b6b0f257dd8f63ee2a3997146c527fd8af251458fee641039
4
+ data.tar.gz: 45a66e25a1a92d8cbd86253a991ad28eeb48d1a8ca68c1f8bbb8df90bbbba37f
5
5
  SHA512:
6
- metadata.gz: 1a177889fb72f197a05895b60c7e73f1493a119fcc7832354772097f121d53b150ea500fdf50ae81bdeb05161f0a3121af204496d54a708a13c427096e126159
7
- data.tar.gz: 55c0270902a86e39e1704d62a4478daf0d04526312d47eb35f560b3b95218045d6699dd428b874fa9059ce3af8585bed97a5c2bd662751ef05885baa452a9c6b
6
+ metadata.gz: a401173a7deb626af01ad8c9adf358259a6ffd7ea292644bb1a5a4553df2f70e9c74bb4ff0807d04914a7aa47e8140e81e0bd46745e33900962d8559950ea914
7
+ data.tar.gz: 6b04fcce611684e73bbe54cdadb01859fc4219f57960604a64c3037b414e56a02c7389a5bfef2341d0a2aa2a44d8a723761469f9c677ec8784c2c9d9e8b6fae0
data/CHANGES.md CHANGED
@@ -1,5 +1,123 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-08-18 v0.0.109
4
+
5
+ ### Added
6
+
7
+ * Support for exporting and importing model options via the `/model` command:
8
+ * New `export_model_options` method in `model_handling.rb` to export
9
+ stored model profiles to a JSON file.
10
+ * New `import_model_options` method in `model_handling.rb` to import
11
+ profiles from a JSON file, featuring selective model picking, diff
12
+ display, and override confirmation.
13
+ * Extended `/model` command with `options export` and `options import`
14
+ subcommands.
15
+ * `-p` pattern flag for `/model options import` to narrow file search, with a
16
+ fallback to `**/*.json`.
17
+ * `-i` flag for `/paste` command to read input from stdin:
18
+ * New `paste_from_stdin` method in `clipboard.rb`.
19
+ * Updated `/paste` command regex and parsing to support both `-e` and
20
+ `-i` flags.
21
+ * `options` metadata to command definitions for `/copy`, `/paste`, `/toggle`,
22
+ and `/model`.
23
+ * Comprehensive test coverage for previously untested or under-tested
24
+ modules:
25
+ * `spec/ollama_chat/conversation_spec.rb` (9 examples).
26
+ * `spec/ollama_chat/favourites_management_spec.rb` (15 examples).
27
+ * `spec/ollama_chat/rag_handling_spec.rb` covering
28
+ `OllamaChat::RAGHandling`.
29
+ * `spec/ollama_chat/prompt_handling_spec.rb` (19 examples).
30
+ * `spec/ollama_chat/prompt_management_spec.rb` (47 examples).
31
+ * `spec/ollama_chat/system_prompt_management_spec.rb` (20 examples).
32
+ * `spec/ollama_chat/config_handling_spec.rb` (15 examples).
33
+ * `spec/ollama_chat/web_searching_spec.rb` (8 examples).
34
+ * `spec/ollama_chat/information_spec.rb` (5 examples).
35
+
36
+ ### Changed
37
+
38
+ * Refactored session management to extract and reuse
39
+ `set_previous_session_on_change` as a shared private method in
40
+ `lib/ollama_chat/session_management.rb`.
41
+ * `set_new_session`, `duplicate_session`, and `change_session` now
42
+ delegate to this method for consistent `@previous_session_id` handling.
43
+ * Migrated test stubs from `allow` to `expect` across multiple spec files to
44
+ eliminate false-green tests where stubs were never invoked.
45
+ * Normalized `context` and `name` arguments in `prompt_handling.rb` to handle
46
+ `Symbol` inputs safely in SQL queries.
47
+ * Updated `s.files` and `s.test_files` in `ollama_chat.gemspec` to include
48
+ new configuration, asset, and test files.
49
+
50
+ ### Fixed
51
+
52
+ * Infinite retry loop in `kramdown_ansi.rb` by using `retried ||= false` to
53
+ persist the guard across `retry` calls.
54
+ * Incorrect YARD `@return` documentation for `#info_prompt` in
55
+ `prompt_management.rb`.
56
+
57
+ ## 2026-08-15 v0.0.108
58
+
59
+ ### Added
60
+
61
+ - Added `require 'io/console'` to support `IO#raw` for key reads.
62
+ - Added `max` to `THINK_MODE_STATES` in `think_control.rb` to support newer
63
+ Ollama releases (e.g., **0.32.13**).
64
+ - Added migration `008_drop_think_mode_constraint.rb` to drop the `think_mode`
65
+ `CHECK` constraint in SQLite.
66
+ - Added `.obsidian` to `.gitignore` and the `GemHadar` `ignore` list in
67
+ `Rakefile`.
68
+ - Added `require_confirmation: true` to `get_current_weather`, `get_cve`,
69
+ `get_endoflife`, `browse`, `get_jira_issue`, `get_rfc`, and `get_ghr`.
70
+
71
+ ### Changed
72
+
73
+ - Refactored `confirm?` key reads to use `STDIN.raw(min:, time:, intr: true)`
74
+ instead of `stty` commands.
75
+ - Updated `confirm?` to print prompts to `output` instead of `STDOUT`.
76
+ - Updated glyphs for confirmation: `\u2328\uFE0F` to `\u2328`, `☑️` to `\u2611`,
77
+ and `🚫` to `\u{1F6AB}`.
78
+ - Scoped `Reline` histories for various interactive prompts:
79
+ - `duplicate_session` name prompt now scoped to `:session_name`.
80
+ - `determine_valid_new_name_for_persona` now scoped to `:persona_name`.
81
+ - `add_persona` `[FILES]` patterns prompt now scoped to `:patterns`.
82
+ - `add_new_prompt` `[FILES]` patterns prompt now scoped to `:patterns`.
83
+ - `determine_valid_output_filename` loop now scoped to `:filename`.
84
+ - `/session summarize -f` filename prompt now scoped to `:filename`.
85
+ - `choose_profile_for_model` profile name prompt now scoped to
86
+ `:profile_name`.
87
+ - Scoped `Reline` histories for RAG prompts:
88
+ - `rename_collection` now scopes to `:collection_name`.
89
+ - `create_collection` scopes to `:collection_name`,
90
+ `:collection_description`, and `:patterns`.
91
+ - `edit_collection` description prompt now uses `:collection_description`.
92
+ - Refactored terminal truncation by moving `truncate_for_terminal` from
93
+ `prepare_last_message` to `last_message_with_user`.
94
+ - Added `height:` keyword argument to `last_message_with_user` and
95
+ `display_formatted_terminal_output`.
96
+ - Refactored `display_formatted_terminal_output` to use `move_home` and
97
+ `erase_in_display(nil)` instead of `clear_screen` to prevent `tmux` flicker.
98
+ - Updated `require_confirmation` to `false` for `open_file_in_editor`,
99
+ `paste_into_editor`, and `generate_image`.
100
+ - Removed `jq` parsing from `OLLAMA_CHAT_TOOLS_TEST_RUNNER` in `.envrc`.
101
+ - Introduced `OLLAMA_CHAT_COLLECTION_NAME_REGEXP` in `lib/ollama_chat.rb` to
102
+ enforce safe identifiers.
103
+ - Updated `OLLAMA_CHAT::Chat#initial_collection` to validate names against the
104
+ new regex.
105
+ - Wrapped web search link list output in `use_pager` within
106
+ `OLLAMA_CHAT::WebSearching`.
107
+
108
+ ### Fixed
109
+ - Added `rescue` for `Interrupt` and `IRB::Abort` during key reads to return
110
+ `nil` and print `"\u274C\uFE0F"`.
111
+ - Fixed `tmux` flicker in vertically split panes by avoiding full-pane redraws.
112
+ - Fixed `OLLAMA_CHAT::Tools::PatchFile` to explicitly warn against redundant
113
+ tool calls.
114
+
115
+ ### Refactored
116
+ - Updated YARD `@param output` and `@return` documentation.
117
+ - Simplified `last_message_with_user` to return a single joined string.
118
+ - Simplified `OLLAMA_CHAT::Tools::SearchKnowledge` to raise
119
+ `OLLAMA_CHAT::ToolFunctionArgumentError` on invalid input.
120
+
3
121
  ## 2026-08-09 v0.0.107
4
122
 
5
123
  ### Added
data/Rakefile CHANGED
@@ -20,7 +20,7 @@ GemHadar do
20
20
  test_dir 'spec'
21
21
  ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock', '.AppleDouble', '.bundle',
22
22
  '.yardoc', 'doc', 'tags', 'corpus', 'coverage', '/config/searxng/*',
23
- '.starscope.db', 'cscope.out', 'errors.lst', 'tmp/*'
23
+ '.starscope.db', 'cscope.out', 'errors.lst', 'tmp/*', '.obsidian'
24
24
  package_ignore '.all_images.yml', '.tool-versions', '.gitignore', 'VERSION',
25
25
  '.rspec', '.github', '.contexts', '.envrc', '.yardopts'
26
26
 
@@ -229,11 +229,14 @@ class OllamaChat::Chat
229
229
  #
230
230
  # @return [ Symbol ] the collection name symbol
231
231
  def initial_collection
232
- (
233
- session&.current_collection.full? ||
232
+ c = session&.current_collection.full? ||
234
233
  config.embedding.collection.full? ||
235
- :default
236
- ).to_sym
234
+ 'default'
235
+ if c =~ /\A#{OllamaChat::COLLECTION_NAME_REGEXP.source}\z/
236
+ c
237
+ else
238
+ 'default'
239
+ end.to_sym
237
240
  end
238
241
 
239
242
  # The initial_system_prompt method returns the system prompt for the initial
@@ -143,4 +143,15 @@ module OllamaChat::Clipboard
143
143
  log(:error, e, data: { method: __method__ })
144
144
  STDERR.puts e.message
145
145
  end
146
+
147
+ # Read the full text from STDIN and return it.
148
+ #
149
+ # @param edit [Boolean] If true, the text is passed through
150
+ # {#edit_text} before being returned.
151
+ # @return [String] The raw (or edited) content read from STDIN.
152
+ def paste_from_stdin(edit: false)
153
+ text = STDIN.read
154
+ edit and text = edit_text(text)
155
+ text
156
+ end
146
157
  end
@@ -27,6 +27,7 @@ module OllamaChat::Commands
27
27
  command(
28
28
  name: :copy,
29
29
  regexp: %r(^/copy(\s+-e)?\s*$),
30
+ options: '[-e]',
30
31
  help: <<~EOT
31
32
  📋 Copy the last response to the clipboard.
32
33
  Options: -e to edit before copying.
@@ -39,15 +40,22 @@ module OllamaChat::Commands
39
40
 
40
41
  command(
41
42
  name: :paste,
42
- regexp: %r(^/paste(\s+-e)?\s*$),
43
+ regexp: %r(^/paste(\s+-[ie])*\s*$),
44
+ options: '[-e|-i]',
43
45
  help: <<~EOT
44
- 📋 Paste content from the clipboard.
45
- Options: -e to edit after pasting.
46
+ 📋 Paste content from the clipboard or stdin.
47
+ Options:
48
+ -e Edit after pasting.
49
+ -i Read from stdin instead of clipboard.
46
50
  EOT
47
51
  ) do |opts|
48
52
  disable_content_parsing
49
- opts = go_command('e', opts)
50
- paste_from_clipboard(edit: opts[?e])
53
+ opts = go_command('ie', opts)
54
+ if opts[?i]
55
+ paste_from_stdin(edit: opts[?e])
56
+ else
57
+ paste_from_clipboard(edit: opts[?e])
58
+ end
51
59
  end
52
60
 
53
61
  ## Settings
@@ -88,6 +96,7 @@ module OllamaChat::Commands
88
96
  name: :toggle,
89
97
  regexp: %r(^/toggle(?:\s+(markdown|stream|location|runtime_info|voice|think_loud|think_strip|embedding)(?:\s+(-[yn]))?)?$),
90
98
  complete: [ 'toggle', %w[ markdown stream location runtime_info voice think_loud think_strip embedding ] ],
99
+ options: '[-y|-n]',
91
100
  help: <<~EOT
92
101
  🎛️ Toggle feature switches
93
102
  (markdown, stream, location, runtime_info,
@@ -139,17 +148,21 @@ module OllamaChat::Commands
139
148
 
140
149
  command(
141
150
  name: :model,
142
- regexp: %r(^/model(?:\s+(change|options(?: copy| delete)?|options from session|options to session))?((?:\s+(?:-m))*)$),
143
- complete: [ 'model', %w[ change options options\ copy options\ delete options\ from\ session options\ to\ session ] ],
151
+ regexp: %r(^/model(?:\s+(change|options(?: copy| delete| export| import)?|options from session|options to session))?((?:\s+(?:-m|-p\s+[\S]+))*)$),
152
+ complete: [ 'model', %w[ change options options\ copy options\ delete options\ export options\ import options\ from\ session options\ to\ session ] ],
153
+ options: '[-m|-p pattern]',
144
154
  help: <<~EOT
145
155
  🤖 Manage AI models & profiles:
146
156
  - change: Switch active model
147
157
  - options: Edit saved profile config
148
158
  - options copy: Copy profile from another model
149
159
  - options delete: Delete a saved profile
160
+ - options export: Export ALL model profiles to JSON
161
+ - options import: Selectively import profiles from JSON
150
162
  - options from session: Save live → Saved
151
163
  - options to session: Apply Saved → Live
152
164
  -m interactively choose a model
165
+ -p PATTERN narrow file search for import
153
166
  EOT
154
167
  ) do |subcommand, opts|
155
168
  if subcommand == 'change'
@@ -163,7 +176,7 @@ module OllamaChat::Commands
163
176
  end
164
177
  next :next
165
178
  end
166
- opts = go_command('m', opts)
179
+ opts = go_command('mp:', opts)
167
180
  model = opts[?m] ? choose_model('', @model) : @model
168
181
  case subcommand
169
182
  when 'options'
@@ -179,6 +192,12 @@ module OllamaChat::Commands
179
192
  when 'options to session'
180
193
  profile = choose_profile_for_model(model) || 'default'
181
194
  copy_model_options_to_session(model, profile:)
195
+ when 'options export'
196
+ export_model_options
197
+ when 'options import'
198
+ pattern = opts[?p] || '**/*.json'
199
+ filename = choose_filename(pattern) or next :next
200
+ import_model_options(filename)
182
201
  end
183
202
  :next
184
203
  end
@@ -296,8 +315,10 @@ module OllamaChat::Commands
296
315
  when 'summarize'
297
316
  opts = go_command('fs', opts)
298
317
  if opts[?f] and
299
- filename = ask?(prompt: "❓ Enter filename: ").full? { Pathname.new(_1) }
300
- then
318
+ filename = switch_history(:filename) {
319
+ ask?(prompt: "❓ Enter filename: ")
320
+ }.full? { Pathname.new(_1) }
321
+ then
301
322
  if filename.exist? && !confirm?(
302
323
  prompt: "🔔 File #{filename.to_s.inspect} already exists, overwrite? (y/n) ",
303
324
  yes: /\Ay/i
@@ -807,7 +828,7 @@ module OllamaChat::Commands
807
828
 
808
829
  command(
809
830
  name: :input,
810
- regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[apre]|c\s*\w+|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
831
+ regexp: %r(^/input(?:\s+(path|context|embedding|summary)(?:\s*(?=\z))?)?((?:\s+-(?:[apre]|c\s*#{OllamaChat::COLLECTION_NAME_REGEXP.source}|w\s*\d+|t\s*[-\w\.]+(?:,[-\w\.]+)*))*)(?:\s+([^-].*))?$),
811
832
  optional: true,
812
833
  complete: [ 'input', %w[ path context embedding summary ] ],
813
834
  options: <<~EOT,
@@ -0,0 +1,130 @@
1
+ # Removes the `think_mode` CHECK constraint from the `sessions` table.
2
+ #
3
+ # The set of valid think modes is model-dependent and evolving upstream
4
+ # (e.g., `max` was only added mid-2026, and numeric token budgets are
5
+ # under discussion), so the schema should not pin a fixed vocabulary.
6
+ # State validation is handled in Ruby via
7
+ # `OllamaChat::ThinkControl::THINK_MODE_STATES` and the state selectors.
8
+ #
9
+ # SQLite cannot alter CHECK constraints in place, so the table is rebuilt
10
+ # using the same shadow-table pattern as
11
+ # `004_add_profile_to_model_options.rb`.
12
+ Sequel.migration do
13
+ up do
14
+ create_table(:sessions_new) do
15
+ primary_key :id
16
+ String :name, null: false
17
+ String :current_model
18
+ String :current_collection
19
+ String :default_persona_name
20
+ String :current_system_prompt
21
+ Boolean :tools_enabled, null: false
22
+ Text :tools_default_enabled
23
+ String :think_mode, null: false
24
+ Bool :think_loud_enabled, null: false
25
+ Bool :embedding_enabled, null: false
26
+ String :document_policy, null: false
27
+ Bool :runtime_info_enabled, null: false
28
+ Bool :think_strip_enabled, null: false
29
+ Boolean :markdown_enabled, null: false
30
+ Boolean :stream_enabled, null: false
31
+ Boolean :location_enabled, null: false
32
+ Boolean :voice_enabled, null: false
33
+ String :current_voice, null: false
34
+ Text :working_directory
35
+ Integer :locked_by_pid
36
+ Text :model_options
37
+ Text :messages, null: false
38
+ Text :links, null: false, default: ''
39
+ Text :history, null: false, default: ''
40
+ String :context_format, null: false, default: 'JSON'
41
+ Time :created_at
42
+ Time :updated_at
43
+ unique [:name]
44
+
45
+ constraint(
46
+ :document_policy,
47
+ document_policy: %w[ ignoring embedding importing summarizing ]
48
+ )
49
+ end
50
+
51
+ source = from(:sessions).select(
52
+ :id, :name, :current_model, :current_collection,
53
+ :default_persona_name, :current_system_prompt,
54
+ :tools_enabled, :tools_default_enabled, :think_mode,
55
+ :think_loud_enabled, :embedding_enabled, :document_policy,
56
+ :runtime_info_enabled, :think_strip_enabled,
57
+ :markdown_enabled, :stream_enabled, :location_enabled,
58
+ :voice_enabled, :current_voice, :working_directory,
59
+ :locked_by_pid, :model_options, :messages, :links,
60
+ :history, :context_format, :created_at, :updated_at
61
+ )
62
+ from(:sessions_new).insert(source)
63
+
64
+ drop_table(:sessions)
65
+ rename_table(:sessions_new, :sessions)
66
+ end
67
+
68
+ down do
69
+ # Restores the original `think_mode` constraint. All rows that
70
+ # predate the up-migration satisfy it; any newer value (e.g. `max`)
71
+ # would fail the insert, which is the desired loud failure on
72
+ # rollback.
73
+ create_table(:sessions_old) do
74
+ primary_key :id
75
+ String :name, null: false
76
+ String :current_model
77
+ String :current_collection
78
+ String :default_persona_name
79
+ String :current_system_prompt
80
+ Boolean :tools_enabled, null: false
81
+ Text :tools_default_enabled
82
+ String :think_mode, null: false
83
+ Bool :think_loud_enabled, null: false
84
+ Bool :embedding_enabled, null: false
85
+ String :document_policy, null: false
86
+ Bool :runtime_info_enabled, null: false
87
+ Bool :think_strip_enabled, null: false
88
+ Boolean :markdown_enabled, null: false
89
+ Boolean :stream_enabled, null: false
90
+ Boolean :location_enabled, null: false
91
+ Boolean :voice_enabled, null: false
92
+ String :current_voice, null: false
93
+ Text :working_directory
94
+ Integer :locked_by_pid
95
+ Text :model_options
96
+ Text :messages, null: false
97
+ Text :links, null: false, default: ''
98
+ Text :history, null: false, default: ''
99
+ String :context_format, null: false, default: 'JSON'
100
+ Time :created_at
101
+ Time :updated_at
102
+ unique [:name]
103
+
104
+ constraint(
105
+ :think_mode,
106
+ think_mode: %w[ disabled enabled low medium high ]
107
+ )
108
+ constraint(
109
+ :document_policy,
110
+ document_policy: %w[ ignoring embedding importing summarizing ]
111
+ )
112
+ end
113
+
114
+ source = from(:sessions).select(
115
+ :id, :name, :current_model, :current_collection,
116
+ :default_persona_name, :current_system_prompt,
117
+ :tools_enabled, :tools_default_enabled, :think_mode,
118
+ :think_loud_enabled, :embedding_enabled, :document_policy,
119
+ :runtime_info_enabled, :think_strip_enabled,
120
+ :markdown_enabled, :stream_enabled, :location_enabled,
121
+ :voice_enabled, :current_voice, :working_directory,
122
+ :locked_by_pid, :model_options, :messages, :links,
123
+ :history, :context_format, :created_at, :updated_at
124
+ )
125
+ from(:sessions_old).insert(source)
126
+
127
+ drop_table(:sessions)
128
+ rename_table(:sessions_old, :sessions)
129
+ end
130
+ end
@@ -1,3 +1,5 @@
1
+ require 'io/console'
2
+
1
3
  # A module that provides interactive selection and configuration functionality
2
4
  # for OllamaChat.
3
5
  #
@@ -35,27 +37,32 @@ module OllamaChat::Dialog
35
37
  # @param default [Object, nil] value returned when the timeout expires
36
38
  # (defaults to `nil`)
37
39
  # @param yes [Object, nil] value that is considered a positive response
38
- # @param output [IO] the IO object to write the prompt to
40
+ # @param output [IO] the IO object to write the prompt and the answer to
39
41
  #
40
- # @return [Object] the character entered by the user, or the `default` value
41
- # if a timeout occurs
42
+ # @return [Object] the character entered by the user, the `default` value
43
+ # if a timeout occurs, or `nil` if the read is interrupted (e.g. Ctrl-C)
42
44
  def confirm?(prompt:, timeout: nil, default: nil, yes: nil, output: STDOUT)
43
45
  return default if timeout&.zero?
44
46
  if prompt.include?('%s')
45
47
  prompt = prompt % (timeout ? ('timeout in %us' % timeout) : 'no timeout')
46
48
  end
47
- print prompt
48
- system 'stty raw'
49
- keypress = nil
50
- c = if timeout
51
- keypress = !!IO.select([ STDIN ], nil, nil, timeout)
52
- keypress ? STDIN.getc : nil
53
- else
54
- keypress = true
55
- STDIN.getc
56
- end
57
- system 'stty cooked'
58
- answer = c || default
49
+ output.print prompt
50
+ min = 1
51
+ time = 0
52
+ if timeout
53
+ min = 0
54
+ time = timeout
55
+ end
56
+ exceptions = [ (IRB::Abort if defined? IRB), Interrupt ].compact
57
+ begin
58
+ keypress = STDIN.raw(min:, time:, intr: true) do |io|
59
+ io.getc
60
+ end
61
+ rescue *exceptions
62
+ output.puts "\u274C\uFE0F"
63
+ return
64
+ end
65
+ answer = keypress || default
59
66
  case
60
67
  when yes.nil?
61
68
  if keypress
@@ -68,12 +75,12 @@ module OllamaChat::Dialog
68
75
  if keypress
69
76
  output.puts "\u2705\uFE0F #{answer}"
70
77
  else
71
- output.puts "☑️ #{answer}"
78
+ output.puts "\u2611\uFE0F #{answer}"
72
79
  end
73
80
  answer
74
81
  else
75
82
  if keypress
76
- output.puts "🚫 #{answer}"
83
+ output.puts "\u{1F6AB} #{answer}"
77
84
  else
78
85
  output.puts "\u231B\uFE0F #{answer}"
79
86
  end
@@ -111,20 +111,22 @@ module OllamaChat::FileEditing
111
111
  # @return [Pathname, nil] the validated filename as a Pathname, or nil if the
112
112
  # operation was cancelled
113
113
  def determine_valid_output_filename(action)
114
- loop do
115
- filename_str = ask?(
116
- prompt: "❓ Enter filename #{action}, C-c ⇒ cancel: "
117
- )
118
- if filename_str.nil?
119
- STDOUT.puts "Cancelled."
120
- return nil
121
- end
114
+ switch_history(:filename) do
115
+ loop do
116
+ filename_str = ask?(
117
+ prompt: "❓ Enter filename #{action}, C-c ⇒ cancel: "
118
+ )
119
+ if filename_str.nil?
120
+ STDOUT.puts "Cancelled."
121
+ return nil
122
+ end
122
123
 
123
- filename = Pathname.new(filename_str)
124
- if filename.exist?
125
- STDERR.puts "File #{filename.to_path.inspect} already exists!"
126
- else
127
- return filename
124
+ filename = Pathname.new(filename_str)
125
+ if filename.exist?
126
+ STDERR.puts "File #{filename.to_path.inspect} already exists!"
127
+ else
128
+ return filename
129
+ end
128
130
  end
129
131
  end
130
132
  end
@@ -39,6 +39,9 @@ class OllamaChat::FollowChat
39
39
  end
40
40
  end
41
41
 
42
+ # Returns the chat object representing the conversation context.
43
+ #
44
+ # @return [OllamaChat::Chat] The chat object.
42
45
  attr_reader :chat
43
46
 
44
47
  # Returns the conversation history (an array of message objects).
@@ -74,7 +77,7 @@ class OllamaChat::FollowChat
74
77
  ensure_assistant_response_exists
75
78
  update_last_message(response)
76
79
  if chat.stream.on?
77
- display_formatted_terminal_output
80
+ display_formatted_terminal_output(height: Tins::Terminal.lines - 1)
78
81
  else
79
82
  if display_output
80
83
  display_formatted_terminal_output
@@ -293,9 +296,9 @@ class OllamaChat::FollowChat
293
296
  def prepare_last_message
294
297
  content, thinking = @messages.last.content, @messages.last.thinking
295
298
  if chat.markdown.on?
296
- content = talk_annotate { truncate_for_terminal chat.kramdown_ansi_parse(content) }
299
+ content = talk_annotate { chat.kramdown_ansi_parse(content) }
297
300
  if chat.think?
298
- thinking = think_annotate { truncate_for_terminal chat.kramdown_ansi_parse(thinking) }
301
+ thinking = think_annotate { chat.kramdown_ansi_parse(thinking) }
299
302
  end
300
303
  else
301
304
  content = talk_annotate { content }
@@ -304,20 +307,28 @@ class OllamaChat::FollowChat
304
307
  return content&.chomp, thinking&.chomp
305
308
  end
306
309
 
307
- # The last_message_with_user method constructs a formatted message array by
310
+ # The last_message_with_user method constructs a formatted string by
308
311
  # combining user information, newline characters, thinking annotations, and
309
312
  # content for display in the terminal output.
310
313
  #
311
- # @return [ Array ] an array containing the user identifier, newline
312
- # character, thinking annotation (if present), and content formatted for
313
- # terminal display
314
- def last_message_with_user
314
+ # @param height [Integer] the maximum number of lines to truncate the output
315
+ # to. Defaults to 0 (no truncation).
316
+ #
317
+ # @return [String] the formatted message string containing the user
318
+ # identifier, newline character, thinking annotation (if present), and
319
+ # content formatted for terminal display
320
+ def last_message_with_user(height: 0)
315
321
  content, thinking = prepare_last_message
316
- if thinking.present?
317
- [ @sender + ?:, ?\n, thinking, ?\n, content ]
318
- else
319
- [ @sender + ?:, ?\n, content ]
322
+ content ||= ''
323
+ result = if thinking.present?
324
+ [ @sender + ":\n", thinking, ?\n, content ]
325
+ else
326
+ [ @sender + ":\n", content ]
327
+ end * ''
328
+ if height > 0
329
+ result = truncate_for_terminal(result, max_lines: height)
320
330
  end
331
+ result
321
332
  end
322
333
 
323
334
  # The display_formatted_terminal_output method formats and outputs the
@@ -327,9 +338,17 @@ class OllamaChat::FollowChat
327
338
  # move home commands. The method takes into account whether markdown and
328
339
  # thinking modes are enabled to determine how to process and display the
329
340
  # content.
330
- def display_formatted_terminal_output(output = nil)
341
+ #
342
+ # @param output [IO, nil] the output stream to print to. Defaults to
343
+ # `@output`.
344
+ # @param height [Integer] the maximum number of lines to truncate the output
345
+ # to. Defaults to 0 (no truncation).
346
+ def display_formatted_terminal_output(output = nil, height: 0)
331
347
  output ||= @output
332
- output.print(*([ clear_screen, move_home, *last_message_with_user ].compact))
348
+ output.print(
349
+ move_home, erase_in_display(nil),
350
+ last_message_with_user(height:)
351
+ )
333
352
  end
334
353
 
335
354
  # The display_output method shows the last message in the conversation.
@@ -345,7 +364,7 @@ class OllamaChat::FollowChat
345
364
  if chat.markdown.on?
346
365
  display_formatted_terminal_output(output)
347
366
  else
348
- output.print(*last_message_with_user)
367
+ output.print(last_message_with_user)
349
368
  end
350
369
  end
351
370
  end
@@ -39,7 +39,7 @@ module OllamaChat::KramdownANSI
39
39
  # @return [ String ] the content formatted with ANSI escape sequences
40
40
  # according to the configured styles
41
41
  def kramdown_ansi_parse(content)
42
- retried = false
42
+ retried ||= false
43
43
  content.nil? and return ''
44
44
  Kramdown::ANSI.parse(content, ansi_styles: @kramdown_ansi_styles)
45
45
  rescue Encoding::UndefinedConversionError