diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 4659cf23..305686fd 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -2193,3 +2193,4 @@ Acceptance: a secondary tab shows its OWN fresh conversation, independent of the ', NULL, '2026-05-23 06:19:51', '2026-05-23 06:19:51', '2026-05-23 06:19:51', NULL, '9e46b8eec7cc5e42132249a81d4925d1', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 06:19:51', '2026-05-23 06:19:51', '2026-05-23 06:19:51', NULL, 'dd4ec6cfbbc3118cd4cf02604b91108b', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-146', 'status', 'in_progress', 'done', NULL, '2026-05-23 06:30:47', '2026-05-23 06:30:47', '2026-05-23 06:30:47', NULL, '93f9c6814b9e82e5b60ac0e57fff5afe', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, 'b0bc1949f98ef05a05717cef80ba194f', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index d6488617..744d169c 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2550,3 +2550,5 @@ Open question to validate live: whether `claude --session-id ` re Acceptance: a secondary tab shows its OWN fresh conversation, independent of the primary; the primary keeps its conversation; both update live. Regression of T-137; relates to D-41. ', 'done', 'high', NULL, NULL, NULL, '2026-05-23 06:19:51', '2026-05-23 06:30:47', NULL, '2e1843c9b540a3d7ced03052d9efcbc4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-147', 'bug', 'T-132', 'Claude pane gets stuck waiting on a stale/legacy session', 'When the bound transcript (/.jsonl) never appears — e.g. attaching via new-session -A to a session created before --session-id binding (T-146), or any unconnectable session — the pane shows ''Waiting for Claude…'' forever. Fix: before spawning, if the bound transcript for our deterministic session id does not exist, kill the clide tmux session of that exact name so new-session creates a clean session with our --session-id. SAFETY (hard requirement): only ever kill clide-initiated sessions — by exact clide-claude- name, on the private -L clide socket (a user''s terminal claude is never on that socket); never delete any transcript file. Also makes clean-install/first-run robust. Live-verify the stuck primary self-heals on relaunch and a secondary still gets a fresh independent session.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, '21f7ebec0ddcab48a3ea7e8be0c0e979', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-148', 'task', 'T-132', 'Claude transcript disk-usage indicator + cleanup button', 'Surface transcript/session disk usage in the Claude panel with a cleanup affordance so the user can pick and choose what to remove. Per-session sizes (the /*.jsonl files + subagents/), a total, and a button to delete selected sessions'' transcripts. User-driven only — clide never auto-deletes transcript content. Future/''at some point'' per user. Relates to T-146 (session ids) and D-75.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:03:18', NULL, '7bbe7f4baf80027516cc46cff93a8dfe', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e128ae..07ad9ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- Claude pane no longer gets stuck on "Waiting for Claude…" when its + session can't be bound (T-147) — e.g. a session left over from before + session-id binding, or a fresh machine. It now retires that stale + clide session and starts a clean one. Only clide's own `-L clide` + sessions are touched (never a terminal Claude, never transcript files). - Secondary Claude tabs showed the primary's conversation instead of their own (T-146). Each pane now binds to its own session via `claude --session-id`, so concurrent sessions in one workspace stop diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 40907845..fe78cdcd 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -150,6 +150,24 @@ class _ClaudePaneState extends State { // Secondary: fresh → always a clean session. _sessionId ??= widget.isPrimary ? primarySessionId(repoRoot) : freshSessionId(); + final home = Platform.environment['HOME'] ?? ''; + final transcriptFile = '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}/$_sessionId.jsonl'; + + // Self-heal (T-147): if no transcript is bound to our session id, any + // clide tmux session of this name is stale (created before --session-id + // binding, or otherwise unconnectable) and `new-session -A` would + // attach to it and leave the pane stuck waiting forever. Kill it so a + // clean session is created with our --session-id. + // + // Safe by construction: this only ever kills clide's OWN session — by + // its exact `clide-claude-` name, on the private `-L clide` + // socket the user's terminal claude never runs on — and never deletes + // any transcript file. A healthy session's transcript already exists, + // so re-attach (D-41 continuity) is preserved. + if (!await File(transcriptFile).exists()) { + await tmux.killSession(_sessionName!); + } + final tmuxConf = await _ensureTmuxConf(); const cols = _cols; const rows = _rows; @@ -222,8 +240,6 @@ class _ClaudePaneState extends State { // showing the primary's conversation (T-146). Each pane gets its own // bus channel so their controllers don't cross-talk. final messages = _kernel()!.messages; - final home = Platform.environment['HOME'] ?? ''; - final transcriptFile = '$home/.claude/projects/${repoRoot.replaceAll('/', '-')}/$_sessionId.jsonl'; final channel = ClaudeConversation.sessionChannel(_sessionId!); _feed = TranscriptPublisher( messages: messages,