Module: OllamaChat::SessionManagement

Included in:
Chat
Defined in:
lib/ollama_chat/session_management.rb

Overview

The OllamaChat::SessionHandling module provides methods for managing chat sessions, including creating, listing, switching, renaming, and deleting sessions.

It integrates closely with the database-backed Session model and ensures that session data is persisted correctly, especially the conversation history.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#sessionOllamaChat::Database::Models::Session (readonly)

The session reader returns the current session object.

Returns:



49
50
51
# File 'lib/ollama_chat/session_management.rb', line 49

def session
  @session
end

Instance Method Details

#change_session(name) ⇒ Object (private)

Changes to a different session, saving the current one and loading the new one.

Parameters:

  • name (String)

    the name or ID of the session to switch to



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/ollama_chat/session_management.rb', line 412

def change_session(name)
  name.full? or name = ??
  previous_session_id = nil
  loop do
    if chosen_session = choose_session(name, offer_new_session: true)
      if chosen_session.nil? || chosen_session == session
        confirm?(
          prompt: "\nāŽ  Same session chosen, Press any key to continue (%s). ",
          timeout: 3
        )
        break
      end
      session_close
      previous_session_id = session.id
      @session = chosen_session
      messages.read_conversation_jsonl(session.messages.to_s)
      set_current_collection(session.current_collection.full? || :default)
      session.current_model.full? { use_model(_1) }
      set_default_persona_name(session.default_persona_name.full? || :none)
      set_current_system_prompt(session.current_system_prompt.full? || 'default')
      if session.lock?
        session_apply
        info_session
        break
      else
        confirm?(
          prompt: "\nāŽ  Session locked: could not switch, Press any key to continue (%s). ",
          timeout: 3
        )
        redo
      end
    else
      STDOUT.puts "Cancelled."
      break
    end
  end
ensure
  if previous_session_id && previous_session_id != session.id
    @previous_session_id = previous_session_id
  end
  session.update(working_directory: Dir.pwd)
end

#choose_session(session_name, except_id: nil, offer_new_session: false) ⇒ OllamaChat::Database::Models::Session? (private)

Finds or selects a session based on a name, ID, or pattern.

Parameters:

  • session_name (String)

    the name, ID, or pattern to search for

  • except_id (String, Integer, nil) (defaults to: nil)

    an ID to exclude from the search results

  • offer_new_session (Boolean) (defaults to: false)

    whether to offer creating a new session

Returns:



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
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
# File 'lib/ollama_chat/session_management.rb', line 461

def choose_session(session_name, except_id: nil, offer_new_session: false)
  session_name = session_name.to_s
  session_query = models::Session
  if except_id
    session_query = session_query.where(Sequel[:id] !~ except_id)
  end
  if session_name =~ /\A\d+\z/ and
    session = session_query.first(id: session_name)
  then
    return session
  end
  selector = if session_name =~ /\A\?+(.*)\z/
               session_name = nil
               Regexp.new($1)
             end
  if session_name and session = session_query.first(name: session_name)
    session
  elsif selector
    now = Time.now
    sessions = session_query.order(Sequel.desc(:updated_at)).map { |session|
      duration    = session.age(now:)
      size_bytes  = session.messages.to_s.size
      tokens      = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
      tokens_size = format_tokens(tokens)
      count       = session.messages.to_s.count(?\n)
      locked      = if pid = session.locked?
                      if pid == $$
                        " šŸ”“#{pid} "
                      else
                        " šŸ”#{pid} "
                      end
                    else
                      ' '
                    end
      display     = <<~EOT.strip
        #{session.name} šŸ†”#{session.id}#{locked}šŸ“Ø#{count} 🧩#{tokens_size} ā³#{duration}
      EOT
      SearchUI::Wrapper.new(
        session.name,
        display:
      )
    }
    selector and sessions = sessions.select { _1 =~ selector }
    session_name = if sessions.size == 1
                     sessions.first.value
                   else
                     offer_new_session and sessions.unshift(SearchUI::Wrapper.new('[new]', display: '[NEW]'))
                     sessions = sessions.unshift(SearchUI::Wrapper.new('[exit]', display: '[EXIT]'))
                     value = choose_entry(sessions)&.value
                     if value == '[new]'
                       return new_session
                     end
                     value unless value == '[exit]'
                   end
    if session_name
      session_query.first(name: session_name)
    end
  end
end

#delete_sessionObject (private)

Deletes the current session and prompts the user to pick a new one to switch to.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/ollama_chat/session_management.rb', line 255

def delete_session
  current_session_name, current_session_id = session.name, session.id
  STDOUT.puts <<~EOT
    The current session
      #{current_session_name.inspect} (#{current_session_id})
    will be deleted, pick a new session to switch to.
  EOT
  confirm?(prompt: "\nāŽ  Press any key to continue (%s). ", timeout: 3)
  if chosen_session = choose_session(??, except_id: current_session_id)
    confirm?(
      prompt: "šŸ”” Delete session #{current_session_name.inspect} (#{current_session_id})? (y/n) ",
      yes: /\Ay/i
    ) or return
    change_session(chosen_session.id)
    models::Session.where(id: current_session_id).destroy
    STDOUT.puts "Just deleted session #{current_session_name.inspect}!"
  end
end

#derive_session_name(length: 128) ⇒ String? (private)

Derives a title for the session based on its content.

Parameters:

  • length (Integer) (defaults to: 128)

    the maximum length of the title (default: 128)

Returns:

  • (String, nil)

    the derived session name, or nil



382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/ollama_chat/session_management.rb', line 382

def derive_session_name(length: 128)
  content = messages.each_message.inject('') do |c, message|
    message.content.present? or next c
    sender_name = sender_name_displayed(message)
    c << "%s: %s\n\n" % [ sender_name, message.content ]
  end
  prompt = prompt(:session_title).to_s % { length:, content: }
  generate(prompt:).response.full? do |name|
    name = name.
      gsub(/(\A(\s|[^A-Za-z])+|(\s|[^A-Za-z])+\z)/m, '').
      gsub(/\s+/, ' ')
    Kramdown::ANSI::Width.truncate(name, length:)
  end
end

#determine_valid_new_name_for_session(action, default_name: nil) ⇒ String? (private)

Interactively prompts the user for a unique session name.

This method will keep prompting the user until a name is provided that does not already exist in the database, or until the user cancels.

Parameters:

  • action (String)

    a description of the action being performed

  • default_name (String, nil) (defaults to: nil)

    an optional prefill value for the prompt

Returns:

  • (String, nil)

    the unique session name, or nil if cancelled



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ollama_chat/session_management.rb', line 149

def determine_valid_new_name_for_session(action, default_name: nil)
  session_name = nil
  loop do
    session_name = ask?(
      prompt: "ā“ Enter new session name #{action}, C-c ⇒ cancel: ",
      prefill: default_name
    )
    if session_name.nil?
      STDOUT.puts "Cancelled."
      return nil
    end
    if models::Session.where(name: session_name).first
      STDOUT.puts "Session named #{bold{session_name}} already exists."
    else
      break
    end
  end
  session_name
end

#duplicate_sessionnil (private)

Duplicates the current session into a new one.

This method creates a copy of the current session's attributes and prompts the user for a new name and whether to clear the duplicated session's message history.

Returns:

  • (nil)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ollama_chat/session_management.rb', line 205

def duplicate_session
  name = determine_valid_new_name_for_session(
    'to create', default_name: session.name
  ) or return
  old_session = session
  old_session.unlock
  @session = session.duplicate
  session.update(name:)
  session.lock? or raise OllamaChat::OllamaChatError,
    "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
  confirm?(
    prompt: "šŸ”” Clear messages of duplicated session? (y/n) ",
    yes: /\Ay/i
  ) and messages.clear
  session.current_model.full? {
    use_model(_1)
    copy_model_options_to_session
  }
  nil
end

#list_sessionsObject (private)

Lists all sessions in a formatted table.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/ollama_chat/session_management.rb', line 86

