Two spot-check fixes (T-178, T-179), both grounded in a boundary test of the stream-json wire (findings folded into the spike doc): - Harness-injected user messages (skill loads, slash-command expansions, system reminders) carry isSynthetic on the wire (isMeta in the transcript). They were rendering as blue "you" cards though the user never typed them; now UserMessage.injected flags them and the view shows a muted, collapsed "context" card instead. - Permission prompts now show the command/input being permitted (a capped, scrollable code block) so you can see what you approve. Instead of fully hiding a prompted tool-use, once resolved it collapses to a one-line summary with a green (approved) or red (denied) border; the session tracks per-tool_use_id outcome and the view colours it. The result is kept. Corrects an earlier wrong assumption: the Skill tool is auto-allowed (no permission prompt); the inject only appears once the Skill tool is actually invoked, which is why deny-captures missed it. T-178, T-179, D-78. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2240 lines
324 KiB
SQL
2240 lines
324 KiB
SQL
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-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 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-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 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-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 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-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 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-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 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-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 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-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 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-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 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-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 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-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 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-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 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-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 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-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 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-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 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-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 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-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 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-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 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-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 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-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 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-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 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-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 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-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 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-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 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-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 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-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 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-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 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-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 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-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 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-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 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-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 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-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 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-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 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-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 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-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 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-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 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-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 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-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 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-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 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-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 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-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 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-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 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-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 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-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 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-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 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-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
|
||
|
||
Neither happens today.
|
||
|
||
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
|
||
|
||
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
|
||
|
||
**Acceptance:**
|
||
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
|
||
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
|
||
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
|
||
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
|
||
|
||
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 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-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 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-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
|
||
|
||
**Acceptance:**
|
||
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
|
||
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
|
||
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
|
||
|
||
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 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-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
|
||
|
||
**Canvas pane (`builtin.canvas`):**
|
||
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
|
||
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
|
||
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
|
||
- Persist layout back to the `.canvas` file on disk.
|
||
|
||
**Graph view (`builtin.graph`):**
|
||
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
|
||
- Hover highlights connected subgraph; click opens the note in the editor.
|
||
- Filter pane: tag include/exclude, file glob, depth-from-active.
|
||
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
|
||
|
||
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 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-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
|
||
|
||
**Extension API (third-party Lua):**
|
||
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
|
||
- Manifest schema, capability gating, sandboxed FS/IPC access.
|
||
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
|
||
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
|
||
|
||
**Settings UI (`builtin.settings-ui`):**
|
||
- Schema-driven settings panel reading from the kernel SettingsStore.
|
||
- Render strategy: form fields keyed off the schema each subsystem registers.
|
||
- Edits write back to `.clide/settings.yaml`.
|
||
|
||
**Theming UI (`builtin.theme-picker` extends):**
|
||
- Live preview of the four bundled themes (D-44).
|
||
- Custom theme: import YAML, validate against schema, register at runtime.
|
||
- Per-component override surface (long horizon).
|
||
|
||
**Distributable builds:**
|
||
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
|
||
- Self-update mechanism — see T-47.
|
||
- License manifest auto-regen as part of the release build.
|
||
|
||
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 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-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
|
||
|
||
**Acceptance:**
|
||
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
|
||
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
|
||
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
|
||
- Fuzzy match against command title; recent / pinned commands float to the top.
|
||
|
||
**Implementation hints:**
|
||
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
|
||
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
|
||
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 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-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 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-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
|
||
|
||
**Acceptance:**
|
||
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
|
||
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
|
||
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
|
||
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
|
||
|
||
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 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-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
|
||
|
||
**Errno constants (audit item #23):**
|
||
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
|
||
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
|
||
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
|
||
|
||
**Logger standardization (audit item #22, partial #26):**
|
||
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
|
||
- The Flutter-host process often consumes stderr, so log lines disappear silently.
|
||
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
|
||
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
|
||
|
||
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 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-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
|
||
|
||
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
|
||
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
|
||
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
|
||
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
|
||
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
|
||
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
|
||
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
|
||
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
|
||
|
||
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 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-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
|
||
|
||
**Acceptance:**
|
||
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
|
||
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
|
||
- WCAG-AA contrast gate (D-22) passes against the theme.
|
||
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
|
||
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
|
||
|
||
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
|
||
|
||
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 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-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
|
||
|
||
**Acceptance:**
|
||
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
|
||
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
|
||
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
|
||
|
||
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
|
||
|
||
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 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-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 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-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 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-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 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-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 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-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
---
|
||
Refinement (2026-05-06):
|
||
|
||
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
|
||
|
||
**Concrete fix shape:**
|
||
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
|
||
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
|
||
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
|
||
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
|
||
|
||
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 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-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 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-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 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-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 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-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
|
||
| `lib/widgets/` | 537 / 1184 | 45.4% |
|
||
| `lib/src/pql/` | 45 / 94 | 47.9% |
|
||
| `lib/builtin/` | 269 / 550 | 48.9% |
|
||
| `lib/kernel/` | 889 / 1691 | 52.6% |
|
||
| `lib/src/daemon/` | 320 / 584 | 54.8% |
|
||
| `lib/src/pty/` | 130 / 216 | 60.2% |
|
||
| `lib/src/git/` | 356 / 518 | 68.7% |
|
||
| `lib/src/ipc/` | 110 / 156 | 70.5% |
|
||
| `lib/src/files/` | 98 / 133 | 73.7% |
|
||
| `lib/extension/` | 41 / 54 | 75.9% |
|
||
| `lib/src/editor/` | 107 / 129 | 82.9% |
|
||
| `lib/src/panes/` | 70 / 73 | 95.9% |
|
||
|
||
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
|
||
|
||
**End state:**
|
||
- Total line coverage ≥ 95% (no per-area carve-outs).
|
||
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
|
||
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
|
||
|
||
**Acceptance criteria for closing this epic:**
|
||
1. `coverage/floor.txt` reads `95` (or higher).
|
||
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
|
||
3. The pre-push gate computes coverage and rejects pushes below the floor.
|
||
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
|
||
|
||
**Strategy / child shape:**
|
||
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
|
||
|
||
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
|
||
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
|
||
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
|
||
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
|
||
|
||
**Out of scope:**
|
||
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
|
||
- Integration / E2E coverage (only widget + unit feed lcov).
|
||
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
|
||
|
||
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
|
||
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 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-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 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-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
|
||
|
||
**What lands:**
|
||
|
||
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
|
||
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
|
||
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
|
||
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
|
||
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
|
||
|
||
**Acceptance:**
|
||
- `make coverage-gate` passes on a clean checkout (floor matches reality).
|
||
- `make push-check` includes the gate.
|
||
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
|
||
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
|
||
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
|
||
|
||
**Out of scope:**
|
||
- Bumping the floor toward 95% — that''s the test-writing children''s job.
|
||
- Branch coverage — D-66 explicitly excludes.
|
||
- Per-area floors — D-66 chose a single global floor for simplicity.
|
||
|
||
**Notes:**
|
||
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
|
||
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
|
||
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
|
||
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 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-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 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-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 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-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 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-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 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-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 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-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 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-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 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-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 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-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
|
||
|
||
**Today''s distribution within `lib/src/terminal/src/`:**
|
||
|
||
| sub-area | hit / total | % | shape |
|
||
|---|---|---|---|
|
||
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
|
||
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
|
||
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
|
||
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
|
||
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
|
||
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
|
||
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
|
||
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
|
||
|
||
**Strategy — pure logic first, then UI:**
|
||
|
||
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
|
||
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
|
||
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
|
||
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
|
||
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
|
||
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
|
||
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
|
||
|
||
**Acceptance:**
|
||
- `lib/src/terminal/` line coverage ≥ 95%.
|
||
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
|
||
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
|
||
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
|
||
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
|
||
|
||
**Strategy on commit cadence:**
|
||
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
|
||
- Adds tests under `test/terminal/<sub-area>/`.
|
||
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
|
||
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
|
||
|
||
**Out of scope:**
|
||
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
|
||
- Branch coverage (D-66 explicitly excludes).
|
||
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
|
||
|
||
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
|
||
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 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-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 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-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
|
||
|
||
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
|
||
|
||
**Trigger paths in production code:**
|
||
|
||
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
|
||
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
|
||
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
|
||
|
||
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
|
||
|
||
**Acceptance:**
|
||
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
|
||
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
|
||
- Runs reflow shrink,
|
||
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
|
||
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
|
||
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
|
||
|
||
**Investigation notes (do not skip):**
|
||
|
||
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
|
||
|
||
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
|
||
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
|
||
|
||
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
|
||
|
||
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
|
||
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 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-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 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-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 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-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 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-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
|
||
|
||
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
|
||
|
||
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
|
||
|
||
**Two layers of "dead" here:**
|
||
|
||
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
|
||
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
|
||
|
||
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
|
||
|
||
**Two clean resolutions:**
|
||
|
||
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
|
||
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
|
||
- Collapse the two into a single hook and document the contract.
|
||
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
|
||
|
||
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
|
||
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
|
||
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
|
||
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 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-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 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-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 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-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 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-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
|
||
|
||
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
|
||
|
||
Trace:
|
||
|
||
1. `TerminalGestureHandler.build` (gesture_handler.dart:65–82) wires:
|
||
```dart
|
||
onTertiaryTapDown: onSecondaryTapDown,
|
||
onTertiaryTapUp: onSecondaryTapUp,
|
||
```
|
||
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138–144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
|
||
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147–152) are therefore **dead** — they''re declared but never invoked anywhere.
|
||
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
|
||
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 24–25, 45–47) and `TerminalGestureDetector` (lines 13–14, 30–31) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
|
||
|
||
**Two layers of "dead" again:**
|
||
|
||
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
|
||
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
|
||
|
||
**User-visible impact:**
|
||
|
||
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
|
||
|
||
**Resolutions, like T-93:**
|
||
|
||
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
|
||
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
|
||
|
||
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
|
||
|
||
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
|
||
```dart
|
||
details.kind == PointerDeviceKind.mouse
|
||
? renderTerminal.selectCharacters(details.localPosition)
|
||
: renderTerminal.selectWord(details.localPosition);
|
||
```
|
||
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
|
||
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
|
||
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
|
||
|
||
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
|
||
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 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-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 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-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 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-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 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-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 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-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 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-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 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-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 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-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 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-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 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-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 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-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 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-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 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-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 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-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 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-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 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-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 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-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 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-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 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-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 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-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 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-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 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-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 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-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 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-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 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-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 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-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 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-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 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-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 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-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 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-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 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-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 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-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 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-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 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-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 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-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 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-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 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-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 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-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 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-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 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-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 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-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 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-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 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-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 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-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 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-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
|
||
|
||
Neither happens today.
|
||
|
||
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
|
||
|
||
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
|
||
|
||
**Acceptance:**
|
||
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
|
||
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
|
||
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
|
||
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
|
||
|
||
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 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-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 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-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
|
||
|
||
**Acceptance:**
|
||
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
|
||
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
|
||
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
|
||
|
||
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 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-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
|
||
|
||
**Canvas pane (`builtin.canvas`):**
|
||
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
|
||
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
|
||
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
|
||
- Persist layout back to the `.canvas` file on disk.
|
||
|
||
**Graph view (`builtin.graph`):**
|
||
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
|
||
- Hover highlights connected subgraph; click opens the note in the editor.
|
||
- Filter pane: tag include/exclude, file glob, depth-from-active.
|
||
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
|
||
|
||
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 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-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
|
||
|
||
**Extension API (third-party Lua):**
|
||
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
|
||
- Manifest schema, capability gating, sandboxed FS/IPC access.
|
||
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
|
||
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
|
||
|
||
**Settings UI (`builtin.settings-ui`):**
|
||
- Schema-driven settings panel reading from the kernel SettingsStore.
|
||
- Render strategy: form fields keyed off the schema each subsystem registers.
|
||
- Edits write back to `.clide/settings.yaml`.
|
||
|
||
**Theming UI (`builtin.theme-picker` extends):**
|
||
- Live preview of the four bundled themes (D-44).
|
||
- Custom theme: import YAML, validate against schema, register at runtime.
|
||
- Per-component override surface (long horizon).
|
||
|
||
**Distributable builds:**
|
||
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
|
||
- Self-update mechanism — see T-47.
|
||
- License manifest auto-regen as part of the release build.
|
||
|
||
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 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-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
|
||
|
||
**Acceptance:**
|
||
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
|
||
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
|
||
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
|
||
- Fuzzy match against command title; recent / pinned commands float to the top.
|
||
|
||
**Implementation hints:**
|
||
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
|
||
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
|
||
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 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-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 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-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
|
||
|
||
**Acceptance:**
|
||
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
|
||
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
|
||
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
|
||
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
|
||
|
||
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 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-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
|
||
|
||
**Errno constants (audit item #23):**
|
||
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
|
||
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
|
||
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
|
||
|
||
**Logger standardization (audit item #22, partial #26):**
|
||
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
|
||
- The Flutter-host process often consumes stderr, so log lines disappear silently.
|
||
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
|
||
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
|
||
|
||
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 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-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
|
||
|
||
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
|
||
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
|
||
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
|
||
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
|
||
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
|
||
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
|
||
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
|
||
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
|
||
|
||
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 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-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
|
||
|
||
**Acceptance:**
|
||
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
|
||
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
|
||
- WCAG-AA contrast gate (D-22) passes against the theme.
|
||
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
|
||
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
|
||
|
||
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
|
||
|
||
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 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-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
|
||
|
||
**Acceptance:**
|
||
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
|
||
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
|
||
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
|
||
|
||
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
|
||
|
||
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 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-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 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-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 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-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 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-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 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-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
---
|
||
Refinement (2026-05-06):
|
||
|
||
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
|
||
|
||
**Concrete fix shape:**
|
||
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
|
||
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
|
||
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
|
||
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
|
||
|
||
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 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-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 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-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 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-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 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-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
|
||
| `lib/widgets/` | 537 / 1184 | 45.4% |
|
||
| `lib/src/pql/` | 45 / 94 | 47.9% |
|
||
| `lib/builtin/` | 269 / 550 | 48.9% |
|
||
| `lib/kernel/` | 889 / 1691 | 52.6% |
|
||
| `lib/src/daemon/` | 320 / 584 | 54.8% |
|
||
| `lib/src/pty/` | 130 / 216 | 60.2% |
|
||
| `lib/src/git/` | 356 / 518 | 68.7% |
|
||
| `lib/src/ipc/` | 110 / 156 | 70.5% |
|
||
| `lib/src/files/` | 98 / 133 | 73.7% |
|
||
| `lib/extension/` | 41 / 54 | 75.9% |
|
||
| `lib/src/editor/` | 107 / 129 | 82.9% |
|
||
| `lib/src/panes/` | 70 / 73 | 95.9% |
|
||
|
||
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
|
||
|
||
**End state:**
|
||
- Total line coverage ≥ 95% (no per-area carve-outs).
|
||
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
|
||
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
|
||
|
||
**Acceptance criteria for closing this epic:**
|
||
1. `coverage/floor.txt` reads `95` (or higher).
|
||
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
|
||
3. The pre-push gate computes coverage and rejects pushes below the floor.
|
||
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
|
||
|
||
**Strategy / child shape:**
|
||
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
|
||
|
||
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
|
||
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
|
||
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
|
||
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
|
||
|
||
**Out of scope:**
|
||
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
|
||
- Integration / E2E coverage (only widget + unit feed lcov).
|
||
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
|
||
|
||
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
|
||
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 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-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 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-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
|
||
|
||
**What lands:**
|
||
|
||
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
|
||
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
|
||
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
|
||
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
|
||
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
|
||
|
||
**Acceptance:**
|
||
- `make coverage-gate` passes on a clean checkout (floor matches reality).
|
||
- `make push-check` includes the gate.
|
||
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
|
||
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
|
||
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
|
||
|
||
**Out of scope:**
|
||
- Bumping the floor toward 95% — that''s the test-writing children''s job.
|
||
- Branch coverage — D-66 explicitly excludes.
|
||
- Per-area floors — D-66 chose a single global floor for simplicity.
|
||
|
||
**Notes:**
|
||
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
|
||
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
|
||
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
|
||
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 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-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 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-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 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-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 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-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 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-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 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-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 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-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 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-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 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-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
|
||
|
||
**Today''s distribution within `lib/src/terminal/src/`:**
|
||
|
||
| sub-area | hit / total | % | shape |
|
||
|---|---|---|---|
|
||
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
|
||
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
|
||
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
|
||
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
|
||
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
|
||
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
|
||
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
|
||
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
|
||
|
||
**Strategy — pure logic first, then UI:**
|
||
|
||
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
|
||
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
|
||
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
|
||
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
|
||
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
|
||
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
|
||
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
|
||
|
||
**Acceptance:**
|
||
- `lib/src/terminal/` line coverage ≥ 95%.
|
||
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
|
||
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
|
||
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
|
||
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
|
||
|
||
**Strategy on commit cadence:**
|
||
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
|
||
- Adds tests under `test/terminal/<sub-area>/`.
|
||
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
|
||
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
|
||
|
||
**Out of scope:**
|
||
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
|
||
- Branch coverage (D-66 explicitly excludes).
|
||
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
|
||
|
||
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
|
||
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 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-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 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-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
|
||
|
||
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
|
||
|
||
**Trigger paths in production code:**
|
||
|
||
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
|
||
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
|
||
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
|
||
|
||
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
|
||
|
||
**Acceptance:**
|
||
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
|
||
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
|
||
- Runs reflow shrink,
|
||
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
|
||
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
|
||
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
|
||
|
||
**Investigation notes (do not skip):**
|
||
|
||
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
|
||
|
||
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
|
||
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
|
||
|
||
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
|
||
|
||
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
|
||
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 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-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 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-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 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-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 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-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
|
||
|
||
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
|
||
|
||
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
|
||
|
||
**Two layers of "dead" here:**
|
||
|
||
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
|
||
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
|
||
|
||
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
|
||
|
||
**Two clean resolutions:**
|
||
|
||
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
|
||
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
|
||
- Collapse the two into a single hook and document the contract.
|
||
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
|
||
|
||
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
|
||
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
|
||
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
|
||
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 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-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 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-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 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-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 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-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
|
||
|
||
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
|
||
|
||
Trace:
|
||
|
||
1. `TerminalGestureHandler.build` (gesture_handler.dart:65–82) wires:
|
||
```dart
|
||
onTertiaryTapDown: onSecondaryTapDown,
|
||
onTertiaryTapUp: onSecondaryTapUp,
|
||
```
|
||
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138–144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
|
||
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147–152) are therefore **dead** — they''re declared but never invoked anywhere.
|
||
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
|
||
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 24–25, 45–47) and `TerminalGestureDetector` (lines 13–14, 30–31) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
|
||
|
||
**Two layers of "dead" again:**
|
||
|
||
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
|
||
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
|
||
|
||
**User-visible impact:**
|
||
|
||
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
|
||
|
||
**Resolutions, like T-93:**
|
||
|
||
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
|
||
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
|
||
|
||
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
|
||
|
||
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
|
||
```dart
|
||
details.kind == PointerDeviceKind.mouse
|
||
? renderTerminal.selectCharacters(details.localPosition)
|
||
: renderTerminal.selectWord(details.localPosition);
|
||
```
|
||
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
|
||
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
|
||
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
|
||
|
||
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
|
||
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 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-62', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '43c1103bad9839fbd2c8058f147cfcba', 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-61', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '65fbf26149226f3fc9c06a446bfa9f41', 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-49', 'status', 'backlog', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, '98b648e7a4a82730e43677c7320c8b0d', 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-63', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'c8b03bf5dadfdc5cb22bc5961919dc23', 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-43', 'status', 'ready', 'in_progress', NULL, '2026-05-03 20:33:10', '2026-05-03 20:33:10', '2026-05-03 20:33:10', NULL, 'fdc5453ac922809e479b6b3418ed7755', 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-21', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:34:52', '2026-05-03 20:34:52', '2026-05-03 20:34:52', NULL, '6da16f33a6aa1e1db733a0a02900e0c1', 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-24', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:36:31', '2026-05-03 20:36:31', '2026-05-03 20:36:31', NULL, 'd15ac0c4b6c7b6057243edcfbf7086f1', 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-17', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:03', '2026-05-03 20:37:03', '2026-05-03 20:37:03', NULL, '3f6f22aa9e420ec2fc869f2530df4c46', 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-18', 'status', 'backlog', 'ready', NULL, '2026-05-03 20:37:30', '2026-05-03 20:37:30', '2026-05-03 20:37:30', NULL, 'e6f3d8bda62656d17b174c8b9933dabc', 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-28', 'status', 'in_progress', 'done', NULL, '2026-05-03 20:40:22', '2026-05-03 20:40:22', '2026-05-03 20:40:22', NULL, '9ace75c4346f321670f7024eebfa0fb9', 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-43', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:51:24', '2026-05-05 12:51:24', '2026-05-05 12:51:24', NULL, 'adedfd3ad28b56cf4e57e20bce3ec497', 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-68', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '60751203904229cc5846ea313f747846', 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-70', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '618b3042be98eecae2168446ffd2bfa6', 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-71', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, '65e4ebb4c60a0feb7c2dec82f0c3faa1', 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-72', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'a3ac83b6f85fbcf6b44a4b0c28c79803', 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-69', 'status', 'backlog', 'done', NULL, '2026-05-05 12:53:11', '2026-05-05 12:53:11', '2026-05-05 12:53:11', NULL, 'b427c00df34390343af95fcd285a09bd', 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-70', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '48865da069da205e0cf7ae7dd8caf27d', 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-72', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '680e39c278e717327dc2557e625abc1b', 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-68', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '8f1ea30ab9663ac07c04fe404937a9fd', 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-71', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, '9284c52dba3b76b117e8d3a97420dd6a', 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-69', 'status', 'done', 'done', NULL, '2026-05-05 12:53:15', '2026-05-05 12:53:15', '2026-05-05 12:53:15', NULL, 'e5c9d41027d52b8fbe7edcc684130c2e', 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-18', 'status', 'ready', 'in_progress', NULL, '2026-05-05 12:54:39', '2026-05-05 12:54:39', '2026-05-05 12:54:39', NULL, 'd8d87b2a3ea7073edde2157f6c90791e', 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-18', 'status', 'in_progress', 'done', NULL, '2026-05-05 12:59:05', '2026-05-05 12:59:05', '2026-05-05 12:59:05', NULL, '3ddd60cd6e2f138e184b3e4e8f892a21', 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-78', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 12:59:50', '2026-05-05 12:59:50', '2026-05-05 12:59:50', NULL, 'a1e1b2c66830239411cac289bcb13050', 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-78', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:02:34', '2026-05-05 13:02:34', '2026-05-05 13:02:34', NULL, 'ffe86cc4530e722c0622fbd4095d0f08', 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-75', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:02:40', '2026-05-05 13:02:40', '2026-05-05 13:02:40', NULL, '3d48174c87be388f391611a4c9ebc4b0', 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-75', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:07:29', '2026-05-05 13:07:29', '2026-05-05 13:07:29', NULL, '579e4939f630479b5987b47de2106ca9', 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-76', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:07:34', '2026-05-05 13:07:34', '2026-05-05 13:07:34', NULL, 'd56b93ae20653ab26c0a99ab7c28fa69', 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-76', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:10:50', '2026-05-05 13:10:50', '2026-05-05 13:10:50', NULL, '5c98774f501f1a6c7bef328c3cd8c5b1', 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-77', 'status', 'backlog', 'in_progress', NULL, '2026-05-05 13:10:57', '2026-05-05 13:10:57', '2026-05-05 13:10:57', NULL, '8558d256a078bda0713c7045d7f1c2bb', 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-77', 'status', 'in_progress', 'done', NULL, '2026-05-05 13:12:20', '2026-05-05 13:12:20', '2026-05-05 13:12:20', NULL, 'aabaf34b3e5982ab7be5c07051e2c1e7', 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-79', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 07:45:02', '2026-05-06 07:45:02', '2026-05-06 07:45:02', NULL, 'e5e0baef07bf6908200527c340130a2e', 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-79', 'status', 'in_progress', 'done', NULL, '2026-05-06 07:56:10', '2026-05-06 07:56:10', '2026-05-06 07:56:10', NULL, '3069b52fc53518956fc0dc7792aa2e9c', 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-21', 'status', 'ready', 'done', NULL, '2026-05-06 09:17:22', '2026-05-06 09:17:22', '2026-05-06 09:17:22', NULL, '56b24bfcf31543cbde7e5c1af8055989', 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-83', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 09:17:50', '2026-05-06 09:17:50', '2026-05-06 09:17:50', NULL, 'cb9fc3d3f6cc5858ce9af1f377257b26', 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-83', 'status', 'in_progress', 'done', NULL, '2026-05-06 10:12:48', '2026-05-06 10:12:48', '2026-05-06 10:12:48', NULL, '133758a6509278aaada7d8f03ac72ae4', 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-84', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 10:17:57', '2026-05-06 10:17:57', '2026-05-06 10:17:57', NULL, '8991e0669094e0969787881506f373dc', 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-84', 'status', 'in_progress', 'done', NULL, '2026-05-06 12:17:27', '2026-05-06 12:17:27', '2026-05-06 12:17:27', NULL, '8dd4f7dba893357485c6a8293e49765e', 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-24', 'status', 'ready', 'in_progress', NULL, '2026-05-06 12:20:58', '2026-05-06 12:20:58', '2026-05-06 12:20:58', NULL, 'e9f7ebbd02eb9bf7f3faf10dca1cf8e1', 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-24', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:21:15', '2026-05-06 13:21:15', '2026-05-06 13:21:15', NULL, '661729cdc568efa42627ea20bfcbeddb', 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-85', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:29:05', '2026-05-06 13:29:05', '2026-05-06 13:29:05', NULL, 'adb576167325591e1ec5e095a38bc6cb', 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-85', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:38:38', '2026-05-06 13:38:38', '2026-05-06 13:38:38', NULL, '4062a1b9aed02d2749b8fd079b6cd6a9', 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-86', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:38:44', '2026-05-06 13:38:44', '2026-05-06 13:38:44', NULL, '668f49fb8c27e4acf51144970d8cccb5', 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-86', 'status', 'in_progress', 'done', NULL, '2026-05-06 13:45:12', '2026-05-06 13:45:12', '2026-05-06 13:45:12', NULL, '28dcd4602bb47f5ca1a8be7ecffd2597', 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-87', 'description', NULL, 'D-41 lifecycle contract: "closing a secondary kills that tmux session" and "secondary numbering (-1, -2, ...) resets between clide runs since ephemeral state is lost" — the second clause only holds if non-primary tmux sessions are reaped on app shutdown.
|
||
|
||
Neither happens today.
|
||
|
||
**Gap 1 — tab close is UI-only.** `ClaudeSessionHostState` (lib/builtin/claude/src/claude_session_host.dart:18) builds tabs via MultitabController; closing a secondary just removes the entry. No `tmux kill-session` shellout anywhere in lib/builtin/claude/ or lib/src/pty/ (verified by grep for `kill-session` / `killSession` / `tmux kill`).
|
||
|
||
**Gap 2 — app shutdown leaks secondaries.** Nothing reaps `clide-claude-<repohash>-N` sessions when the app exits. Repro: spawn two secondaries, quit clide, `tmux -L clide list-sessions` still shows them. Confirmed live: 4 sessions on the clide socket (primary + -1, -2, -3) survived prior runs.
|
||
|
||
**Acceptance:**
|
||
- Closing a secondary tab in the Claude pane runs `tmux -L clide kill-session -t <name>` for that secondary''s session name (use existing `secondarySessionName(repoRoot, n)`).
|
||
- App shutdown enumerates `clide-claude-<repohash>-*` sessions on the clide socket and kills the non-primary ones. Primary is left detached (D-41 explicit).
|
||
- `_nextSecondary` index policy: keep the in-process counter for the running app; cold-start always begins at 1 because secondary sessions are gone.
|
||
- Add a test that exercises the close path and asserts the kill-session command was invoked (mock the tmux runner).
|
||
|
||
**Out of scope:** primary lifecycle (unchanged — survives shutdown by design), general-purpose terminal panes (no tmux wrapping per D-41), Windows behaviour.', NULL, '2026-05-06 13:46:26', '2026-05-06 13:46:26', '2026-05-06 13:46:26', NULL, 'c7dbde5c6c6c89492665abdff0f59be4', 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-87', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 13:46:29', '2026-05-06 13:46:29', '2026-05-06 13:46:29', NULL, '9d047426e61ddd7d29072333da718c03', 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-17', 'description', NULL, 'Wire `dart doc` into the CI pipeline so the public API surface (everything in lib/*.dart barrel exports) renders to HTML on every push to main.
|
||
|
||
**Acceptance:**
|
||
- New CI step in `.gitea/workflows/test.yml` runs `dart doc lib/` and uploads the generated `doc/api/` artefact.
|
||
- Documentation comments on public APIs are validated (no broken references); `dart doc --validate-links` part of the run.
|
||
- Generated docs published somewhere browsable — Gitea Pages, GitHub Pages once the repo moves, or an S3-style static host (decision deferred until Gitea actions land per D-32).
|
||
|
||
**Not in scope:** writing missing docstrings (separate sweep), private-API docs, package.json-style metadata for pub.dev (we never publish to pub.dev — single binary).', NULL, '2026-05-06 15:08:22', '2026-05-06 15:08:22', '2026-05-06 15:08:22', NULL, '396d0d316f7ee331776d4547f9b54b9a', 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-7', 'description', NULL, 'Tier 5 of the build plan: bring the canvas pane (Obsidian-style spatial whiteboard) and the graph view (force-directed view of vault links) to feature parity.
|
||
|
||
**Canvas pane (`builtin.canvas`):**
|
||
- Render `.canvas` files (Obsidian-compatible JSON) — nodes (note, text, group, image), edges, layout state.
|
||
- Pan/zoom via `InteractiveViewer`; node selection, drag, resize.
|
||
- Edit affordances: add note from file picker, add text node, draw edge between nodes.
|
||
- Persist layout back to the `.canvas` file on disk.
|
||
|
||
**Graph view (`builtin.graph`):**
|
||
- Force-directed layout of the current vault (or a filtered subset) — nodes are notes, edges are wikilinks.
|
||
- Hover highlights connected subgraph; click opens the note in the editor.
|
||
- Filter pane: tag include/exclude, file glob, depth-from-active.
|
||
- pql provides the link data (`pql backlinks` / `pql outlinks`); rendering owned in-app.
|
||
|
||
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).', NULL, '2026-05-06 15:08:37', '2026-05-06 15:08:37', '2026-05-06 15:08:37', NULL, '1a91dfb3b80a90e5dd6d84b3df9afef8', 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-8', 'description', NULL, 'Tier 6 of the build plan: the things that make clide a real product instead of a working prototype.
|
||
|
||
**Extension API (third-party Lua):**
|
||
- The Lua runtime supporter tool (D-19) lands as a peer of pql/ptyc.
|
||
- Manifest schema, capability gating, sandboxed FS/IPC access.
|
||
- Same TabContribution / CommandContribution / etc. surface as built-in Dart extensions (D-15).
|
||
- Marketplace / distribution story is OUT OF SCOPE for Tier 6 — local-install only.
|
||
|
||
**Settings UI (`builtin.settings-ui`):**
|
||
- Schema-driven settings panel reading from the kernel SettingsStore.
|
||
- Render strategy: form fields keyed off the schema each subsystem registers.
|
||
- Edits write back to `.clide/settings.yaml`.
|
||
|
||
**Theming UI (`builtin.theme-picker` extends):**
|
||
- Live preview of the four bundled themes (D-44).
|
||
- Custom theme: import YAML, validate against schema, register at runtime.
|
||
- Per-component override surface (long horizon).
|
||
|
||
**Distributable builds:**
|
||
- AppImage / Flatpak for Linux, .dmg for macOS — see T-46.
|
||
- Self-update mechanism — see T-47.
|
||
- License manifest auto-regen as part of the release build.
|
||
|
||
Big epic — children land incrementally. Most concrete child tickets already exist; this is the umbrella.', NULL, '2026-05-06 15:08:53', '2026-05-06 15:08:53', '2026-05-06 15:08:53', NULL, '55bf0c36dc75468fa6a1d3caf532c2e9', 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-23', 'description', NULL, 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
|
||
|
||
**Acceptance:**
|
||
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
|
||
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
|
||
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
|
||
- Fuzzy match against command title; recent / pinned commands float to the top.
|
||
|
||
**Implementation hints:**
|
||
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
|
||
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
|
||
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-06 15:09:05', '2026-05-06 15:09:05', '2026-05-06 15:09:05', NULL, '352e6c0b2af96f63924ad1d1ef4c80f9', 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-73', 'description', NULL, 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', NULL, '2026-05-06 15:09:20', '2026-05-06 15:09:20', '2026-05-06 15:09:20', NULL, '780a2a4a39f15c3ba81648ceb6fdba37', 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-74', 'description', NULL, 'Mouse wheel scroll is currently translated to PgUp/PgDown key input in `lib/src/terminal/src/terminal_view.dart` as a pragmatic workaround — TUIs that capture mouse events without binding the wheel get a no-op otherwise. The downside: any TUI that does its own wheel handling (selection, click-to-position) loses the actual mouse coordinates because all we send is keyboard.
|
||
|
||
**Acceptance:**
|
||
- The terminal forwards `PointerScrollEvent` and click/drag/release as proper xterm mouse-protocol escape sequences (CSI `M` / SGR variant per the report mode the inner program declared via `?1000h` / `?1006h`).
|
||
- Tested with `vim` (mouse mode `a`), `htop`, `less` — all should respond to mouse selection / click correctly.
|
||
- Falls back to PgUp/PgDown only when the inner program has not declared a mouse mode.
|
||
- The `mouseInput` API on `Terminal` is wired through `Listener.onPointerSignal` instead of being bypassed.
|
||
|
||
**Why this isn`t the default already:** the bypass was the cheapest fix to get scroll working at all when the multitab/Scrollable ate scroll events. With those fixes landed, the right-shaped mouse forwarding is the proper next step.', NULL, '2026-05-06 15:09:36', '2026-05-06 15:09:36', '2026-05-06 15:09:36', NULL, '31108dcc7844bf74515dfc1cda0993e9', 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-80', 'description', NULL, 'From the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md). Two cleanup items rolled together:
|
||
|
||
**Errno constants (audit item #23):**
|
||
- Magic numbers (`4` for EINTR, `9` for EBADF, `28` for SIGWINCH, `1` for SIGHUP, `32` for EPIPE) appear inline across `lib/src/pty/session.dart` and `lib/src/pty/native_pty.dart`.
|
||
- Centralize them in `lib/src/pty/errors.dart` or a sibling `posix.dart` as named constants.
|
||
- Existing `lib/src/ipc/errno_mapping.dart` already has a `PosixErrno` class — extend it or move to a shared location both layers import from.
|
||
|
||
**Logger standardization (audit item #22, partial #26):**
|
||
- `lib/src/ipc/server.dart` uses `stderr.writeln(...)` directly; the rest of the daemon either uses no logger or a custom one.
|
||
- The Flutter-host process often consumes stderr, so log lines disappear silently.
|
||
- Pick one logger interface (kernel `log` already exists for the app side), wire `DaemonServer` and the daemon-side handlers to use it.
|
||
- Dispatch error messages should prefix with the request `cmd` so log correlation works (audit item #26).
|
||
|
||
**Out of scope for this ticket:** changes to log-LEVEL policy, log retention, log files vs stderr — pure substitution job.', NULL, '2026-05-06 15:09:50', '2026-05-06 15:09:50', '2026-05-06 15:09:50', NULL, 'e4255ebfc7f9bd8537ced342f2925d77', 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-81', 'description', NULL, 'Catch-all for the medium-priority items from the PTY/IPC error-handling audit (T-18, see docs/audits/pty-ipc-error-handling-2026-05-05.md) that didn`t earn dedicated tickets:
|
||
|
||
- **#17** — `files.read` `readAsStringSync` is unguarded; UTF-8 errors / permissions / mid-read deletion become 500-style dispatch errors. Wrap in try/catch and emit a clean `IpcResponse.err`.
|
||
- **#19** — `PtySession.close` swallows the 500ms timeout silently (`onTimeout: () {}`). Log when the timeout fires so we know SIGKILL was needed.
|
||
- **#20** — Reader isolate treats every negative `read()` return that isn`t EINTR as EOF. Distinguish EBADF/EIO (real EOF) from transient EAGAIN (recoverable) and log the latter.
|
||
- **#21** — `scm_rights.dart` reads cmsg-data fd without verifying `dataOffset + 4 <= msgControllen`. Bounds check before deref so a malformed peer can`t feed garbage as an fd.
|
||
- **#25** — `_gitError` in `lib/src/daemon/git_commands.dart` always reports `tool_error`; push rejections / merge conflicts should map to `IpcExitCode.conflict` when stderr matches known patterns.
|
||
- **#27** — `pane.spawn` returns `ok` even when `registry.write(id, bytes)` returned `n == -1`. Distinguish the failure.
|
||
- **#28** — `IpcResponse.fromJson` throws `TypeError` on a malformed peer response missing `error`. Graceful degrade.
|
||
- **#29** — PATH resolution in `native_pty.dart` uses the first existing match without `X_OK` check; non-executable files shadow valid binaries further along PATH.
|
||
|
||
Land each as a small focused commit; ticket closes when all items above are merged.', NULL, '2026-05-06 15:10:09', '2026-05-06 15:10:09', '2026-05-06 15:10:09', NULL, 'ad1a34f56d30171570970965b5ef3560', 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-82', 'description', NULL, 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44).
|
||
|
||
**Acceptance:**
|
||
- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant).
|
||
- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45).
|
||
- WCAG-AA contrast gate (D-22) passes against the theme.
|
||
- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution.
|
||
- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu.
|
||
|
||
**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase.
|
||
|
||
**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', NULL, '2026-05-06 15:10:22', '2026-05-06 15:10:22', '2026-05-06 15:10:22', NULL, '0a750764071d26afaa1821ca9ee2f3bf', 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-88', 'description', NULL, 'D-59 (bundled git via dugite-native) costs out: "Must track dugite-native releases for security updates." Up to today this lived only in the D-record prose with no tracking — clean-house v1 flagged it as RULE-SUNSET-WITHOUT-TICKET.
|
||
|
||
**Acceptance:**
|
||
- A periodic check (manual or scheduled) compares the bundled `native/dugite/` SHA against the latest release at https://github.com/desktop/dugite-native/releases.
|
||
- When a new release lands with a CVE label or fixes a CVE-tagged git release, schedule a bump; otherwise track the version drift.
|
||
- Bumps follow D-63: rebuild with the new SHA, update `BUILD.md`, update `assets/licenses.yaml` if the dugite or git version changed, refresh the binary in `native/dugite/`.
|
||
|
||
**Periodicity:** quarterly check is enough during normal operation; faster on git CVE announcements (subscribe to https://github.com/git/git/security/advisories, https://github.com/desktop/dugite-native/security/advisories).
|
||
|
||
**Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine.', NULL, '2026-05-06 15:10:35', '2026-05-06 15:10:35', '2026-05-06 15:10:35', NULL, '49050e00e3787be467be278d157bb69e', 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-87', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '0d9f946e59752f061cc431121ac0f708', 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-17', 'status', 'ready', 'in_progress', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, '4a53482a4ec3d3938e8b0bdfd0d9f4d6', 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-73', 'status', 'backlog', 'ready', NULL, '2026-05-06 15:39:53', '2026-05-06 15:39:53', '2026-05-06 15:39:53', NULL, 'e4e9ad42effcd95445b086c527f3471c', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:45:51', '2026-05-06 15:45:51', '2026-05-06 15:45:51', NULL, '0b148a1ff820009a9e2f533ee6fef7f9', 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-17', 'status', 'done', 'in_progress', NULL, '2026-05-06 15:51:26', '2026-05-06 15:51:26', '2026-05-06 15:51:26', NULL, '79dc3457b9819570fbd35d7cffb80e0d', 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-17', 'status', 'in_progress', 'done', NULL, '2026-05-06 15:58:30', '2026-05-06 15:58:30', '2026-05-06 15:58:30', NULL, '78f6f98d8e4b94f6f0fd49f324ee057a', 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-73', 'description', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
Link-back: T-70 is the workaround; this is the proper fix.', 'Workaround in T-70 / `lib/src/terminal/src/ui/painter.dart` suppresses bold rendering entirely because Flutter`s synthetic bold drifts glyph advance widths and breaks the monospace cell grid. We lose semantic bold (bash prompts, emphasis in TUIs) as a side effect.
|
||
|
||
**Acceptance:**
|
||
- A bold-weight monospace face ships alongside the regular weight, with verified-equal advance widths at clide`s render size (cell-grid stable on bold).
|
||
- The painter`s `bold: false` override is reverted; bold attributes from terminal escapes render visually bold.
|
||
- Test: render a known-mixed-weight buffer and assert paragraph maxIntrinsicWidth matches the regular-weight measurement (cell drift = 0).
|
||
|
||
**Options to evaluate:**
|
||
- Re-add JetBrainsMono-Bold.ttf once we`ve verified its advance widths actually equal the Regular file at our cellSize. (We dropped it because the synthetic-bold issue obscured whether the bundled Bold actually matched.)
|
||
- Switch to a variable JetBrainsMono and request a specific weight axis position.
|
||
- Pick a different mono font whose Bold has guaranteed-equal advance (Berkeley Mono, IBM Plex Mono).
|
||
|
||
---
|
||
Refinement (2026-05-06):
|
||
|
||
Investigation showed the perceived over-bolding in the Claude pane was synthetic-bold smearing (Flutter overpaints when no Bold.ttf is registered), not aggressive-but-legitimate bold. Visual A/B confirms a real bold face renders crisp emphasis without smearing. No per-pane renderer config needed.
|
||
|
||
**Concrete fix shape:**
|
||
1. `pubspec.yaml` (family JetBrainsMono): register `JetBrainsMono-Bold.ttf` with `weight: 700` and `JetBrainsMono-BoldItalic.ttf` with `weight: 700, style: italic`. Files are already on disk under `assets/fonts/jetbrains_mono/`.
|
||
2. `assets/licenses.yaml` JetBrainsMono entry: update `weights_bundled` to `[Regular, Italic, Bold, BoldItalic]` per D-42 (must match what is actually wired into the font family).
|
||
3. `lib/src/terminal/src/ui/painter.dart:198`: revert the `bold: false` override and delete the now-stale comment block at lines 193-195.
|
||
4. Test: render a mixed-weight buffer and assert `paragraph.maxIntrinsicWidth` matches the all-regular measurement (cell drift = 0). If parity fails, the fallback options stay open (variable JetBrainsMono / Berkeley Mono / IBM Plex Mono).
|
||
|
||
Workaround comment (`Skip bold rendering — Flutter`s synthetic bold...`) goes away in step 3.', NULL, '2026-05-06 16:13:35', '2026-05-06 16:13:35', '2026-05-06 16:13:35', NULL, 'baa15a2631bef74a4c1f732cee12de1c', 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-73', 'status', 'ready', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, '8e3c8ef2cfa48a2805eef455ec8f5830', 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-74', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 16:13:39', '2026-05-06 16:13:39', '2026-05-06 16:13:39', NULL, 'eb816113ca1ef0ef3514af5f8d3391d3', 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-73', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:29:28', '2026-05-06 20:29:28', '2026-05-06 20:29:28', NULL, 'ba203ed16c6e042457e8e751bf56932a', 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-89', 'description', NULL, 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
|
||
| `lib/widgets/` | 537 / 1184 | 45.4% |
|
||
| `lib/src/pql/` | 45 / 94 | 47.9% |
|
||
| `lib/builtin/` | 269 / 550 | 48.9% |
|
||
| `lib/kernel/` | 889 / 1691 | 52.6% |
|
||
| `lib/src/daemon/` | 320 / 584 | 54.8% |
|
||
| `lib/src/pty/` | 130 / 216 | 60.2% |
|
||
| `lib/src/git/` | 356 / 518 | 68.7% |
|
||
| `lib/src/ipc/` | 110 / 156 | 70.5% |
|
||
| `lib/src/files/` | 98 / 133 | 73.7% |
|
||
| `lib/extension/` | 41 / 54 | 75.9% |
|
||
| `lib/src/editor/` | 107 / 129 | 82.9% |
|
||
| `lib/src/panes/` | 70 / 73 | 95.9% |
|
||
|
||
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
|
||
|
||
**End state:**
|
||
- Total line coverage ≥ 95% (no per-area carve-outs).
|
||
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
|
||
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
|
||
|
||
**Acceptance criteria for closing this epic:**
|
||
1. `coverage/floor.txt` reads `95` (or higher).
|
||
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
|
||
3. The pre-push gate computes coverage and rejects pushes below the floor.
|
||
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
|
||
|
||
**Strategy / child shape:**
|
||
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
|
||
|
||
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
|
||
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
|
||
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
|
||
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
|
||
|
||
**Out of scope:**
|
||
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
|
||
- Integration / E2E coverage (only widget + unit feed lcov).
|
||
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
|
||
|
||
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
|
||
', NULL, '2026-05-06 20:37:40', '2026-05-06 20:37:40', '2026-05-06 20:37:40', NULL, 'c5f416e95632e206449476de472fc4e5', 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-90', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:38:24', '2026-05-06 20:38:24', '2026-05-06 20:38:24', NULL, '11840a870c33e5e94b6adcd3cfb5f10a', 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-90', 'description', NULL, 'First child of T-89. Locks in the no-regression contract before any test-writing children land — so subsequent floor bumps are explicit and reviewable.
|
||
|
||
**What lands:**
|
||
|
||
1. **`coverage/floor.txt`** — committed file containing today''s actual line-coverage number (currently `34` after rounding down from 34.9% to give a small margin against measurement jitter; the gate child re-measures on first install and writes the exact number it observes minus 1 if needed).
|
||
2. **`ci/coverage_gate.sh`** — runs `flutter test --coverage --exclude-tags forkpty`, parses `coverage/lcov.info` (sum of LH / sum of LF * 100), compares to `coverage/floor.txt`, exits non-zero if below, prints the actual percentage either way.
|
||
3. **`Makefile` `coverage-gate` target** — wraps `ci/coverage_gate.sh` for local use.
|
||
4. **`make push-check`** — adds `coverage-gate` to its dependency list. Pre-push hook (already wired via `.githooks/pre-push`) picks this up automatically.
|
||
5. **`coverage/README.md`** — short note explaining the floor file, how to bump it (rule: bump only in the same commit that adds covering tests, with the bump reflecting the measured improvement), and the link to D-66.
|
||
|
||
**Acceptance:**
|
||
- `make coverage-gate` passes on a clean checkout (floor matches reality).
|
||
- `make push-check` includes the gate.
|
||
- A test deletion would fail `make coverage-gate` (verify by experiment, not just by reading the script — invariant has to actually hold).
|
||
- Pre-push hook runs the gate (verify by attempting a deliberately-failing push and seeing it blocked).
|
||
- `coverage/lcov.info` is gitignored (it''s regenerated on every run; only `floor.txt` is committed).
|
||
|
||
**Out of scope:**
|
||
- Bumping the floor toward 95% — that''s the test-writing children''s job.
|
||
- Branch coverage — D-66 explicitly excludes.
|
||
- Per-area floors — D-66 chose a single global floor for simplicity.
|
||
|
||
**Notes:**
|
||
- `lcov` CLI is not installed on this dev machine; parser must be self-contained (awk or python). Don''t introduce `lcov` as a dependency of the gate.
|
||
- `coverage/lcov.info` is the standard Dart output path of `flutter test --coverage`. Don''t relocate.
|
||
- `flutter test --coverage` runs all tests including `forkpty`-tagged ones, which fail under the flutter test runner (see comment in `test/pty/session_test.dart`); `--exclude-tags forkpty` matches `ci/test.sh`. Forkpty-tagged tests don''t currently feed lcov because they run via `dart test`; check whether their coverage can be merged later (separate ticket if not).
|
||
', NULL, '2026-05-06 20:39:05', '2026-05-06 20:39:05', '2026-05-06 20:39:05', NULL, 'a7af4c58b2e4ccbc80d128d6ba64ba0a', 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-90', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, '74247471621e8f69e888066727cffcda', 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-89', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'a81a6e3ef23af1c113f46e467fdde278', 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-74', 'status', 'in_progress', 'ready', NULL, '2026-05-06 20:39:45', '2026-05-06 20:39:45', '2026-05-06 20:39:45', NULL, 'e2692b0d73a61655c310846dda2b2ae2', 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-74', 'status', 'ready', 'ready', NULL, '2026-05-06 20:40:13', '2026-05-06 20:40:13', '2026-05-06 20:40:13', NULL, 'cabddbfab05fa8c3e3705311f895d619', 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-90', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '02f69af134363ce27d815ad3081379f3', 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-89', 'status', 'in_progress', 'in_progress', NULL, '2026-05-06 20:40:16', '2026-05-06 20:40:16', '2026-05-06 20:40:16', NULL, '9695c13174226c39e5bf059d88d8e855', 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-90', 'status', 'in_progress', 'done', NULL, '2026-05-06 20:44:47', '2026-05-06 20:44:47', '2026-05-06 20:44:47', NULL, '8d3f9f4effc90eacfcefd2701a33ede6', 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-91', 'parent_id', NULL, 'T-89', NULL, '2026-05-06 20:49:47', '2026-05-06 20:49:47', '2026-05-06 20:49:47', NULL, 'baf63005fb488a699872715061d2473c', 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-91', 'description', NULL, 'Second child of T-89. The dominant coverage hole — `lib/src/terminal/` is 12 / 3167 lines (0.4%), about 56% of the global gap.
|
||
|
||
**Today''s distribution within `lib/src/terminal/src/`:**
|
||
|
||
| sub-area | hit / total | % | shape |
|
||
|---|---|---|---|
|
||
| `core/buffer/` | 0 / 581 | 0.0% | pure data structures |
|
||
| `core/escape/` | 1 / 473 | 0.2% | pure parser logic |
|
||
| `core/input/` | 4 / 292 | 1.4% | key + keytab tables |
|
||
| `core/(root)` | 0 / 188 | 0.0% | callback wiring |
|
||
| `core/mouse/` | 3 / 51 | 5.9% | encoder logic |
|
||
| `ui/` | 3 / 823 | 0.4% | painters + render objects |
|
||
| `(root)` | 0 / 471 | 0.0% | `Terminal` + `TerminalView` |
|
||
| `utils/` | 1 / 251 | 0.4% | circular buffer etc. |
|
||
|
||
**Strategy — pure logic first, then UI:**
|
||
|
||
1. **`core/buffer/`** (581 lines, biggest single chunk, all pure Dart) — line/segment/cell models, scrollback math, range queries. Unit tests exercise the data structure directly.
|
||
2. **`core/escape/`** (473 lines, pure state machine) — feed ANSI/VT byte streams in, assert the resulting buffer/cursor state. Reusable corpus from xterm.js / xterm.dart upstream is fair game.
|
||
3. **`core/input/`** (292 lines) — key + keytab tables; assert mapping from logical key to escape bytes per terminal mode.
|
||
4. **`core/mouse/`** (51 lines) — encoder for SGR-1006 / X11 mouse protocols. Small, fast.
|
||
5. **`utils/`** (251 lines) — circular buffer + helpers; trivial unit tests.
|
||
6. **`(root)`** (`terminal.dart`, `terminal_view.dart`, 471 lines) — orchestration; some unit-testable, some needs widget tests.
|
||
7. **`ui/`** (823 lines) — painters and render objects; widget + golden tests. Hardest, most pixel-sensitive.
|
||
|
||
**Acceptance:**
|
||
- `lib/src/terminal/` line coverage ≥ 95%.
|
||
- All tests fast (<5 s for the area''s test files combined; no real PTY shell-out).
|
||
- Helper fixtures live in `test/terminal/_helpers/` if they get reused across files.
|
||
- Per D-66, no `// ignore:` directives or analysis_options excludes added to dodge coverage.
|
||
- `coverage_floor:` in `pubspec.yaml` is bumped in lockstep — every commit that adds covering tests bumps the floor by the measured delta (rounded down to the integer below the new percentage).
|
||
|
||
**Strategy on commit cadence:**
|
||
Land tests in batches per sub-area (one commit per `core/buffer/`, `core/escape/`, etc.). Each batch:
|
||
- Adds tests under `test/terminal/<sub-area>/`.
|
||
- Re-measures coverage, bumps `coverage_floor:` to the new integer (or as high as the gate currently allows).
|
||
- Documents in CHANGELOG under "Changed" (test-only changes can also fold into one CHANGELOG entry per area).
|
||
|
||
**Out of scope:**
|
||
- Refactoring `lib/src/terminal/` for testability — if a piece is genuinely untestable, surface it (don''t silence it). Code under `lib/` is owned regardless of upstream attribution; refactor to make it testable rather than carving it out.
|
||
- Branch coverage (D-66 explicitly excludes).
|
||
- Mouse / keyboard *protocol* round-trips against real TUIs (that''s T-74 and integration work).
|
||
|
||
**Cross-references:** D-66 (coverage gate), T-89 (epic), T-90 (gate plumbing).
|
||
', NULL, '2026-05-06 20:50:29', '2026-05-06 20:50:29', '2026-05-06 20:50:29', NULL, 'a165eb2d32141e72feb1ffdf9d98d58e', 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-91', 'status', 'backlog', 'in_progress', NULL, '2026-05-06 20:50:32', '2026-05-06 20:50:32', '2026-05-06 20:50:32', NULL, 'da5f03732cdcd40106cf47c852191706', 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-92', 'description', NULL, '**File:** `lib/src/terminal/src/core/reflow.dart` (`_LineReflow._addPart`).
|
||
|
||
**Bug:** Anchors whose x-coordinate sits past the source line''s `trimmedLength` get reparented onto whatever `_builder._result` is active at that moment. After the main reparenting loop has run with `lineFilled` on the final iteration, `_builder.take()` has already moved the filled line into `_lines` and reset `_result` to a fresh empty `BufferLine`. The post-loop block (lines 144-150 in the current source) then reparents the tail anchor onto that fresh empty line. If no further content lands in the builder before `finish()` exits — which is the case whenever the post-loop block fires on a non-wrapped logical line — the empty builder line is never appended to `_lines`, and the anchor ends up pointing to a `BufferLine` that the reflow result never includes.
|
||
|
||
**Trigger paths in production code:**
|
||
|
||
1. `SelectAllTextIntent` (`lib/src/terminal/src/ui/shortcut/actions.dart:60-63`) creates the end anchor at `x = terminal.viewWidth`. That is *exactly* the past-trimmedLength position for every partially-filled line in the buffer. Any user who Ctrl+A''s and then resizes the terminal narrower hits the bug for every short line in the scrollback.
|
||
2. `selectCharacters` / mouse drag selection (`lib/src/terminal/src/ui/render.dart:286-298`) — anchors are clamped to `[0, viewWidth-1]` but can sit past `trimmedLength` on a partially-filled line. Drag past content end + resize narrower trips the bug.
|
||
3. `selectWord` (`render.dart:264-275`) — `getWordBoundary` walks until a separator OR `viewWidth`, so the end anchor of a near-edge word can land at `x = viewWidth` and trip the bug on resize.
|
||
|
||
**Symptom:** selection extents lose track of content after a width change. Visible highlight stops moving with the underlying selection, or extents disappear off the rendered buffer entirely. No exception is thrown — the dangling anchor still has a valid `BufferLine` reference, it just isn''t on a line in the visible buffer.
|
||
|
||
**Acceptance:**
|
||
- New unit test in `test/terminal/core/core_test.dart` (or a sibling) that:
|
||
- Constructs a partially-filled source line and an anchor past `trimmedLength`,
|
||
- Runs reflow shrink,
|
||
- Asserts the anchor''s owning line is *in the reflow output list*, not dangling.
|
||
- The existing widget-level select-all behaviour still works on resize: a Ctrl+A across the buffer followed by a narrower resize keeps the selection''s end anchor pointing to a line in the reflowed result.
|
||
- No regression in the existing reflow tests (the tail-clamp cases, wide-char boundaries, multi-line wrapped runs).
|
||
|
||
**Investigation notes (do not skip):**
|
||
|
||
The fix is conceptually small — the reparented tail anchor must land on a line that gets emitted. Two candidate shapes:
|
||
|
||
- **Skip the reparent entirely.** Anchors past `trimmedLength` arguably shouldn''t survive a shrink that drops their column. Risk: callers expect every anchor on the source to be reparented somewhere; silently disposing them changes semantics.
|
||
- **Force `finish()` to flush the builder line.** Whenever the tail-anchor branch fired during `_addPart`, set a flag, and have `finish()` emit the builder line even if `isEmpty`. Risk: empty trailing lines start showing up in the reflow result, which would break callers that assume reflow returns only content lines (resize loop in `Buffer.resize` pads to viewport height after, so an extra empty might be benign there but worth checking).
|
||
|
||
Don''t pick one without re-reading the full reflow flow + every caller of `reflow()`. The selection round-trip is also tested at the widget layer indirectly — verify `make test-a11y` still passes after the change.
|
||
|
||
**Surfaced from:** T-91 test sweep (commit 3196c49). The 100% coverage on reflow.dart was achieved by exercising this branch but explicitly *not* asserting on the resulting line being in the output — see the test comment.
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the test in `test/terminal/core/core_test.dart` group `reflow` named "anchors on the source line tail (past trimmedLength) get reparented" documents the current (buggy) behaviour.
|
||
', NULL, '2026-05-07 05:52:26', '2026-05-07 05:52:26', '2026-05-07 05:52:26', NULL, '1e8625251297bbad6ee96d80b05b606b', 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-92', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 05:56:47', '2026-05-07 05:56:47', '2026-05-07 05:56:47', NULL, '3fd3c03fde722d5d5c30813b2ce42822', 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-92', 'status', 'in_progress', 'done', NULL, '2026-05-07 05:58:51', '2026-05-07 05:58:51', '2026-05-07 05:58:51', NULL, '4ffd69490cbda8da72a60b642182213a', 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-93', 'description', NULL, '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', NULL, '2026-05-07 06:34:22', '2026-05-07 06:34:22', '2026-05-07 06:34:22', NULL, 'e549f5ed0e98298ba308aa75a3f674c5', 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-93', 'description', '**File:** `lib/src/terminal/src/ui/terminal_view.dart` + `gesture/gesture_handler.dart` + `gesture/gesture_detector.dart`.
|
||
|
||
**Bug:** The `TerminalView.onTapUp` callback parameter (declared at `terminal_view.dart:91`, documented as "Callback for when the user taps on the terminal") is wired to a path that is never executed. Internal trace:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` is declared (gesture_detector.dart:31) but never invoked.
|
||
|
||
Independently, `TerminalGestureHandler` exposes an `onSingleTapUp` parameter that *is* properly wired (handler line 139 calls `_tapUp(widget.onSingleTapUp, ...)`), but `TerminalView` never passes a value for it.
|
||
|
||
Net result: the public `TerminalView.onTapUp` API is silently a no-op. Any caller passing `onTapUp:` to `TerminalView` thinks they registered for tap notifications, but the callback never fires.
|
||
|
||
**Trigger paths in production:**
|
||
|
||
`TerminalView.onTapUp` is called by anyone who wants to intercept terminal taps (e.g. for a custom click-to-position cursor, hyperlink detection, click telemetry). Surfaced during T-91''s TerminalView widget tests: a test that registered `onTapUp` via `tester.tap` confirmed the callback never fires.
|
||
|
||
**Acceptance:**
|
||
- `TerminalView.onTapUp` fires on every tap up over the widget (or only confirmed single taps — pick one and document the contract; both shapes are reasonable for terminal selection).
|
||
- `TerminalView.onSingleTapUp` either becomes a public parameter on `TerminalView` (mirroring TerminalGestureHandler), or the docstring on `onTapUp` is clarified to match what actually happens.
|
||
- New widget test in `test/terminal/terminal_view_test.dart` that registers a callback, taps with `tester.tap` + `pumpAndSettle`, and asserts the callback ran with the resolved `CellOffset`.
|
||
- Any pre-existing internal use of `TerminalGestureDetector.onTapUp` vs. `onSingleTapUp` should be reviewed: pick one canonical hook for "tap completed" and remove the other, or document the difference.
|
||
|
||
**Investigation notes:**
|
||
|
||
- `TerminalGestureDetector` declares both `onTapUp` and `onSingleTapUp` (lines 31 / 33). Only the latter is ever invoked. The `onTapUp` field is therefore dead surface.
|
||
- The cleanest fix shape: have `_handleTapUp` (gesture_detector.dart:84) call `widget.onTapUp` *in addition to* (or instead of) `widget.onSingleTapUp`, depending on the desired contract:
|
||
- **Option A** — fire `onTapUp` on every up event (raw); fire `onSingleTapUp` only on confirmed singles. Most flexible.
|
||
- **Option B** — collapse to a single hook (`onTapUp`) and remove `onSingleTapUp`. Simpler but loses the double-tap distinction.
|
||
- Either option needs verification against the existing single/double-tap timer flow. Don''t pick without re-reading how `_isDoubleTap` is used.
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. Documented in the test file as "skipped — see T-93".
|
||
|
||
**Cross-references:** T-91 (epic parent for the test sweep that found this), T-89 (coverage epic), the `onTapUp` test stubbed in `test/terminal/terminal_view_test.dart` will become live once the wiring is fixed.
|
||
', '**File:** `lib/src/terminal/src/terminal_view.dart` (param + wiring) + `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (pass-through) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead field).
|
||
|
||
**Bug:** `TerminalView.onTapUp` is documented as "Callback for when the user taps on the terminal" but is wired to a code path that is never executed:
|
||
|
||
1. `TerminalView.build` passes `onTapUp: _onTapUp` to `TerminalGestureHandler` (terminal_view.dart:304).
|
||
2. `TerminalGestureHandler.build` forwards via `onTapUp: widget.onTapUp` to `TerminalGestureDetector` (gesture_handler.dart:67).
|
||
3. `TerminalGestureDetector._handleTapUp` (gesture_detector.dart:84) fires only `widget.onSingleTapUp`, never `widget.onTapUp`. The `onTapUp` field on `TerminalGestureDetector` (declared at line 31) is never invoked anywhere in the file.
|
||
|
||
The `_onTapUp` method on `TerminalViewState` itself is also dead — it''s only referenced from the dead-field path above.
|
||
|
||
**Two layers of "dead" here:**
|
||
|
||
- The **wiring** is dead — nothing inside the terminal renderer calls into `widget.onTapUp` after the field is set.
|
||
- The **public API surface** has zero in-tree callers. The single `TerminalView` callsite is `lib/widgets/src/clide_pty_view.dart`, and it doesn''t pass any tap callback.
|
||
|
||
So no production code depends on this parameter; nothing observable changes regardless of which way it gets resolved. (TerminalView''s `onSecondaryTapDown` / `onSecondaryTapUp` are wired correctly — `TapGestureRecognizer.onSecondaryTap*` fires them directly. Only `onTapUp` is bad.)
|
||
|
||
**Two clean resolutions:**
|
||
|
||
- **A. Fix the wiring.** Call `widget.onTapUp` from `_handleTapUp` (gesture_detector.dart:84) so the parameter does what its docstring claims. Risk: the existing `onSingleTapUp` semantics (gated on `!_isDoubleTap`) need a clearly-defined relationship to `onTapUp` — pick one of:
|
||
- `onTapUp` fires on every tap up (raw); `onSingleTapUp` fires only on confirmed singles.
|
||
- Collapse the two into a single hook and document the contract.
|
||
- **B. Delete the dead surface.** Remove `onTapUp` from `TerminalView`, `TerminalGestureHandler`, and `TerminalGestureDetector` (plus the `_onTapUp` method on `TerminalViewState`). Cleanest because no caller is broken — the surface is documented but unused. If a future caller wants tap notifications, expose `onSingleTapUp` (which works) or re-add `onTapUp` *with wiring* at that point.
|
||
|
||
Recommendation: **option B**. The "remove unused public API" pattern matches the no-pre-existing-excuse / dead-code cleanups in this campaign (the keytab-token defensive throws, the `_LineBuilder.isEmpty` getter, the `_buildDispatcher` rename). If a feature is needed later, add it back deliberately with tests and a real caller.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** `TerminalView.onTapUp` fires on every tap up; new widget test in `test/terminal/terminal_view_test.dart` verifies the callback runs with a `CellOffset`. Document the contract on the docstring.
|
||
- **B:** `TerminalView.onTapUp` parameter removed; `_onTapUp` method on `TerminalViewState` removed; `onTapUp` parameter on `TerminalGestureHandler` removed; `onTapUp` field + setter on `TerminalGestureDetector` removed. Existing tests still pass.
|
||
- No regression in secondary-tap behaviour (those parameters are wired correctly via `TapGestureRecognizer.onSecondary*`).
|
||
|
||
**Surfaced from:** T-91 test sweep on `terminal_view.dart`. The widget test that registered `onTapUp` and tapped via `tester.tap` confirmed the callback never fires regardless of `pumpAndSettle` / gesture flushing.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-89 (coverage epic).
|
||
', NULL, '2026-05-07 06:35:32', '2026-05-07 06:35:32', '2026-05-07 06:35:32', NULL, '2f51a45e06d526d9b47dbc617ea965f8', 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-93', 'status', 'backlog', 'in_progress', NULL, '2026-05-07 06:45:24', '2026-05-07 06:45:24', '2026-05-07 06:45:24', NULL, '82fb868231b1806c9a1cd802110856f7', 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-93', 'status', 'in_progress', 'done', NULL, '2026-05-07 19:04:55', '2026-05-07 19:04:55', '2026-05-07 19:04:55', NULL, 'bc269d5f335553e768041e72019dc3b0', 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-94', 'status', 'backlog', 'cancelled', NULL, '2026-05-07 19:08:52', '2026-05-07 19:08:52', '2026-05-07 19:08:52', NULL, '5f534e88c93e7eef4ad838692ee7b725', 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-95', 'description', NULL, '**File:** `lib/src/terminal/src/ui/gesture/gesture_handler.dart` (build wiring + state-method body) + `lib/src/terminal/src/ui/gesture/gesture_detector.dart` (dead public API).
|
||
|
||
**Bug:** Middle-clicks ("tertiary tap") are routed through the wrong handler chain, and even the unreachable target carries a button-enum typo. Same shape as T-93 but on a different gesture pair.
|
||
|
||
Trace:
|
||
|
||
1. `TerminalGestureHandler.build` (gesture_handler.dart:65–82) wires:
|
||
```dart
|
||
onTertiaryTapDown: onSecondaryTapDown,
|
||
onTertiaryTapUp: onSecondaryTapUp,
|
||
```
|
||
Right-hand sides are the state''s `onSecondaryTapDown` / `onSecondaryTapUp` *methods* (lines 138–144), not the tertiary equivalents. So a middle-click reaches `_tapDown(widget.onSecondaryTapDown, details, TerminalMouseButton.right)` — i.e. fires the **secondary** callback with **button=right**.
|
||
2. The state''s `onTertiaryTapDown` and `onTertiaryTapUp` methods (lines 147–152) are therefore **dead** — they''re declared but never invoked anywhere.
|
||
3. `onTertiaryTapUp` at line 152 has a copy-paste error: `TerminalMouseButton.right` where the function is named tertiary and the comment paragraph clearly intends middle. Even if the wiring were correct, the button reported on tertiary-up would be wrong.
|
||
4. The `onTertiaryTapDown` / `onTertiaryTapUp` *parameters* on `TerminalGestureHandler` (lines 24–25, 45–47) and `TerminalGestureDetector` (lines 13–14, 30–31) are **dead public surface** — no in-tree caller passes them, like T-93''s `onTapUp`.
|
||
|
||
**Two layers of "dead" again:**
|
||
|
||
- The wiring is wrong, so middle-clicks dispatch as if they were right-clicks.
|
||
- The public API to receive proper middle-click notifications doesn''t reach any caller, so even if a consumer passes `onTertiaryTapDown:` to `TerminalView`, it would do nothing today (TerminalView doesn''t expose tertiary callbacks).
|
||
|
||
**User-visible impact:**
|
||
|
||
A consumer relying on right-click for context-menu behaviour on `TerminalView` will accidentally trigger that menu on middle-click as well, which is wrong on every desktop platform (middle-click is conventionally paste / scroll-wheel-button). Tests of secondary-tap will pass for both right- and middle-clicks, masking the issue.
|
||
|
||
**Resolutions, like T-93:**
|
||
|
||
- **A. Fix the wiring + typo.** In `TerminalGestureHandler.build`, change `onTertiaryTapDown: onSecondaryTapDown` to `onTertiaryTapDown: onTertiaryTapDown` (and same for Up). Fix line 152''s `TerminalMouseButton.right` → `middle`. Adds a parameter on `TerminalView` (`onTertiaryTapDown` / `onTertiaryTapUp`) so consumers can register middle-click handlers. Test surfaces the new path.
|
||
- **B. Delete the dead surface.** Remove `onTertiaryTapDown` / `onTertiaryTapUp` parameters from both `TerminalGestureHandler` and `TerminalGestureDetector`, plus the state''s tertiary methods. If middle-click ever needs distinct handling, add it back deliberately. Cleanest because no caller depends on tertiary today.
|
||
|
||
Recommendation: **option B**. Same logic as T-93''s recommendation — the surface is documented but unwired and unused. Adding it back later with a real consumer + tests is cheaper than fixing dead surface today.
|
||
|
||
**Aside:** there''s a related dead branch in `onDragStart` (line 176):
|
||
```dart
|
||
details.kind == PointerDeviceKind.mouse
|
||
? renderTerminal.selectCharacters(details.localPosition)
|
||
: renderTerminal.selectWord(details.localPosition);
|
||
```
|
||
The `PanGestureRecognizer` in `TerminalGestureDetector` is registered with `supportedDevices: {PointerDeviceKind.mouse}` (gesture_detector.dart:143) — so non-mouse drags can''t reach `onDragStart`. The `selectWord` else-branch is unreachable through the current wiring. Consider removing it as part of the same cleanup pass, or extending PanGestureRecognizer to accept touch.
|
||
|
||
**Acceptance:**
|
||
|
||
- Either:
|
||
- **A:** Tertiary tap parameters on `TerminalView` exist and fire on middle-click with `button=middle`. Regression test verifies a middle click reaches the user callback with the resolved cell offset.
|
||
- **B:** `onTertiaryTapDown` / `onTertiaryTapUp` removed from `TerminalGestureHandler`, `TerminalGestureDetector`, and the state''s tertiary methods. Existing tests still pass.
|
||
- The `selectWord` branch in `onDragStart` is either reachable (extend PanGestureRecognizer.supportedDevices) or removed.
|
||
|
||
**Surfaced from:** T-91 ui/ gesture sweep. The `onTertiaryTap*` lines (gesture_handler.dart:147-152) showed as uncovered in the lcov diff — the gestures sweep skipped writing tests for them because they''re unreachable production code.
|
||
|
||
**Cross-references:** T-91 (epic parent), T-93 (same shape on `onTapUp`), T-89 (coverage epic).
|
||
', NULL, '2026-05-08 11:01:01', '2026-05-08 11:01:01', '2026-05-08 11:01:01', NULL, '8088d054dd8c7946075ed6325724dfb3', 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-96', 'description', 'The reader isolate spawned by `NativePty._spawnReaderAsync` (`lib/src/pty/native_pty.dart`) hangs intermittently — about 5% of fresh spawns — with poll() on the master fd never returning POLLIN or POLLHUP, even when the child has clearly written and exited. Probed: 2 hangs out of 30 sequential `/bin/sh -c "printf hello-pty"` spawns over a 30s/spawn timeout.
|
||
|
||
**Symptom:** test sees zero `pane.output` events and no `pane.exit`; the reader isolate sits in `poll(pfd, 1, 100)` returning 0 forever. The hang is per-spawn; a fresh `NativePty.start` recovers cleanly.
|
||
|
||
**Workaround in place:** `retry: 2` on every PTY-output-dependent test in `test/pty/session_test.dart` and `test/panes/registry_test.dart`. Three combined attempts at ~5% per-spawn fail rate ≈ 99.99% success. Comments at the test sites point back here.
|
||
|
||
**Acceptance:**
|
||
- Identify why a fresh master fd sometimes never delivers ready events. Candidates to investigate: a race between `forkpty()` and `Isolate.spawn` (the new isolate may see an FD table snapshot from an awkward moment), a missing fcntl flag on the master fd, lazy `DynamicLibrary.process()` symbol resolution in a freshly-spawned dart isolate, GC interference, or wasmtime/tree-sitter signal handlers (unlikely — the probe ran in a clean dart isolate with no wasmtime).
|
||
- Fix the underlying race so a single spawn delivers reliably.
|
||
- Remove the `retry: 2` decorations and the explanatory comments in both test files.
|
||
|
||
**Out of scope:** changing the PTY threading model or the reader-isolate architecture. The fix should keep `Isolate.spawn(_readLoop, ...)` as the I/O primitive.
|
||
|
||
**Cross-references:** D-5 (Dart core + sidecar dissolution; PTY is owned via FFI), `lib/src/pty/native_pty.dart#L235-262` (`_spawnReaderAsync`), `lib/src/pty/native_pty.dart#L265-293` (`_readLoop`).', 'The reader isolate spawned by `NativePty._spawnReaderAsync` (`lib/src/pty/native_pty.dart`) hangs intermittently — about 5% of fresh spawns — with poll() on the master fd never returning POLLIN or POLLHUP, even when the child has clearly written and exited. Probed: 2 hangs out of 30 sequential `/bin/sh -c "printf hello-pty"` spawns over a 30s/spawn timeout.
|
||
|
||
**Root cause — found 2026-05-17 via forensic probe:**
|
||
|
||
The "child" never reaches `execve`. `/proc/<child-pid>/stat` reports comm `(DartWorker)` (the Dart VM worker thread name) instead of `sh`, with state `S` (sleeping). The pty master fd is a real pty (tty-index assigned, fcntl flags 0x8002 = O_RDWR|O_NOCTTY), but `poll(fd, 500ms)` from the main isolate returns 0 — nothing was ever written.
|
||
|
||
This is **`fork()` in a multithreaded process** — a textbook async-signal-safety violation. The Dart VM runs multiple worker threads that hold libc locks (notably `malloc`). When `forkpty()` calls `fork()`, only the calling thread survives in the child, but the locks held by ghost-threads remain "locked forever." The child deadlocks before it can complete its post-fork → pre-execve setup.
|
||
|
||
Why not always? Lock state at fork() time is timing-dependent. ~95% of the time no Dart worker happens to hold a problematic lock, and execve proceeds. ~5% of the time it deadlocks.
|
||
|
||
**Workaround in place (test-only):** `retry: 2` on every PTY-output-dependent test in `test/pty/session_test.dart` and `test/panes/registry_test.dart`. Three combined attempts at ~5% per-spawn fail rate ≈ 99.99% combined success. Comments at the test sites point back here.
|
||
|
||
**Proper fix:** replace `forkpty()` with `posix_openpt()` + `unlockpt()` + `grantpt()` + `posix_spawn()` (with file actions wiring the pty slave to stdin/stdout/stderr). `posix_spawn` uses `vfork()` under glibc/musl, which keeps the parent suspended until execve completes — no Dart code ever runs in the child, no lock-deadlock possible. Linux and macOS both support this API.
|
||
|
||
**Immediate hardening alternative (Linux-only, simpler):** after spawn, sample `/proc/<pid>/comm` after ~250ms. If it equals the parent process''s comm, execve never ran → kill the child, surface `PtyException(''execve-deadlock'', ...)`, let the caller retry. This catches the deadlocked state deterministically instead of waiting for a poll timeout. It doesn''t fix the bug, but turns silent hangs into reportable errors.
|
||
|
||
**Acceptance for closing this ticket:**
|
||
1. Replace forkpty path with posix_openpt + posix_spawn (or implement the comm-check hardening as an interim).
|
||
2. Probe (200 sequential `printf hello`-and-exit spawns) reports zero hangs.
|
||
3. Remove the `retry: 2` decorations + explanatory comments in `test/pty/session_test.dart` (6 tests) and `test/panes/registry_test.dart` (1 test).
|
||
|
||
**Out of scope:** changing the reader-isolate architecture itself. `Isolate.spawn(_readLoop, ...)` for output stays.
|
||
|
||
**Cross-references:**
|
||
- D-5 (Dart core + sidecar dissolution; PTY is owned via FFI)
|
||
- `lib/src/pty/native_pty.dart:200` (the forkpty call site)
|
||
- `lib/src/pty/native_pty.dart:170-191` (child-side post-fork code that deadlocks)
|
||
- `lib/src/pty/native_pty.dart:235-262` (reader spawn — not the bug; downstream symptom)
|
||
- glibc posix_spawn docs: https://www.gnu.org/software/libc/manual/html_node/Process-Creation-Example.html
|
||
', NULL, '2026-05-17 18:05:33', '2026-05-17 18:05:33', '2026-05-17 18:05:33', NULL, '56d9ba7c49068a498a003e900cde416f', 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-96', 'status', 'backlog', 'done', NULL, '2026-05-17 18:11:18', '2026-05-17 18:11:18', '2026-05-17 18:11:18', NULL, '3215fc763c4f1dce75ea9e0bb2b9bbb6', 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-98', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:51:25', '2026-05-17 18:51:25', '2026-05-17 18:51:25', NULL, '0369c8264ec2c67b3bf3f28057404ec0', 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-98', 'status', 'in_progress', 'done', NULL, '2026-05-17 18:55:57', '2026-05-17 18:55:57', '2026-05-17 18:55:57', NULL, 'cb0ae539d5665bd9720b327d60a2cbed', 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-101', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '55d14b4493a159695d4b40bbd19fff23', 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-102', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '7fa075d7232df5302ffdfb5d1c3d79f2', 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-101', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '56e8da656c5fc53af241900e6313805e', 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-102', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '7f09d2a1501d3d072e071822c191f253', 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-103', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, '2635d907f94402ec506d3b56dfc25b21', 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-106', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, 'cc39b3fd718e99423823e640950ccf1b', 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-113', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:03:18', '2026-05-17 19:03:18', '2026-05-17 19:03:18', NULL, 'cc8605833ba5cb97e068eb5a6d57d8f4', 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-106', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, '8dccc83b93c5005db744a100a5b3baea', 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-113', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, '8f7812831e564005eb796708543c54b3', 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-103', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:18:43', '2026-05-17 19:18:43', '2026-05-17 19:18:43', NULL, 'c7d1b76cb6854efb7e9990cc7204d899', 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-100', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:23:24', '2026-05-17 19:23:24', '2026-05-17 19:23:24', NULL, '607b82f096b5d694fb75b7daf64dc2e0', 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-117', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:28:31', '2026-05-17 19:28:31', '2026-05-17 19:28:31', NULL, 'b5c56dff8a2c7d6f14ecf19231b63268', 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-110', 'description', '`app.dart:90-148` routes all shortcuts through one root `KeyboardListener` — no per-context scoping; will conflict with text-input fields once any pane wants to capture keys. `KeybindingResolver.fromKeyEvent` keys off `logicalKey.keyLabel` which is layout-dependent (US-QWERTY `Ctrl+/` differs from AZERTY `Ctrl+:`).
|
||
|
||
**Fix:**
|
||
1. Replace the root `KeyboardListener` with scoped `Shortcuts`/`Actions` per slot.
|
||
2. Move `KeybindingResolver` to `physicalKey` or a stable mapping layer.
|
||
|
||
Coordinate with T-100 (Focus/Shortcuts wrapper for ClideTappable) and T-105 (focus traversal).
|
||
|
||
Source: consultants.md "UX — Findings — [Major]".', '**Superseded by T-117 — Done.**
|
||
|
||
The consultant findings here — single root `KeyboardListener` will conflict with text-input fields; `KebindingResolver.fromKeyEvent` keys off layout-dependent `logicalKey.keyLabel` — are both addressed by the keystroke mapper layer (T-117):
|
||
|
||
- Keymap-driven dispatch uses `LogicalKeyboardKey.keyId` (stable across layouts), not `keyLabel`.
|
||
- Root `KeyboardListener` now hands events straight to `KeymapService.resolveEvent`, which dispatches resolved Intents via `Actions.maybeInvoke` against the focused context — Actions providers per feature (palette, editor, etc.) handle their own intents; the root only handles global ones (text scale, generic command bridge). This is the scoped Shortcuts/Actions model the consultant prescribed.
|
||
|
||
Remaining cleanup (deletion of the now-vestigial `KeybindingResolver` class + the legacy `KeyboardListener` wrap once Flutter Shortcuts widget integration is on every feature) is small and lands as part of T-100 or its own follow-up.
|
||
|
||
Original text: Replace root KeyboardListener with scoped Shortcuts/Actions; move KebindingResolver off layout-dependent keyLabel.', NULL, '2026-05-17 19:39:08', '2026-05-17 19:39:08', '2026-05-17 19:39:08', NULL, '6364d644fbbfa903390ee78b5b402367', 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-110', 'status', 'backlog', 'done', NULL, '2026-05-17 19:39:12', '2026-05-17 19:39:12', '2026-05-17 19:39:12', NULL, '6073b1bbc12001914d78d4cb419705dc', 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-23', 'description', 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
|
||
|
||
**Acceptance:**
|
||
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
|
||
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
|
||
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
|
||
- Fuzzy match against command title; recent / pinned commands float to the top.
|
||
|
||
**Implementation hints:**
|
||
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
|
||
- Keybinding goes in `lib/kernel/src/commands/keybindings.dart` per the D-54 keymap.
|
||
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', 'D-48 names `⌘P` (fuzzy file open) and `⌘⇧P` (command palette) as the canonical keyboard navigation. The command palette overlay/widget exists; the keybinding is not yet wired.
|
||
|
||
**Progress (2026-05-17, T-117):** The keymap layer now binds `ctrl+shift+p` / `meta+shift+p` to `PaletteOpenIntent` in `assets/keymaps/default.yaml`. The intent resolves end-to-end through `KeymapService.resolveEvent` → `Actions.maybeInvoke`. **Still pending**: an `Actions` provider somewhere in the tree that handles `PaletteOpenIntent` by calling `kernel.palette.open()`, plus the arrow-key / Escape / Enter handlers on `ClidePalette` itself. Those land as part of T-100 (palette keyboard nav).
|
||
|
||
**Acceptance:**
|
||
- `⌘⇧P` (`Ctrl+Shift+P` on Linux, follows the kernel keymap normalization) opens the command palette overlay over the active workspace.
|
||
- Esc dismisses; Enter runs the highlighted command; arrow keys move the highlight.
|
||
- Commands listed are everything registered via `CommandContribution` across all activated extensions.
|
||
- Fuzzy match against command title; recent / pinned commands float to the top.
|
||
|
||
**Implementation hints:**
|
||
- Slot exists: `Slots.commandPalette` is reserved (lib/kernel/src/panels/slot_id.dart).
|
||
- Bindings live in the keymap (T-117) — not in `lib/kernel/src/commands/keybindings.dart` (that file is legacy).
|
||
- The overlay should not shift layout (D-48 chrome budget — no layout shift on palette open).', NULL, '2026-05-17 19:39:23', '2026-05-17 19:39:23', '2026-05-17 19:39:23', NULL, '50203b7db93490ac779fe496865cf3a0', 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-64', 'description', 'Ship a VS Code-compatible keybinding preset that maps standard VS Code shortcuts to clide commands. Users select it in settings. Covers file navigation, editor actions, panel toggles, search, and terminal.', 'Ship a VS Code-compatible keybinding preset that maps standard VS Code shortcuts to clide commands. Users select it in settings. Covers file navigation, editor actions, panel toggles, search, and terminal.
|
||
|
||
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is now in place. Implementation is now just authoring `assets/keymaps/vscode.yaml` against the typed Intents in `lib/kernel/src/keymap/intents.dart` (plus `command:<id>` bindings for VS-Code-specific commands the preset wants to bind to clide commands). Users will switch presets via `app.keymap.preset = vscode` once a settings UI exists, or directly via the setting today.
|
||
|
||
**Acceptance:**
|
||
1. `assets/keymaps/vscode.yaml` ships covering the documented VS Code default keybindings.
|
||
2. `KeymapService.setPreset("vscode")` activates the preset and all asserted bindings resolve as expected.
|
||
3. The preset uses when-clauses where VS Code does (`editor.focused`, `inputFocused`, `palette.open`, …).
|
||
4. A regression test loads the preset and asserts a representative subset (e.g. ctrl+p → quick-open command, ctrl+shift+p → palette).
|
||
|
||
**Out of scope:** clide commands that have no VS Code analogue (those keep their default-preset bindings).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'a97758128bd490f603f11a68883a7193', 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-65', 'description', 'Ship a Vim-compatible keybinding preset with modal editing support (normal/insert/visual modes). Maps Vim motions and commands to clide editor and navigation actions. Users select it in settings.', 'Ship a Vim-compatible keybinding preset with modal editing support (normal/insert/visual modes). Maps Vim motions and commands to clide editor and navigation actions. Users select it in settings.
|
||
|
||
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is in place; modal Vim presets are more involved than the VS Code preset (T-64) because modes need to be expressed as scope flags (`vim.normal`, `vim.insert`, `vim.visual`) that the when-clause grammar can branch on. Implementation work:
|
||
|
||
1. Author `assets/keymaps/vim.yaml` using the typed Intents + `command:<id>` bindings.
|
||
2. Add a small mode-tracking service that publishes `vim.<mode>` scope flags via `KeymapService.setScopeFlag`.
|
||
3. Bind `Esc` to mode-reset → normal; `i` (when `vim.normal`) → enter insert; etc.
|
||
|
||
**Acceptance:**
|
||
1. `assets/keymaps/vim.yaml` ships covering the documented Vim default keybindings for editor / navigation / panes.
|
||
2. `KeymapService.setPreset("vim")` + the mode-tracking service together produce correct mode transitions.
|
||
3. A regression test exercises a representative motion (`j` → cursor down) and a mode change (`i` → insert).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'bf10f0149ccaa1e02050280b28305816', 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-66', 'description', 'Ship a JetBrains/IntelliJ-compatible keybinding preset mapping standard JetBrains shortcuts to clide commands. Covers navigation, refactoring, search, run/debug, and tool windows.', 'Ship a JetBrains/IntelliJ-compatible keybinding preset mapping standard JetBrains shortcuts to clide commands. Covers navigation, refactoring, search, run/debug, and tool windows.
|
||
|
||
**Unblocked by T-117 (2026-05-17):** the keystroke mapper layer is in place. Implementation is authoring `assets/keymaps/jetbrains.yaml` against the typed Intents + `command:<id>` bindings, plus when-clauses for the contexts JetBrains presets typically scope to (`editor.focused`, `inputFocused`, etc.).
|
||
|
||
**Acceptance:**
|
||
1. `assets/keymaps/jetbrains.yaml` ships covering the documented IntelliJ default keybindings.
|
||
2. `KeymapService.setPreset("jetbrains")` activates the preset and all asserted bindings resolve.
|
||
3. A regression test exercises a representative subset (e.g. shift+shift → quick-open command — see Q-9 if the search-everywhere overlay needs its own intent).', NULL, '2026-05-17 19:39:37', '2026-05-17 19:39:37', '2026-05-17 19:39:37', NULL, 'c1129f4cb3454b99e1cbf645ca56bea5', 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-117', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:39:40', '2026-05-17 19:39:40', '2026-05-17 19:39:40', NULL, '75b2fd14ceb7daf28645ccf84cb041f1', 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-100', 'status', 'in_progress', 'in_progress', NULL, '2026-05-17 19:40:44', '2026-05-17 19:40:44', '2026-05-17 19:40:44', NULL, '65600862f5787501dcd057756cdb4313', 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-100', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:47:50', '2026-05-17 19:47:50', '2026-05-17 19:47:50', NULL, '7d3c0ad4c47d3271ac35c5159e6e9d0e', 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-105', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:52:39', '2026-05-17 19:52:39', '2026-05-17 19:52:39', NULL, '129acb5d6b1882cb6ed4c9be46fd3e81', 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-105', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:56:28', '2026-05-17 19:56:28', '2026-05-17 19:56:28', NULL, '62dfdadc31b2c231afd837fb46f0a114', 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-108', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 19:58:13', '2026-05-17 19:58:13', '2026-05-17 19:58:13', NULL, 'a5dbfe5881e152e82538181b0fe56609', 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-108', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:05:38', '2026-05-17 20:05:38', '2026-05-17 20:05:38', NULL, '6815472fbfaa6d7740945b42d4c87f57', 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-112', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:06:30', '2026-05-17 20:06:30', '2026-05-17 20:06:30', NULL, '52271781bf987a984d9f219956034d1f', 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-112', 'status', 'in_progress', 'done', NULL, '2026-05-17 20:11:16', '2026-05-17 20:11:16', '2026-05-17 20:11:16', NULL, '6d51964a3ad7f1948f54a7c769336841', 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-114', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:13:36', '2026-05-17 20:13:36', '2026-05-17 20:13:36', NULL, '0bad40fa7f165f828bb0c0757044491e', 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-118', 'status', 'backlog', 'done', NULL, '2026-05-17 20:53:41', '2026-05-17 20:53:41', '2026-05-17 20:53:41', NULL, 'ff0126655805fed0ebe45f313c5ceff7', 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-114', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:04:38', '2026-05-18 07:04:38', '2026-05-18 07:04:38', NULL, '8cf198f8c824fa8f46277b7e22e68f0c', 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-111', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:43:45', '2026-05-18 07:43:45', '2026-05-18 07:43:45', NULL, '792efcf7c772de7cda7069ea3fc3ac0a', 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-104', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 07:49:44', '2026-05-18 07:49:44', '2026-05-18 07:49:44', NULL, 'bf7f2746c252f8b8893f4fdc06d459b8', 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-104', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:53:10', '2026-05-18 07:53:10', '2026-05-18 07:53:10', NULL, 'ce2b66658977edc3fddc78079d7852e5', 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-109', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 08:02:06', '2026-05-18 08:02:06', '2026-05-18 08:02:06', NULL, 'ee75fea173ae492a6ad8a1d888a2142d', 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-109', 'status', 'in_progress', 'done', NULL, '2026-05-18 08:07:23', '2026-05-18 08:07:23', '2026-05-18 08:07:23', NULL, '9dd61b76946909aa44edaf9d1c1741a0', 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-121', 'status', 'backlog', 'done', NULL, '2026-05-18 08:22:19', '2026-05-18 08:22:19', '2026-05-18 08:22:19', NULL, '861af9256207314d681e51abc77c386f', 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-115', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 08:23:34', '2026-05-18 08:23:34', '2026-05-18 08:23:34', NULL, '94f4a894c1dddfa87ee8306a9758c808', 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-115', 'status', 'in_progress', 'done', NULL, '2026-05-18 08:26:05', '2026-05-18 08:26:05', '2026-05-18 08:26:05', NULL, 'b8978359fc1b83ea0bcfc6cd5d34d5de', 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-115', 'status', 'done', 'done', NULL, '2026-05-18 09:30:59', '2026-05-18 09:30:59', '2026-05-18 09:30:59', NULL, '2039c525646caaf89a735454f7278162', 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-107', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:27:25', '2026-05-18 10:27:25', '2026-05-18 10:27:25', NULL, 'e40d2c9ddd9915ad2e32302bd555c99f', 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-107', 'status', 'in_progress', 'done', NULL, '2026-05-18 10:30:30', '2026-05-18 10:30:30', '2026-05-18 10:30:30', NULL, 'd5ecc2578b5b48991d07531eb2166529', 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-116', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 10:35:47', '2026-05-18 10:35:47', '2026-05-18 10:35:47', NULL, '59b70e93c913952d4f13b9ad2320e060', 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-116', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:22:46', '2026-05-18 11:22:46', '2026-05-18 11:22:46', NULL, '16fd285587d7a37090d2a21f4307c8e8', 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-74', 'status', 'ready', 'in_progress', NULL, '2026-05-18 11:49:28', '2026-05-18 11:49:28', '2026-05-18 11:49:28', NULL, 'e25e13a5afcbc376b3c57312c6f320ea', 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-74', 'status', 'in_progress', 'done', NULL, '2026-05-18 11:51:40', '2026-05-18 11:51:40', '2026-05-18 11:51:40', NULL, '4343b4758e9ffbf3e046d11d65dfeb1f', 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-124', 'description', 'First slice of T-99(a). Foundation for the rest.
|
||
|
||
Open a per-user unix-domain socket on Flutter app boot. Accept JSON-lines per the existing IpcRequest envelope. Route each request through the existing DaemonDispatcher (already wired in main.dart via daemonClientFactory). Tear down on app shutdown.
|
||
|
||
Acceptance:
|
||
1. lib/src/ipc/server.dart exists; binds an AF_UNIX socket at a per-user path (resolve in this ticket; current Q-record references Q-1 / Q-3 for auth/persistence concerns but the bare path strategy belongs here).
|
||
2. App boot starts the server; app shutdown closes the socket file cleanly.
|
||
3. `socat - UNIX-CONNECT:$SOCK <<< ''{"command":"git.status"}''` returns a JSON-line response. Tests use a synthetic socket fixture.
|
||
4. Multi-connection accept loop — concurrent socat invocations serialise through the dispatcher (single Flutter event loop) but don''t fail each other.
|
||
5. No client yet — that lands in T-126.
|
||
|
||
Source: T-99 sketch. Coordinate with: T-127, T-130.', 'First slice of T-99(a). Foundation for the rest.
|
||
|
||
Open a per-workspace unix-domain socket on Flutter app boot. Accept JSON-lines per the existing IpcRequest envelope. Route each request through the existing DaemonDispatcher (already wired in main.dart via daemonClientFactory). Tear down on app shutdown.
|
||
|
||
**Architectural commitments (read these first):**
|
||
- D-70 — socket path is `$XDG_RUNTIME_DIR/clide/<sha256(workspace-root)[:16]>.sock` on Linux, `$HOME/Library/Caches/clide/<sha256(workspace-root)[:16]>.sock` on macOS. No env override. Workspace root = git toplevel.
|
||
- D-71 — socket file is `0600`, parent dir is `0700`. No token auth at this layer.
|
||
- D-72 — multi-connection accept loop, serial dispatch on the main isolate. Per-handler isolate offload as needed; not the IPC layer''s concern.
|
||
|
||
**Acceptance:**
|
||
1. lib/src/ipc/server.dart exists; binds an AF_UNIX socket at the D-70 path; creates the parent dir with the D-71 perms.
|
||
2. App boot starts the server; app shutdown closes the socket file cleanly and removes it.
|
||
3. `socat - UNIX-CONNECT:$SOCK <<< ''{"command":"git.status"}''` returns a JSON-line response. Tests use a synthetic socket fixture (tempdir + XDG_RUNTIME_DIR override at the env level).
|
||
4. Multi-connection accept loop — concurrent socat invocations interleave at the I/O level but serialise through DaemonDispatcher (per D-72) without failing each other.
|
||
5. Stale socket on boot (left over from a crashed clide) is detected and unlinked before binding — same `live-daemon probe` pattern already used elsewhere in the codebase.
|
||
6. No client yet — that lands in T-126.
|
||
|
||
Source: T-99 sketch. Coordinates with: T-127 (InProcessClient swap), T-130 (MCP).', NULL, '2026-05-18 12:42:56', '2026-05-18 12:42:56', '2026-05-18 12:42:56', NULL, 'abb7a039f73d430cdba594b2a4ac381a', 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-124', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 12:45:10', '2026-05-18 12:45:10', '2026-05-18 12:45:10', NULL, 'aa1b70849399f4caee043d40e48d1edf', 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-124', 'status', 'in_progress', 'done', NULL, '2026-05-18 12:51:08', '2026-05-18 12:51:08', '2026-05-18 12:51:08', NULL, 'b928ce09ae0b7dbb77bf93f8c0593657', 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-125', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 15:49:20', '2026-05-18 15:49:20', '2026-05-18 15:49:20', NULL, 'ac0838fac9e3c97f1728256910776bed', 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-125', 'status', 'in_progress', 'done', NULL, '2026-05-18 15:51:09', '2026-05-18 15:51:09', '2026-05-18 15:51:09', NULL, '938624de487aa2f02e034424cae40c77', 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-126', 'status', 'backlog', 'in_progress', NULL, '2026-05-18 15:54:20', '2026-05-18 15:54:20', '2026-05-18 15:54:20', NULL, '3266e7ea943867f49ff20a90b7f12ae6', 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-126', 'status', 'in_progress', 'done', NULL, '2026-05-18 16:04:29', '2026-05-18 16:04:29', '2026-05-18 16:04:29', NULL, 'b6c783f599585e55ecc36f69ccbcefb1', 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-127', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 09:57:18', '2026-05-19 09:57:18', '2026-05-19 09:57:18', NULL, '64a14cb5d8b9274fd9d72d1e7474040c', 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-127', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:03:26', '2026-05-19 10:03:26', '2026-05-19 10:03:26', NULL, 'b8bf877d2057f7baa5acf6af7a13d529', 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-128', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 10:05:09', '2026-05-19 10:05:09', '2026-05-19 10:05:09', NULL, '763a80ec25b5fe712e5ad44b75dbb22f', 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-128', 'status', 'in_progress', 'done', NULL, '2026-05-19 10:06:46', '2026-05-19 10:06:46', '2026-05-19 10:06:46', NULL, 'cc53b46764f28515d735aa77c8eeef2c', 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-129', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 12:06:39', '2026-05-19 12:06:39', '2026-05-19 12:06:39', NULL, '575584f46d21b51bab3e69da1b940718', 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-129', 'status', 'in_progress', 'done', NULL, '2026-05-19 12:19:51', '2026-05-19 12:19:51', '2026-05-19 12:19:51', NULL, 'e9bf50652357521c7ee74bd6577e8108', 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-130', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 12:24:44', '2026-05-19 12:24:44', '2026-05-19 12:24:44', NULL, '63dd7e0c4346a5d3cdd81c9c477339c3', 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-130', 'status', 'in_progress', 'done', NULL, '2026-05-19 12:33:52', '2026-05-19 12:33:52', '2026-05-19 12:33:52', NULL, 'f3060e086423af96f99684e3718cac45', 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-131', 'status', 'backlog', 'in_progress', NULL, '2026-05-19 13:10:27', '2026-05-19 13:10:27', '2026-05-19 13:10:27', NULL, 'c49e76c430de0cc42d7919ade9c5cfa5', 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-119', 'status', 'backlog', 'ready', NULL, '2026-05-19 13:14:01', '2026-05-19 13:14:01', '2026-05-19 13:14:01', NULL, '726631190b62bd38fb34f7201d2f27ee', 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-120', 'description', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
|
||
|
||
This needs design before code:
|
||
- Where the schema lives (per-handler? a central registry?)
|
||
- How it composes with the IpcRequest envelope
|
||
- Whether it generates the existing handler boilerplate or wraps it
|
||
- Coordination with T-99''s IPC architecture decision (if we go socket-server, the schema becomes the wire contract)
|
||
|
||
Source: consultants.md "Security — Findings — [Major]" item 1.', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
|
||
|
||
Re-scope (2026-05-19, post T-99): the wire contract is now real — IpcRequest envelopes flow over the unix socket (per D-70/71/72) and through the C clide client which serialises raw argv under the `_argv` sentinel (per D-72/D-6). The argv parser (lib/src/cli/argv_to_request.dart) is the de-facto schema today; T-120 should formalise it by:
|
||
|
||
- Lifting the per-cmd grammar from argv_to_request.dart into a typed registry keyed by cmd, sharing it with both the argv parser and the dispatcher.
|
||
- Using the same registry to validate inbound IpcRequests at dispatch time (regex/charset constraints on branch/remote/path args).
|
||
- Exposing the registry to the MCP server (T-130) so the `tools/list` payload is generated rather than hand-rolled.
|
||
|
||
Design still open:
|
||
- Where the schema lives (per-handler? central registry?).
|
||
- Whether it generates handler boilerplate or wraps existing handlers.
|
||
|
||
Source: consultants.md "Security — Findings — [Major]" item 1.', NULL, '2026-05-19 13:14:12', '2026-05-19 13:14:12', '2026-05-19 13:14:12', NULL, '3511c5e7731f568de7af33c1970ad97e', 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-99', 'status', 'backlog', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, '5ff47ab0fc7fb8cb8e9e039be48ada2a', 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-131', 'status', 'in_progress', 'done', NULL, '2026-05-19 13:14:41', '2026-05-19 13:14:41', '2026-05-19 13:14:41', NULL, 'a57b3fbe4e6815fcbb28691a08130801', 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-119', 'status', 'ready', 'in_progress', NULL, '2026-05-19 14:44:43', '2026-05-19 14:44:43', '2026-05-19 14:44:43', NULL, '1d5048cf9a031c504aa3df570a7b0cae', 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-119', 'status', 'in_progress', 'done', NULL, '2026-05-20 07:00:06', '2026-05-20 07:00:06', '2026-05-20 07:00:06', NULL, '1f2c2bcc722417d8f4c5bfa27faf0c93', 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-120', 'description', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
|
||
|
||
Re-scope (2026-05-19, post T-99): the wire contract is now real — IpcRequest envelopes flow over the unix socket (per D-70/71/72) and through the C clide client which serialises raw argv under the `_argv` sentinel (per D-72/D-6). The argv parser (lib/src/cli/argv_to_request.dart) is the de-facto schema today; T-120 should formalise it by:
|
||
|
||
- Lifting the per-cmd grammar from argv_to_request.dart into a typed registry keyed by cmd, sharing it with both the argv parser and the dispatcher.
|
||
- Using the same registry to validate inbound IpcRequests at dispatch time (regex/charset constraints on branch/remote/path args).
|
||
- Exposing the registry to the MCP server (T-130) so the `tools/list` payload is generated rather than hand-rolled.
|
||
|
||
Design still open:
|
||
- Where the schema lives (per-handler? central registry?).
|
||
- Whether it generates handler boilerplate or wraps existing handlers.
|
||
|
||
Source: consultants.md "Security — Findings — [Major]" item 1.', 'T-104 covered the spot-fixes (argv-injection rejection, size/count caps on specific commands). What remains is the framework piece: a typed schema per IPC command — branch/remote/path/etc. with regex/charset constraints — applied at DaemonDispatcher dispatch time rather than scattered through individual handlers.
|
||
|
||
---
|
||
Design resolved 2026-05-20 (see D-74). Co-registration model, NOT a central map:
|
||
|
||
- The argv parser (lib/src/cli/argv_to_request.dart) only knows syntactic shape (identifier/flag charset, -- terminator); it has NO per-command argument knowledge. So this is build-fresh, not a lift — each handler currently hand-reads its own keys.
|
||
- Extend the DaemonDispatcher registration API to carry an optional typed schema per command. The dispatcher accumulates a cmd->schema registry as commands register (built-in register*Commands modules supply theirs; extension CommandContributions carry their own — central static map rejected because it cannot see extension-contributed commands, D-46).
|
||
- DaemonDispatcher.dispatch validates req.args against schema[cmd] BEFORE invoking the handler (hook at dispatcher.dart line 39, the `return h(req)` call). Violation -> userError (sysexit 64). Commands with no schema dispatch unvalidated (opt-in per command) until migrated.
|
||
- Constraint vocabulary is hand-rolled (prefer-zero-deps): per-arg required/optional, kind, charset/regex, numeric range. Collapses the _ResizeArgs-style hand-lifts (panel_commands.dart, T-119) into schema.
|
||
- KEEP existing T-104 checks as defense-in-depth: validateGitRef (lib/src/git/operations.dart:46) and the count/path caps (lib/src/daemon/git_commands.dart:18,23). The git client is callable directly from the Flutter UI, not only via the dispatcher — do not remove them.
|
||
- OUT OF SCOPE: MCP tools/list generation from the registry — deferred to the T-130 track.
|
||
|
||
Acceptance:
|
||
1. Dispatcher validates req.args against a registered per-command schema before the handler runs; violations return userError.
|
||
2. At least the security-relevant commands (git ref/remote/path args, panel.resize) carry schemas; argv-injection + caps still enforced (no regression vs T-104).
|
||
3. validateGitRef + git caps retained as defense-in-depth.
|
||
4. No third-party validation dep; no lint suppressions.
|
||
5. Tests cover: schema accept/reject per constraint kind, dispatcher-level rejection, and that an unschema-d command still dispatches.
|
||
|
||
Source: consultants.md "Security — Findings — [Major]" item 1. Decision: D-74.', NULL, '2026-05-20 15:54:35', '2026-05-20 15:54:35', '2026-05-20 15:54:35', NULL, '7f972739c82134897869432a3978d6ec', 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-120', 'status', 'backlog', 'in_progress', NULL, '2026-05-20 15:54:38', '2026-05-20 15:54:38', '2026-05-20 15:54:38', NULL, 'fa11190ac1bd9cd8f67e3041a8ca7236', 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-120', 'status', 'in_progress', 'done', NULL, '2026-05-20 16:02:35', '2026-05-20 16:02:35', '2026-05-20 16:02:35', NULL, 'efffdd07c7281977d37ce0ad4285a36a', 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-97', 'status', 'backlog', 'done', NULL, '2026-05-20 16:02:46', '2026-05-20 16:02:46', '2026-05-20 16:02:46', NULL, '9c36d26b5ac3702441a87c7f88dbf830', 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-122', 'description', 'T-115''s render-only widget test at test/builtin/welcome/widget_test.dart ''sticky-startup toggle renders on a seeded recent row'' verifies the toggle is constructed but does not exercise the tap → setStickyStartup path. Adding a tap-driven assertion hung the 10-minute Flutter test timeout consistently (find.byTooltip, find.bySemanticsLabel, and find.byKey + tap variants all reproduced).
|
||
|
||
Hypothesis: ClideTooltip wraps the toggle in a MouseRegion whose onEnter awaits Future.delayed(showDelay). Some path during tap simulation (or surrounding pump scheduling) keeps that timer pending, and the test runner waits the full 10-minute idle timeout before declaring it stuck.
|
||
|
||
Next steps:
|
||
- Reproduce in isolation against a stripped-down harness pumping just the _StickyToggle widget.
|
||
- Determine whether the hang is the MouseRegion timer or another future (e.g., the welcome-tab activation, toolchain check).
|
||
- Either fix the underlying cause or use fakeAsync.run() to drain the timers explicitly.
|
||
|
||
Until then, ProjectManager unit tests in test/kernel/src/project_test.dart cover the sticky-startup logic (14 cases including round-trip, no-op, idempotent flip, ambiguity, and openStickyOrNothing).', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle.
|
||
|
||
Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout.
|
||
|
||
Investigation 2026-05-22 (narrowed the cause):
|
||
- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting.
|
||
- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable).
|
||
- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation.
|
||
- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it).
|
||
- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer).
|
||
|
||
Next steps:
|
||
- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority.
|
||
- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target).
|
||
|
||
ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).', NULL, '2026-05-22 06:55:02', '2026-05-22 06:55:02', '2026-05-22 06:55:02', NULL, 'b7c13548cef5d962c7b5b4500ea39b9e', 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-122', 'priority', 'low', 'medium', NULL, '2026-05-22 06:55:02', '2026-05-22 06:55:02', '2026-05-22 06:55:02', NULL, 'e7d5dd1b89eded661de7f6d64edea04c', 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-89', 'description', 'Today: total line coverage **34.9%** (2984/8549 lines). Per area:
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
|
||
| `lib/widgets/` | 537 / 1184 | 45.4% |
|
||
| `lib/src/pql/` | 45 / 94 | 47.9% |
|
||
| `lib/builtin/` | 269 / 550 | 48.9% |
|
||
| `lib/kernel/` | 889 / 1691 | 52.6% |
|
||
| `lib/src/daemon/` | 320 / 584 | 54.8% |
|
||
| `lib/src/pty/` | 130 / 216 | 60.2% |
|
||
| `lib/src/git/` | 356 / 518 | 68.7% |
|
||
| `lib/src/ipc/` | 110 / 156 | 70.5% |
|
||
| `lib/src/files/` | 98 / 133 | 73.7% |
|
||
| `lib/extension/` | 41 / 54 | 75.9% |
|
||
| `lib/src/editor/` | 107 / 129 | 82.9% |
|
||
| `lib/src/panes/` | 70 / 73 | 95.9% |
|
||
|
||
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
|
||
|
||
**End state:**
|
||
- Total line coverage ≥ 95% (no per-area carve-outs).
|
||
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
|
||
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
|
||
|
||
**Acceptance criteria for closing this epic:**
|
||
1. `coverage/floor.txt` reads `95` (or higher).
|
||
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
|
||
3. The pre-push gate computes coverage and rejects pushes below the floor.
|
||
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
|
||
|
||
**Strategy / child shape:**
|
||
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
|
||
|
||
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
|
||
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
|
||
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
|
||
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
|
||
|
||
**Out of scope:**
|
||
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
|
||
- Integration / E2E coverage (only widget + unit feed lcov).
|
||
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
|
||
|
||
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
|
||
', '## OUTCOME (closed 2026-05-18)
|
||
|
||
Epic met. Final measured line coverage **95.27%** (9551 / 10025), floor `coverage_floor: 95` in `pubspec.yaml`, gate wired into `make push-check`.
|
||
|
||
Final per-area (vs. opening 34.9% total):
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 3185 / 3188 | **99.9%** (was 0.4%) |
|
||
| `lib/extension/` | 54 / 54 | 100.0% |
|
||
| `lib/src/editor/` | 129 / 129 | 100.0% |
|
||
| `lib/src/files/` | 139 / 140 | 99.3% |
|
||
| `lib/src/pql/` | 91 / 92 | 98.9% |
|
||
| `lib/src/panes/` | 75 / 76 | 98.7% |
|
||
| `lib/src/git/` | 517 / 531 | 97.4% |
|
||
| `lib/src/daemon/` | 616 / 644 | 95.7% |
|
||
| `lib/kernel/` | 2122 / 2233 | 95.0% |
|
||
| `lib/src/ipc/` | 429 / 452 | 94.9% |
|
||
| `lib/widgets/` | 1178 / 1261 | 93.4% |
|
||
| `lib/builtin/` | 754 / 875 | 86.2% |
|
||
| `lib/src/pty/` | 177 / 262 | 67.6% (forkpty paths excluded from gate) |
|
||
|
||
Acceptance: (1) floor reads 95 done (2) total >=95% done (3) gate hard-fails below floor done (4) no coverage-dodging suppressions done. The strategy''s tail per-area sweeps (widgets/builtin/pty) didn''t all individually reach 95%, but the contract is *total* line coverage, which is satisfied; remaining per-area headroom can be its own tickets if desired.
|
||
|
||
---
|
||
|
||
Today: total line coverage **34.9%** (2984/8549 lines). Per area:
|
||
|
||
| area | hit / total | % |
|
||
|---|---|---|
|
||
| `lib/src/terminal/` | 12 / 3167 | **0.4%** |
|
||
| `lib/widgets/` | 537 / 1184 | 45.4% |
|
||
| `lib/src/pql/` | 45 / 94 | 47.9% |
|
||
| `lib/builtin/` | 269 / 550 | 48.9% |
|
||
| `lib/kernel/` | 889 / 1691 | 52.6% |
|
||
| `lib/src/daemon/` | 320 / 584 | 54.8% |
|
||
| `lib/src/pty/` | 130 / 216 | 60.2% |
|
||
| `lib/src/git/` | 356 / 518 | 68.7% |
|
||
| `lib/src/ipc/` | 110 / 156 | 70.5% |
|
||
| `lib/src/files/` | 98 / 133 | 73.7% |
|
||
| `lib/extension/` | 41 / 54 | 75.9% |
|
||
| `lib/src/editor/` | 107 / 129 | 82.9% |
|
||
| `lib/src/panes/` | 70 / 73 | 95.9% |
|
||
|
||
The dominant hole is `lib/src/terminal/` — the terminal emulator code. Header still credits xterm.dart upstream, but per CLAUDE.md ("Own the rendering stack") and the explicit guidance attached to this epic: in-tree code is owned, no carve-outs, same coverage bar.
|
||
|
||
**End state:**
|
||
- Total line coverage ≥ 95% (no per-area carve-outs).
|
||
- Pre-push gate hard-fails on coverage drops via a committed floor (`coverage/floor.txt`); floor ratchets up only.
|
||
- Per D-66 the gate lands at the *current* floor in the first child ticket; subsequent floor bumps come with the test-writing children.
|
||
|
||
**Acceptance criteria for closing this epic:**
|
||
1. `coverage/floor.txt` reads `95` (or higher).
|
||
2. `flutter test --coverage --exclude-tags forkpty` produces ≥95% line coverage.
|
||
3. The pre-push gate computes coverage and rejects pushes below the floor.
|
||
4. No new `// ignore:` / `// ignore_for_file:` / analysis-options excludes were added to dodge the gate. Any suppression added during this campaign needs explicit approval and an inline reason at the suppression site.
|
||
|
||
**Strategy / child shape:**
|
||
The gate plumbing lands first so future test additions are visible as floor bumps. Then per-area sweeps — biggest holes first because they move the global number fastest, but each area is its own child ticket with its own scope.
|
||
|
||
1. **Gate plumbing** (first child) — install the ratchet, lock in current floor, expose `make coverage-gate` and wire it into `make push-check`.
|
||
2. **`lib/src/terminal/` sweep** (second child, by far the largest) — likely splits into sub-children once the area''s structure is mapped (parser, buffer, painter, gesture handling, mouse reporting, escape sequences, cell-grid measurement, etc.).
|
||
3. **Per-area sweeps** — claim a child ticket when starting an area; close when the area hits 95%. Order by impact: terminal → widgets → pql → builtin → kernel → daemon → pty → git → ipc → files → extension → editor.
|
||
4. **Final ratchet** — once total ≥ 95%, set floor to 95 and close the epic.
|
||
|
||
**Out of scope:**
|
||
- Branch coverage (lcov data is weak for branch on Dart; line coverage is the contract).
|
||
- Integration / E2E coverage (only widget + unit feed lcov).
|
||
- `lib/test_app.dart` exclusion: verify in the gate child whether Dart''s coverage tooling already excludes it; if not, decide explicitly rather than carve it out silently.
|
||
|
||
**Cross-references:** D-66 (this epic''s decision record), D-29 (pre-push gate scope), "Own the rendering stack" guardrail (CLAUDE.md).
|
||
', NULL, '2026-05-22 13:37:43', '2026-05-22 13:37:43', '2026-05-22 13:37:43', NULL, 'b83aa45596e35a95e689b922c66c27ef', 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-89', 'status', 'in_progress', 'done', NULL, '2026-05-22 13:37:46', '2026-05-22 13:37:46', '2026-05-22 13:37:46', NULL, '5698b8e8cbd5113a84791ae2dad26c6d', 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-91', 'status', 'in_progress', 'done', NULL, '2026-05-22 13:37:46', '2026-05-22 13:37:46', '2026-05-22 13:37:46', NULL, 'dad314c05e04d8f17357ac2ea054a9bd', 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-134', 'description', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
|
||
|
||
---
|
||
Refinement (2026-05-22, refinement-agent review):
|
||
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
|
||
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
|
||
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
|
||
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
|
||
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
|
||
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
|
||
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
|
||
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, '05456ee9ae72721893f6850158d59791', 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-135', 'description', 'Make clide''s native text widgets selectable so a SelectionArea can select across them. Change lib/widgets/src/clide_markdown.dart RichText (line ~68) -> Text.rich; same for lib/widgets/src/clide_code_block.dart; add a selectionBackground token to SurfaceTokens (lib/kernel/src/theme/tokens.dart + resolver + bundled theme YAMLs); verify copy via existing ClideClipboard (lib/kernel/src/clipboard.dart). Acceptance: a SelectionArea-wrapped column of markdown + code blocks supports select-across-blocks + Ctrl+C copy; widget tests; a11y unaffected; coverage >= floor. Reusable foundation. Blocks T5.', 'Make clide''s native text widgets selectable so a SelectionArea can select across them. Change lib/widgets/src/clide_markdown.dart RichText (line ~68) -> Text.rich; same for lib/widgets/src/clide_code_block.dart; add a selectionBackground token to SurfaceTokens (lib/kernel/src/theme/tokens.dart + resolver + bundled theme YAMLs); verify copy via existing ClideClipboard (lib/kernel/src/clipboard.dart). Acceptance: a SelectionArea-wrapped column of markdown + code blocks supports select-across-blocks + Ctrl+C copy; widget tests; a11y unaffected; coverage >= floor. Reusable foundation. Blocks T5.
|
||
|
||
---
|
||
Refinement (2026-05-22, refinement-agent review):
|
||
- Convert ALL RichText sites, not one: clide_markdown.dart lines 68, 238, 242 + clide_code_block.dart:79 -> Text.rich (Text.rich wires the SelectionArea selectionRegistrar; same underlying render object).
|
||
- Tables (clide_markdown.dart:230) and tappable WidgetSpan links (ClideTappable ~293-310) do NOT participate in SelectionArea. v1 scope: text + code blocks are the selectable surface; tables/link-spans are known non-selectable islands (revisit later). Acceptance = select+copy across text/code blocks.
|
||
- selectionBackground token: add to SurfaceTokens (field + TokenKeys + TokenKeys.all) + resolver.dart (_defaultSurfaceMap default + constructor). Bundled theme YAMLs do NOT need it (default map covers it); per-theme override only if the default reads wrong. Do NOT retune existing palettes (published themes are user contracts).
|
||
- Add a selection contrast pair (globalForeground on selectionBackground) to lib/kernel/src/theme/contrast.dart canonicalPairs; re-run test/a11y. (Note: summer-night.yaml is missing from contrast_test bundledPaths -- pre-existing inconsistency, out of scope.)
|
||
- COPY PATH CORRECTION: SelectionArea Ctrl+C goes through Flutter services.Clipboard directly, NOT ClideClipboard. Test verifies Clipboard.getData returns the selection after copy.
|
||
- Coverage floor is 95%.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, '5e218db621577a309b493611dfb72b63', 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-136', 'description', 'New lib/builtin/claude/src/transcript_reader.dart. Resolve munged project path (/ -> -) + session-id; tail the append-only JSONL; parse records (type: user/assistant/attachment/tool; message; agentId; isSidechain) into ConversationItem models; drift-guard on the version field (log + degrade on unknown schema). Paths validated this session: ~/.claude/projects/<munged>/<sid>.jsonl (lead) and <sid>/subagents/agent-<id>.jsonl (teammates). Acceptance: unit tests against a snapshotted JSONL fixture (assistant text, tool_use, tool_result); appended lines stream in; unknown version degrades gracefully + logs. Blocks T5.', 'New lib/builtin/claude/src/transcript_reader.dart. Resolve munged project path (/ -> -) + session-id; tail the append-only JSONL; parse records (type: user/assistant/attachment/tool; message; agentId; isSidechain) into ConversationItem models; drift-guard on the version field (log + degrade on unknown schema). Paths validated this session: ~/.claude/projects/<munged>/<sid>.jsonl (lead) and <sid>/subagents/agent-<id>.jsonl (teammates). Acceptance: unit tests against a snapshotted JSONL fixture (assistant text, tool_use, tool_result); appended lines stream in; unknown version degrades gracefully + logs. Blocks T5.
|
||
|
||
---
|
||
Refinement (2026-05-22, refinement-agent review): READY. Verified against real transcripts.
|
||
- Munge = absolutePath.replaceAll(''/'',''-'') (KEEP the leading ''-''; dirs start with -var-).
|
||
- Envelope fields: type, uuid, parentUuid, isSidechain, sessionId, version, timestamp. Render: user (message.content = String OR array of {type:text|tool_result}); assistant (message.content = array of {type:thinking|text|tool_use}). Skip attachment/system/last-prompt/permission-mode/file-history-snapshot/queue-operation (bookkeeping).
|
||
- SESSION-ID: claude does not expose its session UUID via argv/env (verified in claude_pane spawn). Approach: newest *.jsonl by mtime in ~/.claude/projects/<munged>/; poll mtime and switch the tail when a newer session appears.
|
||
- Subagent transcripts: <munged>/<session-uuid>/subagents/agent-<id>.jsonl + sibling .meta.json ({agentType,description}); each record carries agentId.
|
||
- Tail: dart:io File.openRead with a byte-position cursor (File.length + RandomAccessFile.setPosition on poll). Do NOT use lib/src/files/watcher.dart. Zero new deps.
|
||
- Logger: lib/kernel/src/log.dart for the version drift-guard. Coverage floor 95%.', NULL, '2026-05-22 16:41:09', '2026-05-22 16:41:09', '2026-05-22 16:41:09', NULL, 'c040ed1bf6bad363c3d199bead879374', 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-136', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '176db3f23c4a758fb601c50c9ae2644b', 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-134', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '5e5f91eab17e4acefcc0c9e286787742', 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-135', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, '79aed523589f14481c115538ae8b357c', 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-133', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 16:46:34', '2026-05-22 16:46:34', '2026-05-22 16:46:34', NULL, 'e11fb2f37bf99262989f1881a7deed9c', 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-133', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '010ab0fda58e4be2ea699c102e1b2695', 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-135', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, '1bfd08b75a99bbf3c46408b0c5f799f2', 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-136', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:37:23', '2026-05-22 17:37:23', '2026-05-22 17:37:23', NULL, 'eea74c531ac4796fe6dba221018e80dd', 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-134', 'status', 'in_progress', 'done', NULL, '2026-05-22 17:55:11', '2026-05-22 17:55:11', '2026-05-22 17:55:11', NULL, '13385e9b6de3d3979b03c22de9db73f6', 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-134', 'description', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
|
||
|
||
---
|
||
Refinement (2026-05-22, refinement-agent review):
|
||
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
|
||
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
|
||
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
|
||
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
|
||
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
|
||
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
|
||
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
|
||
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.', 'Investigate empirically and write a findings note pinned to the current Claude Code version. 1) Team spawn inside tmux -L clide with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + teammateMode: tmux -> teammate panes (list-panes -a) + each teammate''s <sid>/subagents/agent-*.jsonl grows. 2) Lifecycle: tmux -L clide -C control-mode pane-add/pane-died events vs polling list-panes. 3) ~/.claude/teams/<team>/config.json layout: tmuxPaneId, agentId, agentType populate at spawn. 4) Image/file paste intake when driven via send-keys (path/@-ref vs clipboard-image vs unsupported). 5) Active session-id discovery (capture at spawn vs newest JSONL in munged dir). Already validated this session: transcript paths + schema (type/message/agentId/version) + subagent transcript location. Acceptance: findings note answering 1-5, CC version pinned. Blocks T6, T7.
|
||
|
||
---
|
||
Refinement (2026-05-22, refinement-agent review):
|
||
- Environment confirmed: tmux 3.6a; claude 2.1.148 (the version an-idea.md verified for tmux teammate spawning) -- PIN findings to 2.1.148. teammateMode absent from ~/.claude/settings.json (default auto) -> set teammateMode:tmux explicitly OR launch the lead inside tmux and rely on auto.
|
||
- Check 3 ALREADY answered from a real ~/.claude/teams/*/config.json: {name,description,createdAt,leadAgentId,leadSessionId,members[]}; member = {agentId,name,agentType,model,joinedAt,tmuxPaneId (empty for lead, e.g. %154 for teammates),cwd,subscriptions,backendType:''tmux'',isActive,mode}. tmuxPaneId DOES populate -- spike just re-verifies on the pinned version.
|
||
- Subagent path: <munged>/<session-uuid>/subagents/agent-<id>.jsonl (session UUID is a directory).
|
||
- Check 5 (session-id) resolved by T-136: newest *.jsonl by mtime. Spike confirms only.
|
||
- tmux -L clide socket may not exist unless the app is running; spike creates its own: tmux -L clide new-session -d -s teamhost ... claude.
|
||
- Check 4 (paste): scope to @path file references (verifiable via send-keys headlessly). Clipboard-image needs an interactive display ($DISPLAY) -- note as interactive-only.
|
||
- ACP directionality (an-idea.md s5) is OUT OF SCOPE: Zed-client concern; clide owns its GUI and does not use ACP.
|
||
- Deliverable: findings note at docs/spikes/cc-team-internals-2.1.148.md (in-repo), feeding D-75 when authored (T-133). D-75 is claimed-but-unauthored (T-133''s job) -- the reference is forward-looking, not dangling.
|
||
|
||
---
|
||
Findings (2026-05-22): docs/spikes/cc-team-internals-2.1.148.md. All 5 checks answered from real artifacts + tmux manual + synthetic control-mode test (no live run/quota). Key risk for T-139: pane->teammate->transcript identity joins only on agentType (config <-> .meta.json), ambiguous for same-type teammates -> needs joinedAt/mtime or lead-transcript tiebreaker. tmux 3.6a has no %pane-died; poll list-panes -a. Teammate transcripts at <munged>/<sid>/subagents/agent-<hex>.jsonl + .meta.json{agentType,description}.', NULL, '2026-05-22 17:55:11', '2026-05-22 17:55:11', '2026-05-22 17:55:11', NULL, 'dbf4f56d1ec9d2234f255826c5f88f68', 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', '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;
|
||
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-142', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:09:09', '2026-05-22 22:09:09', '2026-05-22 22:09:09', NULL, 'b799657bf436bc44e306ef71658c80e5', 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-142', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:18:02', '2026-05-22 22:18:02', '2026-05-22 22:18:02', NULL, '374be08610920e404c8394bc10b1e12a', 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-143', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:31:25', '2026-05-22 22:31:25', '2026-05-22 22:31:25', NULL, 'e2a16eb8af0afc30a5d0fc88c523e507', 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-143', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:36:09', '2026-05-22 22:36:09', '2026-05-22 22:36:09', NULL, '9b8859ea2e8db7083bfb9d2ab17c1c57', 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-144', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:41:28', '2026-05-22 22:41:28', '2026-05-22 22:41:28', NULL, 'eda6f75e88401ee5b445e7edfc312f82', 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-144', 'status', 'in_progress', 'done', NULL, '2026-05-22 22:43:45', '2026-05-22 22:43:45', '2026-05-22 22:43:45', NULL, 'da17b2710e17b1821882d47724790cbb', 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-139', 'status', 'backlog', 'in_progress', NULL, '2026-05-22 22:50:41', '2026-05-22 22:50:41', '2026-05-22 22:50:41', NULL, 'ad063f0c3c65e5b35d3dcea036412ad9', 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-139', 'status', 'in_progress', 'done', NULL, '2026-05-22 23:16:28', '2026-05-22 23:16:28', '2026-05-22 23:16:28', NULL, 'add82dc118e2081500f19cbb321272d4', 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-140', 'status', 'backlog', 'done', NULL, '2026-05-22 23:31:46', '2026-05-22 23:31:46', '2026-05-22 23:31:46', NULL, 'e6204c6f0a0dbd38396a69dfef2c7080', 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-145', 'description', NULL, 'A compact per-session status strip in the lead Claude pane header AND each teammate tile header (T-140), showing: current model, permission mode (accept-edits / plan / default / bypass), and context-window usage %.
|
||
|
||
Shared status source (this ticket owns it): extract per-session status in the drift-contained reader/observer (D-75) — do NOT parse CC internals in the widgets.
|
||
- Model: team config member.model, overridden by the latest assistant message.model in the transcript when present.
|
||
- Permission mode: the transcript''s `permission-mode` records (TranscriptReader currently SKIPS these — surface the latest instead). Also honor config member.planModeRequired for teammates.
|
||
- Context %: latest assistant message.usage (input_tokens + cache_read_input_tokens + cache_creation_input_tokens + output_tokens) over the model''s context limit.
|
||
|
||
Delivery: emit a per-session status to the UI via the bus/events (e.g. a TeamMemberStatus event or an extra MessageBus channel) so BOTH the header strip and the T-141 sidebar render from one source without re-tailing.
|
||
|
||
Applies to the lead session and every teammate. Re-validate field names against a live team run / CC version bump (undocumented, per T-134).
|
||
|
||
Acceptance: lead pane header and each teammate tile header show current model + an accept-edits/plan-mode badge + a context-usage indicator, updating live from the transcript. Blocked by T-137 (done), T-140 (done).
|
||
', NULL, '2026-05-23 06:07:59', '2026-05-23 06:07:59', '2026-05-23 06:07:59', NULL, '26bb1c89f3fef8d916f3eb6a1ba9d065', 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-141', 'description', 'New sidebar panel contributed to Slots.sidebar: roster from ~/.claude/teams config; live status verb / elapsed / down+up tokens / edit-mode from transcript tailing; team token budget pulled from the Anthropic usage API on a timer. Acceptance: roster matches list-panes/config; budget refreshes on the timer; appears in team mode. Blocked by T7.', 'New sidebar panel (contributed to Slots.sidebar), visible only in team mode: a roster row per team member that MIRRORS the per-session status surfaced by T-145, plus team-wide meta.
|
||
|
||
Per-member row:
|
||
- Identity from the team config + observer: name, agentType, color; membership/lifecycle from TeamMemberJoined/Left.
|
||
- Mirror of T-145''s shared status: current model, permission-mode badge (accept-edits / plan / default / bypass), context-window usage %. Consume the SAME status source/event T-145 emits — do NOT re-tail transcripts or re-parse CC internals here (D-75).
|
||
- Live status verb + elapsed-since-last-activity + down/up token deltas from that same status feed.
|
||
|
||
Team-wide:
|
||
- Team/account token budget pulled from the Anthropic usage API on a timer (distinct from per-conversation context above).
|
||
|
||
Acceptance: roster matches list-panes/config; each row mirrors the member''s model + accept-edits/plan badge + context-usage; status verb and token deltas update live; account budget refreshes on its timer; panel appears only when a team is active. Depends on T-145 (status source) + T-139 (done).
|
||
', NULL, '2026-05-23 06:08:24', '2026-05-23 06:08:24', '2026-05-23 06:08:24', NULL, 'cfa1ce349be286d89a1f8e169605f04d', 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', 'description', NULL, 'Regression from T-137: every Claude pane renders the newest .jsonl in the workspace''s munged dir, so concurrent sessions in one workspace (primary + secondaries) all converge on the same transcript — a secondary tab shows the primary''s conversation (and the primary follows whichever session is newest). The pre-T-137 terminal pane rendered each tab''s own PTY, so tabs were independent.
|
||
|
||
Fix: bind each pane to a specific Claude session id.
|
||
- claude CLI supports `--session-id <uuid>` (verified via --help). Spawn each pane''s claude with its own UUID; a transcript is named <session-id>.jsonl, so the pane''s transcript is <munged-cwd>/<uuid>.jsonl.
|
||
- Reader targets that exact file via TranscriptReader''s `file:` param (added in T-139). Guard _tail against the file not existing yet (claude writes it shortly after spawn).
|
||
- Each pane publishes/subscribes on a per-session MessageBus channel (conversation/<uuid>) so primary and secondary controllers don''t cross-talk.
|
||
- UUID per pane, seeded by the tmux session name (the identity we already have): primary = deterministic from its stable session name (clide-claude-<slug>) so it RESUMES across restarts (D-41 continuity); secondary = random per spawn so each is a clean/fresh session (a deterministic secondary id would make starting a clean session impossible).
|
||
|
||
Migration caveat: an existing tmux session created by the OLD code (no --session-id, claude picked its own id) won''t match the new deterministic id — it must be killed (claude.kill-all-sessions) once so the next spawn binds the controlled id.
|
||
|
||
Open question to validate live: whether `claude --session-id <existing-uuid>` resumes vs. errors when re-spawned after the tmux session was killed (display works either way since the file is <uuid>.jsonl, but worth confirming claude starts).
|
||
|
||
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.
|
||
', 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;
|
||
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;
|
||
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-145', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 08:03:56', '2026-05-23 08:03:56', '2026-05-23 08:03:56', NULL, 'e50db235944f48a5f581ae64a3589385', 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-145', 'status', 'in_progress', 'done', NULL, '2026-05-23 08:43:19', '2026-05-23 08:43:19', '2026-05-23 08:43:19', NULL, 'c9cf511becf7e542d465701e51dba4e2', 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-150', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 09:04:54', '2026-05-23 09:04:54', '2026-05-23 09:04:54', NULL, 'fcc43ef1e1ae3c1dfa0e9c94ffa35bed', 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-150', 'status', 'in_progress', 'done', NULL, '2026-05-23 09:33:03', '2026-05-23 09:33:03', '2026-05-23 09:33:03', NULL, '58c9b0734a70fe8c7083c3cc456a1bd6', 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-151', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 09:54:31', '2026-05-23 09:54:31', '2026-05-23 09:54:31', NULL, '52df08bff6479b6a84c128f5a63ea312', 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-151', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:20:12', '2026-05-23 10:20:12', '2026-05-23 10:20:12', NULL, 'ed8ce28a1b0c7e995887dcfd32456aec', 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-153', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 10:31:31', '2026-05-23 10:31:31', '2026-05-23 10:31:31', NULL, 'd5a1f02df5c8be27b9dc62b1ce277d67', 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-153', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:35:56', '2026-05-23 10:35:56', '2026-05-23 10:35:56', NULL, 'ddf23a60cdfbfcd79b5d68af10f37928', 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-152', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 10:37:57', '2026-05-23 10:37:57', '2026-05-23 10:37:57', NULL, 'e12bfc4f8896514fb13450e0f83cf6c6', 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-152', 'status', 'in_progress', 'done', NULL, '2026-05-23 10:43:52', '2026-05-23 10:43:52', '2026-05-23 10:43:52', NULL, 'edab8c3daaabe51797afbe70b5f9bfde', 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-156', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 11:30:35', '2026-05-23 11:30:35', '2026-05-23 11:30:35', NULL, '66000ad896a8ccd2ec91d2ae6c539f01', 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-156', 'status', 'in_progress', 'done', NULL, '2026-05-23 12:05:19', '2026-05-23 12:05:19', '2026-05-23 12:05:19', NULL, '24db72624201c89a5c0bcb3b11840275', 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-154', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:01', '2026-05-23 14:40:01', '2026-05-23 14:40:01', NULL, 'aff6dbb3095efd46cf786a53d8622d42', 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-155', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:40:05', '2026-05-23 14:40:05', '2026-05-23 14:40:05', NULL, 'e46f5eba5bd678190954744c2f8338bc', 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-154', 'status', 'in_progress', 'done', NULL, '2026-05-23 14:50:17', '2026-05-23 14:50:17', '2026-05-23 14:50:17', NULL, '121736159d2d58380447b6a2803cb80b', 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-155', 'status', 'in_progress', 'done', NULL, '2026-05-23 14:50:17', '2026-05-23 14:50:17', '2026-05-23 14:50:17', NULL, 'b471e923f78ed91f05ca6f2dcbd5c85c', 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-148', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:51:55', '2026-05-23 14:51:55', '2026-05-23 14:51:55', NULL, '3002f91cea5919aaefc3e4bc530c7ac2', 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-141', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 14:51:55', '2026-05-23 14:51:55', '2026-05-23 14:51:55', NULL, '76975080bd78de4d6f69937fd256d4ef', 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-148', 'status', 'in_progress', 'done', NULL, '2026-05-23 15:05:29', '2026-05-23 15:05:29', '2026-05-23 15:05:29', NULL, 'd9d9e4c96d94060f929c7c09935d127c', 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-141', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:49:44', '2026-05-23 20:49:44', '2026-05-23 20:49:44', NULL, 'fb2405478b0d422d39a924ccedec5022', 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-157', 'status', 'backlog', 'in_progress', NULL, '2026-05-23 20:52:47', '2026-05-23 20:52:47', '2026-05-23 20:52:47', NULL, 'b2b31ab5fe70fb5027773775b0945bd5', 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-157', 'status', 'in_progress', 'done', NULL, '2026-05-23 20:57:07', '2026-05-23 20:57:07', '2026-05-23 20:57:07', NULL, 'ce114090979d31512a9aa09f1c9311a1', 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-159', 'status', 'backlog', 'done', NULL, '2026-05-24 08:54:30', '2026-05-24 08:54:30', '2026-05-24 08:54:30', NULL, '478956f24d051f603aab2c7ceec328b2', 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-161', 'status', 'backlog', 'done', NULL, '2026-05-24 09:45:07', '2026-05-24 09:45:07', '2026-05-24 09:45:07', NULL, 'd7b99c3dff59fc5a70a7866567fbfe0f', 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-165', 'status', 'backlog', 'in_progress', NULL, '2026-05-24 16:32:59', '2026-05-24 16:32:59', '2026-05-24 16:32:59', NULL, '1dbcae2366b429ad26e65f1a9cfebc33', 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-173', 'status', 'backlog', 'in_progress', NULL, '2026-05-24 17:00:14', '2026-05-24 17:00:14', '2026-05-24 17:00:14', NULL, 'c0306e7ca9f755e69c76cdba93bca934', 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-173', 'status', 'in_progress', 'done', NULL, '2026-05-24 17:10:07', '2026-05-24 17:10:07', '2026-05-24 17:10:07', NULL, 'f5831d8bfa3e484546cd5dd0c75bbf22', 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-166', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 06:23:55', '2026-05-25 06:23:55', '2026-05-25 06:23:55', NULL, '36cc84f2de6519355a55014b107ecea8', 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-165', 'status', 'in_progress', 'done', NULL, '2026-05-25 06:29:25', '2026-05-25 06:29:25', '2026-05-25 06:29:25', NULL, '34961afff4d3384bced64de93c977460', 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-166', 'status', 'in_progress', 'done', NULL, '2026-05-25 06:31:08', '2026-05-25 06:31:08', '2026-05-25 06:31:08', NULL, '3d6b8f30317a82d0aa810b6da47896be', 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-176', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '7be78982c929ee18d8665a8e662c380d', 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-177', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '855a73a398eb53240d12729ac47e0051', 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-175', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 07:11:44', '2026-05-25 07:11:44', '2026-05-25 07:11:44', NULL, '8e9a19896103139fe8e43aa4b7212768', 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-175', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:25:02', '2026-05-25 07:25:02', '2026-05-25 07:25:02', NULL, '3e63273af832240b9f782fc41ca0047d', 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-177', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:25:02', '2026-05-25 07:25:02', '2026-05-25 07:25:02', NULL, '73982bc82e335f2d0fd80633aef0c082', 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-176', 'status', 'in_progress', 'done', NULL, '2026-05-25 07:27:57', '2026-05-25 07:27:57', '2026-05-25 07:27:57', NULL, '926d5d2694207c65d0f620b2834962aa', 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-179', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 08:10:25', '2026-05-25 08:10:25', '2026-05-25 08:10:25', NULL, '059ff72ce3229f45259b097721bfb08f', 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-178', 'status', 'backlog', 'in_progress', NULL, '2026-05-25 08:10:25', '2026-05-25 08:10:25', '2026-05-25 08:10:25', NULL, '389b95ddaaef1a5ce318bda840b1cde5', 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-178', 'status', 'in_progress', 'done', NULL, '2026-05-25 08:21:46', '2026-05-25 08:21:46', '2026-05-25 08:21:46', NULL, 'ddaad91987208966c2720dee370738ed', 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-179', 'status', 'in_progress', 'done', NULL, '2026-05-25 08:26:21', '2026-05-25 08:26:21', '2026-05-25 08:26:21', NULL, '58dc0afa577b9aee651d683aa1b9bc69', 1) ON CONFLICT(hash) DO NOTHING;
|