Skip to content

Latest commit

 

History

History
280 lines (211 loc) · 11.5 KB

File metadata and controls

280 lines (211 loc) · 11.5 KB

cc-sessions

Agent-focused documentation. See README.md for user-facing docs.

Build & Test

just build    # Build release binary
just test     # Run tests
just install  # Build and install to ~/.local/bin (macOS signing if applicable)
just lint     # Run clippy

Architecture

Code Organization

src/
  main.rs                   # CLI orchestration, display, skim integration
  session.rs                # Session domain model (Session, SessionSource)
  claude_code.rs            # Claude Code JSONL loading/parsing
  message_classification.rs # Shared user-message classification rules
  interactive_state.rs      # Pure reducer for interactive state transitions
  remote.rs                 # Remote sync config + SSH/rsync operations

Boundary principle: If Claude Code changes its storage format, changes should be isolated to claude_code.rs. Session domain types live in session.rs; interactive navigation/search transitions live in interactive_state.rs; shared message filtering rules live in message_classification.rs.

Module Responsibility Changes when...
main.rs CLI args, high-level orchestration, output formatting UI/UX flow or command behavior changes
session.rs Session model and source typing Session field/source semantics change
claude_code.rs JSONL reading, metadata extraction, scan/search text generation Claude Code format changes
message_classification.rs Shared classification for first prompt + turn counting User-content filtering rules change
interactive_state.rs Interactive reducer (Esc, Ctrl+S, arrows, Enter) Navigation/search state machine changes
remote.rs Remote config loading and sync summaries SSH/rsync behavior or remote policy changes

Session Storage Structure

~/.claude/projects/
  -Users-you-project-a/
    abc12345-1234-1234-1234-123456789abc.jsonl   # Session transcript (UUID filename)
    def45678-5678-5678-5678-567890123def.jsonl
  -Users-you-project-b/
    ghi78901-9012-9012-9012-901234567890.jsonl

Sessions are identified by UUID filenames. Non-UUID files (like agent-*) are filtered out.

Session JSONL Message Types

flowchart LR
    subgraph "Session .jsonl file"
        direction TB
        U1[user] --> A1[assistant]
        A1 --> U2[user]
        U2 --> A2[assistant]
    end

    subgraph "Message Content"
        direction TB
        UM["user.message.content<br/>─────────────────<br/>string | ContentBlock[]"]
        AM["assistant.message.content[]<br/>─────────────────<br/>• type: text<br/>• type: thinking<br/>• type: tool_use"]
    end

    subgraph "Other Entry Types"
        SUM[summary]
        CT[custom-title]
        SYS[system]
    end

    U1 -.-> UM
    A1 -.-> AM
Loading

Implementation Details

Session Discovery

