When working across multiple Claude Code sessions, it's easy to lose track of which ones are waiting for your response. There's no ambient signal — you have to check each session manually.
A keyboard-driven TUI that lists Claude Code sessions awaiting your reply, so you can quickly jump into the one that needs attention without hunting through multiple windows.
Claude Code stores session data locally:
- Session metadata:
~/.claude/projects/{project-name}/{sessionId}.jsonl - Default context:
~/.claude/projects/-/{sessionId}.jsonl(or similar, for non-project conversations)
Each .jsonl file is a line-delimited JSON log. Each line is a message with:
type:"user"|"assistant"|"ai-title"| etc.message.role:"user"|"assistant"sessionId: unique identifier for the sessiontimestamp: ISO 8601 stringmessage.content: the message text or structured content
Since Claude always responds, all active sessions are considered "pending" by default. A session is marked as dismissed when its sessionId appears in the dismissal log.
Location: ~/.claude/session.log
Simple text file, one sessionId per line. Sessions whose IDs appear here are hidden from the TUI.
-
From within a Claude Code session: User types
/clear→SessionEndhook fires → logs thesessionIdto~/.claude/session.log- Hook configuration (in
~/.claude/settings.json):{ "hooks": { "SessionEnd": [ { "matcher": "clear", "hooks": [ { "type": "command", "command": "python -c \"import sys,json; print(json.load(sys.stdin)['session_id'])\" >> ~/.claude/session.log" } ] } ] } } - The hook reads session context from stdin as JSON and appends the
session_idvalue (one per line) to~/.claude/session.log
- Hook configuration (in
-
From the TUI: User presses
don a session → TUI appendssessionIdto~/.claude/session.log
A scrollable list of all non-dismissed sessions, sorted by most recent activity (timestamp descending).
Each row displays:
- Project name (derived from directory; "-" for default context)
- Session title (from
ai-titlefield in the JSONL, or first user message, truncated to ~40 chars) - Time since last message (e.g., "2h ago", "just now")
- Last message preview (first line, truncated to fit, from last
assistantmessage)
Example:
┌─ Pending Claude Sessions (3) ────────────────────────────────────────┐
│ │
│ > c--tools-agent [Refactor auth module] 2h ago │
│ "I've outlined three approaches for splitting the..." │
│ │
│ my-project [API endpoint design] 5h ago │
│ "Here's the full implementation. Ready to code?" │
│ │
│ - [Untitled] 1d ago │
│ "That looks good. What should we handle next?" │
│ │
└───────────────────────────────────────────────────────────────────────┘
[j/k] navigate [Space] preview [Enter/o] open [d] dismiss [r] refresh [q] quit
If no sessions are pending: All caught up.
| Key | Action |
|---|---|
j / k or ↑ / ↓ |
Navigate list |
Enter / o |
Open session in Claude Code (claude --resume {sessionId}) |
Space |
Toggle inline preview pane |
d |
Dismiss session (append to ~/.claude/session.log) |
r |
Refresh / re-scan conversation files |
q / Ctrl+C |
Quit |
Pressing Space on a session opens a preview panel below the list showing recent messages (last ~5 exchanges). Display as a scrollable conversation thread (you/Claude alternating). No reply capability from TUI.
┌─ Preview: c--tools-agent / Refactor auth module ──────────────────────┐
│ You: Can you suggest how to split the auth module? │
│ │
│ Claude: I've outlined three approaches for splitting the module... │
│ 1. Extract into a dedicated auth/ directory... │
│ 2. Keep co-located but split by concern... │
│ 3. Full service extraction with an HTTP boundary... │
│ │
│ You: Let's go with option 2. │
│ │
│ Claude: Good choice. Here's how to refactor... │
└────────────────────────────────────────────────────────────────────────┘
[Esc] close preview [↑/↓] scroll [o] open in Claude Code
Pressing o or Enter runs:
claude --resume {sessionId}This opens the session in Claude Code, returning focus to the user. They can now:
- Reply to Claude
- Use
/clearto dismiss when done (triggers theSessionEndhook) - Work as normal
- On launch: scan all
.jsonlfiles in~/.claude/projects/and subdirectories - On
rkey: re-scan - No auto-refresh (user manually restarts TUI when needed)
Language: Python 3.10+
TUI Library: Rich (for rendering) + Textual (for interactive TUI framework)
Package Manager: uv for running and dependency management
Project Structure:
agent-dashboard/
├── pyproject.toml # uv project config
├── spec.md # this file
├── main.py # entry point
└── src/
├── parser.py # JSONL parsing, session loading
├── ui.py # Textual app and views
└── dismiss.py # Dismissal log management
Parsing:
- Scan
~/.claude/projects/recursively for.jsonlfiles - For each file, read all lines and identify the
sessionIdandai-title - Extract the last
assistantmessage for preview - Compute elapsed time from the last message's
timestamp
Dismissal Logic:
- Read
~/.claude/session.logon startup and after each dismiss - Filter out any session whose
sessionIdappears in the log - When user presses
d, appendsessionId\nto the file
Users will need to add a SessionEnd hook to ~/.claude/settings.json:
{
"hooks": {
"SessionEnd": [
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "python -c \"import sys,json; print(json.load(sys.stdin)['session_id'])\" >> ~/.claude/session.log"
}
]
}
]
}
}Claude Code passes session context to the hook via stdin as a JSON object. The inline Python reads that JSON, extracts session_id, and appends it to ~/.claude/session.log. The matcher: "clear" ensures the hook only fires when /clear is used.
- Replying from within the TUI
- Searching/filtering sessions
- Starred or pinned sessions
- Custom session ordering
- Archiving vs. deletion distinction
Decision: Session ID passed via stdin as JSON — {"session_id": "..."} (and other fields)
Rationale:
- Claude Code's hook mechanism passes context to hook commands via stdin as a JSON object
- The
session_idfield is extracted with an inline Python one-liner; no external script needed - The
matcherfield on the hook entry scopes the hook to/clearonly, avoiding spurious writes
Decision: claude --resume {sessionId}
Confirmed. This opens the session in Claude Code with full context restored.
Options:
| Option | Approach | Pros | Cons |
|---|---|---|---|
| A (Recommended) | Single text file: ~/.claude/session.log |
Simple, transparent, auditable, human-readable | Append-only (grows over time, needs occasional cleanup) |
| B | Per-session marker: .dismissed file in each session dir |
Atomic, easy to undo (delete marker), scattered state | Harder to audit globally, file system sprawl |
| C | Structured JSON: ~/.claude/dismissed-sessions.json |
Can include metadata (time, reason), structured | JSON parsing overhead, more complex |
| D | Organized text: ~/.claude/state/dismissed.txt |
Same as A but organized | Just organizational (functionally identical to A) |
Recommendation: Option A (~/.claude/session.log)
Why:
- Simplicity: one plain text file, one session ID per line
- Transparency: users can inspect and manually edit if needed (e.g., undo a dismissal)
- Auditability: full history of all dismissed sessions
- Low overhead: no parsing, just line-by-line reads
- Unix philosophy: simple tools, simple formats
- If growth is a concern, users can archive old entries (rotate the log)
The dismissal log is the source of truth: on every TUI start and refresh, read this file and filter those session IDs from the active list.