Class: OllamaChat::MessageList

Inherits:
Object
  • Object
show all
Includes:
MessageFormat, Pager, Utils::ValueFormatter, Term::ANSIColor
Defined in:
lib/ollama_chat/message_list.rb

Overview

A collection class for managing chat messages with support for system prompts, paged output, and conversation history.

This class provides functionality for storing, retrieving, and displaying chat messages in a structured manner. It handles system prompts separately from regular user and assistant messages, supports pagination for displaying conversations, and offers methods for manipulating message history including clearing, loading, saving, and dropping exchanges. The class integrates with Kramdown::ANSI for formatted output.

Examples:

Creating a new message list

chat = OllamaChat::Chat.new
messages = OllamaChat::MessageList.new(chat)

Adding messages to the list

messages << OllamaChat::Message.new(role: 'user', content: 'Hello')
messages << OllamaChat::Message.new(role: 'assistant', content: 'Hi there!')

Displaying conversation history

messages.list_conversation(5)  # Shows last 5 exchanges

Clearing messages

messages.clear  # Removes all non-system messages

Loading a saved conversation

messages.load_conversation('conversation.json')

Saving current conversation

messages.save_conversation('my_conversation.json')

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils::ValueFormatter

#format_bytes, #format_tokens

Methods included from Pager

#determine_pager_command, #use_pager

Methods included from MessageFormat

#chat, #display_sender, #message_type, #role_color, #role_template, #sender_name_displayed, #talk_annotate, #think_annotate

Constructor Details

#initialize(chat) ⇒ MessageList

The initialize method sets up the message list for an OllamaChat session.

Parameters:



40
41
42
43
# File 'lib/ollama_chat/message_list.rb', line 40

def initialize(chat)
  @chat     = chat
  @messages = []
end

Instance Attribute Details

#messages ⇒ Object (readonly)

The messages attribute reader returns the messages set for this object, initializing it lazily if needed.

The messages set is memoized, meaning it will only be created once per object instance and subsequent calls will return the same OllamaChat::MessageList instance.



64
65
66
# File 'lib/ollama_chat/message_list.rb', line 64

def messages
  @messages
end

#system ⇒ Object (readonly)

The system attribute reader returns the system prompt for the chat session.



48
49
50
# File 'lib/ollama_chat/message_list.rb', line 48

def system
  @system
end

#system_name ⇒ Object (readonly)

The system_name attribute reader returns the name of the current system prompt.



53
54
55
# File 'lib/ollama_chat/message_list.rb', line 53

def system_name
  @system_name
end

Instance Method Details

#<<(message) ⇒ OllamaChat::MessageList

The << operator appends a message to the list of messages and returns self.

Parameters:

Returns:



299
300
301
302
# File 'lib/ollama_chat/message_list.rb', line 299

def <<(message)
  @messages << message
  sync
end

#clean_messages(messages: @messages, what: %i[ tools images ])) ⇒ Array<OllamaChat::Message>

Returns a new list of messages with the content replaced by their stripped versions. This is used to create a "clean" version of the conversation for saving or displaying without mutating the original message objects.

Parameters:

  • messages (Array<OllamaChat::Message>) (defaults to: @messages) —

    the list of messages to clean

Returns:

  • (Array<OllamaChat::Message>) —

    a new array containing duplicated messages with stripped content



410
411
412
413
414
415
416
417
418
419
# File 'lib/ollama_chat/message_list.rb', line 410

def clean_messages(messages: @messages, what: %i[ tools images ])
  what = Array(what).map(&:to_sym)
  messages.map do |message|
    message = message.dup
    message.content = '' if what.include?(:tools) && message.tool?
    message.images = nil if what.include?(:images)
    message.thinking = nil if what.include?(:thinking)
    message
  end
end

#clean_messages!(messages: @messages, what: %i[ tools images ])) ⇒ OllamaChat::MessageList

Cleans the messages in the list by replacing them with stripped versions. This is a destructive (mutating) operation.

Parameters:

  • messages (Array<OllamaChat::Message>) (defaults to: @messages) —

    the messages to clean. Defaults to all current messages.

Returns:



427
428
429
430
# File 'lib/ollama_chat/message_list.rb', line 427

def clean_messages!(messages: @messages, what: %i[ tools images ])
  @messages = clean_messages(messages:, what:)
  self
end

#clear(all: false) ⇒ OllamaChat::MessageList

The clear method removes all non-system messages from the message list.

Returns:



285
286
287
288
289
290
291
292
# File 'lib/ollama_chat/message_list.rb', line 285

def clear(all: false)
  if all
    @messages.clear
  else
    @messages.delete_if { _1.role != 'system' }
  end
  sync
end

#clear_images ⇒ OllamaChat::MessageList

Removes all images from all messages in the current list.

Returns:



747
748
749
750
751
752
# File 'lib/ollama_chat/message_list.rb', line 747

def clear_images
  @messages.each do |message|
    message.images = nil
  end
  sync
end

#compact! ⇒ OllamaChat::Compaction::Result?

Compacts the message list by summarizing old groups and inserting a single summary tool message at the cut point.

Resolves the keep-recent token budget from the current model's context length, finds the cut point, summarizes all groups before it (passing any existing summary as previous_summary for iterative re-compaction), then inserts the new summary at the cut point. Old groups remain in the list for retrieval via lookup_group; the LLM context builder skips them on the next call.

Returns:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ollama_chat/message_list.rb', line 143

def compact!
  ctx    = @chat.current_context_length
  budget = @chat.compact_ratio_tokens(:keep_recent, ctx)

  existing_summary = find_summary
  start = @messages.each_with_index.find do |m, i|
    m.role == 'tool' && m.tool_name == 'summary' and break i + 1
  end
  start ||= 0
  start   = start.clamp(0..(@messages.size - 1))

  # Cut among post-summary groups; the old summary is excluded
  # from the budget window by +start+.
  cut = find_cut_point(start:, keep_recent_tokens: budget)
  return nil if cut.nil? || cut <= 1

  candidates = @messages[start...cut]
    .reject { |m| m.role == 'system' }
    .reject { |m| m.role == 'tool' && m.tool_name == 'summary' }
  return nil if candidates.empty?

  cand_es = OllamaChat::TokenEstimator::Crude.new(
    candidates.sum { |m| m.content.to_s.bytesize }
  ).perform
  context_before        = @chat.context_usage_colored
  context_filled_before = @chat.context_filled

  @chat.log(:info, 'Compaction: starting', data: {
    context_length: ctx,
    budget:         budget,
    cut_index:      cut,
    candidates:     candidates.size,
    candidate_bytes: cand_es.bytes_formatted,
    candidate_tokens: cand_es.tokens_formatted,
    previous_summary: existing_summary ? 'yes' : 'no',
  })

  summary_text, all_tool_entries = @chat.summarize_for_compaction(
    messages:         candidates,
    previous_summary: existing_summary,
  )

  compact_es = OllamaChat::TokenEstimator.estimate(summary_text.bytesize)
  @chat.log(:info, 'Compaction: summary generated', data: {
    summary_bytes:   compact_es.bytes_formatted,
    summary_tokens:  compact_es.tokens_formatted,
  })

  summary_msg = OllamaChat::Message.new(
    role:       'tool',
    tool_name:  'summary',
    content:    summary_text,
    tool_calls: all_tool_entries,
  ).initialize_group_uuid

  # Remove old summary (if any) and adjust cut for the shift.
  if existing_summary
    idx = @messages.index(existing_summary)
    @messages.delete_at(idx)
    cut -= 1 if idx < cut
  end

  @messages.insert(cut, summary_msg)
  @chat.log(:info, 'Compaction: done', data: {
    total_messages: @messages.size,
    summary_at:     cut,
  })
  sync

  OllamaChat::Compaction::Result.new(
    context_before:,
    context_after:         @chat.context_usage_colored,
    context_filled_before: ,
    context_filled_after:  @chat.context_filled,
    candidates:            candidates.size,
    candidate_size:        "#{cand_es.bytes_formatted} / " \
                           "#{cand_es.tokens_formatted}",
    summary_size:          "#{compact_es.bytes_formatted} / " \
                           "#{compact_es.tokens_formatted}",
    stored_total:          @chat.conversation_length,
  )
