Class: Rubino::CLI::Chat::SessionResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/cli/chat/session_resolver.rb

Overview

Resolves which session a chat invocation runs against (–session / –resume / –continue / bare-chat auto-resume) and replays a resumed session’s history through the UI, extracted from ChatCommand (#17). Also owns the resume-facing one-liners (the auto-resume notice and the exit-time resume hint).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ SessionResolver

Returns a new instance of SessionResolver.



15
16
17
# File 'lib/rubino/cli/chat/session_resolver.rb', line 15

def initialize(options)
  @options = options
end

Instance Attribute Details

#auto_resumed_sessionObject (readonly)

The session a bare-‘chat` auto-resume / –continue picked, when one was found. run_interactive gates the auto-resume notice on this.



21
22
23
# File 'lib/rubino/cli/chat/session_resolver.rb', line 21

def auto_resumed_session
  @auto_resumed_session
end

Instance Method Details

Prominent one-line banner shown when a bare ‘chat` auto-resumed the last session (#99, F2): make it OBVIOUS which session was picked up so a dev never accidentally continues/pollutes an old one. Carries the SHORT id, the message count, and the cwd it belongs to — the three facts a “wait, what session am I in?” moment needs — plus how to start fresh. Rendered as a warning (not dim status) so it actually stands out against the resumed history that follows.



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/rubino/cli/chat/session_resolver.rb', line 83

def print_auto_resume_line(ui, session)
  return unless session

  id     = session[:id].to_s[0..7]
  msgs   = session[:message_count].to_i
  msgcnt = "#{msgs} msg#{"s" if msgs != 1}"
  cwd    = pretty_cwd(session[:cwd])
  where  = cwd ? ", #{cwd}" : ""
  banner = "▸ resumed session #{id} (#{msgcnt}#{where}) — /new for fresh"
  ui.respond_to?(:warning) ? ui.warning(banner) : ui.status(banner)
end

On exit, hand the user back the exact command to return to this chat. Claude Code prints no equivalent hint; without this, the session id is buried in ~/.claude state and the user has to guess at –resume or scroll back through history. Prefer the human-friendly title when one is set; fall back to the id otherwise.



100
101
102
103
104
105
106
107
108
109
# File 'lib/rubino/cli/chat/session_resolver.rb', line 100

def print_resume_hint(ui, session)
  return unless session

  id    = session[:id]
  title = session[:title]
  handle = title && !title.to_s.strip.empty? ? %("#{title}") : id
  return unless handle

  ui.info("Resume with: rubino chat --resume #{handle}")
end

— Session history replay (resume / continue) —

PromptAssembler feeds the past turns to the model on every request, but the inline REPL never printed them. On –resume the terminal looked empty even though the model had full context. Replay user, assistant and tool messages through the existing UI methods so the scrolled-back transcript matches what the user originally saw.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rubino/cli/chat/session_resolver.rb', line 118

def print_session_history(ui, session_id)
  return unless session_id

  messages = ::Rubino::Session::Store.new.for_session(session_id)
  return if messages.empty?

  ui.status("Loaded #{messages.size} prior message#{"s" if messages.size != 1}")
  ui.separator

  messages.each do |msg|
    at = parse_msg_timestamp(msg.created_at)
    case msg.role.to_s
    when "user"
      # A `!` bang command persisted its <bash-input>/<bash-stdout>
      # context messages as user rows; replay them as the `! <cmd>`
      # echo + dim output block, never the raw tags.
      next if BangShell.replay(ui, msg.content, at: at)

      ui.replay_user_input(msg.content, at: at)
    when "assistant"
      next if msg.content.nil? || msg.content.to_s.empty?

      # Render the prior assistant turn as markdown, same as a live reply —
      # not the old box (which the M2 redesign repurposed into a "● running"
      # tool-style row, so resume showed assistant turns as fake tool runs
      # with raw markdown).
      ui.assistant_text(msg.content)
    when "tool"
      name      = msg.tool_name || "tool"
      arguments = msg..is_a?(Hash) ? msg.[:arguments] : nil
      ui.tool_started(name, arguments: arguments, at: at)
      ui.tool_finished(name, result: replay_tool_result(msg, name))
    end
  end

  ui.separator
end

#resolve_session_id(auto_resume: false) ⇒ Object

Resolves which session this invocation should run against. auto_resume enables the bare-‘chat` auto-resume (#99) — only the interactive REPL opts in; one-shot (`-q`/scripted) keeps the old “fresh unless asked” behaviour so automation isn’t silently hijacked onto a past session.



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/rubino/cli/chat/session_resolver.rb', line 27

def resolve_session_id(auto_resume: false)
  # Reap sessions orphaned by a hard kill (SIGKILL) or a closed terminal
  # whose SIGHUP never landed (#11): end any "active" row whose owning
  # process is gone before we resolve a resume target, so --continue /
  # auto-resume never treats a dead session as live.
  Session::Repository.new.reap_orphaned_active!

  id = opt(:session)
  return id if id

  resume = opt(:resume) || opt(:r)
  return resume if resume

  if opt(:continue) || opt(:c)
    # Explicit --continue/-c resumes the same session a bare `chat`
    # auto-resume would (#43): the latest RESUMABLE session (any status,
    # message_count > 0), not just an "active" one — otherwise a cleanly
    # ended prior session is invisible and -c silently forks a fresh one,
    # losing context. SCOPED to the launch dir (r5 MF-4 / C-1) so -c in
    # folder B never resumes folder A's conversation, and a session a
    # different live tab is still writing is skipped (no two-tab stomp).
    # When there genuinely is none for this dir, tell the user instead of
    # silently starting over.
    @auto_resumed_session = Session::Repository.new.latest_resumable_for_cwd
    return @auto_resumed_session[:id] if @auto_resumed_session

    warn pastel.yellow("No previous session to continue in this directory — starting a new one.")
    return nil
  end

  # --new forces a brand-new session; otherwise a BARE interactive `chat`
  # auto-resumes the most recent resumable session FOR THIS dir so a user
  # who closed the terminal continues where they left off — without ever
  # grabbing another folder's session (r5 MF-4 / C-1). nil ⇒ no prior
  # session for this dir (true first run here) ⇒ fresh session + welcome.
  return nil if opt(:new) || !auto_resume

  @auto_resumed_session = Session::Repository.new.latest_resumable_for_cwd
  @auto_resumed_session&.dig(:id)
end

#resuming_session?Boolean

True when the chat was started against an existing session (–resume / –continue / explicit –session / bare-chat auto-resume): show its history rather than the first-run welcome panel.

Returns:

  • (Boolean)


71
72
73
74
# File 'lib/rubino/cli/chat/session_resolver.rb', line 71

def resuming_session?
  !!(opt(:session) || opt(:resume) || opt(:r) || opt(:continue) || opt(:c) ||
     @auto_resumed_session)
end