ollama_chat 0.0.106 → 0.0.108

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: c735b80b131af33178cdbc883a31d6c0693371736cfc91366bcd0d9f42bf2ffb
4
- data.tar.gz: 18a1019995611afe96a0fad541aad75324834ce9ac1828392b956988f7c916ff
3
+ metadata.gz: 857b8cf92f68e5856238c1758cfebe463535ded52de3e4dc591cd83b501ccd63
4
+ data.tar.gz: fc90ce1e2145207dc49dfeb6512804dec0e0e3933c943ec0050221b5867bccd1
5
5
  SHA512:
6
- metadata.gz: db82070f04d57c6679e0ec52474d26b1240b9cea164170258e34916a66097b3c6d3fac51e78a428372377fa956dc7732964387062f1c56d0f04906920e6a3e4e
7
- data.tar.gz: d049ed9ba805b0d2ff670ecc7c3427f5644373fab6a8bf877b0260edadabb777354d5d118fc40534801ea023a6f52b8467b705cfa126eb70115c9b420a1710ec
6
+ metadata.gz: 173caf67db6f004400f1f5199e07fcd16acd36c44a531a7d8b5618e5c2fb03f994aa63d03c5e4140d2d443022afd7fccc29d6c17d63f76e2d7085f41b9dd97e9
7
+ data.tar.gz: 5addb13b768f52b9ef3f5e3bd532aa632a37e5b5bf7b3c69c64be2d063b0f74ddc41640dd9e19a87d27e570c581e24f5456cf7b3a4d7c1351eba85e6d152e10b
data/CHANGES.md CHANGED
@@ -1,5 +1,110 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-08-15 v0.0.108
4
+
5
+ ### Added
6
+
7
+ - Added `require 'io/console'` to support `IO#raw` for key reads.
8
+ - Added `max` to `THINK_MODE_STATES` in `think_control.rb` to support newer
9
+ Ollama releases (e.g., **0.32.13**).
10
+ - Added migration `008_drop_think_mode_constraint.rb` to drop the `think_mode`
11
+ `CHECK` constraint in SQLite.
12
+ - Added `.obsidian` to `.gitignore` and the `GemHadar` `ignore` list in
13
+ `Rakefile`.
14
+ - Added `require_confirmation: true` to `get_current_weather`, `get_cve`,
15
+ `get_endoflife`, `browse`, `get_jira_issue`, `get_rfc`, and `get_ghr`.
16
+
17
+ ### Changed
18
+
19
+ - Refactored `confirm?` key reads to use `STDIN.raw(min:, time:, intr: true)`
20
+ instead of `stty` commands.
21
+ - Updated `confirm?` to print prompts to `output` instead of `STDOUT`.
22
+ - Updated glyphs for confirmation: `\u2328\uFE0F` to `\u2328`, `☑️` to `\u2611`,
23
+ and `🚫` to `\u{1F6AB}`.
24
+ - Scoped `Reline` histories for various interactive prompts:
25
+ - `duplicate_session` name prompt now scoped to `:session_name`.
26
+ - `determine_valid_new_name_for_persona` now scoped to `:persona_name`.
27
+ - `add_persona` `[FILES]` patterns prompt now scoped to `:patterns`.
28
+ - `add_new_prompt` `[FILES]` patterns prompt now scoped to `:patterns`.
29
+ - `determine_valid_output_filename` loop now scoped to `:filename`.
30
+ - `/session summarize -f` filename prompt now scoped to `:filename`.
31
+ - `choose_profile_for_model` profile name prompt now scoped to
32
+ `:profile_name`.
33
+ - Scoped `Reline` histories for RAG prompts:
34
+ - `rename_collection` now scopes to `:collection_name`.
35
+ - `create_collection` scopes to `:collection_name`,
36
+ `:collection_description`, and `:patterns`.
37
+ - `edit_collection` description prompt now uses `:collection_description`.
38
+ - Refactored terminal truncation by moving `truncate_for_terminal` from
39
+ `prepare_last_message` to `last_message_with_user`.
40
+ - Added `height:` keyword argument to `last_message_with_user` and
41
+ `display_formatted_terminal_output`.
42
+ - Refactored `display_formatted_terminal_output` to use `move_home` and
43
+ `erase_in_display(nil)` instead of `clear_screen` to prevent `tmux` flicker.
44
+ - Updated `require_confirmation` to `false` for `open_file_in_editor`,
45
+ `paste_into_editor`, and `generate_image`.
46
+ - Removed `jq` parsing from `OLLAMA_CHAT_TOOLS_TEST_RUNNER` in `.envrc`.
47
+ - Introduced `OLLAMA_CHAT_COLLECTION_NAME_REGEXP` in `lib/ollama_chat.rb` to
48
+ enforce safe identifiers.
49
+ - Updated `OLLAMA_CHAT::Chat#initial_collection` to validate names against the
50
+ new regex.
51
+ - Wrapped web search link list output in `use_pager` within
52
+ `OLLAMA_CHAT::WebSearching`.
53
+
54
+ ### Fixed
55
+ - Added `rescue` for `Interrupt` and `IRB::Abort` during key reads to return
56
+ `nil` and print `"\u274C\uFE0F"`.
57
+ - Fixed `tmux` flicker in vertically split panes by avoiding full-pane redraws.
58
+ - Fixed `OLLAMA_CHAT::Tools::PatchFile` to explicitly warn against redundant
59
+ tool calls.
60
+
61
+ ### Refactored
62
+ - Updated YARD `@param output` and `@return` documentation.
63
+ - Simplified `last_message_with_user` to return a single joined string.
64
+ - Simplified `OLLAMA_CHAT::Tools::SearchKnowledge` to raise
65
+ `OLLAMA_CHAT::ToolFunctionArgumentError` on invalid input.
66
+
67
+ ## 2026-08-09 v0.0.107
68
+
69
+ ### Added
70
+
71
+ - New `-e` option to `ollama_chat_send` to open input content in the configured
72
+ editor before sending.
73
+ - New `-F` option to `ollama_chat_send` to specify file extension for syntax
74
+ highlighting in the editor.
75
+ - Confirmation prompts to `write_file` when overwriting existing files in
76
+ `overwrite` mode or creating new files in `append` mode.
77
+ - Support for pluggable voice handler architecture via a new `handler` key in
78
+ the `voice` configuration.
79
+
80
+ ### Changed
81
+
82
+ - Refactored voice handling to use a dynamic `voice_handler` method in
83
+ `OllamaChat::Chat` to resolve configured classes (e.g., `OllamaChat::Say`).
84
+ - Externalized the suggestion prompt template by adding
85
+ `context_template_suggest` to `default_config.yml` under `prompts`, replacing
86
+ hardcoded heredocs in `prompt_management.rb`.
87
+ - Updated `StateSelectors` to delegate voice list retrieval via
88
+ `voice_handler.ask_and_send(:voices)`.
89
+ - Updated `FollowChat` to instantiate the resolved voice handler dynamically.
90
+ - Refactored `OllamaChat::Say.voices` to extract logic from
91
+ `default_config.yml` ERB and remove RSpec-specific branching.
92
+ - Updated `spec/ollama_chat/commands_spec.rb` to use explicit `.jsonl`
93
+ extensions for `/conversation save` and `/conversation load` tests.
94
+
95
+ ### Fixed
96
+
97
+ - Reopened `STDIN` from `/dev/tty` during editing to prevent editor warnings.
98
+ - Ensured `save_conversation` and `load_conversation` expectations match the
99
+ new file naming convention.
100
+
101
+ ### Security & Error Handling
102
+
103
+ - Raised `OllamaChat::ToolFunctionArgumentError` when a user declines a file
104
+ operation confirmation.
105
+ - Added `.to_s` call on `prompt` results to correctly handle `Sequel::Model`
106
+ return types.
107
+
3
108
  ## 2026-08-06 v0.0.106
4
109
 
5
110
  ### Added
data/README.md CHANGED
@@ -344,6 +344,22 @@ functionality:
344
344
  $ echo "Hello world" | ollama_chat_send -d /tmp/my_working_dir -r
345
345
  ```
346
346
 
347
+ - **Edit Content (`-e`)**: Opens the input content in the configured editor
348
+ before sending. This is useful for refining the message or adding more
349
+ context.
350
+
351
+ ```bash
352
+ $ echo "Draft message..." | ollama_chat_send -e
353
+ ```
354
+
355
+ - **File Format (`-F`)**: When using edit mode, specify a file extension to
356
+ enable syntax highlighting in your editor (e.g., `rb` for Ruby, `md` for
357
+ Markdown).
358
+
359
+ ```bash
360
+ $ echo "def hello; end" | ollama_chat_send -e -F rb
361
+ ```
362
+
347
363
  - **Help (`-h` or `--help`)**: Displays usage information and available options.
348
364
 
349
365
  ```bash
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
 
data/bin/ollama_chat_send CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'ollama_chat'
4
+ include OllamaChat::FileEditing
4
5
  require 'tins/go'
5
6
  include Tins::GO
6
7
 
7
- opts = go 'f:d:rtph', ARGV
8
+ opts = go 'f:d:F:rteph', ARGV
8
9
 