end

#compacted_estimate_tokens ⇒ OllamaChat::TokenEstimator::Estimate

Estimates the token and byte size of the messages that will actually be sent to the LLM (i.e. compacted_messages).

When no summary message exists this is identical to the full list. When a summary is present, the pre-summary groups are excluded.

Uses per-message token_estimate which counts only text content (and thinking, unless think_strip is enabled), excluding images and metadata scaffolding that would inflate a raw serialization.

Returns:



257
258
259
260
261
262
263
# File 'lib/ollama_chat/message_list.rb', line 257

def compacted_estimate_tokens
  strip = @chat.think_strip.on?
  bytes = compacted_messages.sum {
    _1.token_estimate(strip_thinking: strip).bytes
  }
  OllamaChat::TokenEstimator.estimate(bytes)
end

#compacted_messages ⇒ Array<OllamaChat::Message>

Returns the messages that will actually be sent to the LLM.

If no summary message exists, returns all messages (identical to to_ary). If a summary is present, returns only the system prompt, the summary, and everything after it — groups before the summary are invisible to the model but remain in the list for lookup_group retrieval.

Returns:



235
236
237
238
239
240
241
242
243
# File 'lib/ollama_chat/message_list.rb', line 235

def compacted_messages
  idx = @messages.rindex do |m|
    m.role == 'tool' && m.tool_name == 'summary'
  end
  return to_ary if idx.nil?

  system = @messages.take_while { |m| m.role == 'system' }
  system + [@messages[idx]] + @messages[(idx + 1)..]
end

#config ⇒ Object (private)

The config method provides access to the chat configuration object.

Returns:

  • (Object) —

    the configuration object associated with the chat instance



759
760
761
# File 'lib/ollama_chat/message_list.rb', line 759

def config
  @chat.config
end

#construct_message_from_hash(hash) ⇒ OllamaChat::Message (private)

Constructs a message instance from a hash, ensuring that a 'content' key is present even if it is nil.

Parameters:

  • hash (Hash) —

    the message data

Returns:



827
828
829
# File 'lib/ollama_chat/message_list.rb', line 827

def construct_message_from_hash(hash)
  OllamaChat::Message.from_hash(hash | { 'content' => nil })
end

#drop(n) ⇒ Integer

Note:

This method automatically synchronizes the message list with the session store.

Removes the last n conversation exchanges from the message list.

An exchange is typically defined as a pair of user and assistant messages. This method iterates backwards through the history and removes messages until the requested number of exchanges have been dropped. It will stop if it encounters a system message.

Parameters:

  • n (Object) —

    The number of exchanges to drop.

Returns:

  • (Integer) —

    The actual number of exchanges that were dropped.



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'lib/ollama_chat/message_list.rb', line 603

def drop(n)
  n = n.to_i.clamp(1, Float::INFINITY)
  i = 0
  m = 0
  current_group = nil
  @messages.reverse_each do |message|
    message.role == 'system' and next
    if current_group.nil?
      current_group = message.group_uuid
      i += 1
    elsif message.group_uuid == current_group
      i += 1
    else
      current_group = message.group_uuid
      m += 1
      if m < n
        i += 1
      else
        break
      end
    end
  end
  i > 0 && m == 0 and m = 1
  i.times do
    @messages.last.role == 'system' and next
    @messages.pop
  end
  @chat.feedback("Dropped the last #{m} exchanges.", type: :info)
  m
ensure
  sync
end

#each_group(role: %w[ user assistant ],, tool: false) {|Array<OllamaChat::Message>| ... } ⇒ Enumerator

Groups messages by their group_uuid, yielding an array of messages belonging to the same conversational turn (User -> Assistant -> Tools).

Parameters:

  • role (Array<String>) (defaults to: %w[ user assistant ],) —

    the message roles to include when grouping. Defaults to %w[user assistant]. Pass %w[user assistant tool] to include tool-result messages as well.

  • tool (Boolean) (defaults to: false) —

    whether to include messages that carry a tool_name (tool calls / responses) among user / assistant roles. Defaults to false.

Yields:

Returns:

  • (Enumerator) —

    if no block is given, returns an enumerator.



585
586
587
588
# File 'lib/ollama_chat/message_list.rb', line 585

def each_group(role: %w[ user assistant ], tool: false, &block)
  block or return enum_for(__method__, role:, tool:)
  each_message(role:, tool:).group_by(&:group_uuid).values.each(&block)
end

#each_message(role: %w[ user assistant ],, tool: false) {|message| ... } ⇒ Enumerator?

Iterates over messages in the conversation, yielding those matching the specified roles.

Parameters:

  • role (Array<String>) (defaults to: %w[ user assistant ],) —

    the roles to include when iterating. Defaults to ['user', 'assistant'].

  • tool (Boolean) (defaults to: false) —

    Whether to include messages that are tool calls/responses. Defaults to false.

Yields:

  • (message) —

    yields each matching message.

Returns:

  • (Enumerator) —

    if no block is given, returns an enumerator.

  • (nil) —

    if a block is given, returns nil after yielding all matching messages.



357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/ollama_chat/message_list.rb', line 357

def each_message(role: %w[ user assistant ], tool: false, &block)
  block or return enum_for(__method__, role:, tool:)

  role = Array(role)

  @messages.each do |message|
    role.include?(message.role) or next
    !tool && message.tool? and next
    yield message
  end
  nil
end

#find_cut_point(start:, keep_recent_tokens:) ⇒ Integer?

Finds the message index at which compaction should cut.

Walks non-system message groups (from start onward) backward from the tail, accumulating per-group token estimates. Returns the index of the first message in the group whose addition causes the running total to meet or exceed keep_recent_tokens. Messages from start up to that index are candidates for summarization; messages from that index onward are preserved.

If every group together still fits under the budget, the index of the first group at or after start is returned (signalling a no-op cut).

Parameters:

  • start (Integer) —

    the index at which to begin scanning (messages before this index are excluded; typically the position just past an existing summary message).

  • keep_recent_tokens (Integer) —

    the token budget for preserved (recent) messages.

Returns:

  • (Integer, nil) —

    the cut index into @messages, or nil if there are no non-system groups at or after start.



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ollama_chat/message_list.rb', line 92

def find_cut_point(start:, keep_recent_tokens:)
  groups = []
  @messages.each_with_index do |msg, i|
    next if msg.role == 'system'
    next if i < start
    tokens = msg.token_estimate(
      strip_thinking: @chat.think_strip.on?
    ).tokens
    uuid = msg.group_uuid
    if last = groups.last and last[:uuid] == uuid
      last[:tokens] += tokens
    else
      groups << { uuid:, start: i, tokens: }
    end
  end
  return nil if groups.empty?

  accumulated = 0
  groups.reverse_each do |group|
    accumulated += group[:tokens]
    return group[:start] if accumulated >= keep_recent_tokens
  end
  groups.first[:start]
end

#find_last(content: false) {|Message| ... } ⇒ OllamaChat::Message?

Note:

The method iterates in reverse order (reverse_each) so that the most recent matching message is returned. It also respects the content flag to skip empty messages, which is handy when the chat history contains empty messages e. g. when tool calling.

Find the last message that satisfies the supplied block.

Examples:

Find the last assistant message that contains content

last_assistant = message_list.find_last(content: true) { |m| m.role == 'assistant' }

Find the last user message regardless of content

last_user = message_list.find_last { |m| m.role == 'user' }

