- Home
- Documentation
- sessions
- Session Switching and Recent Session Listing
Session Switching and Recent Session Listing
Session switching and recent session listing
Section titled “Session switching and recent session listing”This document describes how coding-agent discovers recent sessions, resolves --resume targets, presents session pickers, and switches the active runtime session.
It focuses on current implementation behavior, including fallback paths and caveats.
Implementation files
Section titled “Implementation files”../src/session/session-manager.ts../src/session/agent-session.ts../src/cli/session-picker.ts../src/modes/components/session-selector.ts../src/modes/controllers/selector-controller.ts../src/main.ts../src/sdk.ts../src/modes/interactive-mode.ts../src/modes/utils/ui-helpers.ts
Recent-session discovery
Section titled “Recent-session discovery”Directory scope
Section titled “Directory scope”SessionManager stores sessions under a cwd-scoped directory by default:
~/.xcsh/agent/sessions/--<cwd-encoded>--/*.jsonl
SessionManager.list(cwd, sessionDir?) reads only that directory unless an explicit sessionDir is provided.
Two listing paths with different payloads
Section titled “Two listing paths with different payloads”There are two different listing pipelines:
-
getRecentSessions(sessionDir, limit)(welcome/summary view)- Reads only a 4KB prefix (
readTextPrefix(..., 4096)) from each file. - Parses header + earliest user text preview.
- Returns lightweight
RecentSessionInfowith lazynameandtimeAgogetters. - Sorts by file
mtimedescending.
- Reads only a 4KB prefix (
-
SessionManager.list(...)/SessionManager.listAll()(resume pickers and ID matching)- Reads full session files.
- Builds
SessionInfoobjects (id,cwd,title,messageCount,firstMessage,allMessagesText, timestamps). - Drops sessions with zero
messageentries. - Sorts by
modifieddescending.
Metadata fallback behavior
Section titled “Metadata fallback behavior”For recent summaries (RecentSessionInfo):
- display name preference:
header.title-> first user prompt ->header.id-> filename - name is truncated to 40 chars for compact displays
- control characters/newlines are stripped/sanitized from title-derived names
For SessionInfo list entries:
titleisheader.titleor latest compactionshortSummaryfirstMessageis first user message text or"(no messages)"
--continue resolution and terminal breadcrumb preference
Section titled “--continue resolution and terminal breadcrumb preference”SessionManager.continueRecent(cwd, sessionDir?) resolves the target in this order:
- Read terminal-scoped breadcrumb (
~/.xcsh/agent/terminal-sessions/<terminal-id>) - Validate breadcrumb:
- current terminal can be identified
- breadcrumb cwd matches current cwd (resolved path compare)
- referenced file still exists
- If breadcrumb is invalid/missing, fall back to newest file by mtime in the session dir (
findMostRecentSession) - If none found, create a new session
Terminal ID derivation prefers TTY path and falls back to env-based identifiers (KITTY_WINDOW_ID, TMUX_PANE, TERM_SESSION_ID, WT_SESSION).
Breadcrumb writes are best-effort and non-fatal.
Startup-time resume target resolution (main.ts)
Section titled “Startup-time resume target resolution (main.ts)”--resume <value>
Section titled “--resume <value>”createSessionManager(...) handles string-valued --resume in two modes:
-
Path-like value (contains
/,\\, or ends with.jsonl)- direct
SessionManager.open(sessionArg, parsed.sessionDir)
- direct
-
ID prefix value
- find match in
SessionManager.list(cwd, sessionDir)byid.startsWith(sessionArg) - if no local match and
sessionDiris not forced, trySessionManager.listAll() - first match is used (no ambiguity prompt)
- find match in
Cross-project match behavior:
- if matched session cwd differs from current cwd, CLI prompts whether to fork into current project
- yes ->
SessionManager.forkFrom(...) - no -> throws error (
Session "..." is in another project (...))
No match -> throws error (Session "..." not found.).
--resume (no value)
Section titled “--resume (no value)”Handled after initial session-manager construction:
- list local sessions with
SessionManager.list(cwd, parsed.sessionDir) - if empty: print
No sessions foundand exit early - open TUI picker (
selectSession) - if canceled: print
No session selectedand exit early - if selected:
SessionManager.open(selectedPath)
--continue
Section titled “--continue”Uses SessionManager.continueRecent(...) directly (breadcrumb-first behavior above).
Picker-based selection internals
Section titled “Picker-based selection internals”CLI picker (src/cli/session-picker.ts)
Section titled “CLI picker (src/cli/session-picker.ts)”selectSession(sessions) creates a standalone TUI with SessionSelectorComponent and resolves exactly once:
- selection -> resolves selected path
- cancel (Esc) -> resolves
null - hard exit (Ctrl+C path) -> stops TUI and
process.exit(0)
Interactive in-session picker (SelectorController.showSessionSelector)
Section titled “Interactive in-session picker (SelectorController.showSessionSelector)”Flow:
- fetch sessions from current session dir via
SessionManager.list(currentCwd, currentSessionDir) - mount
SessionSelectorComponentin editor area usingshowSelector(...) - callbacks:
- select -> close selector and call
handleResumeSession(sessionPath) - cancel -> restore editor and rerender
- exit ->
ctx.shutdown()
- select -> close selector and call
Session selector component behavior
Section titled “Session selector component behavior”SessionList supports:
- arrow/page navigation
- Enter to select
- Esc to cancel
- Ctrl+C to exit
- fuzzy search across session id/title/cwd/first message/all messages/path
Empty-list render behavior:
- renders a message instead of crashing
- Enter on empty does nothing (no callback)
- Esc/Ctrl+C still work
Caveat: UI text says Press Tab to view all, but this component currently has no Tab handler and current wiring only lists current-scope sessions.
Runtime switch execution (AgentSession.switchSession)
Section titled “Runtime switch execution (AgentSession.switchSession)”switchSession(sessionPath) is the core in-process switch path.
Lifecycle/state transition:
- capture
previousSessionFile - emit
session_before_switchhook event (reason: "resume", cancellable) - if canceled -> return
falsewith no switch - disconnect from current agent event stream
- abort active generation/tool flow
- clear queued steering/follow-up/next-turn message buffers
- flush session writer (
sessionManager.flush()) to persist pending writes sessionManager.setSessionFile(sessionPath)- updates session file pointer
- writes terminal breadcrumb
- loads entries / migrates / blob-resolves / reindexes
- if missing/invalid file data: initializes a new session at that path and rewrites header
- update
agent.sessionId - rebuild context via
buildSessionContext() - emit
session_switchhook event (reason: "resume",previousSessionFile) - replace agent messages with rebuilt context
- restore default model from
sessionContext.models.defaultif available and present in model registry - restore thinking level:
- if branch already has
thinking_level_change, apply saved session level - else derive default thinking level from settings, clamp to model capability, set it, and append a new
thinking_level_changeentry
- if branch already has
- reconnect agent listeners and return
true
UI state rebuild after interactive switch
Section titled “UI state rebuild after interactive switch”SelectorController.handleResumeSession performs UI reset around switchSession:
- stop loading animation
- clear status container
- clear pending-message UI and pending tool map
- reset streaming component/message references
- call
session.switchSession(...) - clear chat container and rerender from session context (
renderInitialMessages) - reload todos from new session artifacts
- show
Resumed session
So visible conversation/todo state is rebuilt from the new session file.
Startup resume vs in-session switch
Section titled “Startup resume vs in-session switch”Startup resume (--continue, --resume, direct open)
Section titled “Startup resume (--continue, --resume, direct open)”- Session file is chosen before
createAgentSession(...). sdk.tsbuildsexistingSession = sessionManager.buildSessionContext().- Agent messages are restored once during session creation.
- Model/thinking are selected during creation (including restore/fallback logic).
- Interactive mode then runs
#restoreModeFromSession()to re-enter persisted mode state (currently plan/plan_paused).
In-session switch (/resume-style selector path)
Section titled “In-session switch (/resume-style selector path)”- Uses
AgentSession.switchSession(...)on an already-runningAgentSession. - Messages/model/thinking are rebuilt immediately in place.
- Hook
session_before_switch/session_switchevents are emitted. - UI chat/todos are refreshed.
- No dedicated post-switch mode restore call is made in selector flow; mode re-entry behavior is not symmetric with startup
#restoreModeFromSession().
Failure and edge-case behavior
Section titled “Failure and edge-case behavior”Cancellation paths
Section titled “Cancellation paths”- CLI picker cancel -> returns
null, caller printsNo session selected, process exits early. - Interactive picker cancel -> editor restored, no session change.
- Hook cancellation (
session_before_switch) ->switchSession()returnsfalse.
Empty list paths
Section titled “Empty list paths”- CLI
--resume(no value): empty list printsNo sessions foundand exits. - Interactive selector: empty list renders message and remains cancellable.
Missing/invalid target session file
Section titled “Missing/invalid target session file”When opening/switching to a specific path (setSessionFile):
- ENOENT -> treated as empty -> new session initialized at that exact path and persisted.
- malformed/invalid header (or effectively unreadable parsed entries) -> treated as empty -> new session initialized and persisted.
This is recovery behavior, not hard failure.
Hard failures
Section titled “Hard failures”Switch/open can still throw on true I/O failures (permission errors, rewrite failures, etc.), which propagate to callers.
ID prefix matching caveats
Section titled “ID prefix matching caveats”- ID matching uses
startsWithand takes first match in sorted list. - No ambiguity UI if multiple sessions share prefix.
SessionManager.list(...)excludes sessions with zero messages, so those sessions are not resumable via ID match/list picker.