use super::*;
#[derive(Debug, Args)]
#[command(
about = "List sessions with first/last genuine-user + last-agent message and timestamps",
long_about = "List sessions with a fast quick-identity tuple per session, WITHOUT \
parsing the whole file. For each session jsonl under the target(s) it emits: \
session id, the FIRST genuine-user message (+ time), the LAST genuine-user \
message (+ time), the LAST agent message (+ time), plus the decoded cwd / git \
branch / CC version. `git_branch`/`version` are LAST-seen (what the session is \
on NOW - a session that upgraded or switched branches mid-flight reports the \
current value, with the opening value in `*_first` and a `first->last` drift \
arrow in text); `cwd` stays FIRST-seen on purpose (the record cwd follows the \
tracked shell cwd, so last-seen could be a transient subdirectory - the \
session's home is the opening value). A forward HEAD read finds the first user; a backward \
TAIL read finds the last user/agent; neither parses the full file, so it \
stays fast on 200 MB+ transcripts. Files are scanned in parallel across the \
corpus, then sorted for deterministic output.\n\n\
A PATH is either a real cwd (path-encoded for you) or an already-encoded \
`-Users-...` token; with no PATH, every project is listed. Note: genuine-user \
excludes tool_result carriers, isMeta pseudo-turns (\"Continue from where you \
left off.\"), and compaction summaries.",
after_help = "EXAMPLES\n \
csift list # every session, all projects\n \
csift list . # sessions for the current cwd's project\n \
csift list /Users/testuser/Projects/widget_app_prototype # a real path (gets encoded)\n \
csift list -Users-testuser-Projects-widget-app-prototype # a pre-encoded dir token\n \
csift list ~/.claude/projects/-Users-testuser-Projects-widget-app-prototype\n \
csift list @<uuid> --no-subagents # JUST the one top-level session row\n \
csift list --format json . # machine-readable index\n\n\
SCOPE: because the default SPANS subagents, a `csift list @<uuid>` can return 1 \
top-level + N subagent rows. The text output then leads with a `scope N sessions in \
scope (1 top-level + M subagent)` banner, brands each subagent row \
`SUBAGENT <hex> · parent SESSION <uuid>` (a bare hex is NOT a re-feedable target; \
re-feed the parent uuid), and a top-level row keeps the plain `SESSION <uuid>` header.\n\n\
JSON SCHEMA (per --format json)\n \
The standard envelope v2 (same as every command): a `{kind:\"header\", \
command:\"list\", sessions_in_scope, top_level_sessions, subagent_sessions}` line, then \
ONE `{kind:\"session\", …}` row per session: {session_id, is_subagent, \
parent_session_id, path, cwd, git_branch, git_branch_first, git_branch_last, version, \
version_first, version_last, first_user, last_user, last_agent, \
skipped_lines, sidecar_present, pending_elicitations, with_elicitation_sidecar, \
minted_by, cleared_from, cleared_from_distance_ms, cleared_from_candidates, \
continued_in, session_kind, session_kind_first_line, session_kind_last_line, \
lineage_scanned}, then \
a closing `{kind:\"summary\", sessions, skipped_lines, \
dropped_by_cap}`. `is_subagent` flags a bare-hex subagent row; `parent_session_id` is \
the re-feedable owning uuid (= session_id for a top-level row); never re-feed a \
subagent `session_id`. The `first_user`/`last_user`/`last_agent` fields are {excerpt, \
ts_utc, ts_local} sub-objects (or null when absent). `dropped_by_cap` > 0 when the \
unscoped-flood cap trimmed rows (the most-recent 50 are kept).\n\n\
CLEAR LINEAGE (an inference, and the row says so)\n \
A `/clear` mints a new session id inside the same process and writes no lineage \
anywhere on disk, so a cleared transcript names no predecessor. csift reads two \
facts instead: a transcript whose FIRST non-isMeta user record is the `/clear` \
wrapper was minted by that clear (`minted_by:\"clear\"`, text row `cleared`), and its \
predecessor is the sibling in the same project directory whose cost-ledger \
checkpoint (`startTime + totalDuration` on a `cost-state` line) closes within \
2000 ms of the wrapper's timestamp (`cleared_from`, with the distance in \
`cleared_from_distance_ms`). The nearest line wins; if two different files tie, \
both are reported in `cleared_from_candidates` and NEITHER is joined. Never a file \
mtime, and never the time adjacency of ordinary records - two busy sessions share \
those.\n\n\
BACKGROUND HANDOFF (`continued_in`, text row `handoff`) - a STATED fact\n \
When a session is handed to a background child (the left-arrow gesture, or the \
background-fork command), Claude Code appends a `type:\"continued-in\"` line to the \
PARENT transcript naming the child's session id. Unlike the clone and clear \
lineage above, nothing is inferred here: the harness writes the child id itself, \
and csift reports it verbatim in `continued_in` (text: `handoff continued in \
<first8> ...`). The line carries four keys and no `uuid`, so it is not a chain \
node and its only address is its jsonl line. An in-place `/fork` writes no such \
line, so its absence never means the session was not forked.\n \
WINDOW LIMIT: the line is written after the last conversation record, so the \
backward tail read normally reaches it. The tail read STOPS once it has both of \
its anchors - the last genuine-user and the last agent message - so a later \
`--resume` of the parent that appends both of those below the line pushes it \
outside the window and `continued_in` reads null. (A transcript missing one \
anchor is walked to the top, so nothing is lost there.) Two whole-file answers \
exist: `--lineage` below, and the line-type census `csift stats` prints \
(`continued-in` is one of its `types` keys).\n\n\
LANE LINEAGE (`session_kind`, text row `lane`)\n \
Every record a BACKGROUND lane writes carries a top-level \
`sessionKind:\"bg\"`; a foreground lane's records carry no such key at all, so \
`session_kind` is an EMPTY array for an ordinary session rather than a sentinel \
value. It is an array because the stamp is a per-PROCESS fact: one transcript can \
hold records from a background lane and, below them, records a foreground resume \
of that same session appended without the key. That is also why the SPAN needs \
`--lineage`: the stamp can stop mid-file and no head or tail window can see \
where. With the flag, `session_kind_first_line` / `session_kind_last_line` give \
the first and last carrier's jsonl line and the row reads `lane bg on \
L<a>..L<b>`; without it both are null, the row says the span needs the flag, and \
`lineage_scanned` tells a consumer which of the two it is looking at.\n\n\
SKIPPED_LINES SEMANTICS (window census, NOT a whole-file verdict)\n \
`list` reads only the head/tail lines it needs (the §7 fast-overview contract; it \
never scans the middle of a transcript), so its `skipped_lines` counts malformed lines \
among the LINES READ: 0 means \"the lines read were clean\", and a mid-file tear is \
OUTSIDE the windows by design. The full-scan corruption census over the same bytes is \
`csift stats` (every full-scan command, search/files/recover, agrees with it). \
Within the read windows every malformed line, and every sidecar marker the current \
schema cannot read, is booked exactly once. `--lineage` does NOT widen this count: \
its pass is a key walk with no record parse, so it has no parse failure to report \
and cannot book a line the windows already booked."
)]
pub struct ListArgs {
#[arg(
value_name = "PATH",
allow_hyphen_values = true,
value_parser = parse_project_target
)]
pub paths: Vec<PathBuf>,
#[arg(long = "sessions-from", value_name = "FILE|-")]
pub sessions_from: Option<std::path::PathBuf>,
#[arg(long, value_name = "N")]
pub max_count: Option<usize>,
#[arg(long, value_name = "WHEN")]
pub since: Option<String>,
#[arg(long, value_name = "WHEN")]
pub until: Option<String>,
#[arg(long = "no-subagents")]
pub no_subagents: bool,
#[arg(long = "subagents", conflicts_with = "no_subagents")]
pub subagents: bool,
#[arg(long = "lineage")]
pub lineage: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
pub format: OutputFormat,
}
impl ListArgs {
#[must_use]
pub fn want_subagents(&self) -> bool {
self.subagents || !self.no_subagents
}
}