Parameters:

  • content (true, false) (defaults to: false) —

    If true, skip messages that have no content (m.content.present? is false). This is useful when you only care about messages that actually contain a payload (e.g. assistant replies, user queries, etc.).

Yields:

  • (Message) —

    yields each message in reverse order (from newest to oldest) until the block returns a truthy value.

Yield Parameters:

Yield Returns:

  • (true, false) —

    whether the message matches the criteria

Returns:

  • (OllamaChat::Message, nil) —

    the first message that matches the block, or nil if none match.



338
339
340
341
342
343
# File 'lib/ollama_chat/message_list.rb', line 338

def find_last(content: false, &block)
  @messages.reverse_each.find { |m|
    content and !m.content.present? and next
    block.(m)
  }
end

#find_summary ⇒ OllamaChat::Message?

Finds the most recent summary message in the list.

Scans backward from the tail for a message with role == 'tool' and tool_name == 'summary'.

Returns:

  • (OllamaChat::Message, nil) —

    the summary message, or nil if no summary has been inserted yet.



124
125
126
127
128
# File 'lib/ollama_chat/message_list.rb', line 124

def find_summary
  messages.reverse_each.find do |m|
    m.role == 'tool' && m.tool_name == 'summary'
  end
end

#full_estimate_tokens ⇒ OllamaChat::TokenEstimator::Estimate

Estimates the token and byte size of the full stored message list (i.e. @messages), using per-message token_estimate.

Unlike Session#estimate_tokens which measures raw JSONL bytes (including base64 images and JSON scaffolding), this counts only text content and thinking (unless think_strip is enabled).

Returns:



274
275
276
277
278
279
280
# File 'lib/ollama_chat/message_list.rb', line 274

def full_estimate_tokens
  strip = @chat.think_strip.on?
  bytes = @messages.sum {
    _1.token_estimate(strip_thinking: strip).bytes
  }
  OllamaChat::TokenEstimator.estimate(bytes)
end

#last ⇒ OllamaChat::Message

Returns the last message from the conversation.

Returns:

  • (OllamaChat::Message) —

    The last message in the conversation, or nil if there are no messages.



308
309
310
# File 'lib/ollama_chat/message_list.rb', line 308

def last
  @messages.last
end

#list_conversation(last = nil, think_loud: @chat.think_loud.on?) ⇒ OllamaChat::MessageList

Displays the most recent messages from the conversation history.

This method prints a specified number of trailing messages to the console using the pager for better readability. Tool messages are automatically excluded from the output. If no count is provided, the entire conversation is displayed.

Parameters:

  • last (Integer, nil) (defaults to: nil) —

    The number of recent messages to display. Defaults to the total size of the messages list if nil.

  • think_loud (Boolean) (defaults to: @chat.think_loud.on?) —

    Whether to force show or suppress thinking content. Defaults to the global chat setting.

Returns:



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/ollama_chat/message_list.rb', line 445

def list_conversation(last = nil, think_loud: @chat.think_loud.on?)
  messages = @messages.reject(&:tool?)
  last = (last || messages.size).clamp(0, messages.size)
  messages = messages[-last..-1].to_ary
  use_pager do |output|
    messages = clean_messages(messages:)
    messages = messages.with_infobar(
      output:  STDERR,
      label:   'Message',
      total:   messages.size,
      message: @chat.infobar_message,
    )
    messages.each do |message|
      output.puts message_text_for(message, think_loud:)
      +infobar
    end
  end
  self
end

#load_conversation(filename) ⇒ OllamaChat::MessageList

The load_conversation method loads a conversation from a file and populates the message list.

Parameters:

  • filename (String) —

    the path to the file containing the conversation

Returns:



376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/ollama_chat/message_list.rb', line 376

def load_conversation(filename)
  filename = Pathname.new(filename).expand_path
  unless filename.exist?
    @chat.feedback("File #{filename.to_s.inspect} doesn't exist. " \
                   "Choose another filename.", type: :warn)
    return
  end
  @messages = OllamaChat::Utils::JSONJSONLIO.new(filename).read(
    jsonl_transform: method(:parse_message_from_json),
    json_transform:  method(:construct_message_from_hash)
  ).to_a
  sync
end

#load_conversation_jsonl(filename) ⇒ Array<OllamaChat::Message> (private)

Loads a conversation from a JSONL (JSON Lines) file.

Parameters:

  • filename (Pathname) —

    the path to the JSONL file

Returns:



796
797
798
799
800
# File 'lib/ollama_chat/message_list.rb', line 796

def load_conversation_jsonl(filename)
  filename.each_line.map {
    parse_message_from_json(_1)
  }
end

#message_text_for(message, think_loud: @chat.think_loud.on?) ⇒ String (private)

The message_text_for method generates formatted text representation of a message including its role, content, thinking annotations, and associated images. It applies color coding to different message roles and uses markdown parsing when enabled. The method also handles special formatting for thinking annotations and image references within the message.

Parameters:

  • message (Object) —

    the message object containing role, content, thinking, and images

  • think_loud (Boolean) (defaults to: @chat.think_loud.on?) —

    Whether to force show or suppress thinking content. Defaults to the global chat setting.

Returns:

  • (String) —

    the formatted text representation of the message



775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
# File 'lib/ollama_chat/message_list.rb', line 775

def message_text_for(message, think_loud: @chat.think_loud.on?)
  thinking = if think_loud
               think_annotate(think_loud:) do
                 message.thinking.full? { @chat.markdown.on? ? @chat.kramdown_ansi_parse(_1) : _1 }
               end
             end
  content       = message.content.full? { @chat.markdown.on? ? @chat.kramdown_ansi_parse(_1) : _1 }
  message_text  = display_sender(message)
  if thinking
    message_text += [ ?:, thinking, talk_annotate(think_loud:) { content } ].compact.
      map(&:chomp) * ?\n
  else
    message_text += ":\n#{content}"
  end
  message_text
end

#parse_message_from_json(string) ⇒ OllamaChat::Message (private)

Parse a message from a JSON string.

Parameters:

  • string (String) —

    the JSON string representing the message

Returns:



818
819
820
# File 'lib/ollama_chat/message_list.rb', line 818

def parse_message_from_json(string)
  construct_message_from_hash(JSON.parse(string))
end

#read_conversation_jsonl(input) ⇒ OllamaChat::MessageList

Loads conversation messages from a JSONL (JSON Lines) input stream. Each line in the input is expected to be a valid JSON representation of a message. The method parses each line and adds the resulting message to the current conversation.

Parameters:

  • input (IO) —

    the input stream containing JSONL formatted messages

Returns:



736
737
738
739
740
741
742
# File 'lib/ollama_chat/message_list.rb', line 736

def read_conversation_jsonl(input)
  @messages = OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').read_io(
    input:,
    jsonl_transform: method(:parse_message_from_json)
  ).to_a
  self
end

#save_conversation(filename, messages: @messages) ⇒ OllamaChat::MessageList

The save_conversation method saves the current conversation to a file.

Parameters:

  • filename (String) —

    the path where the conversation will be saved

  • messages (Array<OllamaChat::Message>) (defaults to: @messages) —

    the messages to save. Defaults to all current messages in the list.

Returns:



397
398
399
400
# File 'lib/ollama_chat/message_list.rb', line 397

def save_conversation(filename, messages: @messages)
  OllamaChat::Utils::JSONJSONLIO.new(filename).write(collection: messages)
  self
end

#save_conversation_jsonl(filename, messages: @messages) ⇒ OllamaChat::MessageList (private)

Saves the conversation to a JSONL (JSON Lines) file.

Parameters:

  • filename (Pathname) —

    the path to the JSONL file

  • messages (Array) (defaults to: @messages) —

    the messages to save

Returns:



807
808
809
810
811
812
# File 'lib/ollama_chat/message_list.rb', line 807

def save_conversation_jsonl(filename, messages: @messages)
  filename.open(?w) do |output|
    write_conversation_jsonl(output, messages:)
  end
  self
end

#set_system_prompt(system_name) ⇒ OllamaChat::MessageList

Note:

This method:

  • Removes all existing system prompts from the message list
  • Adds the new system prompt to the beginning of the message list if provided
  • Handles edge cases such as clearing prompts when system is nil or false

Sets the system prompt for the chat session.

Parameters:

  • system_name (String, nil) —

    The name of the new system prompt. If nil or false, clears the system prompt.

Returns:



650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
# File 'lib/ollama_chat/message_list.rb', line 650

def set_system_prompt(system_name)
  @system_name = system_name
  if system_name == 'model_default'
    system = @chat.model_default_system_prompt.to_s
  else
    system = @chat.prompt(system_name, context: 'system').to_s
  end
  @messages.reject! { |msg| msg.role == 'system' }
  templates_values = {
    persona:        @chat.default_persona_profile,
    persona_name:   @chat.default_persona_name.full?,
    runtime_info:   (@chat.static_runtime_information if @chat.runtime_info.on?),
  }
  if new_system_prompt = system.full? { _1.to_s.named_placeholders_interpolate(templates_values) }
    @system = new_system_prompt
    @messages.unshift(
      OllamaChat::Message.new(role: 'system', content: self.system).initialize_group_uuid
    )
  else
    @system = nil
  end
  sync
end

#show_last(n = nil, pager: true, think_loud: @chat.think_loud.on?) ⇒ OllamaChat::MessageList?

Displays the most recent messages that were not authored by the user.

This is particularly useful for quickly reviewing the assistant's last responses without having to scroll through the user's own input. Output is routed through the pager.

Parameters:

  • n (Integer, nil) (defaults to: nil) —

    The number of non-user messages to display. Defaults to 1 if not specified.

  • pager (Boolean) (defaults to: true) —

    whether to use a pager for output (default: true).

  • think_loud (Boolean) (defaults to: @chat.think_loud.on?) —

    Whether to force show or suppress thinking content. Defaults to the global chat setting.

Returns:

  • (OllamaChat::MessageList, nil) —

    self if messages were displayed, or nil if no valid messages were found to show.



479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/ollama_chat/message_list.rb', line 479

def show_last(n = nil, pager: true, think_loud: @chat.think_loud.on?)
  n ||= 1
  messages = @messages.reject { |message| message.role == 'user' }
  n = n.clamp(0..messages.size)
  n <= 0 and return
  last_message_user_message = (last.content if last&.role == 'user')
  outputter = -> output do
    last_messages = messages[-n..-1].to_a
    last_messages = last_messages.with_infobar(
      output:  STDERR,
      label:   'Message',
      total:   last_messages.size,
      message: @chat.infobar_message,
    )
    last_messages.each do |message|
      output.puts message_text_for(message, think_loud:)
      +infobar
    end
  ensure
    if last_message_user_message
      message_content = Kramdown::ANSI::Width.truncate(
        last_message_user_message.inspect,
        length: Tins::Terminal.columns * 0.9
      )
      msg = <<~EOT

        ⚠️ Last message is actually a #{bold{'user message'}}, see:

        #{message_content}

        You might want to /drop it or /regenerate it.
      EOT
      output.puts msg
    end
  end
  if pager
    use_pager(&outputter)
  else
    outputter.(STDOUT)
  end
  self
end

#show_system_prompt ⇒ self, NilClass

The show_system_prompt method displays the system prompt configured for the chat session.

It retrieves the system prompt from the @system instance variable, parses it using Kramdown::ANSI, and removes any trailing newlines. If the resulting string is empty, the method returns immediately.

Otherwise, it prints a formatted message to the console, including the configured system prompt and its length in characters.

Returns:

  • (self, NilClass) —

    nil if the system prompt is empty, otherwise self.



685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/ollama_chat/message_list.rb', line 685

def show_system_prompt
  current_system = system.to_s
  es             = OllamaChat::TokenEstimator.estimate(current_system)
  system_prompt  = @chat.kramdown_ansi_parse(current_system).
     gsub(/\n+\z/, '').full?
  if system_prompt.blank?
    if current_system.present?
      system_prompt = current_system
    else
      return
    end
  end
  use_pager do |output|
    output.puts <<~EOT
    Configured system prompt is:
    #{system_prompt}

    System prompt name:   #{bold{system_name}}
    System prompt length: 👾#{es.bytes_formatted} 🧩#{es.tokens_formatted}
    EOT
  end
  self
end

#size ⇒ Integer

Returns the number of messages stored in the message list.

Returns:

  • (Integer) —

    The size of the message list.



69
70
71
# File 'lib/ollama_chat/message_list.rb', line 69

def size
  @messages.size
end

#speak_last(n = nil) ⇒ OllamaChat::MessageList?

Speaks the most recent assistant messages aloud via the session's voice handler (TTS or Say).

Each message is printed to STDOUT (without thinking content) and then spoken on a background thread. The method blocks until each utterance finishes before moving to the next unless interrupted by entering C-c. If the last message in the list is a user message, a warning is shown and no audio is produced.

Parameters:

  • n (Integer, nil) (defaults to: nil) —

    the number of assistant messages to speak. Defaults to 1 if not specified.

Returns:

  • (OllamaChat::MessageList, nil) —

    self if messages were spoken, or nil if voice is disabled, the last message is a user message, or there are no assistant messages to speak.



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/ollama_chat/message_list.rb', line 537

def speak_last(n = nil)
  unless @chat.voice.on?
    @chat.feedback('Voice output is disabled.', type: :info)
    return
  end
  n ||= 1
  messages = @messages.reject { |message| message.role == 'user' }
  n = n.clamp(0..messages.size)
  n <= 0 and return
  last_message_user_message = (last.content if last&.role == 'user')
  if last_message_user_message
    @chat.feedback('Last message is a user message!', type: :warn)
    return
  end
  last_messages = messages[-n..-1].to_a
  last_messages = last_messages.with_infobar(
    output:  STDERR,
    label:   'Message',
    total:   last_messages.size,
    message: @chat.infobar_message,
  )
  last_messages.each do |message|
    content = message.content.full? or next
    STDOUT.puts message_text_for(message, think_loud: false)
    speaker = @chat.speak(content, background: true)
    begin
      speaker&.wait_for_speaker
    rescue Interrupt
      @chat.feedback('Skipping voice output.', type: :info)
    end
  ensure
    +infobar
  end
  self
end

#sync ⇒ OllOamaChat::MessageList (private)

Synchronizes the message list state with the active chat session.

This method triggers the persistence of the current messages into the database via the associated @chat instance, ensuring that any recent mutations (like adding, clearing, or dropping messages) are immediately captured in the persistent session store.

Returns:

  • (OllOamaChat::MessageList) —

    the current instance to allow for method chaining.



840
841
842
843
# File 'lib/ollama_chat/message_list.rb', line 840

def sync
  @chat.store_messages_in_session
  self
end

#to_ary ⇒ Array

The to_ary method converts the message list into an array of OllamaChat::Message objects.

Returns:

  • (Array) —

    An array of OllamaChat::Message objects representing the messages in the list.



714
715
716
# File 'lib/ollama_chat/message_list.rb', line 714

def to_ary
  @messages.dup
end

#write_conversation_jsonl(output, messages: @messages) ⇒ OllamaChat::MessageList

Writes each message in the conversation to the output as a JSON line.

Parameters:

  • output (IO) —

    the output stream to write the JSON lines

  • messages (Array<OllamaChat::Message>) (defaults to: @messages) —

    the messages to write. Defaults to all current messages in the list.

Returns:



724
725
726
727
# File 'lib/ollama_chat/message_list.rb', line 724

def write_conversation_jsonl(output, messages: @messages)
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').write_io(output:, collection: messages)
  self
end