9
10
  # Displays the usage information for the ollama_chat_send command.
10
11
  #
@@ -23,6 +24,8 @@ def usage(rc = 0)
23
24
  -p Send input with source parsing enabled (defaults to disabled)
24
25
  -f CONFIG file to read
25
26
  -d DIR the working directory to derive the socket file from
27
+ -e Edit content in editor before sending
28
+ -F FORMAT file extension for editor (e.g., rb, md)
26
29
  -h Show this help message
27
30
 
28
31
  Send data to a running Ollama Chat client via standard input.
@@ -38,8 +41,15 @@ begin
38
41
  else
39
42
  opts[?r] ? :socket_input_with_response : :socket_input
40
43
  end
44
+ content = STDIN.read
45
+ if opts[?e]
46
+ basename = %w[ text ]
47
+ basename << opts[?F]&.sub(/\A(?=[^.])/, ?.) || '.md'
48
+ STDIN.reopen('/dev/tty')
49
+ content = edit_text(content, basename:)
50
+ end
41
51
  response = OllamaChat::ServerSocket.send_to_server_socket(
42
- STDIN.read,
52
+ content,
43
53
  type:,
44
54
  config:,
45
55
  working_dir: opts[?d],
@@ -160,6 +160,21 @@ class OllamaChat::Chat
160
160
  # @return [OllamaChat::RedisCache, nil] the cache instance
161
161
  attr_reader :cache
162
162
 
163
+
164
+ # Returns the voice handler class based on the configuration.
165
+ #
166
+ # This method resolves the voice handler class specified in the config.
167
+ # If running within RSpec, it returns an empty array to avoid side effects.
168
+ #
169
+ # @return [Class, Array] the voice handler class or an empty array if in test mode
170
+ def voice_handler
171
+ defined?(RSpec) and return []
172
+ case config.voice.handler
173
+ when 'OllamaChat::Say'
174
+ OllamaChat::Say
175
+ end
176
+ end
177
+
163
178
  # The start method initializes the chat session by displaying information,
164
179
  # then prompts the user for input to begin interacting with the chat.
165
180
  def start
@@ -214,11 +229,14 @@ class OllamaChat::Chat
214
229
  #
215
230
  # @return [ Symbol ] the collection name symbol
216
231
  def initial_collection
217
- (
218
- session&.current_collection.full? ||
232
+ c = session&.current_collection.full? ||
219
233
  config.embedding.collection.full? ||
220
- :default
221
- ).to_sym
234
+ 'default'
235
+ if c =~ /\A#{OllamaChat::COLLECTION_NAME_REGEXP.source}\z/
236
+ c
237
+ else
238
+ 'default'
239
+ end.to_sym
222
240
  end
223
241
 
224
242
  # The initial_system_prompt method returns the system prompt for the initial
@@ -296,8 +296,10 @@ module OllamaChat::Commands
296
296
  when 'summarize'
297
297
  opts = go_command('fs', opts)
298
298
  if opts[?f] and
299
- filename = ask?(prompt: "❓ Enter filename: ").full? { Pathname.new(_1) }
300
- then
299
+ filename = switch_history(:filename) {
300
+ ask?(prompt: "❓ Enter filename: ")
301
+ }.full? { Pathname.new(_1) }
302
+ then
301
303
  if filename.exist? && !confirm?(
302
304
  prompt: "🔔 File #{filename.to_s.inspect} already exists, overwrite? (y/n) ",
303
305
  yes: /\Ay/i
@@ -807,7 +809,7 @@ module OllamaChat::Commands
807
809
 
808
810
  command(
809
811
  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+([^-].*))?$),
812
+ 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
813
  optional: true,
812
814
  complete: [ 'input', %w[ path context embedding summary ] ],
813
815
  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
@@ -29,12 +29,19 @@ class OllamaChat::FollowChat
29
29
  super(output:)
30
30
  @chat = chat
31
31
  @output.sync = true
32
- @say = voice ? Handlers::Say.new(voice:) : NOP
33
32
  @messages = messages
34
33
  @group_uuid = group_uuid
35
34
  @sender = nil
35
+ @say = if voice && @chat.voice_handler.respond_to?(:new)
36
+ @chat.voice_handler.new(chat:, voice:)
37
+ else
38
+ NOP
39
+ end
36
40
  end
37
41
 
42
+ # Returns the chat object representing the conversation context.
43
+ #
44
+ # @return [OllamaChat::Chat] The chat object.
38
45
  attr_reader :chat
39
46
 
40
47
  # Returns the conversation history (an array of message objects).
@@ -70,7 +77,7 @@ class OllamaChat::FollowChat
70
77
  ensure_assistant_response_exists
71
78
  update_last_message(response)
72
79
  if chat.stream.on?
73
- display_formatted_terminal_output
80
+ display_formatted_terminal_output(height: Tins::Terminal.lines - 1)
74
81
  else
75
82
  if display_output
76
83
  display_formatted_terminal_output
@@ -289,9 +296,9 @@ class OllamaChat::FollowChat
289
296
  def prepare_last_message
290
297
  content, thinking = @messages.last.content, @messages.last.thinking
291
298
  if chat.markdown.on?
292
- content = talk_annotate { truncate_for_terminal chat.kramdown_ansi_parse(content) }
299
+ content = talk_annotate { chat.kramdown_ansi_parse(content) }
293
300
  if chat.think?
294
- thinking = think_annotate { truncate_for_terminal chat.kramdown_ansi_parse(thinking) }
301
+ thinking = think_annotate { chat.kramdown_ansi_parse(thinking) }
295
302
  end
296
303
  else
297
304
  content = talk_annotate { content }
@@ -300,20 +307,28 @@ class OllamaChat::FollowChat
300
307
  return content&.chomp, thinking&.chomp
301
308
  end
302
309
 
303
- # The last_message_with_user method constructs a formatted message array by
310
+ # The last_message_with_user method constructs a formatted string by
304
311
  # combining user information, newline characters, thinking annotations, and
305
312
  # content for display in the terminal output.
306
313
  #
307
- # @return [ Array ] an array containing the user identifier, newline
308
- # character, thinking annotation (if present), and content formatted for
309
- # terminal display
310
- 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)
311
321
  content, thinking = prepare_last_message
312
- if thinking.present?
313
- [ @sender + ?:, ?\n, thinking, ?\n, content ]
314
- else
315
- [ @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)
316
330
  end
331
+ result
317
332
  end
318
333
 
319
334
  # The display_formatted_terminal_output method formats and outputs the
@@ -323,9 +338,17 @@ class OllamaChat::FollowChat
323
338
  # move home commands. The method takes into account whether markdown and
324
339
  # thinking modes are enabled to determine how to process and display the
325
340
  # content.
326
- 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)
327
347
  output ||= @output
328
- 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
+ )
329
352
  end
330
353
 
331
354
  # The display_output method shows the last message in the conversation.
@@ -341,7 +364,7 @@ class OllamaChat::FollowChat
341
364
  if chat.markdown.on?
342
365
  display_formatted_terminal_output(output)
343
366
  else
344
- output.print(*last_message_with_user)
367
+ output.print(last_message_with_user)
345
368
  end
346
369
  end
347
370
  end
@@ -255,7 +255,9 @@ module OllamaChat::ModelHandling
255
255
  STDOUT.puts "Cancelled."
256
256
  return
257
257
  when '[NEW]'
258
- name = ask?(prompt: 'Enter new profile name: ') or return
258
+ name = switch_history(:profile_name) do
259
+ ask?(prompt: 'Enter new profile name: ')
260
+ end or return
259
261
  if models::ModelOptions.where(model_name:, profile: name).present?
260
262
  STDERR.puts "Profile #{name.inspect} already exists!"
261
263
  return