All metadata is extracted directly from .jsonl files (no index dependency):

  1. Walk ~/.claude/projects/*/ for .jsonl files
  2. Validate filename is a UUID (8-4-4-4-12 hex format)
  3. Single-pass scan collects: cwd, first user message, forkedFrom, turn count, last summary/custom-title/tag entries, skip flags
  4. Timestamps from filesystem (created, modified)
  5. Filter out sidechain/teammate sessions and empty sessions (no cwd, no user message, no summary)

Uses rayon for parallel processing across files.

UUID Validation

Session filenames must match UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

This filters out:

  • agent-* files (subagent sessions spawned by Task tool)
  • Other non-session files

Metadata Extraction

All fields come from a single full-file pass with a reused line buffer. The first HEADER_SCAN_LINES (16) lines are parsed fully to capture session-level metadata (cwd, forkedFrom, isSidechain, teamName — stamped on every entry, so reliably present early). After that, a SIMD-accelerated byte scan (memchr::memmem) skips lines that don't mention a content-bearing "type": (user/assistant/summary/ custom-title/tag), avoiding JSON parsing for the ~99% of lines that are progress/attachment chatter in long-running sessions.

Field Source Selection
project_path cwd field First occurrence
first_message First user entry passing filters First occurrence
forked_from forkedFrom.sessionId field First occurrence
summary summary type entry Last well-formed occurrence
name (customTitle) custom-title type entry Last well-formed occurrence
tag tag type entry Last occurrence; empty string clears
skip isSidechain:true or teamName present Early return on match
created / modified Filesystem metadata.created() / .modified()

Summary and custom-title entries can appear anywhere (compaction mid-session, /rename at any point), so last-wins is the correct semantic.

Entry-level skips (processed for cwd/forkedFrom but excluded from first-prompt, turn count, and search text):

  • isMeta:true — synthetic messages (attachment context, proactive ticks)
  • isCompactSummary:true — post-compaction summary text

Session Names (customTitle)

  • Set via /rename command in Claude Code
  • Stored as {"type":"custom-title","customTitle":"...","sessionId":"..."}
  • Displayed with prefix: ★ name - summary
  • Indicates user-marked important sessions

Forked Sessions

Sessions can be forked via /fork command or claude --fork-session. A fork creates a new session file that copies the conversation history up to a point, allowing the user to explore an alternate path.

Fork Relationship Structure

Parent Session (511623e7-...)
  ├── Fork A (c33eb693-...)    # forkedFrom: 511623e7-...
  └── Fork B (cc0f0043-...)    # forkedFrom: 511623e7-...

Each entry in a forked session contains a forkedFrom field:

{
  "type": "user",
  "message": {...},
  "sessionId": "cc0f0043-...",
  "forkedFrom": {
    "sessionId": "511623e7-...",      // Parent session ID
    "messageUuid": "23ede6bc-..."     // Corresponding message in parent
  }
}

Key Points

Aspect Behavior
Storage Fork is a separate .jsonl file with full conversation copy
Relationship forkedFrom.sessionId on entries links to parent
Index sessions-index.json does NOT store fork relationships
customTitle Claude Code auto-appends "(Fork)" to forked session titles
Detection Must extract forkedFrom from JSONL file head (first ~50 lines)

Extraction Strategy

  1. Read first ~50 lines of each session file
  2. Look for forkedFrom.sessionId field on any entry
  3. Build parent→children map after loading all sessions
  4. A session without forkedFrom is a "root" session
  5. Multiple forks can share the same parent (siblings)

Interactive Mode

Uses embedded skim crate (no external fzf dependency):

  1. Build SkimOptions with preview command pointing to self
  2. Send SessionItems through crossbeam channel
  3. Preview generates content directly (no subprocess)
  4. On selection, spawns zsh -c "cd <project> && claude -r <session-id>"

Fork Navigation (Subtree Drill-down)

Interactive mode uses a navigation stack for exploring fork trees:

  • Root view: Shows only root sessions (those without a parent in the session set)
  • indicator: Session has child forks
  • indicator: Focused parent (distinguishes from children with forks)
  • → (right arrow): Drill into selected session, showing parent + direct children only
  • ← (left arrow): Go back to previous view (pop from stack)
  • Esc: Return to root view (clear entire stack)
Root View                    After → on e5e2d           After → on 398eb
─────────────────────        ─────────────────────      ─────────────────────
▶ e5e2d  (parent)            ▷ e5e2d  (focused)         ▷ 398eb  (focused)
  f1064  (no forks)          ▶ 398eb  (child w/forks)     5b88a  (direct child)
  ...                          c0ff2  (child)             c0ff2  (direct child)

Note: Each drill-down shows only direct children, not all descendants. To see grandchildren, drill into the child session.

Transcript Search (Ctrl+S)

Ctrl+S performs literal full-text search across session transcripts and replaces the view with matching results. Search respects active filters (-r, -p) — it only searches sessions already loaded in the picker, not the entire filesystem.

  • Search: Type query in filter prompt, press Ctrl+S to search
  • Results: List shows only sessions containing the search pattern
  • Preview: Matching lines highlighted in preview pane
  • Esc: Clears search, returns to previous view (root or subtree)

What gets searched: Only user/assistant message content. Tool outputs, system messages, and JSON metadata are excluded. This ensures search results match what the preview shows.

Design choice: Search replaces the view temporarily rather than filtering within the current subtree. This ensures you can find any session regardless of navigation state. The search results persist until explicitly cleared with Esc.

Performance note: The lowercase transcript index is built on a background thread after the picker renders (not during discovery), so startup stays fast and list mode never pays the memory cost. First Ctrl+S joins the thread — typically already finished by the time the user has typed a query.

Normal View                  After Ctrl+S "api"
─────────────────────        ─────────────────────
▶ session-a                  search: "api" (3 matches) │ esc to clear
  session-b                  session-b    (has "api")
▶ session-c                  session-d    (has "api")
  session-d                  session-f    (has "api")

Navigation while searching:

  • Subtree navigation (← →) is disabled during search mode
  • Enter still selects the session to resume
  • Esc clears search before clearing subtree focus

Column Layout

Interactive mode displays a header with column legend:

  CRE  MOD  MSG SOURCE PROJECT      SUMMARY
Column Description
CRE Created timestamp (relative)
MOD Modified timestamp (relative)
MSG Turn count (user messages, excludes system content)
SOURCE Session source (local, remote)
PROJECT Project directory name
SUMMARY ★ name > #tag > summary > first message

Turn Counting

The MSG column shows actual user turns, filtering out system-generated content:

  • Entries with isMeta:true or isCompactSummary:true
  • Known system tag prefixes (<command-...>, <local-command-...>, <bash-...>, <ide_...>, <tick>, etc.) — but NOT arbitrary <text> like <Button>
  • [...] bracketed content
  • /... slash commands

Debug Mode

--debug flag adds a 5-character session ID prefix column for debugging navigation.

Dependencies

  • Build: Rust 1.88+ (edition 2024)
  • Runtime: None (skim is embedded, preview is self-contained)

Key crates

Crate Purpose
skim Embedded fuzzy finder (replaces fzf)
rayon Parallel file processing
serde_json JSONL parsing
memchr SIMD substring search for the line prefilter
clap CLI argument parsing