On this page
Joins every profile's session registry with the machine-wide lifecycle store into one scrolling table, and marks, prunes and re-reads what it lists.
#claudewheel.sessions_overview
#claudewheel.sessions_overview
Every Claude Code session on this machine, on one framed scrolling table.
The screen joins the two things that know about a session and neither of which knows all of it:
- Claude Code's own per-process registry (:mod:
claudewheel.session_registry),
which is exact while a process lives and says nothing once it is gone -- and is per profile, so reading one profile's registry shows one profile's work;
- claudewheel's lifecycle store (:mod:
claudewheel.lifecycle), which is
machine-wide and outlives every process, but knows only what was recorded.
So the table is gathered across EVERY profile the workspace discovers (the vanilla default profile included) and every session the lifecycle store has a file for, and each row's state is :func:claudewheel.lifecycle.derive_state over both answers, with observation beating the record.
Gathering writes ----------------
Opening the screen is not a read-only act, and deliberately so. Two writes happen while gathering, both idempotent and both through the lifecycle store's own doors:
- :func:
claudewheel.lifecycle.sweep_crashedrecords anendedfor a session
that has a started, no end, no live process and is past the grace period. Nothing else would ever notice that session died.
- :func:
claudewheel.lifecycle.capture_namecopies a live session's display
name out of the registry, which is the only place it exists, into the store that outlives it.
Snapshot, never a poll ----------------------
The gather happens when the screen opens and again only when the user asks -- the refresh key, or any key that changed the world (a prune, a mark). Uptimes are measured against the clock of that gather, so an untouched screen is internally consistent instead of half-live, and nothing renumbers rows under a cursor someone is moving through.
Drawing -------
:mod:claudewheel.sessions_table owns the whole layout and emits styled spans; this module is the only place a style name becomes an escape sequence, and every colour it uses comes from the theme's sessions section.
#OverviewOutcome
What the screen changed while it was open.
pruned is every registry record whose file it deleted, marked how many mark events the user wrote, and swept how many ended events the gathering passes recorded for sessions that died without one.
#style_sequence
def style_sequence(theme: ThemeColors, style: str) -> strThe escape sequence style is drawn in, under theme.
The only place a style name from :mod:claudewheel.sessions_table becomes colour. BOLD and DIM are the two attributes applied directly: they say "this one wants you" and "this one is over" on top of whatever hue the theme gave the state, which no single colour can do.
A name this function does not know raises :class:ValueError: a misspelled style is a bug in the layout, and drawing it in the plain row colour would hide it behind a screen that merely looks slightly wrong.
#draw
def draw(terminal: Terminal, theme: ThemeColors, frame: Frame, *, footer: str, message: bool, rows: int, cols: int) -> NoneDraw frame over a cleared screen, with footer on the last row.
The footer is clipped one column short of the terminal's width: a line that filled the last cell of the last row would leave the cursor in a pending wrap, and the next write would scroll the screen the frame was just drawn onto.
#_verified
def _verified(record: SessionRecord) -> boolWhether record's process identity could actually be checked.
Both halves of the phantom filter must have answered: the record carries a kernel start token, and the kernel still offers one for that pid. Where either is missing the process may be the recorded one or may be whatever took over its number, and the row says so rather than picking.
#_recordable
def _recordable(session: str | None) -> boolTrue when session can be written to the lifecycle store.
A lifecycle file is named after its session, so a registry record carrying something that is not a session uuid is read but never written about.
#_row_for_record
def _row_for_record(record: SessionRecord, life: SessionLifecycle | None, *, profile: str, config_dir: Path, rss_kib: int | None, identity: SessionIdentity | None, now_ms: int) -> SessionRowOne table row from a registry record, filled out from its lifecycle.
The registry is the authority on everything it carries; the lifecycle supplies what a registry file has never held -- the model and the transcript path -- and stands in for a name the record lost.
#_row_for_lifecycle
def _row_for_lifecycle(life: SessionLifecycle, *, profiles: dict[str, str], now_ms: int) -> SessionRowOne table row for a session no registry record answers for.
Its process is gone (or was never registered under a profile this workspace knows), so everything comes from what the store recorded, and the Kind cell says so: nothing ever wrote down what kind of session it was.
#gather_rows
def gather_rows(workspace: Workspace, *, now_ms: int, identity: SessionIdentity | None) -> tuple[list[SessionRow], int, int]Read the whole machine into sorted rows; return them with what was written.
The two counts are the two writes the pass performs: how many crashed sessions it recorded an end for, and how many live names it copied into the lifecycle store. When either wrote something the store is read again, so the rows show the file as it now stands rather than as it was a moment before.
#_crashed_records
def _crashed_records(rows: Iterable[SessionRow]) -> list[SessionRecord]The registry files the prune key offers up, from the rows on screen.
#_refocus
def _refocus(rows: Sequence[SessionRow], session: str | None, focus: int) -> intWhere the focus belongs after a re-gather, given what it was on.
By session id rather than index: rows come and go between gathers, and an index would silently land the focus on a different session. A session that is no longer listed falls back to the clamped index.
#_row_key
def _row_key(row: SessionRow) -> str | NoneWhat identifies row across a re-gather, or None when nothing does.
The session id where there is one; otherwise the path of the registry file the row was read from, which is what a row with no session id has instead. A row with neither cannot be followed, and the caller drops it rather than matching it to a neighbour.
#_re_expand
def _re_expand(rows: Sequence[SessionRow], key: str | None) -> int | NoneWhich row the details belong to after a re-gather, given whose they are.
By the same kind of key the focus is kept by, and for the same reason: the details are one session's, so an index would leave them under whichever session the re-gather put in that slot. A session that is no longer listed collapses.
#run_overview
def run_overview(workspace: Workspace, *, theme: ThemeColors, terminal: Terminal, clock: Callable[[], int], identity: SessionIdentity | None, home: str) -> OverviewOutcomeShow the machine's sessions until the user leaves, and report what changed.
Up and down move the focus, page up and down move it by a window, home and end jump to the ends, left and right scroll the columns, Enter expands the focused row into its details, a reveals what is finished, p prunes the registry files of the rows that crashed, r gathers again and m enters mark mode, where one more key records the user's own word about the session (on hold, blocked, done, or cleared). Every other key is ignored rather than doing something adjacent.
The frame is rebuilt at the terminal's current size on every draw, so a resize reflows it and a window too small for the table draws the prefix that fits instead of raising.