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
# File 'lib/rubino/cli/chat/session_resolver.rb', line 118

def print_session_history(ui, session_id)
  replay_session(ui, session_id)
end

#replay_messages(ui, messages, banner: true) ⇒ Object

Replay an ALREADY-FETCHED message list (the attach view passes the child’s ‘entry.messages` straight through, no second store hit). A no-op on an empty list, framed by the same “Loaded N” status + separators the resume path shows. `banner:` false drops that “Loaded N” header + framing separators for an INCREMENTAL tail (the attached-agent watcher replays a few-message delta every ~0.4s; re-printing “Loaded N prior messages” on each one floods the focused view with banner noise) — the messages still render through the same per-message seam, just without the resume frame.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rubino/cli/chat/session_resolver.rb', line 140

def replay_messages(ui, messages, banner: true)
  messages = Array(messages)
  return if messages.empty?

  if banner
    ui.status("Loaded #{messages.size} prior message#{"s" if messages.size != 1}")
    ui.separator
  end
  # The accumulated assistant text already rendered in the CURRENT turn,
  # used to de-dupe a final message that RESTATES its earlier segments
  # (see #replay_assistant_text). Reset at each user-turn boundary.
  @assistant_turn_text = +""
  messages.each { |msg| replay_message(ui, msg) }
  ui.separator if banner
end

#replay_session(ui, session_id) ⇒ Object

Replay a session’s persisted transcript through the live UI render hooks so the scrolled-back history matches what the user originally saw. Shared by –resume (#print_session_history) and the agent-attach view switch, which clears the screen and replays the SELECTED agent’s own session.



126
127
128
129
130
# File 'lib/rubino/cli/chat/session_resolver.rb', line 126

def replay_session(ui, session_id)
  return unless session_id

  replay_messages(ui, ::Rubino::Session::Store.new.for_session(session_id))
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