diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index 5a392bf1..85b19c53 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -2142,3 +2142,4 @@ Findings (2026-05-22): docs/spikes/cc-team-internals-2.1.148.md. All 5 checks an 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-137', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 17:58:07', '2026-05-22 17:58:07', '2026-05-22 17:58:07', NULL, '4e67262d4b04f8541e71fe8f03106fbf', 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-137', 'status', 'in_progress', 'done', NULL, '2026-05-22 18:14:32', '2026-05-22 18:14:32', '2026-05-22 18:14:32', NULL, 'a284c7249dda3c63a52a202bad07f105', 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-138', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 21:08:05', '2026-05-22 21:08:05', '2026-05-22 21:08:05', NULL, 'c3dfefe24d912081cd89ac0be066b641', 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-138', 'status', 'in_progress', 'done', NULL, '2026-05-22 21:49:09', '2026-05-22 21:49:09', '2026-05-22 21:49:09', NULL, 'e4be02e339d638da70a4e7ce0f850691', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 86924ae7..713a33ff 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -2487,3 +2487,4 @@ Findings (2026-05-22): docs/spikes/cc-team-internals-2.1.148.md. All 5 checks an 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-137', 'story', 'T-132', 'ConversationView + replace the terminal in the Claude pane', 'New lib/builtin/claude/src/conversation_view.dart rendering user / assistant(markdown via ClideMarkdown) / tool-call card / tool-result+diff card; whole tree under one SelectionArea. Swap ClidePtyView -> ConversationView in lib/builtin/claude/src/claude_pane.dart (~line 287). Claude still runs in tmux (detached / output ignored) but content comes from the transcript (TranscriptReader). Keep the terminal builtin + ClidePtyView for general use. Surface conversation as events too (D-6). Cite D-75. Acceptance: make run shows the native Claude conversation from the live transcript; select+copy across cards works; no terminal in the Claude pane; terminal builtin still works; widget tests. Blocked by T3, T4.', 'in_progress', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 17:58:07', NULL, 'e8244b90386314574545f0c1a7bd012c', 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-137', 'story', 'T-132', 'ConversationView + replace the terminal in the Claude pane', 'New lib/builtin/claude/src/conversation_view.dart rendering user / assistant(markdown via ClideMarkdown) / tool-call card / tool-result+diff card; whole tree under one SelectionArea. Swap ClidePtyView -> ConversationView in lib/builtin/claude/src/claude_pane.dart (~line 287). Claude still runs in tmux (detached / output ignored) but content comes from the transcript (TranscriptReader). Keep the terminal builtin + ClidePtyView for general use. Surface conversation as events too (D-6). Cite D-75. Acceptance: make run shows the native Claude conversation from the live transcript; select+copy across cards works; no terminal in the Claude pane; terminal builtin still works; widget tests. Blocked by T3, T4.', 'done', 'high', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 18:14:32', NULL, '6651a2c3c9099f7d38553f6cf9e41e98', 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-138', 'task', 'T-132', 'Native composer + file/image paste', 'New lib/builtin/claude/src/claude_composer.dart: native text field -> tmux -L clide send-keys -t (Enter submit, Shift+Enter newline). Paste text inline; file/image per the T2 spike finding (likely temp-file + path/@-reference, since send-keys carries text only). Acceptance: typing in the composer reaches Claude; pasting a text file and an image both work via the validated mechanism. Blocked by T2, T5.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 21:08:05', NULL, '2e6b932ad78ac78d83c86c5e08290ed3', 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-138', 'task', 'T-132', 'Native composer + file/image paste', 'New lib/builtin/claude/src/claude_composer.dart: native text field -> tmux -L clide send-keys -t (Enter submit, Shift+Enter newline). Paste text inline; file/image per the T2 spike finding (likely temp-file + path/@-reference, since send-keys carries text only). Acceptance: typing in the composer reaches Claude; pasting a text file and an image both work via the validated mechanism. Blocked by T2, T5.', 'done', 'medium', NULL, NULL, NULL, '2026-05-22 15:59:34', '2026-05-22 21:49:09', NULL, '8b247ce6f9fc63c6f56030303aece2e8', 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 b5932f0d..dabfc824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added - Native composer in the Claude pane (T-138) — type below the - conversation and press Enter to send (Shift+Enter for a newline); - input reaches Claude over `pane.write`. Multi-line text is sent as a - bracketed paste so it submits as one message. + conversation and press Enter to send (Shift+Enter for a newline). + Submits via the tmux server (bracketed paste + Enter), so input + reaches Claude even when no tmux client is attached; multi-line goes + as one message. - File and image paste in the composer (T-138) — Ctrl/Cmd+V of a copied file or a clipboard image inserts an `@path` reference (images are saved to a cache dir first); plain text pastes inline. Backed by a diff --git a/lib/builtin/claude/src/claude_pane.dart b/lib/builtin/claude/src/claude_pane.dart index 450a88d5..0333ef81 100644 --- a/lib/builtin/claude/src/claude_pane.dart +++ b/lib/builtin/claude/src/claude_pane.dart @@ -49,6 +49,7 @@ class _ClaudePaneState extends State { String _statusLine = 'attaching…'; bool _spawned = false; + bool _usingTmux = false; @override void didChangeDependencies() { @@ -193,8 +194,10 @@ class _ClaudePaneState extends State { setState(() => _error = resp.error?.message ?? 'spawn failed'); return; } + _usingTmux = false; setState(() => _statusLine = 'no-tmux · fresh every launch'); } else { + _usingTmux = true; setState(() => _statusLine = 'tmux · $_sessionName'); } @@ -213,10 +216,17 @@ class _ClaudePaneState extends State { setState(() {}); } - // Send composed text to Claude's tmux session. pane.write delivers it - // to the PTY (the attached tmux client), which forwards to claude — the - // same input verb the terminal pane uses, so D-6 parity holds. + // Send composed text to Claude. On the tmux path, submit via the tmux + // server (paste-buffer + Enter) — it reaches Claude even with no client + // attached, unlike pane.write to the (now-detached) spawned client PTY. + // The no-tmux fallback runs claude directly in our PTY, where pane.write + // does reach it. void _send(String text) { + if (_usingTmux) { + final session = _sessionName; + if (session != null) unawaited(tmux.sendMessage(session, text)); + return; + } final id = _paneId; final ipc = _ipc(); if (id == null || ipc == null) return; diff --git a/lib/builtin/claude/src/tmux_session.dart b/lib/builtin/claude/src/tmux_session.dart index 96ac8f67..f2ac1586 100644 --- a/lib/builtin/claude/src/tmux_session.dart +++ b/lib/builtin/claude/src/tmux_session.dart @@ -54,3 +54,21 @@ Future killAllForRepo(String primaryName) async { } } } + +/// Named tmux paste buffer clide loads composed messages into. +const _composeBuffer = 'clide-compose'; + +/// Submit [text] to [session] as a single message: load it into a named +/// paste buffer, paste it in bracketed mode (so multi-line content and +/// special characters arrive as one block rather than a stream of +/// submits), then press Enter. +/// +/// This goes through the tmux *server*, so it reaches Claude whether or +/// not a client is attached. Writing to the app's spawned client PTY +/// (`pane.write`) does not — once that client detaches, the PTY is dead +/// and keystrokes vanish (the bug this replaces). +Future sendMessage(String session, String text) async { + await tmuxRunner([..._socket, 'set-buffer', '-b', _composeBuffer, '--', text]); + await tmuxRunner([..._socket, 'paste-buffer', '-p', '-d', '-b', _composeBuffer, '-t', session]); + await tmuxRunner([..._socket, 'send-keys', '-t', session, 'Enter']); +} diff --git a/test/builtin/claude/tmux_session_test.dart b/test/builtin/claude/tmux_session_test.dart index 75bb154b..d66a308b 100644 --- a/test/builtin/claude/tmux_session_test.dart +++ b/test/builtin/claude/tmux_session_test.dart @@ -103,4 +103,15 @@ void main() { ]); }); }); + + group('sendMessage', () { + test('loads a bracketed paste buffer then submits with Enter', () async { + await tmux.sendMessage('clide-claude-foo', 'hello\nworld'); + expect(runner.calls, [ + ['-L', 'clide', 'set-buffer', '-b', 'clide-compose', '--', 'hello\nworld'], + ['-L', 'clide', 'paste-buffer', '-p', '-d', '-b', 'clide-compose', '-t', 'clide-claude-foo'], + ['-L', 'clide', 'send-keys', '-t', 'clide-claude-foo', 'Enter'], + ]); + }); + }); }