fix Claude secondary pane copy + false "session exited"
Two things surfaced in the secondary pane: the tab said "session 1" while the banner said "secondary 1" — now both say "session N". And the banner showed "session exited" right after starting, even though Claude was alive: a transient tmux client process can exit during spawn while the session itself is fine. pane.exit now verifies via `tmux has-session` and only reports exited when the session is actually gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2196,3 +2196,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
|
||||
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;
|
||||
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', 'in_progress', 'done', NULL, '2026-05-23 07:06:54', '2026-05-23 07:06:54', '2026-05-23 07:06:54', NULL, 'b6b88954fdc04ecf7847dea204a92e14', 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-149', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 07:14:36', '2026-05-23 07:14:36', '2026-05-23 07:14:36', NULL, '75bb1fa2d964f284910201b1c15995b9', 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-149', 'status', 'in_progress', 'done', NULL, '2026-05-23 07:27:32', '2026-05-23 07:27:32', '2026-05-23 07:27:32', NULL, 'd020ddfea99355676800af1e39249c85', 1) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -2554,3 +2554,4 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority,
|
||||
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 <munged-cwd>/*.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);
|
||||
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 (<munged-cwd>/<session-id>.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-<slug> 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.', 'done', 'high', NULL, NULL, NULL, '2026-05-23 07:03:18', '2026-05-23 07:06:54', NULL, '4c08227959a8fe6905521ebdc42487c1', 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-149', 'task', 'T-132', 'Native Claude pane startup banner (empty state)', 'Replace the bland ''Waiting for Claude…'' empty state with a native clide banner: clide logo, a Claude label (brand orange), the session role (primary/secondary N), the workspace path (home-collapsed), and the tmux session/status line, plus a ''warming up'' hint. Owned/native — no tmux capture-pane, no CC-internals coupling. ConversationView gains an optional emptyState widget; ClaudePane supplies a ClaudeBanner built from data it already has (repoRoot, statusLine, isPrimary).', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-23 07:14:36', '2026-05-23 07:14:36', NULL, '741b021184df1596b364aa2d09167e85', 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-149', 'task', 'T-132', 'Native Claude pane startup banner (empty state)', 'Replace the bland ''Waiting for Claude…'' empty state with a native clide banner: clide logo, a Claude label (brand orange), the session role (primary/secondary N), the workspace path (home-collapsed), and the tmux session/status line, plus a ''warming up'' hint. Owned/native — no tmux capture-pane, no CC-internals coupling. ConversationView gains an optional emptyState widget; ClaudePane supplies a ClaudeBanner built from data it already has (repoRoot, statusLine, isPrimary).', 'done', 'low', NULL, NULL, NULL, '2026-05-23 07:14:36', '2026-05-23 07:27:32', NULL, '1d922f79ed8f98bea75042af8662dd74', 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);
|
||||
|
||||
@@ -123,6 +123,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Claude secondary panes no longer flash a false "session exited" while
|
||||
the session is alive (a transient tmux client exit is now verified
|
||||
against the live session), and the tab and banner agree on the label
|
||||
("session N") (T-149).
|
||||
- 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
|
||||
|
||||
@@ -275,10 +275,18 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
final kernel = _kernel();
|
||||
if (kernel == null) return;
|
||||
// Lifecycle only — content comes from the transcript, not pane.output.
|
||||
_eventSub = kernel.events.on<DaemonEvent>().listen((e) {
|
||||
_eventSub = kernel.events.on<DaemonEvent>().listen((e) async {
|
||||
if (e.subsystem != 'pane' || e.data['id'] != _paneId) return;
|
||||
switch (e.kind) {
|
||||
case 'pane.exit':
|
||||
// A transient tmux client can exit (e.g. during spawn/respawn)
|
||||
// while the session — and Claude — stay alive. Don't report
|
||||
// "exited" then; only when the tmux session is actually gone.
|
||||
// (The no-tmux fallback has no session, so the exit is real.)
|
||||
if (_usingTmux && _sessionName != null && await tmux.hasSession(_sessionName!)) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() => _statusLine = widget.isPrimary ? 'session exited — restart clide to retry' : 'session exited');
|
||||
case 'pane.closed':
|
||||
_paneId = null;
|
||||
@@ -317,7 +325,7 @@ class _ClaudePaneState extends State<ClaudePane> {
|
||||
child: ConversationView(
|
||||
controller: _conversation!,
|
||||
emptyState: ClaudeBanner(
|
||||
role: widget.isPrimary ? 'primary' : 'secondary ${widget.secondaryIndex}',
|
||||
role: widget.isPrimary ? 'primary' : 'session ${widget.secondaryIndex}',
|
||||
workspace: _repoRoot,
|
||||
statusLine: _statusLine,
|
||||
),
|
||||
|
||||
@@ -24,6 +24,14 @@ Future<void> killSession(String name) async {
|
||||
await tmuxRunner([..._socket, 'kill-session', '-t', name]);
|
||||
}
|
||||
|
||||
/// Whether [name] is a live session on the clide socket. Used to suppress
|
||||
/// a spurious "session exited" when a transient tmux client process exits
|
||||
/// but the session itself is fine (T-149 follow-up).
|
||||
Future<bool> hasSession(String name) async {
|
||||
final r = await tmuxRunner([..._socket, 'has-session', '-t', name]);
|
||||
return r.exitCode == 0;
|
||||
}
|
||||
|
||||
/// Return the names of all sessions currently alive on the clide
|
||||
/// socket. Empty list if the server is not running.
|
||||
Future<List<String>> listClideSessions() async {
|
||||
|
||||
@@ -104,6 +104,16 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('hasSession', () {
|
||||
test('true on exit 0, false otherwise', () async {
|
||||
runner.exitCode = 0;
|
||||
expect(await tmux.hasSession('clide-claude-foo'), isTrue);
|
||||
expect(runner.calls.last, ['-L', 'clide', 'has-session', '-t', 'clide-claude-foo']);
|
||||
runner.exitCode = 1;
|
||||
expect(await tmux.hasSession('clide-claude-foo'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('sendMessage', () {
|
||||
test('loads a bracketed paste buffer then submits with Enter', () async {
|
||||
await tmux.sendMessage('clide-claude-foo', 'hello\nworld');
|
||||
|
||||
Reference in New Issue
Block a user