I built a dashboard for my Claude Code sessions. A Textual TUI called ccboard. It shows all my workstreams at a glance — clones, branches, session counts, last activity. It works fine.
It’s also fundamentally wrong.
Not wrong like “has bugs.” Wrong like “the data model doesn’t match how I actually think about work.” And I didn’t realize this until I sat down to add features and couldn’t figure out where to put them.
Then I sat down to redesign it, figured out the real data model, wrote a feature wishlist — and built the entire thing in one session. This post started as a design exploration. It became a build log.
The scale of the problem
On any given day I have 15-25 active Claude Code sessions spread across a dozen yolo clones, a couple mango branches, and a few personal projects. Each clone lives in ~/mcode/ with a descriptive suffix — yolo-torch-c, yoloacr, yolo-dep, yolo-pyright-blog. Some of these are active. Some are experiments that died two weeks ago. Some are experiments that died two weeks ago but might come back.
ccboard v1 treats each clone directory as one row. Name, branch, last commit, session count, freshness. A flat table. The kind of thing you’d build if you thought about your work the way a filesystem thinks about your work.
Here’s why that’s wrong: yolo-torch-a, yolo-torch-b, and yolo-torch-c show up as three unrelated rows. In reality they’re three approaches to one task (system torch in Docker) within one project (Bazel torch migration). Two of them are dead. One landed as PR #21169. The flat model can’t express any of this. It’s like organizing your photos by filename instead of by event — technically complete, semantically useless.
Meanwhile yolo (main) shows up as a workstream. It’s not a workstream. It’s a reference copy. I pull from it. I never commit to it. But ccboard treats it the same as yoloacr where I’m actively shipping ACR fixes.
The discovery: three layers
Through the redesign conversation I realized the data model isn’t flat — it’s three layers that map onto each other:
Layer 1: Task management — the why
This is how I think about work. Projects contain tasks. Tasks may have multiple approaches (experiments), and only one approach wins. The Bazel torch migration is a project. “Get torch working in Bazel CI” is a task. Options A, B, and C were approaches. Option C won.
Layer 2: Filesystem / Git — the where
This is where work physically lives. A remote repo (yolo), a clone on disk (yolo-torch-c), a branch (experiment/torch-option-c), a PR (#21169). The filesystem is a projection of the task layer, but it’s not 1:1 — one task might span multiple clones, or one clone might host work for multiple tasks.
Layer 3: Claude Code — the who
This is the AI collaborator’s view. Claude Code has its own concept of “projects” (keyed by working directory), sessions (conversations), agents (main + subagents), context windows (ephemeral memory), skills, MCP servers. Each clone might have 5 sessions, and only one of them matters right now.
The three layers look like this:
-- Task Layer -----------------------------------------------
Project: "Bazel Torch Migration"
+-- Task: "System torch in Docker"
+-- Approach: Option C (active)
+-- Approach: Option A (dead)
+-- Approach: Option B (dead)
-------------------------------------------------------------
| maps to | maps to
v v
-- Git Layer ------------------- -- Claude Layer --------------------
Clone: yolo-torch-c CC Project: yolo-torch-c
Branch: experiment/ +-- Session: 810b6609...
torch-option-c | +-- Agent (main)
PR: #21169 | | +-- Subagent: Engineer
| | +-- Subagent: Architect
Clone: yolo-torch-a +-- Session: a3f21c...
Branch: experiment/ +-- Session: ...
torch-option-a
------------------------------- ------------------------------------
v1 only had Layer 2. The dashboard was a filesystem browser wearing a project management costume.
What this means for navigation
The big design question: what’s the primary navigation unit?
Option A: Clone-centric (v1 with lipstick). Keep directories as rows, add a “project” grouping column. Easy to build, but you’re still navigating by where things are, not why they exist. You’d group yolo-torch-a/b/c under “Bazel Torch Migration” but the mental model mismatch remains — the dead approaches sit right next to the winner with equal visual weight.
Option B: Task-centric. Navigate by project and task first. Drill into a task to see its approaches, then into an approach to see its clones/branches/sessions. This matches how I think about work. But it requires maintaining a task-to-clone mapping somewhere, and that mapping doesn’t exist yet. Someone has to write it. That someone is me. I already have 15-25 sessions to manage; adding metadata management on top is a tax.
Option C: Session-centric. Navigate by Claude Code session. Too noisy. Most sessions are dead context windows I forgot to close. The signal-to-noise ratio is terrible.
Option D: Hybrid with progressive enrichment. Start clone-centric (zero config). If a clone has task metadata, group it. If it doesn’t, show it as an orphan. Over time, as you tag things, the view becomes increasingly task-centric. The unmapped stuff stays visible but deprioritized.
I’m leaning D. It respects the reality that I won’t sit down and tag 25 clones on day one. The dashboard should work immediately (like v1 does) but get better as you tell it more. Progressive enrichment, not upfront configuration.
The metadata could live in a .ccboard.toml at the clone root, or in a central ~/.ccboard/projects.toml. Central is probably better — I don’t want to pollute work directories with dashboard config, and I’d need it to survive clone deletion.
What got built
The design exploration above was supposed to end with “here’s what I want to build someday.” Instead, everything shipped in one session. Here’s the feature list and what actually happened.
1. Archive repos
Dead clones everywhere. yolo-torch-a, yolo-torch-b, yolory — all superseded experiments sitting in ~/mcode/ pretending to be relevant. I wanted to select several in the TUI and bulk-move them to ~/mcode/archived/.
What shipped: archive.py — a full archive flow triggered by the A key.
The interesting part isn’t the shutil.move. It’s the safety checks that run before the move. check_unpushed() does three things:
git status --porcelain— dirty files?git rev-parse @{u}— does the branch even have an upstream?git log @{u}..— commits that haven’t been pushed?
And here’s the design choice that matters: git errors assume the worst. If git status times out, dirty=True. If the upstream check fails, no_upstream=True. Because the failure mode of “I thought it was clean but it wasn’t” is losing work, and the failure mode of “I thought it was dirty but it wasn’t” is a false warning you dismiss with Y. Those aren’t symmetric risks.
except (subprocess.TimeoutExpired, OSError) as exc:
dirty = True
details_parts.append("git status timed out or errored")
The confirm modal shows all the safety results — green checkmarks for clean repos, yellow warnings for dirty/unpushed ones. Y to proceed, N to bail. Batch mode works too: select multiple repos with Space, then A archives them all with one confirmation.
Collision handling: if ~/mcode/archived/yolo-torch-a already exists (because you archived it once, unarchived, archived again), it appends a datestamp. Same-day collision? Full timestamp. The edge cases are boring but they matter.
2. Batch open in iTerm
Select several clones, batch-open each one in a new iTerm tab, spawn Claude Code with --continue to resume the last session. The “I’m starting my work day” action.
What shipped: iterm.py — AppleScript automation via osascript.
script = f'''\
tell application "iTerm2"
activate
if (count of windows) = 0 then
create window with default profile
end if
tell current window
create tab with default profile
tell current session
write text {_applescript_string(cmd)}
end tell
end tell
end tell'''
Each tab gets cd /path/to/clone && claude --continue --dangerously-skip-permissions. The --dangerously-skip-permissions flag was an open question in the design — should it auto-approve tool calls? For the “opening my workday” use case, yes. You’re resuming known sessions in known directories. The friction of approving every file read across 5 sessions simultaneously is worse than the risk.
Two modes: batch_open_iterm() opens each workstream in its own tab, open_iterm_split() puts them in vertical split panes within one tab. There’s a 300ms delay between opens so iTerm can settle — without it, AppleScript races itself and tabs get created out of order.
Auto-detects iTerm at /Applications/iTerm.app. If it’s not there, the I keybinding tells you so instead of crashing.
3. Session knowledge extraction
This was the ambitious one. And the design doc got it exactly right — “session merge” is a misleading name for what you actually want, which is knowledge extraction + bootstrap.
What shipped: sessions.py — two features behind the S and M keys.
S (session detail): Shows all sessions for a project — even orphaned Claude projects with no directory on disk. Each session gets: ID, age, message count, file size, first user message (truncated to 60 chars). This alone is useful — before this, the only way to see session details was manually ls-ing JSONL files.
M (merge to bootstrap): Reads all JSONL sessions for a project, builds a compact summary of each, writes a unified SESSION-BOOTSTRAP.md:
def build_session_summary(session_path: Path) -> str:
# ...
lines = [
f"Session: {session_id} ({age_str})",
f"Messages: {total_messages} | Tools: {tool_count} | ~{approx_tokens_k}k tokens",
f"Started: {first_msg}",
f"Last: {last_msg}",
]
Each summary: session ID, age, message count, tool call count, rough token estimate (total_chars // 4000), first and last user messages. The token estimate is a lie, but it’s a useful lie — you mostly want “small / medium / huge,” not “exactly 47,293 tokens.”
Backs up existing SESSION-BOOTSTRAP.md before overwriting. The generated doc has an ## Action Items section at the bottom for you to fill in what the next session should focus on. Then you start a fresh session, run /user:recover, and the new context has a compressed version of everything that came before.
The original design doc predicted this: “The ‘merge sessions’ framing is misleading. What’s actually feasible is ’extract knowledge from N sessions, synthesize into bootstrap doc, start fresh.’” That prediction was exactly right. A fresh context window with good bootstrap context outperforms a bloated merged history every time.
4. The tmux fix
v1 had a tmux launch feature that quit after 5 seconds. The bug: subprocess.run(["tmux", "attach-session", ...], timeout=5). tmux attach-session is a blocking call that takes over the terminal — it’s supposed to run until you detach. The 5-second timeout was killing it.
The fix is os.execvp:
# Replace this process with tmux attach — no timeout, no subprocess.
# This never returns on success.
os.execvp("tmux", ["tmux", "attach-session", "-t", session_name])
execvp replaces the current process entirely. No Python sitting around waiting. The TUI exits, tmux takes over, and when you detach you’re back to your shell.
Inside tmux, it uses switch-client instead (you’re already in a tmux session, you just want to switch to the ccboard one). Outside tmux, it’s the execvp path. The detection is just os.environ.get("TMUX").
Both tmux and iTerm are supported now. Tmux for universality (works in any terminal), iTerm for macOS comfort (native tabs, no prefix keys). The I key goes to iTerm, Enter/L goes to tmux.
5. The Tab binding hack
The dual-view toggle is Tab. Simple, right? Except Textual uses Tab for focus cycling between widgets. Pressing Tab was moving focus from the DataTable to the filter input instead of switching views.
The fix: priority=True on the binding:
Binding("tab", "toggle_view", "Git/Claude", show=True, priority=True),
This tells Textual “this keybinding takes precedence over the framework’s built-in focus cycling.” One keyword argument, an hour of debugging to discover it was needed. The Textual docs mention priority bindings but don’t make it obvious that Tab is claimed by the focus system by default.
6. Help screen
? toggles a full help overlay showing all 16 keybindings grouped by category — Views, Navigation, Launch Sessions, Session Management, General. Esc or ? again to dismiss. Small feature, but without it the keybinding discoverability is terrible. You’d have to read the source code to know M does merge.
The dual-view solution
Instead of implementing the full three-layer model with progressive enrichment and central config files and all that design astronautics — I did something simpler that turned out to be smarter.
Two views. One keybinding. Tab to toggle.
Git/FS view starts from what’s on disk. 22 clone directories. Columns: icon, name, branch, age, sessions. This is the “where am I working” view — the thing v1 already did, just cleaner.
Claude Code view starts from what Claude knows about. 60 CC projects (18 matched, 42 orphaned). Columns: icon, name, session ID, token estimate, age, sessions. This is the “what does Claude remember” view. Orphaned projects get a † marker.
Why two views instead of one merged table
They overlap about 80%, but the edges are different and the edges are the interesting part:
- Git view shows repos with no Claude sessions — reference copies, fresh clones, stuff you just pulled and haven’t talked to Claude about yet
- Claude view shows projects with no repo on disk — deleted experiments, ClaudeProjects work, temp dirs that got
rm -rf’d last week - The columns that matter are different: branch is the thing you care about in git-land, token count is the thing you care about in Claude-land
A merged superset table would try to show everything and end up showing nothing well. Two focused views, each optimized for one question: “where is my code” vs “what does Claude know.”
The encoding problem
Here’s a fun one. Claude Code stores projects by encoding the working directory path — but the encoding isn’t just “replace slashes with dashes.” It also replaces dots. /Users/shiyuanzheng/mcode/yolo-torch-c becomes -Users-shiyuanzheng-mcode-yolo-torch-c. A dotfile like .claude becomes --claude (the dot becomes a dash, so you get a double dash).
Going the other direction is ambiguous. mai-agents could be mai/agents or mai.agents or literally mai-agents. You can’t decode reliably.
The solution: don’t decode. Match forward instead. Encode every known real path on disk, build a lookup table, and do a dictionary match. Paths that match a real directory get the full git metadata. Paths that don’t match anything — the orphans — get a best-guess display name.
def _encode_project_path(project_path: Path) -> str:
"""Encode an absolute path the way Claude stores project dirs."""
return "-" + str(project_path).strip("/").replace("/", "-").replace(".", "-")
# Build lookup from known paths
for clone_dir in scan_clones():
encoded = _encode_project_path(clone_dir)
lookup[encoded] = clone_dir
# Match Claude projects against lookup
for project_dir in PROJECTS_DIR.iterdir():
real_path = lookup.get(project_dir.name)
if real_path and real_path.exists():
# matched — enrich with git data
else:
# orphan — display name from encoded string
The lookup table also indexes extra directories beyond just ~/mcode/ clones — the home directory itself, .claude, .emacs.d, OneDrive-synced ClaudeProjects folders. Anything that might have a Claude Code project associated with it. Cast a wide net, match what you can.
Stats from real data
Running this against my actual ~/.claude/projects/:
- 60 Claude Code projects total
- 18 match existing directories on disk
- 42 are orphaned (deleted clones, old experiments, temp dirs)
That 42 number is wild. 70% of Claude’s project memory points to directories that don’t exist anymore. Deleted experiments with session history still intact. ClaudeProjects work that never had a git repo. Temp directories from one-off investigations. All invisible in a filesystem-first dashboard. One project — yoloyp — has 24 sessions, 1588 messages in the latest one, 460 tool calls, roughly 12k tokens estimated.
Progressive enrichment, confirmed
This validates the hybrid approach from the navigation design. Start with what’s auto-discoverable — 100% coverage from day one, no config files, no tagging ceremony. The Tab toggle IS the progressive enrichment. Git view for daily work (“which clone has my ACR fix”). Claude view for archaeology (“wait, when did I last work on that thing I deleted”).
Two views turned out to be less code and more useful than one clever merged table would have been. Sometimes the right abstraction isn’t a unified model — it’s two simple models with a keybinding between them.
The Claude Code data layer
The most interesting part of v2 is surfacing Claude Code internals. Things the session viewer now exposes:
- Message count — total JSONL lines per session. Proxy for “how much happened here.”
- Tool call count — assistant messages with
tool_usecontent blocks. Proxy for “how much doing happened” (vs. conversation). - Token estimate —
total_chars // 4000. A lie, but a calibrated one. Enough to distinguish “small planning session” from “massive implementation session.” - First and last user message — tells you what the session started doing and what it ended doing. Sometimes these are hilariously different.
- Session age — time since last JSONL entry. Falls back to file mtime if no parseable timestamps exist in the content.
Things I’d still want to surface:
- Active subagents — is this session running an Engineer? An Architect? (Parseable from tool_use messages in the JSONL)
- Session state — is it actively running, idle, or dead? (Harder — you’d need to check if the Claude Code process is still running, maybe via
pgrepor checking for a lock file) - Skills triggered — which custom commands were used? (Grep for
/user:patterns in the transcript) - MCP connections — which external tools are connected? (Available from the session’s settings, not from the transcript)
The JSONL format is straightforward. Each line is a JSON object with a type field — user, assistant, tool_use, tool_result. The content extraction handles multiple formats because the schema evolved over time — nested message.content, flat content, list-of-blocks content, display-field history entries. The _extract_text_from_content helper normalizes all of these.
The projects/ directory structure maps working directories to session groups. The directory name is the absolute path with slashes and dots replaced by dashes. So ~/mcode/yolo-torch-c becomes ~/.claude/projects/-Users-shiyuanzheng-mcode-yolo-torch-c/. Inside that: CLAUDE.md (project-specific instructions) and the session JSONL files.
The hidden goldmine in ~/.claude/
Here’s the thing nobody tells you: Claude Code keeps everything. Every prompt you’ve ever typed. Every file it edited, versioned. Every shell environment it snapshotted. Every debug log from every session. And it’s all just sitting there in ~/.claude/, in plain text, completely readable, totaling about a gigabyte on my machine.
I found this while building ccboard — I needed to understand the data layer, so I went spelunking. What I found was less “data directory” and more “forensic evidence locker.”
The inventory
| Path | What it is | Size/Count | Gold? |
|---|---|---|---|
history.jsonl |
Every user message ever sent | 938K, 3519 entries | ⭐⭐⭐ THE goldmine |
projects/ |
Per-project session transcripts | 556M, 60 project dirs | ⭐⭐⭐ |
debug/ |
Debug logs per session | 362M, 397 files | ⭐ verbose, has timing |
file-history/ |
Version history of files Claude edited | 15M, 156 session dirs | ⭐⭐ undo/audit trail |
shell-snapshots/ |
Shell environment snapshots | 11M, 77 snapshots | ⭐ |
session-env/ |
Environment variables per session | 232 dirs | ⭐ |
paste-cache/ |
Cached clipboard pastes | 504K, 68 files | meh |
settings.json |
Global settings (model, plugins, effort) | — | ⭐⭐ |
settings.local.json |
Permissions (auto-allowed tools) | — | ⭐⭐ |
memory/ |
Modular CLAUDE.md config files | 13 files | ⭐⭐⭐ (your brain) |
agents/ |
Subagent definitions | 8 files | ⭐⭐ |
commands/ |
Slash commands (custom skills) | 37 files | ⭐⭐ |
plans/ |
Plan mode artifacts | 19 files | ⭐ |
CLAUDE.md |
Top-level user config | — | ⭐⭐ |
skills/ |
Skill registrations | 8 dirs | ⭐ |
plugins/ |
Plugin configs | 7 dirs | ⭐ |
.git/ |
Yes, ~/.claude is a git repo |
— | ⭐⭐ |
That’s right. A gigabyte of your interaction history, sitting unencrypted on your filesystem, organized in readable JSON and markdown. No binary formats. No encryption. Just files.
history.jsonl — the real goldmine
This is the one. Every prompt you’ve ever sent to Claude Code, with millisecond timestamps, the working directory, and the session ID:
{
"display": "if i want to clone my github private repo...",
"pastedContents": {},
"timestamp": 1769478035417,
"project": "/Users/shiyuanzheng",
"sessionId": "53ee7fdc-9fbc-483d-847b-3d2734a0c127"
}
3519 entries on my machine. That’s every question, every instruction, every “fix this” and “what does this do” and “try option B instead.” It’s a complete history of how you use AI — what you ask, when you ask it, where you’re working when you ask it.
The project field is the working directory at time of prompt. The display field is the user-visible message text. pastedContents captures whatever was on your clipboard when you pasted. And timestamp is in milliseconds, not seconds.
That millisecond thing will bite you. I spent an embarrassing amount of time staring at dates in 2055 before realizing I needed to divide by 1000. If your timestamps look like they’re from the future, that’s why. JavaScript-style epoch milliseconds. The kind of gotcha that feels obvious in retrospect and costs you 20 minutes in practice.
projects/ — full session transcripts
This is where the bulk lives. 556 megabytes across 60 project directories. Each directory contains JSONL files — one per session — with the complete transcript. User turns, assistant turns, tool calls, tool results. Everything.
The directory names are encoded paths (see the encoding problem above). Inside each:
*.jsonl— session transcripts, one JSON object per line<uuid>/subagents/— subagent transcripts (agent-a2ad788.jsonl)- Each line has
type(user/assistant),message.content,timestamp
The subagent transcripts are particularly interesting. If you use subagents (Engineer, Architect, etc.), each one gets its own JSONL file nested under the parent session. You can reconstruct the entire delegation tree — who spawned what, what they found, how their output fed back into the main session.
file-history/ — Claude’s undo stack
Every file Claude edits gets versioned. The directory structure is file-history/<session-id>/, and inside you’ll find files named <hash>@v1, <hash>@v2, and so on. This is Claude Code’s internal undo history — every version of every file it touched, across every session.
15 megabytes across 156 session directories on my machine. That’s not a lot of space, but it’s a lot of information. If you’ve ever wondered “what exactly did Claude change in that file three sessions ago,” the answer is here.
ccboard doesn’t use this yet. But the potential is obvious: a “files touched” panel for each session, with diffs between versions. Time-travel debugging for AI-assisted edits.
shell-snapshots/ — your terminal frozen in amber
Files named snapshot-zsh-<timestamp>-<random>.sh. Full shell environment dumps: aliases, functions, environment variables. Claude captures these to understand your shell context when it runs commands.
77 snapshots on my machine. Each one is a complete picture of what your terminal looked like at that moment — what tools were available, what env vars were set, what aliases you had defined. Useful if you’re ever trying to reconstruct “what was my environment when that build broke?”
plans/ — auto-generated poetry
When Claude uses plan mode, it saves artifacts with auto-generated names following an adjective-verb-noun pattern. My plans directory contains gems like streamed-swinging-wilkinson.md and cosmic-shimmying-codd.md. It’s like a band name generator that read too many computer science papers.
The surprise: ~/.claude is a git repo
I saved the best for last. There’s a .git directory in ~/.claude/. Your entire Claude Code configuration has version history. git -C ~/.claude log shows the evolution of your CLAUDE.md, memory files, agent definitions, commands — everything.
This is accidental archaeology gold. You can git diff any two points in time and see exactly how your Claude Code setup evolved. When did you add that agent? When did you change that slash command? What did your CLAUDE.md look like three weeks ago before you rewrote it? It’s all there.
I don’t know if this is intentional or a side effect of Claude Code’s internal tooling. Either way, it’s genuinely useful. Your configuration is a codebase, and it has a commit history.
What ccboard uses vs what’s untapped
ccboard currently reads two things from this goldmine:
history.jsonl— timestamps and project mapping for freshness signalsprojects/*/— session JSONL for transcript parsing, session listing, merge-to-bootstrap
Everything else is untapped:
| Data source | What it could power |
|---|---|
file-history/ |
“Files touched” panel per session, cross-session diff view |
debug/ |
Performance timing, error rate tracking |
shell-snapshots/ |
Shell context at time of session |
projects/*/subagents/ |
Subagent activity visualization, delegation tree |
.git history |
Config evolution timeline |
The data is there. It’s all plain text. It’s all readable. The only question is what to build on top of it.
Freshness as a real signal
v1 had a “freshness” indicator but it was just filesystem mtime on the clone directory. That’s useless. Opening a directory to check something bumps the mtime. Pulling from upstream bumps the mtime. Neither means “I’m actively working here.”
What v2 uses:
| Signal | Threshold | Icon |
|---|---|---|
| Active | < 1 hour | Green circle |
| Recent | < 8 hours | Yellow circle |
| Stale | < 3 days | Orange circle |
| Dormant | > 3 days | White circle |
The primary signal is the most recent JSONL entry timestamp — “when did someone last talk to Claude here.” Falls back to file mtime only when there are no parseable timestamps. The history.jsonl provides millisecond-precision timestamps that get converted to seconds for comparison.
Better freshness signals I’d still want, ranked by reliability:
- Process liveness — is a Claude Code process currently running in this directory? Best for live status but requires process inspection.
- Git commit recency — last commit on the current branch. Strong signal but misses sessions that are still exploring/planning.
The composite freshness score would weight these. A clone with a live Claude process and a commit from 5 minutes ago is “hot.” A clone with a 3-day-old session and no recent commits is “warm.” A clone whose last session is from last month and whose branch was merged — that’s the archive candidate you can now actually archive with A.
The metadata question
Task-to-clone mapping has to live somewhere. The options:
Central config file (~/.ccboard/projects.toml):
[projects.bazel-torch]
name = "Bazel Torch Migration"
[projects.bazel-torch.tasks.system-torch]
name = "System torch in Docker"
approaches = [
{ name = "Option A", clone = "~/mcode/yolo-torch-a", status = "dead" },
{ name = "Option B", clone = "~/mcode/yolo-torch-b", status = "dead" },
{ name = "Option C", clone = "~/mcode/yolo-torch-c", status = "landed", pr = 21169 },
]
Per-clone dotfile (.ccboard.toml in each clone):
project = "bazel-torch"
task = "system-torch"
approach = "option-c"
status = "landed"
Central wins. Per-clone means N files to maintain, and deleting/archiving a clone loses the metadata with it. Central means one file, survives clone deletion, and the TUI can read everything without scanning 25 directories.
But here’s the thing — I could also auto-infer a lot of this. Clone names follow a convention (yolo-torch-* = torch-related). Branches follow a convention (experiment/torch-option-*). Git remotes tell you the upstream repo. PR status is queryable via gh. The manual metadata layer adds the semantic meaning (project, task, approach), but the structural data (clone path, branch, PR) is already there.
This is the one piece that’s still design-only. The v2 build shipped everything except the task-layer metadata. The dual-view turns out to be useful enough without it that the metadata layer dropped from “must have” to “nice to have.” Which is exactly what progressive enrichment should do — the tool works on day one, and you add the semantic layer when you want it, not because you need it.
Open questions
Some of these got answered. Some didn’t.
1. Should ccboard manage sessions or just observe them?
Right now it’s read-only — it shows you what’s there.
Answered: it manages them now. Archive moves directories. iTerm launch spawns processes. Merge writes SESSION-BOOTSTRAP.md. That’s three write operations. The observer became a manager.
The concern was “a manager can corrupt your session data if it’s buggy.” In practice, the safety checks make the archive flow safer than doing it manually (rm -rf doesn’t check for unpushed commits). And the merge flow writes to a new file (with backup), never touching the session JSONL itself. The risk profile turned out to be fine.
2. How do you handle multi-clone tasks?
Still open. Some tasks span yoloacr and yolo-add-retry-manifold. The dual-view doesn’t group these — they’re separate rows. This needs the task-layer metadata to solve properly.
3. Token estimation accuracy.
Partially answered. total_chars // 4000 is the current approach. It’s wrong, but it’s wrong in a consistent direction (overestimates, because not all JSONL content is conversation tokens). The dashboard shows ~12k tokens not 12,000 tokens — the tilde does a lot of work. For “should I start a fresh session or continue this one,” approximate is good enough.
4. Real-time vs snapshot.
Still open, and less urgent than expected. The 30-second auto-refresh (REFRESH_SECONDS = 30) plus manual R key handles most cases. Real-time file watching would be nice for seeing a session’s token count grow live, but it’s a complexity/value tradeoff that doesn’t pay off yet.
5. What about non-Claude-Code sessions?
Still open. The Git view already shows all clones regardless of Claude involvement, so the data is there. The question is whether to actively track non-Claude work (editor sessions, manual git operations). Probably not — scope creep alarm still going off.
Where this goes
The v2 rewrite isn’t happening tomorrow. This is a design exploration — figuring out the data model before writing code. v1 works. It’s wrong, but it works.
v2 is built. Here’s what’s shipped and what’s left:
Shipped:
- Dual-view with Tab toggle (Git/FS + Claude Code views)
- Archive with safety checks and batch mode
- iTerm batch-open with
--continue --dangerously-skip-permissions - Session knowledge extraction and merge-to-bootstrap
- tmux launch (fixed), iTerm launch, both supported
- Help screen, filter bar, detail sidebar, sessions panel
- 16 keybindings, all discoverable via
?
Still on the roadmap:
- Task-layer metadata — the central
projects.tomlthat maps clones to projects/tasks. This is the missing Layer 1. - Real-time monitoring —
watchdogfile watchers on JSONL files for live token count updates. - Process liveness detection —
pgrepor lock file checking to show which sessions are actively running. - Subagent visibility — parse
tool_usecalls for Task tool invocations to show active subagents. - Session compression via LLM — the current merge is mechanical (metadata extraction). The ambitious version would feed transcripts through Claude API to get actual summaries, not just “first message / last message.”
The three-layer model is still the insight. v2 implemented Layers 2 and 3. Layer 1 — the task management semantic layer — is the remaining design work. But the dual-view approach proved that you can ship a useful tool without solving the hardest layer first. Start with what’s auto-discoverable, add semantics later.
The data model was wrong the whole time. Now I know what right looks like — and most of it is running.