def list_sessions
  use_pager do |output|
    table = Terminal::Table.new
    table.style = {
      all_separators: true,
      border:         :unicode_round,
    }
    table.headings = %w[ ID NAME SIZE #TOK COUNT UPDATED ].map { |header| bold { header } }
    now = Time.now
    models::Session.order(Sequel.desc(:updated_at)).each do |s|
      name        = Kramdown::ANSI::Width.truncate(s.name, length: 32)
      name        = session.id == s.id ? bold { name } : name
      name        = if pid = s.locked?
                      if pid == $$
                        "#{name} šŸ”“"
                      else
                        "#{name} šŸ”"
                      end
                    else
                      name
                    end
      size_bytes  = s.messages.to_s.size
      size        = format_bytes(size_bytes)
      tokens      = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
      tokens_size = format_tokens(tokens)
      table << [
        s.id.to_s,
        name,
        size,
        tokens_size,
        s.messages.to_s.count(?\n),
        s.age(now:),
      ]
    end
    table.align_column 0, :right
    table.align_column 2, :left
    table.align_column 3, :right
    table.align_column 4, :right
    table.align_column 5, :right
    output.puts table
  end
end

Loads the collection of links from the current session.

This method reads the links attribute from the session and deserializes it from JSONL format.

Returns:

  • (Array<String>)

    The list of links associated with the session.



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

def load_links_from_session
  input = StringIO.new(session.links)
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').read_io(input:)
end

#new_sessionOllamaChat::Database::Models::Session (private)

Creates a new, default session instance.

Returns:



57
58
59
# File 'lib/ollama_chat/session_management.rb', line 57

def new_session
  OllamaChat::Database::Models::Session.with_defaults(self)
end

#preferred_sessionOllamaChat::Database::Models::Session (private)

Retrieves the preferred session from the database, or creates a new one if none exist.

Returns:



66
67
68
69
70
71
# File 'lib/ollama_chat/session_management.rb', line 66

def preferred_session
  models::Session.
    where(working_directory: Dir.pwd).
    order(:updated_at).last ||
    new_session
end

#previous_sessionOllamaChat::Database::Models::Session? (private)

Returns the session associated with the stored @previous_session_id, provided the session exists in the database and is not currently locked.

Returns:



78
79
80
81
82
83
# File 'lib/ollama_chat/session_management.rb', line 78

def previous_session
  @previous_session_id  or return
  prev = models::Session.where(id: @previous_session_id).first or return
  prev.locked? and return
  prev
end

#rename_sessionObject (private)

Note:

The use of 1.times do and redo ensures a single-retry capability for automatic name derivation.

Prompts the user to rename the current session interactively.

This method manages a sophisticated renaming workflow:

  1. It presents an interactive prompt using ask?.
  2. If the user provides an empty string, it attempts to automatically derive a new name using derive_session_name.
  3. After derivation, it uses redo to re-prompt the user, now pre-filling the prompt with the newly suggested name.
  4. If the user provides an arbitrary string, the session is renamed.
  5. If the user interrupts (e.g., via C-c), the process is cancelled.


287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/ollama_chat/session_management.rb', line 287

def rename_session
  switch_history(:session_name) do
    name = nil
    1.times do
      derived = false
      prefill ||= session.name
      name = ask?(
        prompt: "ā“ Enter the new name for the session (C-u ⇒ auto, C-c ⇒ cancel): ",
        prefill:
      )
      if name.nil?
        STDERR.puts "\nInterrupt: Session renaming was cancelled."
        return
      end
      if name.empty?
        if derived
          break
        else
          derived = true
          if prefill = derive_session_name.full?
            redo
          end
        end
      end
    end
    if name == session.name
      STDOUT.puts "Keeping the old name #{name.inspect}."
    elsif name.present?
      if exists = models::Session.where(name:).first
        STDOUT.puts "Session with name #{name.inspect} already exists."
      else
        session.update(name:)
        STDOUT.puts "Renamed current session to #{name.inspect}."
      end
    else
      STDERR.puts "Could not rename current session!"
    end
  rescue Sequel::UniqueConstraintViolation
    STDERR.puts "Could not rename session to #{name.inspect}, already exists!"
  end
end

#session_applyObject (private)



247
248
249
250
251
# File 'lib/ollama_chat/session_management.rb', line 247

def session_apply
  session.update(working_directory: Dir.pwd)
  init_history
  session
end

#session_closeObject (private)

Closes the current session by persisting final messages and releasing the process lock. This should be called during application shutdown or when switching sessions to ensure the session is available for future instances.



401
402
403
404
405
406
# File 'lib/ollama_chat/session_management.rb', line 401

def session_close
  store_messages_in_session
  links.sync
  save_history
  session.unlock
end

#set_new_sessionnil (private)

Creates and activates a new session with a unique name.

This method prompts for a name, initializes a new session record, locks it, and sets up the associated model and options.

Returns:

  • (nil)


175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/ollama_chat/session_management.rb', line 175

def set_new_session
  name = switch_history(:session_name) do
    determine_valid_new_name_for_session('to create')
  end
  session_close
  @previous_session_id = @session.id
  @session = new_session
  session.lock? or raise OllamaChat::OllamaChatError,
    "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
  if name.full?
    session.update(name:)
  else
    session.touch
  end
  session_apply
  messages.clear
  session.current_model.full? {
    use_model(_1)
    copy_model_options_to_session
  }
  nil
end

#setup_sessionOllamaChat::Database::Models::Session (private)

Sets up the current session based on command-line options or the last used session.

Returns:



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/ollama_chat/session_management.rb', line 230

def setup_session
  @session = if session_name = @opts[?l]
               choose_session(session_name)
             elsif @opts[?n]
               new_session
             else
               preferred_session
             end
  session or abort "No session named #{bold{session_name.inspect}} found."
  if session.lock?
    session_apply
  else
    raise OllamaChat::OllamaChatError,
      "Could not lock session #{session.id} #{session.errors.full?(:inspect)}"
  end
end

#show_session(output: STDOUT) ⇒ Object (private)

Displays information about the current session.

Parameters:

  • output (IO) (defaults to: STDOUT)

    the output stream to write the information to (default: STDOUT)



132
133
134
135
136
137
138
139
# File 'lib/ollama_chat/session_management.rb', line 132

def show_session(output: STDOUT)
  size_bytes = session.messages.to_s.size
  messages_size  = format_bytes(size_bytes)
  tokens         = OllamaChat::Utils::TokenEstimator.estimate(size_bytes)
  tokens_size    = format_tokens(tokens)
  messages_count = session.messages.to_s.count(?\n)
  output.puts "#{bold{session.name}} (#{italic{session.id}}), #{messages_size}/#{tokens_size}, #{messages_count} messages"
end

Persists a collection of links to the session in the database.

This method serializes the links into JSONL format and updates the links attribute of the current session.

Parameters:

  • links (Enumerable)

    The collection of links to save.



25
26
27
28
29
30
31
32
# File 'lib/ollama_chat/session_management.rb', line 25

def store_links_in_session(links)
  output = StringIO.new
  OllamaChat::Utils::JSONJSONLIO.new('as.jsonl').write_io(
    output:, collection: links
  )
  session.update(links: output.string)
  self
end

#store_messages_in_sessionObject

Persists the current conversation messages to the database.

This method serializes the current message list into JSONL format and updates the messages attribute of the current session.



12
13
14
15
16
17
# File 'lib/ollama_chat/session_management.rb', line 12

def store_messages_in_session
  output = StringIO.new
  messages.write_conversation_jsonl(output)
  session.update(messages: output.string)
  self
end

#summarize_session(pretty: false, sentence: false, &block) ⇒ String? (private)

Generates a summary of the current session's conversation.

Parameters:

  • pretty (Boolean) (defaults to: false)

    whether to format the summary in markdown (default: false)

  • sentence (Boolean) (defaults to: false)

    whether to summarize each message in one sentence (default: false)

  • block (Proc)

    a block to handle each summary fragment

Returns:

  • (String, nil)

    the session summary or nil if empty



335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/ollama_chat/session_management.rb', line 335

def summarize_session(pretty: false, sentence: false, &block)
  unit                  = sentence ? 'sentence' : 'paragraph'
  contents              = []
  messages_to_summarize = messages.each_message
  messages_to_summarize = messages_to_summarize.with_infobar(
    label:   'Summarizing message',
    total:   messages_to_summarize.count,
    message: infobar_message,
  )
  messages_to_summarize.each do |message|
    message_content  = message.content.full?
    message_thinking = message.thinking.full?
    unless message_content || message_thinking
      -infobar
      next
    end
    sender_name_output = sender_name_displayed(message)
    sender_name        = sender_name_displayed(message, template: false)
    context            = contents * "\n\n"
    summary            = generate(
      prompt:  prompt(:session_summarize).to_s % {
        sender_name:, unit:, message_content:, message_thinking:, context:
      }
    ).response
    content = if pretty
                '**%s**: %s' % [ sender_name_output, summary ]
              else
                '%s: %s' % [ sender_name_output, summary ]
              end
    block&.(content)
    contents << content
    +infobar
  end
  contents.empty? and return

  if pretty
    contents.unshift(%{# Summary of session "#{session.name}"})
    contents * "\n\n"
  else
    contents * ?\n
  end
end