claudesessions

package
v1.63.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: Apache-2.0, MIT Imports: 14 Imported by: 0

Documentation

Overview

Package claudesessions enumerates the Claude Code sessions recorded for a working directory. Claude writes one JSONL transcript per session under ~/.claude/projects/<escaped-cwd>/<session-id>.jsonl; this package maps a CWD to that directory and reads a short, human-recognizable title out of each transcript.

Pure reads with no process spawning and no network I/O: every failure (missing directory, unreadable file, malformed JSON) degrades to "fewer sessions" rather than an error, so session discovery can never block pane creation. Titles are sanitized of control characters — a transcript records whatever the user typed, and the value is rendered into a TUI.

Index

Constants

View Source
const (
	// MaxSessions bounds how many sessions List returns. Claude never prunes
	// transcripts, so a long-lived project accumulates hundreds; the newest
	// ones are the only plausible resume targets and the cap keeps both the
	// scan cost and the IPC frame bounded.
	MaxSessions = 200

	// MaxTitleRunes bounds a title before it reaches the wire. Consumers
	// truncate again to their render width; this only stops a pasted wall of
	// text from riding along in every response.
	MaxTitleRunes = 240

	// MaxPromptRunes bounds each prompt ReadDetail returns. Larger than
	// MaxTitleRunes because the detail view shows a prompt as a paragraph
	// rather than a row, but still bounded: a prompt can carry a pasted file.
	MaxPromptRunes = 1200
)

Variables

This section is empty.

Functions

func ConfigDir added in v1.62.5

func ConfigDir() string

ConfigDir returns Claude Code's config directory: $CLAUDE_CONFIG_DIR when set, else ~/.claude. Returns "" when neither can be resolved.

A "~/"-prefixed or relative value is expanded here so every caller gets an absolute path. Resolution belongs on whichever machine owns the disk — the daemon — because the directory describes that machine's filesystem; a client-supplied path would be the laptop's answer about the server's.

func EscapeCWD

func EscapeCWD(cwd string) string

EscapeCWD mirrors Claude Code's per-project directory naming under ~/.claude/projects/, transcribed from the claude binary (2026-07-05, v2.x):

t = cwd.replace(/[^a-zA-Z0-9]/g, "-")           // per UTF-16 code unit
if (t.length <= 200) return t
return t.slice(0, 200) + "-" + base36(abs(h))   // h = Java-31x hash
// h: for each UTF-16 unit u: h = (h<<5) - h + u | 0   (int32 wrap)

Operating per UTF-16 code unit (not rune) matches the JS regex without the /u flag: an astral char (emoji) becomes TWO dashes. Note the hash argument is the ORIGINAL cwd, not the dashified string.

Getting this wrong fails quietly rather than loudly, which is why it is transcribed rather than approximated: an earlier version replaced only : \ / and _ and missed '.', so any CWD containing a dot probed a nonexistent directory and every restored pane fell back to --continue, all resuming the SAME session after a daemon restart (2026-07-05 incident).

func ProjectDir

func ProjectDir(cwd string) string

ProjectDir returns the absolute directory Claude stores this CWD's session transcripts in, or "" when the config directory cannot be resolved.

func ProjectDirIn added in v1.62.5

func ProjectDirIn(configDir, cwd string) string

ProjectDirIn maps a CWD to its transcript directory under an explicit config dir. Takes the directory rather than reading the environment so a caller that already resolved one is not at the mercy of a concurrent Setenv, and so tests need no environment at all.

func SanitizePrompt

func SanitizePrompt(s string) string

SanitizePrompt is sanitizeTitle's multi-line counterpart, for text rendered as a paragraph rather than a row: line breaks are PRESERVED (the shape of a prompt is most of its readability) while every other control character and Unicode format character is dropped, for the same display-spoofing reasons documented on SanitizeTitle. Runs of blank lines collapse to one so a prompt padded with newlines cannot push the rest of the panel off-screen, and trailing whitespace goes because it only ever renders as ragged rows.

Exported for the same reason SanitizeTitle is: the TUI re-applies it to text arriving over IPC, which reaches the screen without passing through the VT emulator.

func SanitizeTitle

func SanitizeTitle(s string) string

sanitizeTitle collapses a prompt to a single clean line, then truncates it on a rune boundary.

Whitespace controls (newline, tab, CR) become spaces so the words either side stay separated; every other control character — notably ESC, the lead byte of an ANSI sequence — is dropped outright, so a prompt containing a pasted terminal capture cannot inject control codes into the TUI. Dropping rather than space-substituting matters for readability: substituting would split "\x1b[31mtext" into two visible fragments.

Unicode format characters (category Cf) are dropped too, which IsControl does NOT cover: a bidi override or isolate (U+202A–U+202E, U+2066–U+2069) survives both IsControl and strings.Fields, and would render the row reversed while still measuring its pre-override width — Trojan-source-style display spoofing that could have you resume a different session than the one you read. A user only has to paste web or repo content into their first prompt to trigger it. ZWSP goes for the same reason: invisible, but it defeats whitespace collapse. Exported so the TUI can re-apply it to titles arriving over IPC — a title is user-authored text that reaches the screen without passing through the VT emulator, so it is worth sanitizing on both sides of the socket.

func TranscriptPath

func TranscriptPath(cwd, sessionID string) string

TranscriptPath returns the absolute path of one session's transcript, or "" when the home directory is unavailable or either argument is empty.

func TranscriptPathIn added in v1.62.7

func TranscriptPathIn(configDir, cwd, sessionID string) string

TranscriptPathIn returns one session's transcript path under an explicit config dir, or "" when the directory cannot be resolved or either remaining argument is empty. It takes the directory for the same reason ProjectDirIn does: a caller that already resolved one must not be at the mercy of a concurrent Setenv, and tests need no environment at all.

This is the ONLY place the session-id-to-filename join lives. ReadDetailIn used to inline its own copy, which left the exported spelling below with no production caller and its test certifying a join nothing ran.

It does NOT validate sessionID, and deliberately so — it is a path constructor, not a gate. filepath.Join CLEANS a traversal rather than refusing it, so a caller building a path from untrusted input must reject separators and ".." itself first. ReadDetailIn does exactly that before it calls here; any new caller owes the same check.

Types

type Detail

type Detail struct {
	// ID is the session UUID the detail describes.
	ID string
	// FirstPrompt and LastPrompt are the first and last prompts the user
	// typed, sanitized but NOT collapsed to one line: the detail view renders
	// them as paragraphs. Either may be empty.
	FirstPrompt string
	LastPrompt  string
	// UserPrompts counts entries the user typed. Tool results are recorded as
	// type "user" too, so they are excluded by the same promptSource=="typed"
	// test the title scan uses — counting raw user entries would report
	// hundreds for a conversation with a few dozen actual prompts.
	//
	// Deliberately the ONLY count reported. An assistant-entry count was tried
	// and dropped: measured against real transcripts it reads 11638 against 87
	// prompts, because every tool call is its own entry — a number no human
	// reads as "replies". Dropping it also made the scan ~20× faster, since
	// confirming those entries meant unmarshaling the bulk of the file.
	UserPrompts int
	// Started is the timestamp on the transcript's first dated entry; zero when
	// none of the opening entries carried one.
	Started time.Time
	// Modified is the file's mtime — when the session was last active.
	Modified time.Time
	// SizeBytes is the transcript size on disk.
	SizeBytes int64
}

Detail is the on-demand deep read of a single transcript, answering "is this the conversation I want to resume?" without opening it. Unlike List — which head-reads every transcript in a directory — this reads one file end to end, so it is issued per user request, never per listing.

func ReadDetail

func ReadDetail(ctx context.Context, cwd, sessionID string) (Detail, error)

ReadDetail reads one session's transcript for cwd and summarizes it.

sessionID is a filename component, so it is validated here as well as at the IPC boundary: this package is importable on its own, and a caller that passes through an unchecked id would otherwise turn "../../.ssh/id_rsa" into a read outside the project directory. Unlike List, a cancelled ctx here is an ERROR rather than a degraded result. ReadDetail answers about one specific highlighted session, and a partial read would silently under-report that session's prompt count — a wrong number the user has no way to distinguish from a right one. An empty panel is honest; a truncated one is not.

func ReadDetailIn added in v1.62.5

func ReadDetailIn(ctx context.Context, configDir, cwd, sessionID string) (Detail, error)

ReadDetailIn is ReadDetail against an explicit config directory. ReadDetail resolves one from the environment and delegates here.

type Session

type Session struct {
	// ID is the session UUID — the transcript's filename stem, and the value
	// passed to `claude --resume`.
	ID string
	// Title is the first prompt the user typed in this session, sanitized and
	// truncated. Empty when no typed prompt was found in the scanned head.
	Title string
	// Modified is the transcript file's modification time, i.e. when the
	// session was last active.
	Modified time.Time
}

Session is one Claude Code transcript discovered for a working directory.

func List

func List(ctx context.Context, cwd string) (sessions []Session, truncated bool, err error)

List returns the sessions recorded for cwd, newest first, capped at MaxSessions. truncated reports whether older sessions were dropped by that cap, so callers can say so rather than inferring it from a full-looking result (a directory holding exactly MaxSessions is not truncated).

A CWD Claude has never been run in has no project directory; that is a normal state, not an error, and yields an empty slice with a nil error. Individual unreadable transcripts are skipped rather than failing the whole listing. A cancelled ctx is treated as one more way to end up with fewer sessions: it yields what was gathered, with a nil error. The listing feeds a pane-creation dialog that must never be blocked by discovery, so surfacing cancellation as a failure there would be a downgrade.

func ListIn added in v1.62.5

func ListIn(ctx context.Context, configDir, cwd string) (sessions []Session, truncated bool, err error)

ListIn is List against an explicit config directory. List resolves one from the environment and delegates here.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL