bind each Claude pane to its own session id (T-146)

A regression from T-137: every pane rendered the newest .jsonl in the
workspace dir, so concurrent sessions collided — a secondary tab showed
the primary's conversation. Each pane now spawns claude with its own
--session-id (a transcript is named <session-id>.jsonl), tails that
exact file via TranscriptReader's file: param, and uses a per-session
MessageBus channel so controllers don't cross-talk.

The primary's id is deterministic from its session name (stable → it
resumes across restarts, like /resume off the same history file);
secondaries get a fresh random id so a clean session is always available.
The reader now waits for the bound file to appear rather than throwing.

Migration: an existing tmux session created before this (no --session-id,
claude chose its own id) must be killed once (claude.kill-all-sessions)
so the next spawn binds the controlled id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 08:28:55 +02:00
co-authored by Claude Opus 4.7
parent 24f68e7aaa
commit e1cd4653c0
10 changed files with 222 additions and 4 deletions
@@ -294,10 +294,13 @@ class TranscriptReader {
}
Future<void> _tick(StreamController<ConversationItem> controller) async {
// A teammate reader tails one fixed file; otherwise discover the
// A pane/teammate reader tails one fixed file; otherwise discover the
// newest session `.jsonl` in the munged dir.
final newest = _explicitFile ?? await _newestJsonl(_mungedDir());
if (newest == null) return;
// An explicit file may not exist yet (claude writes it shortly after
// spawn) — wait for it rather than throwing in the poll loop.
if (!await File(newest).exists()) return;
if (newest != _currentPath) {
// New session file. Start from the recent tail rather than byte 0: