In live stream-json sessions a sub-agent's spawning prompt is tagged with parent_tool_use_id, not the transcript JSONL's isSidechain + parentUuid. The parser ignored that field, so the prompt parsed as a main-thread user turn and rendered as a blue "you" card above the Activity Agent card instead of folding into it. Carry parent_tool_use_id onto ConversationItem; its presence now marks the item as a sidechain message. The sidechain fold resolves ownership directly by tool-use id (no transcript-only uuid chain to walk), so the prompt folds into its Agent card and the run nests under it as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3190 lines
524 KiB
SQL
3190 lines
524 KiB
SQL
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MFNNCC0MMY2TCNRM', 'status', 'backlog', 'done', NULL, '2026-06-01 06:49:47', '2026-06-01 06:49:47', '2026-06-01 06:49:47', NULL, 'd01cd9475509936871f5e07807a097d4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4M2Z9YZ0SA81E5Q08', 'status', 'backlog', 'done', NULL, '2026-06-01 07:08:16', '2026-06-01 07:08:16', '2026-06-01 07:08:16', NULL, 'ae07d922d86b83eea2fbb963bbcb6d92', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7G8QBJ2PNQ0VREN24', 'status', 'backlog', 'done', NULL, '2026-06-01 10:45:14', '2026-06-01 10:45:14', '2026-06-01 10:45:14', NULL, '1037eb231ea647a4e17aea463f708b32', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6NTYAB8K6T50F8W00', 'status', 'backlog', 'done', NULL, '2026-06-01 10:45:14', '2026-06-01 10:45:14', '2026-06-01 10:45:14', NULL, '8fe7feadcc03bb1e3ceeaeca835361c1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6HQXNNF05GXWK7BWM', 'status', 'backlog', 'done', NULL, '2026-06-01 11:31:39', '2026-06-01 11:31:39', '2026-06-01 11:31:39', NULL, 'd4ad9825be347244bc6624b52d76f34c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM68APBCQTMGJAZ2B9C', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 11:38:40', '2026-06-01 11:38:40', '2026-06-01 11:38:40', NULL, '7609e453ad138021a14142027f2f9194', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM68APBCQTMGJAZ2B9C', 'status', 'in_progress', 'done', NULL, '2026-06-01 12:35:04', '2026-06-01 12:35:04', '2026-06-01 12:35:04', NULL, '25b0507cee1798ad969ed31b7e498bff', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RAKSX59W1HJCZ1M8', 'status', 'backlog', 'done', NULL, '2026-06-01 14:04:24', '2026-06-01 14:04:24', '2026-06-01 14:04:24', NULL, '8be6673880b1cc36c8e0199ca088ae27', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM57GY6PTG78SD3961M', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 14:27:48', '2026-06-01 14:27:48', '2026-06-01 14:27:48', NULL, 'f7ab0f672fac7ca65089062bf84f7a68', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM57GY6PTG78SD3961M', 'status', 'in_progress', 'done', NULL, '2026-06-01 14:45:39', '2026-06-01 14:45:39', '2026-06-01 14:45:39', NULL, 'a1942351599d79a7a3a17ca1119f38ca', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7G8QBJ2PNQ0VREN24', 'description', 'Opening a file in the editor (the reader''s edit pencil, a non-.md file-tree click, a decision''s edit) calls editor.open and the daemon opens the buffer, but the editor pane never appears in the workspace slot above the Claude pane. Root cause: EditorExtension contributes a workspace tab (editor.active, priority 80) but has no activate() that reveals/activates it. editor.opened is emitted on the DaemonBus, but the EditorController that handles it only exists once EditorView is mounted — and nothing ever activates the editor tab to mount it. Fix: add EditorExtension.activate() subscribing to the editor.opened / editor.active-changed DaemonEvents and calling panels.activateTab(Slots.workspace, ''editor.active''). EditorView.hydrate() already pulls the active buffer on mount, so reveal-then-hydrate avoids any publish/subscribe race.', 'Opening a file in the editor (the reader''s edit pencil, a non-.md file-tree click, a decision''s edit) calls editor.open and the daemon opens the buffer, but the editor pane never appears in the workspace slot above the Claude pane. Root cause: EditorExtension contributes a workspace tab (editor.active, priority 80) but has no activate() that reveals/activates it. editor.opened is emitted on the DaemonBus, but the EditorController that handles it only exists once EditorView is mounted — and nothing ever activates the editor tab to mount it. Fix: add EditorExtension.activate() subscribing to the editor.opened / editor.active-changed DaemonEvents and calling panels.activateTab(Slots.workspace, ''editor.active''). EditorView.hydrate() already pulls the active buffer on mount, so reveal-then-hydrate avoids any publish/subscribe race.
|
||
|
||
Reopened fix (2026-06-01): the original fix called panels.activateTab(Slots.workspace, ''editor.active''), but _WorkspaceSlot renders its editor split off arrangement.editorOpen, NOT the active tab — so the editor never appeared. Real fix: EditorExtension.activate now calls arrangement.openEditor() on editor.opened / active-changed(non-null) and closeEditor() on active-changed(null). Test asserts arrangement.editorOpen.', NULL, '2026-06-01 16:36:38', '2026-06-01 16:36:38', '2026-06-01 16:36:38', NULL, 'f726405105c00c3b78d27ea41dbddb69', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM643F7Z648KNFPT7GR', 'status', 'backlog', 'done', NULL, '2026-06-01 16:48:56', '2026-06-01 16:48:56', '2026-06-01 16:48:56', NULL, '6e7ea043976756151dce11d92afe46eb', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5A3984KFPD4NKRG6M', 'status', 'backlog', 'done', NULL, '2026-06-01 16:48:56', '2026-06-01 16:48:56', '2026-06-01 16:48:56', NULL, 'b7ac3a9ef35ca1e86e9f4b7d1c5a716a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5V7GHH42RHV04DPTR', '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.
|
||
|
||
**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).', '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).
|
||
|
||
Refinement (2026-06-01): scoped Vim-first for a Vim-power-user demo this weekend. ''Author a YAML file'' was wrong — T-117 shipped single-chord resolution + scope flags + when-clauses only. Decomposed into children: T-204 (fix dead default preset — live bug), T-205 (key-sequence + count resolution), T-206 (modal editor motion/edit intents), T-207 (Vim mode service + status indicator). T-65 itself becomes assets/keymaps/vim.yaml + regression tests once children land. T-64/T-66 deferred (single-chord, easy; T-205 hands JetBrains shift+shift later). Foundation is shared, not Vim-only.', NULL, '2026-06-01 18:49:02', '2026-06-01 18:49:02', '2026-06-01 18:49:02', NULL, 'afdcc3d8c3172572295bda81d14d57c3', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM65R8CNFNE1MA5GGR0', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 18:49:17', '2026-06-01 18:49:17', '2026-06-01 18:49:17', NULL, 'd1d00e87c8a85caf4423088b1f878986', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM65R8CNFNE1MA5GGR0', 'status', 'in_progress', 'done', NULL, '2026-06-01 18:51:08', '2026-06-01 18:51:08', '2026-06-01 18:51:08', NULL, '0eb0b244f4042b1c824377cd1c1f6ee2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M83TTXHRXESFZY14', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 18:51:08', '2026-06-01 18:51:08', '2026-06-01 18:51:08', NULL, '916d8491cc454c7d44a104d3b812508e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M83TTXHRXESFZY14', 'status', 'in_progress', 'done', NULL, '2026-06-01 18:59:19', '2026-06-01 18:59:19', '2026-06-01 18:59:19', NULL, 'b22efca89b75981440429ab2ea7b3145', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4PH1J6ENYT4PJZRHG', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 19:07:16', '2026-06-01 19:07:16', '2026-06-01 19:07:16', NULL, '0431c8509983a583f02561f9a2a98584', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4PH1J6ENYT4PJZRHG', 'status', 'in_progress', 'done', NULL, '2026-06-01 19:12:01', '2026-06-01 19:12:01', '2026-06-01 19:12:01', NULL, 'e7711ec17cf8ce4889d9add12fe0ec78', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5REFZXMY8ESWN0Z90', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 19:16:58', '2026-06-01 19:16:58', '2026-06-01 19:16:58', NULL, '8706054876918dcd7b72d627ad5a0886', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5REFZXMY8ESWN0Z90', 'status', 'in_progress', 'done', NULL, '2026-06-01 19:27:18', '2026-06-01 19:27:18', '2026-06-01 19:27:18', NULL, '0c14bf0e4b32866f53fddea3a4d39874', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5V7GHH42RHV04DPTR', 'status', 'backlog', 'in_progress', NULL, '2026-06-01 19:29:15', '2026-06-01 19:29:15', '2026-06-01 19:29:15', NULL, '40a6c8cde0f6e877c94789be50d6072e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4C2PSN2GVJY9JDSZG', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, '187776e23e5382336dca17791c1b91ad', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48QJV4N7J62436GC0', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, '4fbd68d687c6114c261d72d01694c130', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7171NRB1T82TGP5RG', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 08:57:22', '2026-06-03 08:57:22', '2026-06-03 08:57:22', NULL, 'bb31db8ac05e0781039b5f2723f52900', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48QJV4N7J62436GC0', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, '35bb14c9f80fb36fa2d7decd958a588f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4C2PSN2GVJY9JDSZG', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, '6bd82e40072507fa0e384385c54d7569', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7171NRB1T82TGP5RG', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:00:37', '2026-06-03 09:00:37', '2026-06-03 09:00:37', NULL, 'afa0e367a353939ab53a9b6d5407c16e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54WTNQDMYT0AVR4WM', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:02:26', '2026-06-03 09:02:26', '2026-06-03 09:02:26', NULL, 'b8797e42960837b03a01d99703ad4e9e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54WTNQDMYT0AVR4WM', 'description', 'Gap 5 from self-analysis.md — foundational. Two distinct ''Claude inside clide'' stories with different gaps: (A) the clide-HOSTED stream-json session (D-77/D-78) — the user''s primary Claude pane, which itself needs `clide` on PATH to drive the surrounding IDE; (B) an EXTERNAL agent (e.g. a Claude Code harness, TERM_PROGRAM=zed as observed) driving via `clide ...` IPC, which clide cannot fully observe because plain file reads / `make test` / `git` bypass clide. Decide the intended model — hosted, external, or both — and record it under governance/ (claim via `pql decisions claim`). Gates how Epic B bootstraps and how Epic C frames observability.', 'Gap 5 from self-analysis.md — foundational. Two distinct ''Claude inside clide'' stories with different gaps: (A) the clide-HOSTED stream-json session (D-77/D-78) — the user''s primary Claude pane, which itself needs `clide` on PATH to drive the surrounding IDE; (B) an EXTERNAL agent (e.g. a Claude Code harness, TERM_PROGRAM=zed as observed) driving via `clide ...` IPC, which clide cannot fully observe because plain file reads / `make test` / `git` bypass clide. Decide the intended model — hosted, external, or both — and record it under governance/ (claim via `pql decisions claim`). Gates how Epic B bootstraps and how Epic C frames observability.
|
||
|
||
Resolved (2026-06-03): recorded as D-83. Decision — clide commits to BOTH agent models with the clide-HOSTED stream-json session (D-77/D-78) as the PRIMARY dogfood target (the one clide spawns, so the one Epic B/T-214 bootstraps: CLIDE_SOCK/CLIDE_WORKSPACE + PATH + context note + Bash(clide *) allow rule), and the EXTERNAL CLI driver (D-68) as a first-class but SECONDARY, best-effort integration (manual install via T-212; no promise to observe its non-clide tool use). Epic C/T-218 parity is scoped to clide''s own surfaces reflected through the CLI in both directions; an external agent''s side-channel reads/tests/git are out of parity scope.', NULL, '2026-06-03 09:05:48', '2026-06-03 09:05:48', '2026-06-03 09:05:48', NULL, 'a4f8bca0289b2780e9f33e8e06526d18', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54WTNQDMYT0AVR4WM', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:05:50', '2026-06-03 09:05:50', '2026-06-03 09:05:50', NULL, '6d719202dfc0f7ede73f662877866339', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TP6KXTB1GHKBW1G4', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:09:43', '2026-06-03 09:09:43', '2026-06-03 09:09:43', NULL, '0ac74e2c7e822baccd7c3ece16e7d45a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7WYF8ESBSGAQF1S48', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:09:43', '2026-06-03 09:09:43', '2026-06-03 09:09:43', NULL, '4e2c7153e283f0adae41bf07065f52d2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM51CNEJP52P79RDVJW', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:09:43', '2026-06-03 09:09:43', '2026-06-03 09:09:43', NULL, '7d241cc8007d77fae8b9e16831cbab0f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TP6KXTB1GHKBW1G4', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:18:28', '2026-06-03 09:18:28', '2026-06-03 09:18:28', NULL, '200a9081bd33a4605032d41299d0c941', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM51CNEJP52P79RDVJW', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:18:28', '2026-06-03 09:18:28', '2026-06-03 09:18:28', NULL, '2f7785e83b6d61ac50f496c63c08ed9f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7WYF8ESBSGAQF1S48', 'status', 'in_progress', 'done', NULL, '2026-06-03 09:18:28', '2026-06-03 09:18:28', '2026-06-03 09:18:28', NULL, '41464449dc880ed73c650939c86dea8d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78J2H2S0R79SSSPW8', 'status', 'backlog', 'done', NULL, '2026-06-03 09:18:38', '2026-06-03 09:18:38', '2026-06-03 09:18:38', NULL, 'a2404bd89e073a8d1de6ddc0cfdf1ed1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6EGSD0SB7NYNSP1BM', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:27:42', '2026-06-03 09:27:42', '2026-06-03 09:27:42', NULL, '0308666b00afe834a02b5609cf60bb0d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6Y5SET5WGSS2W78F8', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:27:42', '2026-06-03 09:27:42', '2026-06-03 09:27:42', NULL, '1619d3e444a106191b1cfda6ba904d9a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'status', 'backlog', 'ready', NULL, '2026-06-03 09:45:20', '2026-06-03 09:45:20', '2026-06-03 09:45:20', NULL, '973b1a79789f833bb05211bf7a2b4ee3', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XZQJAK31WRVQ0HGW', 'parent_id', NULL, 'T-229', NULL, '2026-06-03 09:51:40', '2026-06-03 09:51:40', '2026-06-03 09:51:40', NULL, '050fec48818561926864cde99393b993', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SA9TEX5CNMTG67MW', 'parent_id', 'T-132', 'T-229', NULL, '2026-06-03 09:51:40', '2026-06-03 09:51:40', '2026-06-03 09:51:40', NULL, '531388662e6a061043a78e1686d6b6df', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5PFAYE4XH7WV6VDW8', 'parent_id', NULL, 'T-229', NULL, '2026-06-03 09:51:40', '2026-06-03 09:51:40', '2026-06-03 09:51:40', NULL, '9f2293a98e0b87246feaac42f150a826', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XZQJAK31WRVQ0HGW', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 09:54:14', '2026-06-03 09:54:14', '2026-06-03 09:54:14', NULL, 'ca8dd4bb0889d0ff8d8592ddc1d6cde0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XZQJAK31WRVQ0HGW', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:02:02', '2026-06-03 10:02:02', '2026-06-03 10:02:02', NULL, 'a61466ef8c2a93b46b35daacc65139af', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SA9TEX5CNMTG67MW', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 10:02:02', '2026-06-03 10:02:02', '2026-06-03 10:02:02', NULL, 'c74a338b9ca8abc44e5757385f182117', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5PFAYE4XH7WV6VDW8', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 10:05:55', '2026-06-03 10:05:55', '2026-06-03 10:05:55', NULL, '3d9ffdedd706c9d22a8a1b5ed3d79368', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SA9TEX5CNMTG67MW', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:05:55', '2026-06-03 10:05:55', '2026-06-03 10:05:55', NULL, 'b18f69871ba45d519a33aaddb77ea445', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5PFAYE4XH7WV6VDW8', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:09:26', '2026-06-03 10:09:26', '2026-06-03 10:09:26', NULL, '7854c38c9e89ea8dd00b01feb717123d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM68T660K07YH3X5DH0', 'status', 'backlog', 'done', NULL, '2026-06-03 10:09:26', '2026-06-03 10:09:26', '2026-06-03 10:09:26', NULL, '8b19904e021d0fddd1b9e66c5e7afdd8', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6Y5SET5WGSS2W78F8', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:31:10', '2026-06-03 10:31:10', '2026-06-03 10:31:10', NULL, '7e93a13c82485ac3c85432616f7fc3ae', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6EGSD0SB7NYNSP1BM', 'description', 'For UI-driven (non-IPC) opens and focus changes, mirror the active file + selection into EditorRegistry (lib/src/editor/registry.dart, read by editor_commands.dart:120/136) so `editor active` / `editor list` track what the user is actually looking at, not only IPC-opened buffers. Acceptance: focusing/opening a file in the GUI is reflected in `clide editor active`.', 'For UI-driven (non-IPC) opens and focus changes, mirror the active file + selection into EditorRegistry (lib/src/editor/registry.dart, read by editor_commands.dart:120/136) so `editor active` / `editor list` track what the user is actually looking at, not only IPC-opened buffers. Acceptance: focusing/opening a file in the GUI is reflected in `clide editor active`.
|
||
|
||
Resolved (2026-06-03): done by prior editor work, not new code. Verified by reading the flow: openWorkspaceFile (file_open.dart) routes non-.md opens through the editor.open IPC verb -> EditorRegistry.open -> _setActive, so file-tree/quick-open opens already populate ''editor active''/''editor list''; tab switches go through editor.activate; and editor_view._onTextChanged pushes editor.set-content on caret-only moves too (its guard skips only when BOTH text and selection are unchanged), so selection is reflected. The probe saw editor active=null only because no code file was open (just the Claude pane / a markdown reader) -- correct, not a gap. The genuine remainder -- the read-only markdown/decisions READER''s currently-viewed file (routed via the D-81 ReaderNav bus, not editor.open, and rightly NOT an EditorRegistry buffer) -- is folded into T-221 (clide status: focused file + selection). Editor-reflects-opens claim is code-read, to be confirmed on the next live run.', NULL, '2026-06-03 10:59:22', '2026-06-03 10:59:22', '2026-06-03 10:59:22', NULL, 'd1839e13b815c81ca14968e9380e2da4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6EGSD0SB7NYNSP1BM', 'status', 'in_progress', 'done', NULL, '2026-06-03 10:59:25', '2026-06-03 10:59:25', '2026-06-03 10:59:25', NULL, 'd71b7b9293734e4022bd942fd2cb2cf2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM758PXMJH16PA3JHFW', 'description', 'Gap 6. Add a `status` command to the dispatcher (currently unknown -> exit 3) returning a single snapshot: active pane, focused file + selection, git summary, layout — the natural first call an agent makes to orient. Acceptance: `clide status` returns a structured snapshot with exit 0. Depends on the pane/editor registries (C1/C2) for its pane and file fields.', 'Gap 6. Add a `status` command to the dispatcher (currently unknown -> exit 3) returning a single snapshot: active pane, focused file + selection, git summary, layout — the natural first call an agent makes to orient. Acceptance: `clide status` returns a structured snapshot with exit 0. Depends on the pane/editor registries (C1/C2) for its pane and file fields.
|
||
|
||
Scope addition (2026-06-03, from T-220): clide status must surface the focused file + selection from BOTH the editor (EditorRegistry.active, already populated) AND the read-only markdown/decisions reader''s current file (D-81 ReaderNav), since viewer files rightly do not live in EditorRegistry. So ''focused file'' = whichever of {active editor buffer, active reader doc} the user is currently looking at. Pane field comes from the T-219 view-pane snapshot; git summary from git.status; layout from the arrangement.', NULL, '2026-06-03 10:59:31', '2026-06-03 10:59:31', '2026-06-03 10:59:31', NULL, '589b102131052de0cf8eeaabfeef3cb6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM758PXMJH16PA3JHFW', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 11:17:49', '2026-06-03 11:17:49', '2026-06-03 11:17:49', NULL, 'e62ce5f754be4992321967879bcf8152', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'status', 'ready', 'in_progress', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, '60597124c6a7f8deea5263098811234e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'description', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.
|
||
|
||
Refinement (2026-06-03): Shift+Tab is REJECTED as the trigger — Tab/Shift+Tab are real a11y focus-traversal intents (focus.next/focus.previous) since T-204, so hijacking Shift+Tab would break keyboard navigation. Instead: (a) Ctrl/Cmd+M cycles, intercepted at the focused Claude composer (so it targets that pane''s session, no global ''find focused session'' needed); (b) the status-line mode label becomes a FOCUSABLE button — cycles on click and on Enter/Space when focused (a11y-native, Tab reaches it); (c) a ''Claude: Cycle permission mode'' palette command. Safe trio default->acceptEdits->plan->default; bypassPermissions only via explicit confirmed path.', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, '770abc73c9c46301ac5f88da05e58742', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ET3VQP90WRXVSMG', 'status', 'done', 'in_progress', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, 'ace0fa6e21b83ac23c1046c0a20572a9', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ET3VQP90WRXVSMG', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, 'a954ee95cfbb232e73107dd38a39f533', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ET3VQP90WRXVSMG', 'description', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).
|
||
|
||
Verified done (2026-06-03): the cockpit per-agent permission-mode cycle badge is implemented and wired in claude_meta_sidebar.dart — _PermissionModeBadge (safe cycle default->acceptEdits->plan, Shift-click bypass behind a confirm, live mode from SessionStatus). No further work; closing. The primary-pane equivalent is T-226.', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, 'eb80295065cb5e3fcb8bfcbc346e6774', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:45:56', '2026-06-03 11:45:56', '2026-06-03 11:45:56', NULL, 'fb9d92168548d7f2530ad4b90410e194', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM758PXMJH16PA3JHFW', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:50:02', '2026-06-03 11:50:02', '2026-06-03 11:50:02', NULL, '45affa5ad9b05a3d510b3b5433a7bcf5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7AMEYEBTF8YGJ4BVG', 'status', 'backlog', 'done', NULL, '2026-06-03 11:50:08', '2026-06-03 11:50:08', '2026-06-03 11:50:08', NULL, '6a100bfce5f8800ac6c963ccc89e21d0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'status', 'done', 'done', NULL, '2026-06-03 11:50:23', '2026-06-03 11:50:23', '2026-06-03 11:50:23', NULL, '08a641c9a728aae240e84d837412dafb', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427BVZYN6MD7ZZX74', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 13:21:05', '2026-06-03 13:21:05', '2026-06-03 13:21:05', NULL, '98e2b543462f5539cde6c4b324e56a2c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GH6VP8TZA95NMYK0', 'status', 'backlog', 'ready', NULL, '2026-06-03 13:23:43', '2026-06-03 13:23:43', '2026-06-03 13:23:43', NULL, '9553d7677bf355d235e0cf7258ec39dd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JR6QZDMKE167C4MR', 'status', 'backlog', 'ready', NULL, '2026-06-03 13:24:18', '2026-06-03 13:24:18', '2026-06-03 13:24:18', NULL, '8994ae3199b15e8ee6a87be2dafbf3e7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427BVZYN6MD7ZZX74', 'status', 'in_progress', 'done', NULL, '2026-06-03 13:30:05', '2026-06-03 13:30:05', '2026-06-03 13:30:05', NULL, 'af6cdaa34ddfc160531feac4a04607b5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78Q3VYWSE830CTVB4', 'description', 'VS Code-style: the GUI offers to install the `clide` shell command on PATH, for users who run the .app without `make install`. A command + command-palette entry that copies/symlinks the bundled C client to a PATH dir and reports success/failure. Acceptance: invoking it makes `clide` resolve on PATH from a fresh shell.', 'VS Code-style: the GUI offers to install the `clide` shell command on PATH, for users who run the .app without `make install`. A command + command-palette entry that copies/symlinks the bundled C client to a PATH dir and reports success/failure. Acceptance: invoking it makes `clide` resolve on PATH from a fresh shell.
|
||
|
||
Refinement (2026-06-03, from live dogfooding): scope should include PROACTIVE detection on launch, not just a palette command. When the clide IDE starts in a repo, check whether ''clide'' resolves on PATH AND points to the C client (not a stale symlink to the GUI bundle runner) -- we hit exactly this: ~/.local/bin/clide was a May-6 symlink to ~/.local/lib/clide/clide (the Flutter GUI), so a bare ''clide pane list'' launched a second app instead of querying. If missing or stale, prompt/offer to install (copy the bundled C client to a PATH dir, VS Code ''Install code command'' style) and report success. This is what lets a fresh agent actually reach the CLI (D-83 names the hosted session primary, but an external agent benefits too). Detecting ''stale GUI symlink'' specifically: the target should be an ELF/Mach-O executable, not a symlink into the bundle. Alternative path the user raised: instead of/alongside this, make the /ide MCP surface reachable (T-225) -- but CLI is primary per D-68.', NULL, '2026-06-03 13:33:43', '2026-06-03 13:33:43', '2026-06-03 13:33:43', NULL, 'e144d47ac16f8badb7f35b086d0686f4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BJCK5YG3R0NEEF70', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 13:35:22', '2026-06-03 13:35:22', '2026-06-03 13:35:22', NULL, '41afdaa7ca24597e843fc8ec43f02e61', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BJCK5YG3R0NEEF70', 'description', 'Major dogfood finding (2026-06-03), verified live: parameterized subsystem commands are NOT reachable from the clide CLI. The C client sends raw argv; parseArgv (lib/src/cli/argv_to_request.dart) turns ''clide editor open X'' into args={positional:[X]} (also flags:{}, passthrough:[]). But the typed handlers read NAMED top-level keys: editor.open reads args[''path''] (editor_commands.dart:64), files.read reads args[''path''], pane.close/editor.activate read args[''id''], etc. Nothing maps positional/flags -> those names, so every arg-taking verb returns ''X is required'' from the CLI. Confirmed: ''clide editor open pubspec.yaml'' and ''--path=pubspec.yaml'' both -> ''path is required''; ''clide files read pubspec.yaml'' -> ''files.read requires a path''. grep shows ONLY the new ui_command.dart reads args[''positional'']. Net: an external agent can OBSERVE (no-arg reads: status, git status, files root, pane list, editor active) but cannot DRIVE anything parameterized -- undercutting CLI-first (D-1) and the D-6 parity premise behind the whole T-208 initiative. Needs a decision on the mapping contract: most ergonomic is a per-command POSITIONAL/flag schema declared where the handler registers (extends the co-registered schema of D-74) so ''clide editor open <path>'' binds positional[0]->path; alternative is lifting --flags into top-level named args. Then either remap in argv_dispatch/parseArgv before dispatch, or have handlers read a normalized accessor. High priority: this is the gating bug for ''Give Claude hands''.', 'Major dogfood finding (2026-06-03), verified live: parameterized subsystem commands are NOT reachable from the clide CLI. The C client sends raw argv; parseArgv (lib/src/cli/argv_to_request.dart) turns ''clide editor open X'' into args={positional:[X]} (also flags:{}, passthrough:[]). But the typed handlers read NAMED top-level keys: editor.open reads args[''path''] (editor_commands.dart:64), files.read reads args[''path''], pane.close/editor.activate read args[''id''], etc. Nothing maps positional/flags -> those names, so every arg-taking verb returns ''X is required'' from the CLI. Confirmed: ''clide editor open pubspec.yaml'' and ''--path=pubspec.yaml'' both -> ''path is required''; ''clide files read pubspec.yaml'' -> ''files.read requires a path''. grep shows ONLY the new ui_command.dart reads args[''positional'']. Net: an external agent can OBSERVE (no-arg reads: status, git status, files root, pane list, editor active) but cannot DRIVE anything parameterized -- undercutting CLI-first (D-1) and the D-6 parity premise behind the whole T-208 initiative. Needs a decision on the mapping contract: most ergonomic is a per-command POSITIONAL/flag schema declared where the handler registers (extends the co-registered schema of D-74) so ''clide editor open <path>'' binds positional[0]->path; alternative is lifting --flags into top-level named args. Then either remap in argv_dispatch/parseArgv before dispatch, or have handlers read a normalized accessor. High priority: this is the gating bug for ''Give Claude hands''.
|
||
|
||
Resolved (2026-06-03): NO new mechanism or decision needed -- the contract already existed. D-74''s CommandSchema.normalize (lib/src/ipc/command_schema.dart) already folds the argv shape {positional,flags} into named args using a declared positional ordering, and the dispatcher already runs normalize+validate. The arg-taking commands simply never registered a schema (adoption is opt-in per D-74). Fix = adopt it: added positional schemas (non-required, so missing-arg errors stay as handlers produce them; only effect is positional->named mapping + numeric coercion of line/cols/rows) to editor.open/activate/read/save/close, files.read/ls, pane.close/focus/resize/write. Handlers unchanged. Tests: CLI-shape ({positional:[...]}) dispatch now binds (editor/pane/files command tests). DEFERRED (still named-arg only; in-process UI works, CLI-arg niche): editor.insert/replace-selection/set-selection/set-content (content/selection encoding + active-buffer fallback make positional ambiguous), pane.spawn (argv is a list, not scalar positional), git.* arg verbs (agents use plain git; D-83 external-agent scope). NOTE: takes effect on app RESTART, not hot reload -- the dispatcher is built once at boot.', NULL, '2026-06-03 13:41:38', '2026-06-03 13:41:38', '2026-06-03 13:41:38', NULL, '00f04276595d3ca0eacea70ace311514', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BJCK5YG3R0NEEF70', 'status', 'in_progress', 'done', NULL, '2026-06-03 13:41:42', '2026-06-03 13:41:42', '2026-06-03 13:41:42', NULL, '7af8d830470a89fef73b0ca8341cceff', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM492DTRZJK9CGQPGDC', 'status', 'backlog', 'ready', NULL, '2026-06-03 14:25:47', '2026-06-03 14:25:47', '2026-06-03 14:25:47', NULL, 'e3619215908a44a1d821ae9c0935649b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JR6QZDMKE167C4MR', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, '4c3010b9a2f5f653efe8ea06ad644457', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GH6VP8TZA95NMYK0', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, '5c31029277155620830aa75e3fb14224', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM492DTRZJK9CGQPGDC', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, '9692b5e80c02c9a55ed66de8875f8714', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JR6QZDMKE167C4MR', 'status', 'in_progress', 'done', NULL, '2026-06-03 14:56:07', '2026-06-03 14:56:07', '2026-06-03 14:56:07', NULL, '5df5897d91f2310e585ec8df8507b02c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM492DTRZJK9CGQPGDC', 'status', 'in_progress', 'done', NULL, '2026-06-03 15:04:07', '2026-06-03 15:04:07', '2026-06-03 15:04:07', NULL, '28d42a5626ada0071e158767c9223017', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GH6VP8TZA95NMYK0', 'description', 'The Claude pane renders every transcript item as its own row, so a heavy agent turn becomes a wall of tool-call/result rows (Bash <cmd> / Bash · result {…} / ''completed with no output'') that buries the messages that matter (user + Claude prose). Wireframe: docs/design/wireframes/claude/meta-activity-card.png.
|
||
|
||
Fold runs of consecutive ''meta'' items into one live, collapsible activity card:
|
||
- The card shows the MOST RECENT meta line as a live ticker and updates in place as new ones stream in — a running ''what''s happening now''. A step count (''14 steps'') sits on the right.
|
||
- Collapsed by default; click the chevron to expand the full list of folded steps, click to re-collapse.
|
||
- ''Sticky'' items never fold — they render first-class. A sticky item SEALS the current card and a new cluster begins after it. A cluster = one unbroken run of foldable items between two sticky items.
|
||
|
||
Taxonomy over the sealed ConversationItem set (transcript_reader.dart): UserMessage, AssistantTextMessage, AssistantThinkingMessage, AssistantToolUse, ToolResultMessage.
|
||
- Always sticky: UserMessage, AssistantTextMessage. Permission/AskUserQuestion prompts already live in the interaction zone (D-78) and are inherently sticky / cluster-breaking — no change, but note they break the run too.
|
||
- Foldable: AssistantToolUse + its paired ToolResultMessage (pairing already exists via toolUseById, T-168).
|
||
|
||
Refinement decisions (2026-06-03, user):
|
||
1. WHAT FOLDS is a 3-level setting (conservative → aggressive); DEFAULT = level 1. Build the foldable predicate so all three are switchable:
|
||
- L1 (default): fold tool calls + results only; diffs (Edit/Write results) AND AssistantThinkingMessage stay first-class/sticky.
|
||
- L2: also fold thinking; diffs stay sticky.
|
||
- L3: fold everything except UserMessage + AssistantTextMessage (incl. diffs + thinking).
|
||
2. ERRORS: a failed/error ToolResultMessage SURFACES — treated as sticky, stays visible and breaks the cluster (see wireframe state B). Start here; the user noted some non-fatal errors are themselves clutter, so expect to tune which errors surface vs. fold.
|
||
3. DEFAULT/RUN STATE: always collapsed, showing the last line + step count, with the ticker updating live. NO setting. (Rejected auto-expanding the in-flight cluster: it scrolls the last useful sticky message out of view during long runs and only collapses when done — worst timing. Collapsed-with-live-ticker keeps the last sticky message anchored while preserving the motion/dynamism.)
|
||
|
||
Implementation home: a ''collapse adjacent foldable items'' grouping pass over ConversationController.items, consumed by ConversationView; the card is a new conversation_card variant. Keep it accessible (keyboard expand/collapse + semantics: announce step count and collapsed/expanded state).
|
||
|
||
Acceptance:
|
||
1. Consecutive foldable items collapse into one card; the collapsed card shows the latest line + a live-updating step count.
|
||
2. A sticky message (user / Claude prose / surfaced error / interaction-zone prompt) seals the card and starts a new cluster after it.
|
||
3. Expanding shows every folded step (tool call + result pairs) in order; collapsing returns to the one-line view.
|
||
4. The foldable level is a setting (default L1: diffs + thinking stay first-class); switching levels re-groups the transcript.
|
||
5. Failed tool results surface as their own (sticky) row and break the cluster.
|
||
6. Keyboard + screen-reader accessible (expand/collapse, step count announced).', 'The Claude pane renders every transcript item as its own row, so a heavy agent turn becomes a wall of tool-call/result rows (Bash <cmd> / Bash · result {…} / ''completed with no output'') that buries the messages that matter (user + Claude prose). Wireframe: docs/design/wireframes/claude/meta-activity-card.png.
|
||
|
||
Fold runs of consecutive ''meta'' items into one live, collapsible activity card:
|
||
- The card shows the MOST RECENT meta line as a live ticker and updates in place as new ones stream in — a running ''what''s happening now''. A step count (''14 steps'') sits on the right.
|
||
- Collapsed by default; click the chevron to expand the full list of folded steps, click to re-collapse.
|
||
- ''Sticky'' items never fold — they render first-class. A sticky item SEALS the current card and a new cluster begins after it. A cluster = one unbroken run of foldable items between two sticky items.
|
||
|
||
Taxonomy over the sealed ConversationItem set (transcript_reader.dart): UserMessage, AssistantTextMessage, AssistantThinkingMessage, AssistantToolUse, ToolResultMessage.
|
||
- Always sticky: UserMessage, AssistantTextMessage. Permission/AskUserQuestion prompts already live in the interaction zone (D-78) and are inherently sticky / cluster-breaking — no change, but note they break the run too.
|
||
- Foldable: AssistantToolUse + its paired ToolResultMessage (pairing already exists via toolUseById, T-168).
|
||
|
||
Refinement decisions (2026-06-03, user):
|
||
1. WHAT FOLDS is a 3-level setting (conservative → aggressive); DEFAULT = level 1. Build the foldable predicate so all three are switchable:
|
||
- L1 (default): fold tool calls + results only; diffs (Edit/Write results) AND AssistantThinkingMessage stay first-class/sticky.
|
||
- L2: also fold thinking; diffs stay sticky.
|
||
- L3: fold everything except UserMessage + AssistantTextMessage (incl. diffs + thinking).
|
||
2. ERRORS: a failed/error ToolResultMessage SURFACES — treated as sticky, stays visible and breaks the cluster (see wireframe state B). Start here; the user noted some non-fatal errors are themselves clutter, so expect to tune which errors surface vs. fold.
|
||
3. DEFAULT/RUN STATE: always collapsed, showing the last line + step count, with the ticker updating live. NO setting. (Rejected auto-expanding the in-flight cluster: it scrolls the last useful sticky message out of view during long runs and only collapses when done — worst timing. Collapsed-with-live-ticker keeps the last sticky message anchored while preserving the motion/dynamism.)
|
||
|
||
Implementation home: a ''collapse adjacent foldable items'' grouping pass over ConversationController.items, consumed by ConversationView; the card is a new conversation_card variant. Keep it accessible (keyboard expand/collapse + semantics: announce step count and collapsed/expanded state).
|
||
|
||
Acceptance:
|
||
1. Consecutive foldable items collapse into one card; the collapsed card shows the latest line + a live-updating step count.
|
||
2. A sticky message (user / Claude prose / surfaced error / interaction-zone prompt) seals the card and starts a new cluster after it.
|
||
3. Expanding shows every folded step (tool call + result pairs) in order; collapsing returns to the one-line view.
|
||
4. The foldable level is a setting (default L1: diffs + thinking stay first-class); switching levels re-groups the transcript.
|
||
5. Failed tool results surface as their own (sticky) row and break the cluster.
|
||
6. Keyboard + screen-reader accessible (expand/collapse, step count announced).
|
||
|
||
Implemented (2026-06-03): pure grouping in activity_cluster.dart (groupConversation + FoldLevel none/tools/thinking/everything + RenderGroup StickyItem/FoldedCluster), fully unit-tested. Activity card + grouping wired into conversation_view.dart (collapsed live ticker of the latest step + step count; click/Enter expands to the folded steps in order; Semantics announces count + expanded/collapsed). Default level = L1 (tools): tool calls+results fold; user/Claude prose, FAILED results (sticky, surfaced), diffs (Edit/Write), and thinking stay first-class. Added FoldLevel.none (no folding) used by the existing item-level renderer tests. DEFERRED (acceptance 4''s persistence): the fold level is a switchable ConversationView parameter (proven by tests; none/L1/L2/L3 re-group) but is NOT yet wired to a persisted user setting + a UI control — the live pane uses the L1 default. Follow-up: read it from settings (ctx.settings) + a control to change it. Did NOT match the wireframe pixel-for-pixel; functional shape per the refinement decisions.', NULL, '2026-06-03 15:17:10', '2026-06-03 15:17:10', '2026-06-03 15:17:10', NULL, 'e320516c4fee5c1d23de4772712a6fe6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GH6VP8TZA95NMYK0', 'status', 'in_progress', 'done', NULL, '2026-06-03 15:17:33', '2026-06-03 15:17:33', '2026-06-03 15:17:33', NULL, '45157bbf9208bebe5bb78b5650fc259c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM46ANXPW02XGD3D3NG', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 15:43:55', '2026-06-03 15:43:55', '2026-06-03 15:43:55', NULL, 'f4802b804089ec788e4c87ee78d34aee', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM46ANXPW02XGD3D3NG', 'status', 'in_progress', 'done', NULL, '2026-06-03 21:11:32', '2026-06-03 21:11:32', '2026-06-03 21:11:32', NULL, '9a06d177b18f27eaf0f1b6dfd935db42', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM46ANXPW02XGD3D3NG', 'status', 'done', 'done', NULL, '2026-06-03 21:29:47', '2026-06-03 21:29:47', '2026-06-03 21:29:47', NULL, '5c6c0567db81e42c9272a491d615aee6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM46ANXPW02XGD3D3NG', 'description', 'User feedback on the T-234 status-bar theme popover (screenshot 2026-06-03): (1) the menu order looks random — the -hc variants are interleaved. (2) Hide all the high-contrast theme entries and replace them with a single ''[ ] High contrast'' checkbox at the top; toggling it applies the -hc sibling of the chosen base theme. (3) Widen the popover so long names (''Catppuccin Mocha'') don''t wrap; rows ellipsize. (4) The status-bar items (''application ok'' + theme switcher) float in the middle (boxed into the workspace column between the per-column bottom rails) instead of hugging the window''s right edge — align them right. (5) Replace the swatch dot on the trigger with a Phosphor icon (palette, 0xe6c8, already in the registry — paint-roller has no registered codepoint). Apply the -hc-as-checkbox + sort to the modal picker_view too for consistency. Helpers: base = name without -hc/-cb; show base themes only; resolve(base, hc) -> base+''-hc'' if it exists.', 'User feedback on the T-234 status-bar theme popover (screenshot 2026-06-03): (1) the menu order looks random — the -hc variants are interleaved. (2) Hide all the high-contrast theme entries and replace them with a single ''[ ] High contrast'' checkbox at the top; toggling it applies the -hc sibling of the chosen base theme. (3) Widen the popover so long names (''Catppuccin Mocha'') don''t wrap; rows ellipsize. (4) The status-bar items (''application ok'' + theme switcher) float in the middle (boxed into the workspace column between the per-column bottom rails) instead of hugging the window''s right edge — align them right. (5) Replace the swatch dot on the trigger with a Phosphor icon (palette, 0xe6c8, already in the registry — paint-roller has no registered codepoint). Apply the -hc-as-checkbox + sort to the modal picker_view too for consistency. Helpers: base = name without -hc/-cb; show base themes only; resolve(base, hc) -> base+''-hc'' if it exists.
|
||
|
||
Done (2026-06-03): popover collapses -hc into a ''High contrast'' toggle, lists base themes sorted by display name, widened to 280 with ellipsis, palette icon on the trigger, lowercased bar label; status bar split so the global right group (tool status + theme switcher) hugs the window''s right edge past the context rail (643417e), keeping the 3-rail structure. Pure theme_families helpers unit-tested + popover widget tests. DEFERRED: applying the same -hc-toggle + sort to the modal picker_view (ctrl+k) for consistency — minor, follow-up.', NULL, '2026-06-03 21:29:47', '2026-06-03 21:29:47', '2026-06-03 21:29:47', NULL, 'e92a06a4035a0eb5984ea80b84a9b106', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BWXW61EVYGMZD47G', 'status', 'in_progress', 'done', NULL, '2026-06-04 12:00:09', '2026-06-04 12:00:09', '2026-06-04 12:00:09', NULL, '0b5b1c74b5a43d83d7a7109695f6b267', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W624VD4JW1GHDHAR', 'status', 'in_progress', 'done', NULL, '2026-06-04 12:00:09', '2026-06-04 12:00:09', '2026-06-04 12:00:09', NULL, '875e34a0516f4797988ecb4152e797ff', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5ABRPK2SPW8VY7RP4', 'status', 'in_progress', 'done', NULL, '2026-06-04 12:00:09', '2026-06-04 12:00:09', '2026-06-04 12:00:09', NULL, '8813d96e9fdde24a1107a2788f7b0995', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5V7GHH42RHV04DPTR', 'status', 'in_progress', 'done', NULL, '2026-06-04 12:00:09', '2026-06-04 12:00:09', '2026-06-04 12:00:09', NULL, '9122fa1a1ad891b12102022c38203b25', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM56FKPV4WSPWA0KMB4', 'status', 'in_progress', 'done', NULL, '2026-06-04 12:00:09', '2026-06-04 12:00:09', '2026-06-04 12:00:09', NULL, '989cb032dc8c8f5a2fd85de7e0927b1e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QJC039TMDF5JGMXC', 'description', 'Muscle-memory gap from the Claude Code CLI: in clide''s interaction-zone prompts (D-78), the user reaches for number keys 1/2/3 to pick a button but has to click. Map number keys (1..N) to the visible buttons/options, and PREFIX each button label with its number for discoverability (CLI shows ''1. Allow / 2. ... / 3. No'').\n\nSurface: ToolPromptCard in lib/builtin/claude/src/prompt_card.dart. Two modes:\n- PERMISSION (_permission, ~line 119): buttons Allow (primary) / ''Allow & don''t ask again'' (only when permissionSuggestions present) / Deny. Map 1=Allow, 2=''Allow & don''t ask again'' (when shown) else Deny, 3=Deny when the middle one is shown. Match the CLI''s ordering/numbering.\n- ASKUSERQUESTION (_question / _optButton, ~line 280/294): each option per question gets a number (1..N) that selects (single) or toggles (multiSelect) it; keep Enter for Submit/Next and Esc/back as-is. Prefix option labels with the number alongside the existing radio/checkbox glyph (the ''○/●'' in _optButton).\n\nKEY CAVEAT (do this right): the prompt card hosts a free-text note field (_NoteField) and AskUserQuestion ''Other'' free-text. Number keys MUST NOT be captured while focus is in a text field (otherwise typing ''1'' triggers a button). Gate the shortcut on focus not being in an editable, i.e. intercept at the prompt''s Focus/FocusScope and bail when a text field has focus — same consumer-interception discipline as D-82 (Vim) / the editor. Keep the ClideButtons (clicks + AT) intact; the number key is an additional accelerator. Accessibility: include the number in the button''s semantics label.\n\nAcceptance: with a permission prompt or AskUserQuestion open and focus not in a text field, pressing 1/2/3 (and up to N for question options) activates the matching button/option; labels are number-prefixed; typing in a note/Other field is unaffected; mouse + screen-reader paths still work.', 'Muscle-memory gap from the Claude Code CLI: in clide''s interaction-zone prompts (D-78), the user reaches for number keys 1/2/3 to pick a button but has to click. Map number keys (1..N) to the visible buttons/options, and PREFIX each button label with its number for discoverability (CLI shows ''1. Allow / 2. ... / 3. No'').\n\nSurface: ToolPromptCard in lib/builtin/claude/src/prompt_card.dart. Two modes:\n- PERMISSION (_permission, ~line 119): buttons Allow (primary) / ''Allow & don''t ask again'' (only when permissionSuggestions present) / Deny. Map 1=Allow, 2=''Allow & don''t ask again'' (when shown) else Deny, 3=Deny when the middle one is shown. Match the CLI''s ordering/numbering.\n- ASKUSERQUESTION (_question / _optButton, ~line 280/294): each option per question gets a number (1..N) that selects (single) or toggles (multiSelect) it; keep Enter for Submit/Next and Esc/back as-is. Prefix option labels with the number alongside the existing radio/checkbox glyph (the ''○/●'' in _optButton).\n\nKEY CAVEAT (do this right): the prompt card hosts a free-text note field (_NoteField) and AskUserQuestion ''Other'' free-text. Number keys MUST NOT be captured while focus is in a text field (otherwise typing ''1'' triggers a button). Gate the shortcut on focus not being in an editable, i.e. intercept at the prompt''s Focus/FocusScope and bail when a text field has focus — same consumer-interception discipline as D-82 (Vim) / the editor. Keep the ClideButtons (clicks + AT) intact; the number key is an additional accelerator. Accessibility: include the number in the button''s semantics label.\n\nAcceptance: with a permission prompt or AskUserQuestion open and focus not in a text field, pressing 1/2/3 (and up to N for question options) activates the matching button/option; labels are number-prefixed; typing in a note/Other field is unaffected; mouse + screen-reader paths still work.
|
||
|
||
Refinement (2026-06-05, user): match the Claude Code CLI''s actual behavior for the text-field interaction. The number key triggers the button/option shortcut when it would be the FIRST character typed -- i.e. focus is NOT in a text field, OR focus IS in the note/Other field but that field is currently EMPTY. Once the field has any content, digits type normally (no shortcut). This supersedes the earlier ''never capture digits while a text field has focus'' gate: it''s better because focus often defaults into the (empty) note field, so the shortcut still fires there (muscle-memory case) while a digit mid-note still types. Implementation: intercept the digit at the prompt focus scope; consume+activate only when the focused editable (if any) is empty, else let it through to type. The CLI exhibits this exact ''footgun'' (digit-as-first-char in an empty field acts as the choice) and we intentionally mirror it.', NULL, '2026-06-05 09:40:57', '2026-06-05 09:40:57', '2026-06-05 09:40:57', NULL, '14ab9c3fceabd48bab7c461051ca8ab5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F4D2Q363QF7V40K8', 'status', 'backlog', 'in_progress', NULL, '2026-06-05 09:45:51', '2026-06-05 09:45:51', '2026-06-05 09:45:51', NULL, '10b4a203cccc9b0fed0804704eb857ac', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F4D2Q363QF7V40K8', 'description', 'Unresolved after T-237 (commits 643417e then reverted by dc84f4c). GOAL: in the single bottom status bar, left items (git branch ''main'', skills count) sit flush-LEFT and the global right group (ipc ''application ok'' + theme switcher) sits flush-RIGHT of the CENTER (workspace) block — NOT the window edge, and NOT two separate left-aligned columns. Keep the 3-rail bottom bar (sidebar rail | workspace status | context tab-rail). CURRENT STATE (dc84f4c): StatusbarHost is a single Container(Row[ ...left, Spacer(), ...right ]) with NO alignment, sitting in Expanded(StatusbarHost()) inside the bottom-bar Row (app.dart ~261). In theory the Spacer should right-align the right group within the workspace Expanded; in practice (verified live by the user, even after a hot RESTART) the right group does NOT hug the center block''s right edge. ATTEMPTS THAT FAILED: (a) removing ''alignment: Alignment.center'' from the StatusbarHost Container; (b) splitting into two hosts and trailing the right group past the context rail (overshot to the window edge — wrong). LEAD / NEXT STEP: instrument the actual constraints rather than guess. Prime suspect: the OUTER Column at app.dart ~214 has NO crossAxisAlignment (defaults to center), so the bottom-bar Container may receive LOOSE width and size to its content, collapsing the Spacer (right group ends up adjacent to the left group, both effectively left-aligned). Try CrossAxisAlignment.stretch on that Column (or give the status Container an explicit full width), and confirm Expanded(StatusbarHost) actually receives a bounded full-workspace width. A widget/integration test asserting the right group''s x-offset == workspace-region right edge would lock it. Files: lib/app.dart (StatusbarHost + the bottom-bar Row + the outer Column).', 'Unresolved after T-237 (commits 643417e then reverted by dc84f4c). GOAL: in the single bottom status bar, left items (git branch ''main'', skills count) sit flush-LEFT and the global right group (ipc ''application ok'' + theme switcher) sits flush-RIGHT of the CENTER (workspace) block — NOT the window edge, and NOT two separate left-aligned columns. Keep the 3-rail bottom bar (sidebar rail | workspace status | context tab-rail). CURRENT STATE (dc84f4c): StatusbarHost is a single Container(Row[ ...left, Spacer(), ...right ]) with NO alignment, sitting in Expanded(StatusbarHost()) inside the bottom-bar Row (app.dart ~261). In theory the Spacer should right-align the right group within the workspace Expanded; in practice (verified live by the user, even after a hot RESTART) the right group does NOT hug the center block''s right edge. ATTEMPTS THAT FAILED: (a) removing ''alignment: Alignment.center'' from the StatusbarHost Container; (b) splitting into two hosts and trailing the right group past the context rail (overshot to the window edge — wrong). LEAD / NEXT STEP: instrument the actual constraints rather than guess. Prime suspect: the OUTER Column at app.dart ~214 has NO crossAxisAlignment (defaults to center), so the bottom-bar Container may receive LOOSE width and size to its content, collapsing the Spacer (right group ends up adjacent to the left group, both effectively left-aligned). Try CrossAxisAlignment.stretch on that Column (or give the status Container an explicit full width), and confirm Expanded(StatusbarHost) actually receives a bounded full-workspace width. A widget/integration test asserting the right group''s x-offset == workspace-region right edge would lock it. Files: lib/app.dart (StatusbarHost + the bottom-bar Row + the outer Column).
|
||
|
||
FIXED (2026-06-05): root cause confirmed by probe at 3440px width — the bottom bar used Row[ ...left (incl. a Flexible flex:1 loose item, the Claude status marquee), Spacer(), ...right ]. The Spacer (Expanded, flex:1) and the left flex:1 item SPLIT the free space 50/50, so the Spacer only pushed the right group by HALF the free space. That drift is proportional to width: tiny at 1200px (why probes/normal screens looked fine), ~1500px at 3440px (the right group floated to mid-bar) — hence ''only on ultrawide''. Probe: OLD R.right=1934 at a 3440 edge; NEW R.right=3440. FIX (app.dart StatusbarHost): explicit two-column layout — left group wrapped in Expanded(Row[...]) so it absorbs ALL free space (flex item flexes within it), right group trails at intrinsic width → hugs the workspace block''s right edge by construction, width-independent. Regression test test/app_statusbar_test.dart asserts the right group at width-8 for BOTH 600px and 3440px. Full fast suite green.', NULL, '2026-06-05 10:02:12', '2026-06-05 10:02:12', '2026-06-05 10:02:12', NULL, '1c7d83b62aa242a2cc67715225a7cf1b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F4D2Q363QF7V40K8', 'status', 'in_progress', 'done', NULL, '2026-06-05 10:02:12', '2026-06-05 10:02:12', '2026-06-05 10:02:12', NULL, 'bdd320e30b5741955925e68a87a6c705', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66TK8TQ1X5T9GK6V0', 'status', 'backlog', 'ready', NULL, '2026-06-05 11:04:12', '2026-06-05 11:04:12', '2026-06-05 11:04:12', NULL, 'bc94db15c95b740f2ca3f0c0d3921542', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYY4FVPX9KRVHCMC', 'status', 'backlog', 'ready', NULL, '2026-06-05 11:04:25', '2026-06-05 11:04:25', '2026-06-05 11:04:25', NULL, '7012c8f3ef5f83bc114211666e6763dd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM41S6RRTMTY9TYZA50', 'status', 'backlog', 'ready', NULL, '2026-06-05 12:15:59', '2026-06-05 12:15:59', '2026-06-05 12:15:59', NULL, '8e598b3ac94c0759219816735f773964', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM41S6RRTMTY9TYZA50', 'status', 'ready', 'in_progress', NULL, '2026-06-05 13:07:20', '2026-06-05 13:07:20', '2026-06-05 13:07:20', NULL, '97e0697ae51d47c730e6636b87693a58', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM41S6RRTMTY9TYZA50', 'description', '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).', '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).
|
||
|
||
ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', NULL, '2026-06-05 13:08:02', '2026-06-05 13:08:02', '2026-06-05 13:08:02', NULL, '8459f860f51ab035507cd3a99e869989', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM41S6RRTMTY9TYZA50', 'status', 'in_progress', 'done', NULL, '2026-06-05 13:08:05', '2026-06-05 13:08:05', '2026-06-05 13:08:05', NULL, '9102b9fb80d116f4443fe4b478003a08', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66TK8TQ1X5T9GK6V0', 'description', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.
|
||
|
||
DECIDED 2026-06-05 (D-84): diff view = editor-mode surface, inline above Claude (NOT a workspace tab, NOT a context-panel viewer), spawned from the git sidebar. Rationale: a diff is a review+intervention surface needing room for hunk stage/discard and conflict-resolution widgets — too cramped for the ~420px context panel; the middle column above Claude gives space while keeping Claude''s prompt bar fixed (D-47/D-49). This decision ticket is satisfied; the actual re-placement (generalize the editor-mode slot to host a diff, wire git-sidebar spawn, build resolution widgets) is a follow-up implementation story.', NULL, '2026-06-05 15:14:18', '2026-06-05 15:14:18', '2026-06-05 15:14:18', NULL, 'f963b14dc689b03e3e6bf53bfd5f7d73', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66TK8TQ1X5T9GK6V0', 'status', 'ready', 'done', NULL, '2026-06-05 15:17:09', '2026-06-05 15:17:09', '2026-06-05 15:17:09', NULL, '71c38038b7804fb5afd636c27de1a53c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WT3BHB8JZY77G5AW', 'description', NULL, 'Implements D-84. Today the diff view is a workspace TAB (lib/builtin/diff/src/extension.dart, slot: Slots.workspace) — remove that. Instead: (1) generalize the editor-mode surface (currently hosts the single inline editor in _WorkspaceSlot, app.dart, gated by arrangement.editorOpen/editorRatio) so it can host EITHER the editor or a diff (mutually exclusive in that slot, like the viewer<->editor swap in D-49); (2) selecting a changed file in the git sidebar opens its diff in that above-Claude surface; (3) the diff keeps Claude''s prompt bar fixed (D-47) and gets a draggable divider like the editor. Acceptance: diff no longer appears as a workspace tab; clicking a file in the git panel opens its diff above Claude; closing it (Ctrl/Cmd+W) returns to Claude full-height; coexists with the editor by swap, not both at once. Hunk stage/discard + conflict-resolution widgets can be a further follow-up. Tests: arrangement state for the diff surface; git-sidebar -> diff open wiring; widget render.', NULL, '2026-06-05 15:17:09', '2026-06-05 15:17:09', '2026-06-05 15:17:09', NULL, 'fe174b3a1ef5357f0c527d6209f4c671', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYY4FVPX9KRVHCMC', 'description', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).
|
||
|
||
REFINED 2026-06-05. Build (bottom-right, system + proof emitter):
|
||
- kernel ToastService (ChangeNotifier): show(message, {severity, duration}) -> id, dismiss(id), queue of active toasts, per-toast auto-dismiss Timer (default ~4s; errors longer/sticky), cancel on manual dismiss. ToastSeverity {success, warning, error, info}.
|
||
- ClideToast widget (custom, NO Material per D-7): severity accent from status tokens (statusSuccess/Warning/Error/info), message text, dismiss affordance (x / Esc-less, click); slide+fade in from bottom-right, auto slide-out.
|
||
- ToastOverlay: mounted in the app-root Stack (app.dart) alongside ClidePalette/QuickOpenOverlay, anchored bottom-right; stacks multiple toasts vertically; animates enter/exit.
|
||
- a11y: Semantics liveRegion for the message + labelled dismiss; keyboard-dismissable.
|
||
- Proof emitter: git push success/failure raises a toast (wire in the git command path). Broad wiring (extension activated, update available, generic errors) = follow-up tickets.
|
||
- Tests: ToastService queue + auto-dismiss (fakeAsync timers) + severity mapping; ClideToast render per severity; a11y contract; overlay mount.', NULL, '2026-06-05 15:17:36', '2026-06-05 15:17:36', '2026-06-05 15:17:36', NULL, '210739d5d63388a41de15b8e3bc6dfc7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYY4FVPX9KRVHCMC', 'status', 'ready', 'in_progress', NULL, '2026-06-05 15:17:36', '2026-06-05 15:17:36', '2026-06-05 15:17:36', NULL, 'aed11ee397b37d20662eabf1741fc4db', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', 'status', 'backlog', 'ready', NULL, '2026-06-05 15:21:04', '2026-06-05 15:21:04', '2026-06-05 15:21:04', NULL, 'f7966535989e601c2d724e65701f8191', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KPN4VK4EZP013NNG', 'description', NULL, 'When a Claude Code session hits context compaction, the Claude pane gives no signal that anything is happening: the conversation surface shows no in-pane indicator and the status bar shows no progress. From the user''s side the pane just appears to stall until compaction finishes and the next response streams in.
|
||
|
||
Expected: compaction is a visible, first-class state.
|
||
- In-pane: a clear "Compacting context…" affordance on the conversation surface (e.g. a transient status card / banner in the live-status line from T-168) so it''s obvious the session is busy, not wedged.
|
||
- Status bar: a progress indicator in the focus-driven Claude slot (T-150) reflecting the active compaction, consistent with the long-running-operation pattern in T-59.
|
||
|
||
Implementation notes:
|
||
- Source the state from the stream-json event stream (T-164/T-165), not transcript tailing — identify the compaction signal (system/compact_boundary or equivalent event) and thread it through the conversation controller into both the in-pane live-status line and the status-bar slot.
|
||
- Clear the indicator when compaction completes and normal streaming resumes.
|
||
|
||
Acceptance: triggering /compact (or an auto-compaction) shows an in-pane "compacting" indicator and a status-bar progress affordance for the duration, both of which clear when it finishes; covered by a unit test against a canned event fixture containing the compaction event.', NULL, '2026-06-05 15:24:35', '2026-06-05 15:24:35', '2026-06-05 15:24:35', NULL, 'd752e15e3e771292aa0084dc91181318', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYY4FVPX9KRVHCMC', 'status', 'in_progress', 'done', NULL, '2026-06-05 21:12:23', '2026-06-05 21:12:23', '2026-06-05 21:12:23', NULL, 'd9a116530e54d9766adb050be6db160d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66DNEK5FS9C0T4J5R', 'status', 'backlog', 'done', NULL, '2026-06-05 21:24:52', '2026-06-05 21:24:52', '2026-06-05 21:24:52', NULL, 'c089018f0a5131bc60a2c4b467dac861', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66DNEK5FS9C0T4J5R', 'description', NULL, 'Implements the agent/CLI drive-half of T-50 (parity, like ui.open/T-231): clide ui toast "message" [--severity success|warning|error|info] [--duration MS]. Registered as ui.toast in lib/src/daemon/ui_command.dart; publishes {message,severity,durationMs?} on the kernel MessageBus ''toast'' channel (literal kept Flutter-free), which the ToastService consumes. Lets a hosted Claude session (or any script) surface ''done/failed'' on the user''s screen. Validates message-required, severity, integer duration; toolError when no live GUI. Tested in test/daemon/ui_command_test.dart. NOTE: a running GUI must be rebuilt to pick this up.', NULL, '2026-06-05 21:24:52', '2026-06-05 21:24:52', '2026-06-05 21:24:52', NULL, 'cb7834de6d3591d77a66d288e125097f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6DH7C6GQN9WNKQNXR', 'description', NULL, '`clide pane list` reports every GUI tab (T-219, D-83) but only its generic chrome: `id`, `title`, `active`, `visible`. For the context-slot detail panes the title is a static word — `tickets.detail` → "Ticket", `decisions.detail` → "Decision", `editor.active` → "Editor" — so the CLI cannot tell *which* ticket / decision / file the pane currently shows. The loaded subject lives in the view''s reader-nav state and never reaches the wire.
|
||
|
||
Concrete failure: with a ticket open in the right-hand context pane, there is no `clide` verb that answers "what ticket am I looking at". `pane list` shows `{"id":"tickets.detail","title":"Ticket","active":true}` and stops there. This breaks D-6 parity — the UI surfaces the open ticket; the CLI can''t observe it.
|
||
|
||
Fix: give `ViewPane` (lib/src/panes/view_pane.dart) an optional `subject` (and/or `subtitle`) field and have `snapshotViewPanes` (lib/kernel/src/panels/view_pane_snapshot.dart) populate it from the active reader-nav selection for detail panes — `tickets.detail` → `T-NNN`, `decisions.detail` → `D-NNN`, `editor.active` → the file path. Serialise it in `ViewPane.toJson` so `pane list` carries it. Keep ViewPane Flutter-free (it must stay usable under `dart test`); read the selection in the kernel-side snapshot, not in the value object.
|
||
|
||
Acceptance: with a ticket open in the context pane, `clide pane list` returns that pane with its loaded id (e.g. `"subject":"T-244"`); same for an open decision and an open editor file; panes with no subject omit the field; covered by a unit test over a snapshot with a populated reader-nav selection.', NULL, '2026-06-05 21:29:50', '2026-06-05 21:29:50', '2026-06-05 21:29:50', NULL, 'bd7e5281457bb9cd90e25fb210313296', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6QCYT7JBF352YVJ48', 'description', NULL, 'The `clide` CLI does not honor the `CLIDE_SOCK` env var — it auto-discovers a running instance instead. Proof: with `CLIDE_SOCK=/run/user/1000/clide/DOES_NOT_EXIST.sock`, `clide version` still returns `{"version":"2.1.0"}` from the live app. Separately, the runtime socket dir accumulates orphaned socket files: `/run/user/1000/clide/` held two `.sock` entries while only one GUI process (the `build/linux/x64/debug/bundle/clide` bundle) was running, so a previous run''s socket was never cleaned up.
|
||
|
||
Why it matters: today (single instance) it''s harmless, but it''s a latent split-brain + observability hole. If two clide instances are ever live on the same machine, the CLI attaches to whichever discovery resolves first, with (a) no way to target a specific instance and (b) no way to find out which one you''re talking to. Combined with the stale-socket litter, `clide` could silently drive the wrong window. This is a D-6 surface gap — the CLI must be able to address the same instance the user is in.
|
||
|
||
Fix (scope to confirm):
|
||
- Honor `CLIDE_SOCK` when set (explicit target beats discovery); error clearly if that socket is dead rather than silently falling back.
|
||
- Clean up orphaned/stale socket files on app startup (and on clean shutdown) — detect a dead listener and unlink before binding a new hash.
|
||
- Add a way to enumerate/identify live instances (e.g. a `clide instances` verb, or include the instance id/socket path + pid in `clide version`) so a human or agent can pick the right one.
|
||
|
||
Acceptance: a bogus `CLIDE_SOCK` fails loudly instead of returning data from a different instance; a valid `CLIDE_SOCK` pins the CLI to that instance; startup leaves exactly one live socket for one running app (no orphan accumulation); there is a CLI affordance to list/identify running instances.', NULL, '2026-06-05 21:30:04', '2026-06-05 21:30:04', '2026-06-05 21:30:04', NULL, '453237303b4ea9c6a396f9f0ad23280a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F4D2Q363QF7V40K8', 'status', 'in_progress', 'done', NULL, '2026-06-06 06:44:40', '2026-06-06 06:44:40', '2026-06-06 06:44:40', NULL, '4b87229500c5bf1514d4eb4835f9da82', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F2GC0ABFFVAH34EC', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 06:51:47', '2026-06-06 06:51:47', '2026-06-06 06:51:47', NULL, '9c1ed7e465d7c41078928c3fce667668', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'description', NULL, 'Create a `/clide` skill (none exists today — only legacy templates under legacy/clide/templates/skills/) that advertises the clide CLI surface to Claude so new affordances become discoverable.
|
||
|
||
Problem: my knowledge of the clide surface comes from a hand-curated session blurb. There is no runtime discovery — `clide help` is a stub, `clide <subsystem>` with no verb just prints `usage:`, and the dispatcher''s registered command table (e.g. pane_commands.dart) is never exposed. D-6 guarantees a verb EXISTS for every UI action, but parity != discoverability: a correctly-registered verb is still unreachable if nothing tells me it''s there.
|
||
|
||
Scope:
|
||
- Self-description first (prereq for a non-rotting skill): add a discovery verb that reflects the live dispatcher registry — e.g. `clide capabilities` (machine-readable JSON: subsystems -> verbs -> arg schema) and/or flesh out `clide help` to enumerate subsystems/verbs. Sourced from the registry so it never drifts.
|
||
- `/clide` SKILL.md: trigger description always visible to Claude; body points at the discovery verb rather than hard-coding a verb list, plus conventions (slots: sidebar/workspace/context, pane kinds, focus/spawn/close/write/resize). Thin and always-correct.
|
||
|
||
This is what makes T-249 (image viewer) and future panels reachable by Claude the moment they register — no skill edit per panel.
|
||
|
||
Refs: D-6 (CLI/event-surface parity). Related: T-249.', NULL, '2026-06-06 07:21:30', '2026-06-06 07:21:30', '2026-06-06 07:21:30', NULL, 'b08b5ff7848bfe37cb507d261ed5fa63', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7JXD3FKW91934D0W0', 'description', NULL, 'Add an image-viewer card to the Claude conversation log, with the CLI verb that drives it (D-6 parity).
|
||
|
||
Scope:
|
||
- Conversation card: render an image inline in the Claude conversation widget — clide-owned rendering (no opinionated package), display-only per D-78 (no inline interactive controls; any controls belong in the interaction zone). Respect theme tokens / ui-design.
|
||
- Plumbing: a `clide` verb to show an image in the log (e.g. `clide image show <path>` or via the pane subsystem), accepting a workspace path; define accepted formats (PNG/JPEG/...), path resolution (workspace-relative), and sizing/scaling behavior.
|
||
- Parity (D-6): the verb is the CLI counterpart of the card; ensure it registers in the dispatcher so it shows up in the discovery verb from T-248.
|
||
|
||
Dependency: pairs with T-248 — without the /clide skill + discovery verb, Claude won''t know this card/verb exists even once shipped. Build the plumbing here; T-248 makes it discoverable.
|
||
|
||
Refs: D-6 (parity), D-78 (interaction zone, display-only conversation widgets). Related: T-248.', NULL, '2026-06-06 07:21:36', '2026-06-06 07:21:36', '2026-06-06 07:21:36', NULL, '63ed4e687ffb9dd923ba3fe0e4a428ec', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'status', 'backlog', 'ready', NULL, '2026-06-06 07:22:48', '2026-06-06 07:22:48', '2026-06-06 07:22:48', NULL, '74e5e7966bc30bb36c2c657c04ba0e6a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7JXD3FKW91934D0W0', 'status', 'backlog', 'ready', NULL, '2026-06-06 07:22:48', '2026-06-06 07:22:48', '2026-06-06 07:22:48', NULL, '9799cee0cac8cb51d8aa0bf66f76351c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F2GC0ABFFVAH34EC', 'description', 'T-237 redesigned the status-bar theme POPOVER (theme_status_item.dart): base themes only, sorted by display name, a ''High contrast'' toggle that applies the -hc sibling, palette icon. The MODAL picker (picker_view.dart, opened by theme.pick / ctrl+k) still lists every theme including the -hc rows in available order. For consistency, apply the same treatment to picker_view: use the theme_families helpers (baseThemes / isHcName / resolveThemeName, already built + unit-tested in lib/builtin/theme_picker/src/theme_families.dart) to show base themes sorted, with a High contrast checkbox at the top. Low priority — the modal works; this is consistency polish.', 'T-237 redesigned the status-bar theme POPOVER (theme_status_item.dart): base themes only, sorted by display name, a ''High contrast'' toggle that applies the -hc sibling, palette icon. The MODAL picker (picker_view.dart, opened by theme.pick / ctrl+k) still lists every theme including the -hc rows in available order. For consistency, apply the same treatment to picker_view: use the theme_families helpers (baseThemes / isHcName / resolveThemeName, already built + unit-tested in lib/builtin/theme_picker/src/theme_families.dart) to show base themes sorted, with a High contrast checkbox at the top. Low priority — the modal works; this is consistency polish.
|
||
|
||
DONE (2026-06-06). Re-scoped per user: instead of just applying the -hc-checkbox+sort polish to the theme modal, the modal was PROMOTED to a general Settings modal (picker_view.dart -> settings_view.dart, ThemePickerView -> SettingsView). ctrl+k / theme.pick now opens Settings, whose only section today is Appearance: base themes (sorted) + a High contrast toggle, reusing the theme_families helpers (shared with the status-bar popover, T-237). Command id kept as theme.pick (welcome link + tests reference it); retitled ''Settings...''. Extensible: more sections later (or split to a builtin/settings extension when it grows). Tests in test/builtin/theme_picker/widget_test.dart (base-only list, hc toggle applies sibling). NOTE: status-bar popover (T-237) was already done — that is what the user saw ''done in the ui''.', NULL, '2026-06-06 07:24:00', '2026-06-06 07:24:00', '2026-06-06 07:24:00', NULL, '0bb9be512fd59f90b2d893f4241cb096', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F2GC0ABFFVAH34EC', 'status', 'in_progress', 'done', NULL, '2026-06-06 07:24:00', '2026-06-06 07:24:00', '2026-06-06 07:24:00', NULL, '95c12acd291bb53b28c0669a04c894f0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BVE5EYM92CWGSXA4', 'description', NULL, 'Symptom: the permission-mode badge in the Claude status line does nothing visible when clicked, and Ctrl/Cmd+M (cycle mode, T-226) likewise appears to do nothing. Either the mode isn''t actually changing, or — more likely — it changes in the Claude process but the status-bar tracker is never updated.
|
||
|
||
Likely root cause (confirmed by reading the code): the cycle is fire-and-forget with no status feedback.
|
||
- Composer Ctrl/Cmd+M -> widget.onCycleMode -> _ClaudePaneState._cycleMode (lib/builtin/claude/src/claude_pane.dart:302) -> s.setPermissionMode(nextSafePermissionMode(...)).
|
||
- The badge click path (_ModeBadge.onCycle) routes to the same _cycleMode, so both symptoms share one cause.
|
||
- StreamJsonSession.setPermissionMode (lib/builtin/claude/src/stream_json_session.dart:671) only writes a `set_permission_mode` control_request to the process. It does NOT optimistically update `_status` nor emit on `_statusCtl`.
|
||
- `_status.permissionMode` is only ever set from a `system/init` event via `_statusFromEvent` (stream_json_session.dart:567-571). A set_permission_mode control_request returns a control_response, which is not folded into status — so `_status` never re-emits, the badge label (claude_pane.dart:105-122, driven by `_status` over statusStream) is stale, and the UI looks dead.
|
||
|
||
So the status bar / badge value is not updated on change — matching the reported hypothesis.
|
||
|
||
Fix direction:
|
||
- Optimistically merge the new mode into `_status` and emit on `_statusCtl` immediately after writing the control_request (mirror how other state transitions surface), and/or handle the `control_response` for set_permission_mode and reconcile from it.
|
||
- Separately verify the control_request is actually honored by the Claude process (the ''they don''t work'' branch) — if the response reports failure, surface it rather than silently assuming success.
|
||
|
||
Acceptance:
|
||
- Clicking the badge and pressing Ctrl/Cmd+M both visibly cycle default -> accept-edits -> plan -> default in the status line.
|
||
- The displayed mode reflects the session''s actual mode (reconciled from the response / next init), not just an optimistic guess.
|
||
- Regression test at the session level: setPermissionMode emits an updated SessionStatus on statusStream.
|
||
|
||
Refs: T-226 (interactive mode badge + Ctrl/Cmd+M), T-181 (bypassPermissions behind confirmed path — keep excluded from the safe cycle), D-78 (interaction-zone / display-only conventions).', NULL, '2026-06-06 07:31:06', '2026-06-06 07:31:06', '2026-06-06 07:31:06', NULL, '5f293364d4b8cc2ea694e594007b1bc5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BVE5EYM92CWGSXA4', 'status', 'backlog', 'ready', NULL, '2026-06-06 07:32:57', '2026-06-06 07:32:57', '2026-06-06 07:32:57', NULL, '9622e6e1c057e278f1f355fddd7c22d9', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', '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.
|
||
|
||
**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).', '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).
|
||
|
||
DONE (2026-06-06). Keybinding + nav were already wired (T-117 binds ctrl/meta+shift+p -> PaletteOpenIntent; _RootShell handles it -> palette.open(); T-100 added arrow/Enter/Esc nav + selected-index). Remaining acceptance implemented now: (1) FUZZY match — PaletteController.filtered() uses a shared subsequence matcher (lib/kernel/src/fuzzy.dart, extracted from quick_open so both share one source of truth), ranked best-score-first; (2) RECENCY — invoked commands float to the top on empty filter and break fuzzy-score ties (in-session MRU). DEFERRED: ''pinned'' commands + cross-session recency persistence need a pin affordance + settings storage — filed as a follow-up. Tests: test/kernel/src/commands/palette_test.dart + test/kernel/src/fuzzy_test.dart.', NULL, '2026-06-06 07:40:43', '2026-06-06 07:40:43', '2026-06-06 07:40:43', NULL, '3169ee6fdb1c4c2c0243b6b5708166aa', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', 'status', 'ready', 'done', NULL, '2026-06-06 07:40:43', '2026-06-06 07:40:43', '2026-06-06 07:40:43', NULL, 'ad3e8ce905d36af8e853d9097f3d1609', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BVE5EYM92CWGSXA4', 'status', 'ready', 'done', NULL, '2026-06-06 07:49:08', '2026-06-06 07:49:08', '2026-06-06 07:49:08', NULL, '2f7a63272c39b62e02adb8355ac73d36', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'status', 'ready', 'done', NULL, '2026-06-06 07:57:16', '2026-06-06 07:57:16', '2026-06-06 07:57:16', NULL, '2929c0012d12a978d20507b6289d4347', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XMQ44AVCAARQZX14', 'status', 'backlog', 'ready', NULL, '2026-06-06 08:06:55', '2026-06-06 08:06:55', '2026-06-06 08:06:55', NULL, '2154d682e253c8d82a5baee32ccf8c20', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QYYKHPQ03H3QD61W', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 08:12:45', '2026-06-06 08:12:45', '2026-06-06 08:12:45', NULL, '043bc847ebe807e4a470d21b2daaaa7e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XMQ44AVCAARQZX14', 'status', 'ready', 'in_progress', NULL, '2026-06-06 08:12:45', '2026-06-06 08:12:45', '2026-06-06 08:12:45', NULL, '783dc9628035b4a7b15b1d7518447bf6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'description', 'Create a `/clide` skill (none exists today — only legacy templates under legacy/clide/templates/skills/) that advertises the clide CLI surface to Claude so new affordances become discoverable.
|
||
|
||
Problem: my knowledge of the clide surface comes from a hand-curated session blurb. There is no runtime discovery — `clide help` is a stub, `clide <subsystem>` with no verb just prints `usage:`, and the dispatcher''s registered command table (e.g. pane_commands.dart) is never exposed. D-6 guarantees a verb EXISTS for every UI action, but parity != discoverability: a correctly-registered verb is still unreachable if nothing tells me it''s there.
|
||
|
||
Scope:
|
||
- Self-description first (prereq for a non-rotting skill): add a discovery verb that reflects the live dispatcher registry — e.g. `clide capabilities` (machine-readable JSON: subsystems -> verbs -> arg schema) and/or flesh out `clide help` to enumerate subsystems/verbs. Sourced from the registry so it never drifts.
|
||
- `/clide` SKILL.md: trigger description always visible to Claude; body points at the discovery verb rather than hard-coding a verb list, plus conventions (slots: sidebar/workspace/context, pane kinds, focus/spawn/close/write/resize). Thin and always-correct.
|
||
|
||
This is what makes T-249 (image viewer) and future panels reachable by Claude the moment they register — no skill edit per panel.
|
||
|
||
Refs: D-6 (CLI/event-surface parity). Related: T-249.', 'Create a `/clide` skill (none exists today — only legacy templates under legacy/clide/templates/skills/) that advertises the clide CLI surface to Claude so new affordances become discoverable.
|
||
|
||
Problem: my knowledge of the clide surface comes from a hand-curated session blurb. There is no runtime discovery — `clide help` is a stub, `clide <subsystem>` with no verb just prints `usage:`, and the dispatcher''s registered command table (e.g. pane_commands.dart) is never exposed. D-6 guarantees a verb EXISTS for every UI action, but parity != discoverability: a correctly-registered verb is still unreachable if nothing tells me it''s there.
|
||
|
||
Scope:
|
||
- Self-description first (prereq for a non-rotting skill): add a discovery verb that reflects the live dispatcher registry — e.g. `clide capabilities` (machine-readable JSON: subsystems -> verbs -> arg schema) and/or flesh out `clide help` to enumerate subsystems/verbs. Sourced from the registry so it never drifts.
|
||
- `/clide` SKILL.md: trigger description always visible to Claude; body points at the discovery verb rather than hard-coding a verb list, plus conventions (slots: sidebar/workspace/context, pane kinds, focus/spawn/close/write/resize). Thin and always-correct.
|
||
|
||
This is what makes T-249 (image viewer) and future panels reachable by Claude the moment they register — no skill edit per panel.
|
||
|
||
Refs: D-6 (CLI/event-surface parity). Related: T-249.
|
||
|
||
RE-SCOPE (2026-06-06, from T-208 completeness review): partly overtaken by reality — do NOT build from scratch.
|
||
- The `clide capabilities` verb already EXISTS in code: registered in lib/src/daemon/dispatcher.dart:12 (built-in) and as an umbrella command in lib/src/cli/argv_to_request.dart:28. Its handler (_capabilities, dispatcher.dart:86) enumerates the live registry with arg schemas — so the non-rotting design is already implemented server-side.
|
||
- A `/clide` skill is ALSO already present this session (other ongoing work), and it instructs agents to "Start with `clide capabilities` to enumerate the live tool surface."
|
||
- BUT: against the live instance, `clide capabilities` returns a usage stub at exit 0 (`usage: clide capabilities <verb> [args...]`) instead of the command table — i.e. the discoverability entry point the skill advertises is currently DEAD. Likely a stale running build or a C-client umbrella mismatch (native/clide-cli/clide.c forwards argv but the live server treats `capabilities` as a subsystem needing a verb).
|
||
|
||
New scope for this ticket:
|
||
1. Verify/fix the live `clide capabilities` path end-to-end (rebuild/restart vs real wiring gap in the C client / server umbrella handling). Acceptance: `clide capabilities` returns the JSON command table against a running instance.
|
||
2. Reconcile with the already-shipped `/clide` skill rather than authoring anew — confirm it points at the working verb and covers the conventions.
|
||
Net: this is now a verify-and-reconcile task, not a build-from-scratch one. Related: T-249, T-208.', NULL, '2026-06-06 08:12:53', '2026-06-06 08:12:53', '2026-06-06 08:12:53', NULL, 'a8e6b6c8c4b66c575bf0fa125406dbab', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'status', 'done', 'review', NULL, '2026-06-06 08:14:01', '2026-06-06 08:14:01', '2026-06-06 08:14:01', NULL, 'f7a8ffee6531ed6f6646855335cb1631', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7JXD3FKW91934D0W0', 'status', 'ready', 'done', NULL, '2026-06-06 08:21:40', '2026-06-06 08:21:40', '2026-06-06 08:21:40', NULL, '2731b543315820500f0456dec9144c0d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'status', 'review', 'done', NULL, '2026-06-06 08:31:25', '2026-06-06 08:31:25', '2026-06-06 08:31:25', NULL, 'd44a8900af4cb745252d71d996e80db4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NEXC0P46NJHACNM0', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 08:33:42', '2026-06-06 08:33:42', '2026-06-06 08:33:42', NULL, '1ba6fe86835b5c6a53c17e111d70fab6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NEXC0P46NJHACNM0', 'status', 'in_progress', 'done', NULL, '2026-06-06 08:50:12', '2026-06-06 08:50:12', '2026-06-06 08:50:12', NULL, '932999018f2d5f258a734fb521c9ea3f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NEXC0P46NJHACNM0', 'status', 'done', 'in_progress', NULL, '2026-06-06 09:04:03', '2026-06-06 09:04:03', '2026-06-06 09:04:03', NULL, 'f3c5361ca5ea98f01575790cb9eefbeb', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NEXC0P46NJHACNM0', 'status', 'in_progress', 'done', NULL, '2026-06-06 09:20:02', '2026-06-06 09:20:02', '2026-06-06 09:20:02', NULL, '1c96df771c2b4ee65728028de006085f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'description', NULL, 'The Claude conversation panel detects URLs and colorizes them, but they are not interactive. Make detected links clickable (a plain click, or control/cmd-click) so they are handed off to the OS URL opener.
|
||
|
||
## Behaviour
|
||
- Click (or ctrl/cmd-click) on a colorized link opens it via the OS default handler.
|
||
- Hover affordance (cursor change / underline) so it reads as clickable.
|
||
- Keep the existing colorization.
|
||
|
||
## Notes
|
||
- Honour user/Claude parity (D-6) where relevant.
|
||
- Use the platform URL launcher; avoid pulling in an opinionated package if a thin native/url_launcher shim already exists in the tree.
|
||
- Guard against non-http schemes / malformed URLs.', NULL, '2026-06-06 09:33:01', '2026-06-06 09:33:01', '2026-06-06 09:33:01', NULL, 'e9ae63ee723e46643f5b2c0456e12900', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'description', NULL, 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.', NULL, '2026-06-06 09:33:04', '2026-06-06 09:33:04', '2026-06-06 09:33:04', NULL, 'ce15a7fb32f8d4cd997d179bb7d58319', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78Q3VYWSE830CTVB4', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 09:43:49', '2026-06-06 09:43:49', '2026-06-06 09:43:49', NULL, '2453ca10f223362581a2205ea46ed5d0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'description', NULL, '## Problem
|
||
|
||
While a Claude turn is in flight, the composer swaps in a static, muted
|
||
`running…` label next to the Stop button (`lib/builtin/claude/src/claude_composer.dart:486`,
|
||
shown when `widget.busy && widget.onInterrupt != null`). It''s gray, motionless,
|
||
and a little lifeless — it gives no sense that anything is actually happening.
|
||
|
||
## Goal
|
||
|
||
Make the in-flight indicator feel alive:
|
||
|
||
1. **Animation.** Add subtle motion — e.g. an animated ellipsis (`running` → `running.`
|
||
→ `running..` → `running...`), a shimmer/pulse on the text, or a small spinner glyph.
|
||
Custom-painted / token-driven per the "own the rendering stack" guardrail; no
|
||
opinionated animation packages.
|
||
2. **Rotating status verbs.** Cycle through playful gerunds the way the Claude Code CLI
|
||
does (e.g. "Clauding…", "Flibbertigibbeting…", "Pondering…", "Conjuring…"). Pick a
|
||
word from a curated list and rotate it every few seconds while the turn runs.
|
||
|
||
## Open question — can we reuse the CLI''s words?
|
||
|
||
The Claude Code CLI ships its own list of these status verbs. Before hand-rolling our
|
||
own, check whether that list is something we''re allowed to reuse / surface (licensing,
|
||
where it lives, whether stream-json exposes the current one). If we can''t pull the CLI''s
|
||
list, ship our own curated, on-brand list instead. Document the decision.
|
||
|
||
## Notes / constraints
|
||
|
||
- Respect reduced-motion / accessibility settings — animation must be disable-able and
|
||
the a11y semantics should still read sensibly (the Stop button already carries the
|
||
interrupt hint).
|
||
- Use theme tokens for color; keep it muted/tasteful, not distracting.
|
||
- Touch point today is the `widget.busy` branch in `claude_composer.dart`; consider
|
||
whether the rotating-word state belongs there or in the session/orchestrator layer.
|
||
- Add a widget/golden test for the animated states (bounded pumps — no real timers).', NULL, '2026-06-06 09:57:12', '2026-06-06 09:57:12', '2026-06-06 09:57:12', NULL, 'ba353c0ca89d203805c4b72dfe2ba359', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78Q3VYWSE830CTVB4', 'status', 'in_progress', 'done', NULL, '2026-06-06 10:02:03', '2026-06-06 10:02:03', '2026-06-06 10:02:03', NULL, 'fda523ee07c6aec223b8b9f3f4a5048a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QYYKHPQ03H3QD61W', 'status', 'in_progress', 'done', NULL, '2026-06-06 10:02:06', '2026-06-06 10:02:06', '2026-06-06 10:02:06', NULL, 'b53bbacef5897da88d1327c0c381d3fa', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72ACQSDBJTKRBC58W', 'description', 'Return all events since a cursor plus a new cursor, alongside the existing stream (T-129). Acceptance: `clide events --since <c>` returns events + next-cursor with exit 0; repeated calls neither drop nor duplicate. Coordinate the cursor/persistence design with open questions Q-2 (event back-pressure) and Q-3 (event persistence + audit) — this dovetails with both.', 'Return all events since a cursor plus a new cursor, alongside the existing stream (T-129). Acceptance: `clide events --since <c>` returns events + next-cursor with exit 0; repeated calls neither drop nor duplicate. Coordinate the cursor/persistence design with open questions Q-2 (event back-pressure) and Q-3 (event persistence + audit) — this dovetails with both.
|
||
|
||
Refinement (2026-06-06): blockers resolved. Q-2 + Q-3 closed by D-85 (event bus delivery). Design is now fixed: serve --since from a bounded in-memory ring keyed by a monotonic cursor; return events-after-cursor + next-cursor, and a gap marker (per-subscriber dropped-count) when the requested cursor has aged out so callers detect loss rather than silently miss events. Back-pressure is drop-oldest (producer never blocks, subscribers never killed). No on-disk persistence in v1. Ready to implement.', NULL, '2026-06-06 10:24:57', '2026-06-06 10:24:57', '2026-06-06 10:24:57', NULL, '7df5c81270c33469ed7eafc5d49f496a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5DYC7CRP8TMHADW8W', 'description', 'Gap 6 from self-analysis.md. The MCP SSE server (T-130, D-68) is live but mcp__ide__* tools are not exposed to an external agent out of the box, and getDiagnostics/executeCode were noted as stubs. The CLI path is the priority (Epics A-C); MCP follows. Decide + wire the minimal reachable MCP surface, or explicitly defer with a note. Relates to open question Q-32 (minimum MCP tool surface).', 'Gap 6 from self-analysis.md. The MCP SSE server (T-130, D-68) is live but mcp__ide__* tools are not exposed to an external agent out of the box, and getDiagnostics/executeCode were noted as stubs. The CLI path is the priority (Epics A-C); MCP follows. Decide + wire the minimal reachable MCP surface, or explicitly defer with a note. Relates to open question Q-32 (minimum MCP tool surface).
|
||
|
||
Refinement (2026-06-06): tool-surface question resolved. Q-32 closed by D-86 — expose the full mcp__clide__* namespace, but GENERATE tools/list from the co-registered command registry (D-74) that already feeds the CLI + palette, so there is no hand-maintained second surface; add a per-command MCP opt-out for poor-fit verbs (long-lived streams, UI-side-effecting). Transport stays SSE-only per D-73 (Q-33 re-confirmed, not reopened). Remaining stubs to make real: getDiagnostics + executeCode. Scope is now: registry->MCP tool-definition adapter (arg-schema -> JSON-Schema), served over the existing SSE transport.', NULL, '2026-06-06 10:25:02', '2026-06-06 10:25:02', '2026-06-06 10:25:02', NULL, 'cd82bcfcee3505798a084a326fc2cd3c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4K2AAPK52Y8BEJ3YR', 'description', NULL, '**Symptom:** In the editor with the Vim preset active, pressing `Esc` to leave insert/visual mode and return to normal mode closes the current file/pane instead of switching modes.
|
||
|
||
**Repro:**
|
||
1. Select the Vim keybinding preset (T-65).
|
||
2. Open a file in the editor pane; enter insert mode (`i`).
|
||
3. Press `Esc`.
|
||
|
||
**Expected:** Editor returns to Vim normal mode (`-- NORMAL --`), file/pane stays open. This is T-207''s contract: `Esc -> normal from any mode`.
|
||
|
||
**Actual:** The file/pane closes.
|
||
|
||
**Root-cause hypothesis:** `assets/keymaps/default.yaml:26-27` binds `escape -> intent: dismiss` with NO `when:` clause, so it matches globally. The Vim mode-reset binding (`assets/keymaps/vim.yaml:69-71`, `escape -> command:vim.mode.normal`, gated `when: vim.insert || vim.visual`) is being out-resolved or shadowed by the unconditional `dismiss`, which falls through to closing the active pane/file when no overlay is open. Either the Vim preset still inherits an unconditional dismiss-on-escape, or the when-clause resolver lets the unguarded binding win over the more-specific scoped one for the same chord.
|
||
|
||
**Likely fix direction:**
|
||
- Ensure the keymap resolver prefers the more-specific `when`-scoped binding over an unconditional one for the same chord, and/or gate the global `dismiss`-on-escape so it does not fire when `editor.focused && (vim.insert || vim.visual)`.
|
||
- Sanity-check that `dismiss` should close a pane/file at all here — closing the active file on a bare Esc is surprising even outside Vim.
|
||
|
||
**Related:** epic T-65 (Vim preset), T-207 (Vim mode service owns Esc -> normal), T-205 / T-117 (key-sequence resolution + when-clauses).
|
||
**Files:** `assets/keymaps/default.yaml`, `assets/keymaps/vim.yaml`, `lib/builtin/editor/src/editor_view.dart`, `lib/kernel/src/keymap/`.', NULL, '2026-06-06 14:01:03', '2026-06-06 14:01:03', '2026-06-06 14:01:03', NULL, '01f136ca149c821a56a37d36821fd959', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4K2AAPK52Y8BEJ3YR', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 18:44:58', '2026-06-06 18:44:58', '2026-06-06 18:44:58', NULL, 'd1ce20399e57562703a5f061d9a8e012', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4K2AAPK52Y8BEJ3YR', 'status', 'in_progress', 'done', NULL, '2026-06-06 18:58:25', '2026-06-06 18:58:25', '2026-06-06 18:58:25', NULL, '968e8833c027634439b39dc6c6f57ff1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5DYC7CRP8TMHADW8W', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 19:06:30', '2026-06-06 19:06:30', '2026-06-06 19:06:30', NULL, '6fd4f5a99810ca96fa7db1de1c1511ed', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72ACQSDBJTKRBC58W', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 19:06:30', '2026-06-06 19:06:30', '2026-06-06 19:06:30', NULL, '873de3e88de3b6d9bd74c96753af3568', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', 'status', 'ready', 'done', NULL, '2026-06-06 19:06:30', '2026-06-06 19:06:30', '2026-06-06 19:06:30', NULL, 'e3c10829d6afbf5d2682ef9e0df257a8', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72ACQSDBJTKRBC58W', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:24:44', '2026-06-06 19:24:44', '2026-06-06 19:24:44', NULL, '3138697fe1501716a729fd70ce390099', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM620F5ENRYNT9S0JDW', 'status', 'backlog', 'done', NULL, '2026-06-06 19:24:44', '2026-06-06 19:24:44', '2026-06-06 19:24:44', NULL, '5abe1c20278eb488c1e743769be37542', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5DYC7CRP8TMHADW8W', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:37:47', '2026-06-06 19:37:47', '2026-06-06 19:37:47', NULL, '1e98081ef3b9dca916bf47fcef83aa9e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6S7KHYPE9W7S1JHZ8', 'status', 'backlog', 'in_progress', NULL, '2026-06-06 19:45:03', '2026-06-06 19:45:03', '2026-06-06 19:45:03', NULL, '2aa119d66370d885457a7687ade45723', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6S7KHYPE9W7S1JHZ8', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:53:07', '2026-06-06 19:53:07', '2026-06-06 19:53:07', NULL, '0f6ef4536136a8b715e1a0f06e999ae0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XMQ44AVCAARQZX14', 'status', 'in_progress', 'done', NULL, '2026-06-06 19:53:07', '2026-06-06 19:53:07', '2026-06-06 19:53:07', NULL, 'c8767f309187c2af7dcbc185ff808356', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GCVGCH9RMGYHENCC', 'status', 'backlog', 'ready', NULL, '2026-06-06 20:00:45', '2026-06-06 20:00:45', '2026-06-06 20:00:45', NULL, 'b0c8f6f0883b0737db525a3d08010afd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GCVGCH9RMGYHENCC', 'description', 'Bottom panel or context tab showing daemon logs, build output, extension logs, and pql sync output. Filterable by source. Auto-scrolls to latest. Useful for debugging extension and IPC issues.', 'Bottom panel or context tab showing daemon logs, build output, extension logs, and pql sync output. Filterable by source. Auto-scrolls to latest. Useful for debugging extension and IPC issues.
|
||
|
||
UX design (2026-06-06, D-87): the panel is a bottom OUTPUT DOCK, read-only, two tabs — Output (the Logger stream, filter by source/level/text, auto-scroll) + Problems (moved out of the sidebar; no duplication). Toggled by a single status-bar widget that REPLACES the app-status indicator (merged health+log: green check when clean, warn/error counts when not, chevron for open state) — opens with click or Cmd/Ctrl+J. Needs a bounded in-memory ring sink on the Logger (no history today). Layout amends D-47 (dock pushes Claude up, capped so Claude stays >=50%). Terminal is NOT in the dock — kept first-class in the editor pane, tracked by T-258. Resolves Q-28. Wireframe: docs/design/wireframes/output-dock/.', NULL, '2026-06-06 20:56:04', '2026-06-06 20:56:04', '2026-06-06 20:56:04', NULL, '8e9ea74ba5a360b0211ae1b3898d60df', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'status', 'backlog', 'review', NULL, '2026-06-06 20:56:20', '2026-06-06 20:56:20', '2026-06-06 20:56:20', NULL, 'c4bb10f75b8ac6ab0bb472fe4e5bff74', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QW8VH454J8V3E174', 'status', 'backlog', 'ready', NULL, '2026-06-06 21:00:34', '2026-06-06 21:00:34', '2026-06-06 21:00:34', NULL, '6f96d3cc06ca1d904ae02910499d1e36', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GCVGCH9RMGYHENCC', 'status', 'ready', 'in_progress', NULL, '2026-06-06 21:07:50', '2026-06-06 21:07:50', '2026-06-06 21:07:50', NULL, '15e3a73b9493513a9e2e3eadb1ec414e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'description', 'Parse file references from Claude output. Swap viewer content when right panel is open. Badge on spine when collapsed. Live-sync viewer when editing .md.', 'When an editor is open in edit mode on a renderable doc (.md), the right-hand context panel auto-opens a read-only view that mirrors the buffer and updates live as the user types (D-50 behavior 4). If the editor is on a non-renderable file, no auto-viewer (D-50 behavior 5, already holds).
|
||
|
||
Wiring sketch: EditorRegistry already emits editor.opened / editor.active-changed / editor.edited (lib/src/editor/registry.dart:85). The markdown reader (lib/builtin/markdown/src/markdown_viewer.dart) currently pulls content once via files.read with no subscription. The live-sync work is: (1) on editor.opened for a renderable file, auto-activate the markdown reader in the context panel; (2) subscribe the reader to editor.edited and re-render from the in-memory buffer rather than re-reading disk; (3) read-only — no edit affordances in the mirror.
|
||
|
||
HISTORY: T-36 originally bundled four D-50 clauses (parse Claude''s output for file references, swap the open viewer, badge the spine when collapsed, live-sync). The give-clide-hands push (T-208) superseded the first three: instead of clide scraping the terminal for references, the agent explicitly drives the reader via `clide ui open markdown <path>` (T-231) and ui.open -> diff (T-233). The spine badge was dropped (the open is now an intentional agent act, not a passive notification). Re-scoped 2026-06-06 to the one UI-owned piece that give-clide-hands did not deliver: the live-sync read-mirror. Re-homed from T-7 (Tier 5 canvas/graph, a mis-parent) to T-259 (interaction model). See the D-50 amendment.', NULL, '2026-06-06 21:14:16', '2026-06-06 21:14:16', '2026-06-06 21:14:16', NULL, '94a5b6a263c0d1ba8922a753db4434c5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'title', 'Context auto-behavior: right panel reacts to Claude references', 'Live-sync read-mirror: context panel tracks the open editor buffer', NULL, '2026-06-06 21:14:16', '2026-06-06 21:14:16', '2026-06-06 21:14:16', NULL, 'ed09e0656722f18cfa69461f3ed2af9d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'parent_id', 'T-7', 'T-259', NULL, '2026-06-06 21:14:19', '2026-06-06 21:14:19', '2026-06-06 21:14:19', NULL, '828d07396d886d58d6cf09902d9b6e9a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'status', 'review', 'backlog', NULL, '2026-06-06 21:14:22', '2026-06-06 21:14:22', '2026-06-06 21:14:22', NULL, '38410b8cab1366094cac896ff6ff1257', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'status', 'backlog', 'ready', NULL, '2026-06-06 21:43:40', '2026-06-06 21:43:40', '2026-06-06 21:43:40', NULL, '51798182bc568f87d420859e4389a4f0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GCVGCH9RMGYHENCC', 'status', 'in_progress', 'done', NULL, '2026-06-07 08:03:17', '2026-06-07 08:03:17', '2026-06-07 08:03:17', NULL, '6b1cfe3dd2263ea586eee797748dfc0a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QW8VH454J8V3E174', 'status', 'ready', 'in_progress', NULL, '2026-06-07 08:07:42', '2026-06-07 08:07:42', '2026-06-07 08:07:42', NULL, '54492c75e5f9415a70de079767bc00fe', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QW8VH454J8V3E174', 'status', 'in_progress', 'done', NULL, '2026-06-07 08:32:36', '2026-06-07 08:32:36', '2026-06-07 08:32:36', NULL, '96c3f6af3d4274629ec0d2b1809b3cb8', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71P14PAF7K4QE61G0', 'description', 'Today a Claude tool call renders as TWO stacked cards in the conversation log: the tool-use card (e.g. "Write <path>" with a collapsible call body) and a separate success/result card ("Write · result / File created successfully..."). Collapse the successful pair into ONE card; failure keeps the current two-card interaction.
|
||
|
||
Current code: lib/builtin/claude/src/conversation_view.dart — _toolUse (~L294) renders AssistantToolUse, _toolResult (~L329) renders ToolResultMessage; the two ConversationItems are linked by toolUseId (ToolResultMessage.isError marks failure). ConversationCard header is built in lib/builtin/claude/src/conversation_card.dart _header (~L200). Builds on T-168 (per-tool body rendering).
|
||
|
||
Desired behavior (success):
|
||
- One card per successful tool call — the tool-use card. The standalone success result card is suppressed (folded in), not rendered separately.
|
||
- A success-green check mark sits at the RIGHT END of the card header row (reuse tokens.statusSuccess + a check glyph: PhosphorIcons.check or CheckIcon). No new theme tokens.
|
||
- Collapsed by default: header shows the existing summary + the check. Expanding the card reveals the CALL segment (the input — path / diff / command, as today) and, BELOW it, the RESULT segment (the swallowed output). This applies to ALL tools, not just Write/Edit: for Bash/Read/Grep/LS the real output relocates into the expandable body so nothing is lost; for Write/Edit the trivial confirmation folds in the same way.
|
||
- Render the result segment as an inline ClideCodeBlock (lib/widgets/src/clide_code_block.dart) WITH editor syntax colorization. ClideCodeBlock already colorizes via TreeSitterService when handed a language — so the work is inferring/passing the right language per tool: Read → from the file extension/path (language_map.grammarForPath), Bash → bash, Grep/LS/others → text fallback. Where a path is available, prefer it so highlighting matches the editor.
|
||
|
||
Desired behavior (failure) — UNCHANGED:
|
||
- Leave the current interaction as-is: the tool-use card plus the prominent red "· error" result card (borderColor statusError, expanded-by-default). Optionally show a red status mark on the tool-use header for symmetry, but do NOT fold the error into one card.
|
||
|
||
What to build:
|
||
1. ConversationCard: a trailing header status slot (e.g. status: success|error|none) rendered in _header between the Spacer/summary and the hover action buttons.
|
||
2. ConversationCard: support a second body segment so an expanded card can show CALL then RESULT with a clear visual separator / sub-label (e.g. a divider or a muted "result" label) so the user can tell the call from its output.
|
||
3. conversation_view: a result-by-toolUseId lookup (reverse of the existing toolUseById) so a tool-use card knows its outcome; on a successful pair, stamp the check + fold the result body and SUPPRESS the standalone success ToolResultMessage from the rendered list. Errors render both cards as today.
|
||
4. Per-tool result language inference for the folded code block.
|
||
|
||
Edge cases to handle:
|
||
- In-flight tool-use with no result yet: render the call card as today (no check, no folded result); status/result appear once the result arrives.
|
||
- Orphan result with no paired tool-use: keep rendering it standalone.
|
||
- Permission-resolved tool-use cards (conversation_view.dart ~L297, already green/red bordered + collapsed): reconcile so the merged-card + check treatment is consistent and not duplicated with the existing border-outcome styling.
|
||
- Result items may not be strictly adjacent to their tool-use in the list; suppression must be keyed by toolUseId, not list position.
|
||
|
||
Tests: unit/widget coverage for the merged success card (collapsed shows check; expanded shows call + colorized result), language inference per tool, the unchanged error path, and the in-flight/orphan cases. Add a golden for the merged success card.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets). Builds on T-168 (per-tool tool-use/result body rendering).', 'Today a Claude tool call renders as TWO stacked cards in the conversation log: the tool-use card (e.g. "Write <path>" with a collapsible call body) and a separate success/result card ("Write · result / File created successfully..."). Collapse the successful pair into ONE card; failure keeps the current two-card interaction.
|
||
|
||
Current code: lib/builtin/claude/src/conversation_view.dart — _toolUse (~L294) renders AssistantToolUse, _toolResult (~L329) renders ToolResultMessage; the two ConversationItems are linked by toolUseId (ToolResultMessage.isError marks failure). ConversationCard header is built in lib/builtin/claude/src/conversation_card.dart _header (~L200). Builds on T-168 (per-tool body rendering).
|
||
|
||
Desired behavior (success):
|
||
- One card per successful tool call — the tool-use card. The standalone success result card is suppressed (folded in), not rendered separately.
|
||
- A success-green check mark sits at the RIGHT END of the card header row (reuse tokens.statusSuccess + a check glyph: PhosphorIcons.check or CheckIcon). No new theme tokens.
|
||
- Collapsed by default: header shows the existing summary + the check. Expanding the card reveals the CALL segment (the input — path / diff / command, as today) and, BELOW it, the RESULT segment (the swallowed output). This applies to ALL tools, not just Write/Edit: for Bash/Read/Grep/LS the real output relocates into the expandable body so nothing is lost; for Write/Edit the trivial confirmation folds in the same way.
|
||
- Render the result segment as an inline ClideCodeBlock (lib/widgets/src/clide_code_block.dart) WITH editor syntax colorization. ClideCodeBlock already colorizes via TreeSitterService when handed a language — so the work is inferring/passing the right language per tool: Read → from the file extension/path (language_map.grammarForPath), Bash → bash, Grep/LS/others → text fallback. Where a path is available, prefer it so highlighting matches the editor.
|
||
|
||
Desired behavior (failure) — UNCHANGED:
|
||
- Leave the current interaction as-is: the tool-use card plus the prominent red "· error" result card (borderColor statusError, expanded-by-default). Optionally show a red status mark on the tool-use header for symmetry, but do NOT fold the error into one card.
|
||
|
||
What to build:
|
||
1. ConversationCard: a trailing header status slot (e.g. status: success|error|none) rendered in _header between the Spacer/summary and the hover action buttons.
|
||
2. ConversationCard: support a second body segment so an expanded card can show CALL then RESULT with a clear visual separator / sub-label (e.g. a divider or a muted "result" label) so the user can tell the call from its output.
|
||
3. conversation_view: a result-by-toolUseId lookup (reverse of the existing toolUseById) so a tool-use card knows its outcome; on a successful pair, stamp the check + fold the result body and SUPPRESS the standalone success ToolResultMessage from the rendered list. Errors render both cards as today.
|
||
4. Per-tool result language inference for the folded code block.
|
||
|
||
Edge cases to handle:
|
||
- In-flight tool-use with no result yet: render the call card as today (no check, no folded result); status/result appear once the result arrives.
|
||
- Orphan result with no paired tool-use: keep rendering it standalone.
|
||
- Permission-resolved tool-use cards (conversation_view.dart ~L297, already green/red bordered + collapsed): reconcile so the merged-card + check treatment is consistent and not duplicated with the existing border-outcome styling.
|
||
- Result items may not be strictly adjacent to their tool-use in the list; suppression must be keyed by toolUseId, not list position.
|
||
|
||
Tests: unit/widget coverage for the merged success card (collapsed shows check; expanded shows call + colorized result), language inference per tool, the unchanged error path, and the in-flight/orphan cases. Add a golden for the merged success card.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets). Builds on T-168 (per-tool tool-use/result body rendering).
|
||
|
||
IMPLEMENTATION NOTES (from streamlining analysis):
|
||
|
||
C — Error-card symmetry decision: this ticket merges only SUCCESS into the tool card; the error path intentionally stays a separate prominent red "· error" card (conversation_view.dart ~L337) for failure visibility. Decide explicitly: keep that asymmetry (success folds, failure stays two-card) OR also stamp a red status mark on the tool-use header for visual symmetry while still keeping the separate red card. Default leaning: keep the separate red card; optionally add the red header mark.
|
||
|
||
D — Activity-cluster coupling: activity_cluster.groupConversation currently folds a tool CALL and its RESULT as TWO separate foldable items into a cluster (activity_cluster.dart ~L100-109, classifying each by toolUseId). Once this ticket makes call+result a single self-contained card, the grouping pass must treat the tool call as ONE unit (its result is part of the card, no longer a separate foldable item) or the result will double-render (once folded into the card, once as a cluster item). Update _isFoldable / the pairing logic accordingly and add a test that a merged tool card is not double-counted.', NULL, '2026-06-07 08:40:45', '2026-06-07 08:40:45', '2026-06-07 08:40:45', NULL, 'ce5df7afb04b516724c06e891f9ca0a4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XXSERDHKR0EEH3N4', 'description', 'When Claude launches a sub-agent (the Agent/Task tool), the sub-agent conversation runs as a SIDECHAIN. The prompt Claude wrote for that sub-agent comes through the transcript as a UserMessage with isSidechain=true but injected=false. The conversation view does not look at isSidechain, so it renders that prompt with the blue "you" label exactly like real user input — FALSELY implying the user sent it. It sits as a standalone block below the Agent tool-use card. This is misleading: the user did not write that prompt.
|
||
|
||
Confirmed facts from the code: every transcript envelope carries BOTH isSidechain AND parentUuid (transcript_reader.dart ~L547 parses isSidechain but never USES it; parentUuid is dropped entirely). The Agent/Task tool-use is rendered by _toolUse in lib/builtin/claude/src/conversation_view.dart (only AskUserQuestion is special-cased today). The "you" vs "context" label is chosen in conversation_view.dart (~L200): UserMessage with injected=true → "context" (muted); else → "you" (blue, tokens.globalFocus). isSidechain is currently never read by any rendering/grouping logic.
|
||
|
||
Design (confirmed with user):
|
||
1. Never label a sidechain prompt "you". A UserMessage with isSidechain=true is an AGENT PROMPT, not user input — label it "agent prompt" with muted / de-emphasized styling (same precedent as injected "context" messages, D-78). Never the blue "you" treatment.
|
||
2. Fold the prompt INTO the Agent tool-use card above it, COLLAPSED BY DEFAULT. The Agent card becomes the container that owns its prompt: expand the card to reveal the prompt; collapsed (default) it is hidden. The standalone prompt block is SUPPRESSED from the list — same fold-a-standalone-item-into-its-card pattern as T-262.
|
||
3. Prompt folds into the CALL, not the results. Only the prompt folds into the collapsed Agent call card. The sub-agent work (its thinking / tool calls / results) stays VISIBLE as the results chain after the call — not buried. Shape: [collapsed Agent call, owning the prompt] then [the run results], with no misleading "you" block anywhere. Agent prompts are valuable enough to keep with the call rather than fold into the subsequent results chain.
|
||
4. Link the prompt to the RIGHT Agent card via parentUuid. Parse parentUuid (currently dropped) so each sidechain prompt attaches to its spawning Agent tool-use. This matters when several agents run in parallel in one turn — a positional "nearest preceding Agent" heuristic would misattach; the parent link is the robust route. Heuristic only as a fallback when the link cannot be resolved.
|
||
|
||
Touch points:
|
||
- transcript_reader.dart: capture parentUuid from the envelope and expose it on ConversationItem (it is present in the JSONL, just not parsed).
|
||
- conversation_view.dart: relabel sidechain UserMessage to "agent prompt" (stop the "you" path); fold it into the Agent card body; special-case the Agent/Task tool name (today only AskUserQuestion is special-cased); suppress the standalone block.
|
||
- activity_cluster.dart: ensure the suppressed prompt is not double-counted in grouping; leave the sidechain results in the chain.
|
||
|
||
Edge cases:
|
||
- Parallel agents in one turn → each prompt attaches to its own card (the reason for parentUuid).
|
||
- Orphan prompt (no resolvable parent) → still render as "agent prompt", muted/collapsed, never "you".
|
||
- Nested / background sub-agents.
|
||
|
||
Tests: unit/widget coverage for sidechain-prompt relabel (never "you"), folding into the Agent card (collapsed hides, expand reveals), suppression of the standalone block, parentUuid attachment with parallel agents, and the orphan fallback. Add a golden for the Agent card with a folded prompt.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets; injected/context muting precedent). Related: T-262 (fold-standalone-item-into-its-card pattern).', 'When Claude launches a sub-agent (the Agent/Task tool), the sub-agent conversation runs as a SIDECHAIN. The prompt Claude wrote for that sub-agent comes through the transcript as a UserMessage with isSidechain=true but injected=false. The conversation view does not look at isSidechain, so it renders that prompt with the blue "you" label exactly like real user input — FALSELY implying the user sent it. It sits as a standalone block below the Agent tool-use card. This is misleading: the user did not write that prompt.
|
||
|
||
Confirmed facts from the code: every transcript envelope carries BOTH isSidechain AND parentUuid (transcript_reader.dart ~L547 parses isSidechain but never USES it; parentUuid is dropped entirely). The Agent/Task tool-use is rendered by _toolUse in lib/builtin/claude/src/conversation_view.dart (only AskUserQuestion is special-cased today). The "you" vs "context" label is chosen in conversation_view.dart (~L200): UserMessage with injected=true → "context" (muted); else → "you" (blue, tokens.globalFocus). isSidechain is currently never read by any rendering/grouping logic.
|
||
|
||
Design (confirmed with user):
|
||
1. Never label a sidechain prompt "you". A UserMessage with isSidechain=true is an AGENT PROMPT, not user input — label it "agent prompt" with muted / de-emphasized styling (same precedent as injected "context" messages, D-78). Never the blue "you" treatment.
|
||
2. Fold the prompt INTO the Agent tool-use card above it, COLLAPSED BY DEFAULT. The Agent card becomes the container that owns its prompt: expand the card to reveal the prompt; collapsed (default) it is hidden. The standalone prompt block is SUPPRESSED from the list — same fold-a-standalone-item-into-its-card pattern as T-262.
|
||
3. Prompt folds into the CALL, not the results. Only the prompt folds into the collapsed Agent call card. The sub-agent work (its thinking / tool calls / results) stays VISIBLE as the results chain after the call — not buried. Shape: [collapsed Agent call, owning the prompt] then [the run results], with no misleading "you" block anywhere. Agent prompts are valuable enough to keep with the call rather than fold into the subsequent results chain.
|
||
4. Link the prompt to the RIGHT Agent card via parentUuid. Parse parentUuid (currently dropped) so each sidechain prompt attaches to its spawning Agent tool-use. This matters when several agents run in parallel in one turn — a positional "nearest preceding Agent" heuristic would misattach; the parent link is the robust route. Heuristic only as a fallback when the link cannot be resolved.
|
||
|
||
Touch points:
|
||
- transcript_reader.dart: capture parentUuid from the envelope and expose it on ConversationItem (it is present in the JSONL, just not parsed).
|
||
- conversation_view.dart: relabel sidechain UserMessage to "agent prompt" (stop the "you" path); fold it into the Agent card body; special-case the Agent/Task tool name (today only AskUserQuestion is special-cased); suppress the standalone block.
|
||
- activity_cluster.dart: ensure the suppressed prompt is not double-counted in grouping; leave the sidechain results in the chain.
|
||
|
||
Edge cases:
|
||
- Parallel agents in one turn → each prompt attaches to its own card (the reason for parentUuid).
|
||
- Orphan prompt (no resolvable parent) → still render as "agent prompt", muted/collapsed, never "you".
|
||
- Nested / background sub-agents.
|
||
|
||
Tests: unit/widget coverage for sidechain-prompt relabel (never "you"), folding into the Agent card (collapsed hides, expand reveals), suppression of the standalone block, parentUuid attachment with parallel agents, and the orphan fallback. Add a golden for the Agent card with a folded prompt.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets; injected/context muting precedent). Related: T-262 (fold-standalone-item-into-its-card pattern).
|
||
|
||
IMPLEMENTATION NOTE (E — Agent-card layered ordering, from streamlining analysis):
|
||
|
||
With this ticket (fold prompt) and T-262 (merge success result) both landing, the Agent/Task card ends up owning multiple segments: the tool INPUT, the folded PROMPT, and — via T-262 — the sub-agents final returned RESULT (the Task ToolResultMessage). Define a deliberate layered order when expanded (e.g. call/input → prompt → returned result) with clear sub-labels/dividers so the Agent card stays readable and does not become a kitchen sink. Coordinate with T-262 (result merge) and T-264 (nesting the whole run): the nested run region vs the returned-result segment must not duplicate the sub-agent output.', NULL, '2026-06-07 08:40:49', '2026-06-07 08:40:49', '2026-06-07 08:40:49', NULL, '6c5fa64d1114b050708456bd650e2213', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XXSERDHKR0EEH3N4', 'parent_id', NULL, 'T-267', NULL, '2026-06-07 08:43:00', '2026-06-07 08:43:00', '2026-06-07 08:43:00', NULL, '0d2ee5d7b3d6623a90e36ccaca03916d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYMMKFD5Z8WG9F60', 'parent_id', NULL, 'T-267', NULL, '2026-06-07 08:43:00', '2026-06-07 08:43:00', '2026-06-07 08:43:00', NULL, '223065799d6e12580c20492ea0233057', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', 'parent_id', NULL, 'T-267', NULL, '2026-06-07 08:43:00', '2026-06-07 08:43:00', '2026-06-07 08:43:00', NULL, '517313811d3de674226344c51953e787', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71P14PAF7K4QE61G0', 'parent_id', NULL, 'T-267', NULL, '2026-06-07 08:43:00', '2026-06-07 08:43:00', '2026-06-07 08:43:00', NULL, '694c1016bf75a0104bef6ab9e152a9d6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4Q1Q97CHA0XNK2G4W', 'parent_id', NULL, 'T-267', NULL, '2026-06-07 08:43:00', '2026-06-07 08:43:00', '2026-06-07 08:43:00', NULL, 'b94daf10a59ddbb4bacec73eb5ccb716', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'description', '## Problem
|
||
|
||
While a Claude turn is in flight, the composer swaps in a static, muted
|
||
`running…` label next to the Stop button (`lib/builtin/claude/src/claude_composer.dart:486`,
|
||
shown when `widget.busy && widget.onInterrupt != null`). It''s gray, motionless,
|
||
and a little lifeless — it gives no sense that anything is actually happening.
|
||
|
||
## Goal
|
||
|
||
Make the in-flight indicator feel alive:
|
||
|
||
1. **Animation.** Add subtle motion — e.g. an animated ellipsis (`running` → `running.`
|
||
→ `running..` → `running...`), a shimmer/pulse on the text, or a small spinner glyph.
|
||
Custom-painted / token-driven per the "own the rendering stack" guardrail; no
|
||
opinionated animation packages.
|
||
2. **Rotating status verbs.** Cycle through playful gerunds the way the Claude Code CLI
|
||
does (e.g. "Clauding…", "Flibbertigibbeting…", "Pondering…", "Conjuring…"). Pick a
|
||
word from a curated list and rotate it every few seconds while the turn runs.
|
||
|
||
## Open question — can we reuse the CLI''s words?
|
||
|
||
The Claude Code CLI ships its own list of these status verbs. Before hand-rolling our
|
||
own, check whether that list is something we''re allowed to reuse / surface (licensing,
|
||
where it lives, whether stream-json exposes the current one). If we can''t pull the CLI''s
|
||
list, ship our own curated, on-brand list instead. Document the decision.
|
||
|
||
## Notes / constraints
|
||
|
||
- Respect reduced-motion / accessibility settings — animation must be disable-able and
|
||
the a11y semantics should still read sensibly (the Stop button already carries the
|
||
interrupt hint).
|
||
- Use theme tokens for color; keep it muted/tasteful, not distracting.
|
||
- Touch point today is the `widget.busy` branch in `claude_composer.dart`; consider
|
||
whether the rotating-word state belongs there or in the session/orchestrator layer.
|
||
- Add a widget/golden test for the animated states (bounded pumps — no real timers).', '## Problem
|
||
|
||
While a Claude turn is in flight, the composer swaps in a static, muted
|
||
`running…` label next to the Stop button (`lib/builtin/claude/src/claude_composer.dart:486`,
|
||
shown when `widget.busy && widget.onInterrupt != null`). It''s gray, motionless,
|
||
and a little lifeless — it gives no sense that anything is actually happening.
|
||
|
||
## Goal
|
||
|
||
Make the in-flight indicator feel alive:
|
||
|
||
1. **Animation.** Add subtle motion — e.g. an animated ellipsis (`running` → `running.`
|
||
→ `running..` → `running...`), a shimmer/pulse on the text, or a small spinner glyph.
|
||
Custom-painted / token-driven per the "own the rendering stack" guardrail; no
|
||
opinionated animation packages.
|
||
2. **Rotating status verbs.** Cycle through playful gerunds the way the Claude Code CLI
|
||
does (e.g. "Clauding…", "Flibbertigibbeting…", "Pondering…", "Conjuring…"). Pick a
|
||
word from a curated list and rotate it every few seconds while the turn runs.
|
||
|
||
## Open question — can we reuse the CLI''s words?
|
||
|
||
The Claude Code CLI ships its own list of these status verbs. Before hand-rolling our
|
||
own, check whether that list is something we''re allowed to reuse / surface (licensing,
|
||
where it lives, whether stream-json exposes the current one). If we can''t pull the CLI''s
|
||
list, ship our own curated, on-brand list instead. Document the decision.
|
||
|
||
## Notes / constraints
|
||
|
||
- Respect reduced-motion / accessibility settings — animation must be disable-able and
|
||
the a11y semantics should still read sensibly (the Stop button already carries the
|
||
interrupt hint).
|
||
- Use theme tokens for color; keep it muted/tasteful, not distracting.
|
||
- Touch point today is the `widget.busy` branch in `claude_composer.dart`; consider
|
||
whether the rotating-word state belongs there or in the session/orchestrator layer.
|
||
- Add a widget/golden test for the animated states (bounded pumps — no real timers).
|
||
|
||
Refinement (2026-06-07): open question resolved — ship OUR OWN curated verb list, do not reuse the CLI''s. Rationale: the spinner words are a TUI cosmetic not surfaced by the stream-json control protocol (so there''s nothing to read live), and extracting Anthropic''s bundled list is a licensing gray area. A clide-owned list aligns with ''own the rendering stack'' and D-75 (isolate/version-pin CC coupling). State lives in the WIDGET layer (a RunningIndicator in lib/builtin/claude/), not the orchestrator — it''s ephemeral UI. Animation via AnimationController (no Timers, so tests use bounded pumps); reduced-motion (MediaQuery.disableAnimations) shows a static verb. Ready to implement.', NULL, '2026-06-07 08:44:32', '2026-06-07 08:44:32', '2026-06-07 08:44:32', NULL, '420aecd96d2137e5ed9c09ebae4c6c38', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'status', 'ready', 'in_progress', NULL, '2026-06-07 08:44:32', '2026-06-07 08:44:32', '2026-06-07 08:44:32', NULL, 'fc3d71b83ffcc1a213a2f0a74219bffc', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', 'description', 'DESIGN DISCUSSION PENDING — do not build until T-262/T-263/T-264/T-265 settle. Captured from user feedback; the interaction model needs a design pass first.
|
||
|
||
Problem: the current activity card (lib/builtin/claude/src/conversation_view.dart _ActivityCard ~L427, grouping in activity_cluster.dart) is a COLLAPSER HEADER that, when expanded, reveals the subsequent cards rendered below it. Two issues:
|
||
1. Model: it reads as a header-plus-loose-cards rather than a single container. Proposal: make it a HOLDER CARD that visually CONTAINS its sub-cards, where clicking anywhere in the card space (the holder) toggles collapse/expand — the whole card is the affordance, not just a small caret/header at the top.
|
||
2. Scroll race: when a holder card is actively being written to (a live agent turn), the view keeps auto-scrolling to the bottom (conversation_view._onChanged ~L119 jumps to maxScrollExtent on every change). The collapse control lives at the TOP of the card, so the user gets into a scrolling race trying to reach it while new content keeps yanking the viewport down. The collapse affordance must remain reachable during live writes — e.g. card-space toggle (so any visible part of the holder toggles it), a sticky/pinned control, and/or pausing autoscroll on user interaction.
|
||
|
||
Likely scope (to be refined after the design discussion):
|
||
- Re-model _ActivityCard as a container that wraps its children rather than a header followed by siblings.
|
||
- Make the whole holder card the collapse/expand hit target (ClideTappable over the card body), keeping keyboard + AT semantics.
|
||
- Resolve the autoscroll-vs-reach-the-control conflict (sticky control, whole-card toggle, and/or suspend tail-follow on user scroll/interaction).
|
||
|
||
This interacts with T-264 (nesting a whole sub-agent run is itself a holder/container) — settle the container model once so the agent-run region and the activity card share it.
|
||
|
||
Refs: D-78. Related: T-230 (activity card), T-264 (nested agent run), T-262, T-263.', 'Restyle the activity card (and, via a shared primitive, the nested sub-agent run from T-264) so a folded run reads as ONE container that holds its sub-cards, with a reachable collapse/expand affordance that survives live writes.
|
||
|
||
DESIGN RESOLVED (with user) — supersedes the earlier design-pending note.
|
||
|
||
1. HOLDER = a shared container primitive. Extract one container widget (e.g. a ClideHolderCard / conversation-level container) that renders a titled/attributed frame WRAPPING its child sub-cards. Both the activity card (_ActivityCard, lib/builtin/claude/src/conversation_view.dart ~L427) and the nested sub-agent run (T-264) consume it, so the container model is settled once. NOTE: this makes T-266 a DEPENDENCY of T-264 (the shared primitive must exist before the agent-run nesting can consume it) — update epic T-267 sequencing: T-266''s primitive lands before/with T-264, no longer strictly last.
|
||
|
||
2. Toggle = the HOLDER BACKGROUND, not the children. Clicking the container''s own background/chrome (its padding, the gaps between sub-cards, its gutter — any region NOT covered by a child) toggles collapse/expand. Crucially:
|
||
- Taps on a child sub-card lying on top of the background MUST NOT toggle the holder. Child cards opaquely consume their own hit region across their FULL bounds (including their body), so a click on a sub-card interacts with that card, never the holder.
|
||
- The copy button (and any child control / ClideTappable) keeps its own interaction — never swallowed by the holder toggle.
|
||
- Implementation: a background gesture target behind the children that only fires for hits the children do not consume (HitTestBehavior; children opaque over their bounds). No whole-card overlay that would intercept child taps.
|
||
|
||
3. Autoscroll stays ALWAYS-FOLLOW. Keep the current tail-follow (conversation_view._onChanged ~L119 jumps to maxScrollExtent on every change). Reachability of the collapse control no longer depends on getting to a top header: because the toggle is the ever-present holder BACKGROUND, a click on whatever background area is currently in view (near the latest content while following) collapses the holder — ending the race. Once collapsed the card shrinks to its ticker, so the scroll-yank stops.
|
||
|
||
4. Keep the collapsed ticker. Collapsed (default) still shows the one-line live ticker (latest step) + step count, as today. Expanded shows the contained sub-cards.
|
||
|
||
5. Accessibility (obligation): a background tap is not keyboard/AT reachable on its own, so keep an EXPLICIT focusable, Enter/Space-activatable collapse control (e.g. the ticker row remains a Semantics button / a focusable caret) in addition to the background-click affordance. Announce expanded/collapsed + step count (as _ActivityCard does today).
|
||
|
||
Touch points:
|
||
- New shared container primitive (extracted from _ActivityCard).
|
||
- conversation_view.dart: _ActivityCard adopts the primitive; background-toggle wiring; ensure child ConversationCards opaquely consume their bounds so they do not bubble taps to the holder.
|
||
- T-264 consumes the same primitive for the nested agent run.
|
||
|
||
Edge cases:
|
||
- A holder whose child is itself collapsible (a tool card): tapping the child''s caret/body toggles the CHILD, never the holder.
|
||
- Text selection inside a child (ClideSelectionArea wraps the list): selection drags on child bodies must not be hijacked by the holder toggle — another reason the holder responds only to its own background, and only to taps (not drags).
|
||
- Empty / single-item holder.
|
||
|
||
Tests: holder background tap toggles; tap on a child sub-card does NOT toggle the holder; copy button still copies (not swallowed); keyboard/AT path toggles via the explicit control; collapsed ticker + step count preserved. Add a golden for the holder container (collapsed + expanded).
|
||
|
||
Refs: D-78. Provides the shared container consumed by T-264. Related: T-230 (activity card), T-262 / T-263 / T-265. Parent: T-267.', NULL, '2026-06-07 08:48:47', '2026-06-07 08:48:47', '2026-06-07 08:48:47', NULL, '55264f40c2a84b70cfc8b2d006644423', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZGZ9GQ5W1MKV00Y4', 'description', 'Home for the cohesive set of work that streamlines how the Claude conversation log renders, so a heavy agent turn reads clearly and nothing is mislabeled or duplicated. Three recurring moves bind these tickets: (1) FOLD a redundant standalone item into its owning card (success result → tool card; agent prompt → agent card); (2) fix MISLEADING ATTRIBUTION (sub-agent prompt shown as "you"; sub-agent prose shown as "claude"); (3) settle the CONTAINER / interaction model (holder card that contains sub-cards, nested agent run, autoscroll-vs-reach-the-control).
|
||
|
||
Common substrate across the children: the flat ConversationItem list and its renderers in lib/builtin/claude/src/conversation_view.dart, the pure grouping pass in activity_cluster.dart, the ConversationCard template in conversation_card.dart, and the so-far-unused ConversationItem.isSidechain / parentUuid transcript fields (transcript_reader.dart).
|
||
|
||
Children: T-262 (merge tool-call + success result into one card with header status check), T-263 (fold sub-agent prompt into the Agent card; relabel you → agent prompt), T-264 (nest the whole sub-agent run under its Agent card via parentUuid), T-265 (relabel sidechain assistant prose/thinking — agent, not claude), T-266 (restyle activity/holder card as a container of sub-cards + fix the collapse-control scroll race). Sequencing: T-262/T-263 first, T-264/T-265 build the sidechain story, T-266 settles the shared container model last.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets). Built on T-168 (per-tool body rendering) and T-230 (activity card).', 'Home for the cohesive set of work that streamlines how the Claude conversation log renders, so a heavy agent turn reads clearly and nothing is mislabeled or duplicated. Three recurring moves bind these tickets: (1) FOLD a redundant standalone item into its owning card (success result → tool card; agent prompt → agent card); (2) fix MISLEADING ATTRIBUTION (sub-agent prompt shown as "you"; sub-agent prose shown as "claude"); (3) settle the CONTAINER / interaction model (holder card that contains sub-cards, nested agent run, autoscroll-vs-reach-the-control).
|
||
|
||
Common substrate across the children: the flat ConversationItem list and its renderers in lib/builtin/claude/src/conversation_view.dart, the pure grouping pass in activity_cluster.dart, the ConversationCard template in conversation_card.dart, and the so-far-unused ConversationItem.isSidechain / parentUuid transcript fields (transcript_reader.dart).
|
||
|
||
Children: T-262 (merge tool-call + success result into one card with header status check), T-263 (fold sub-agent prompt into the Agent card; relabel you → agent prompt), T-264 (nest the whole sub-agent run under its Agent card via parentUuid), T-265 (relabel sidechain assistant prose/thinking — agent, not claude), T-266 (restyle activity/holder card as a container of sub-cards + fix the collapse-control scroll race). Sequencing: T-262/T-263 first, T-264/T-265 build the sidechain story, T-266 settles the shared container model last.
|
||
|
||
Refs: D-78 (interaction zone / display-only conversation widgets). Built on T-168 (per-tool body rendering) and T-230 (activity card).
|
||
|
||
SEQUENCING UPDATE (after T-266 refinement): the shared container/holder primitive now lives in T-266 and is CONSUMED by T-264 (nested agent run), so T-266 is no longer "last" — its primitive lands before/with T-264. T-264 is now blocked by T-266. Revised order: T-262 / T-263 (fold success result, fold agent prompt) → T-266 (shared holder/container primitive + activity-card restyle) → T-264 (nest the whole agent run on that primitive) → T-265 (relabel sidechain prose) can land anytime alongside.', NULL, '2026-06-07 08:49:16', '2026-06-07 08:49:16', '2026-06-07 08:49:16', NULL, 'c298fbc83059f3d273cef241597df2b1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'status', 'in_progress', 'done', NULL, '2026-06-07 08:51:08', '2026-06-07 08:51:08', '2026-06-07 08:51:08', NULL, '1c60d10586ea686fabb448e72e0a2d79', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM74JAV0MJMB4VQNTRG', 'status', 'backlog', 'done', NULL, '2026-06-07 09:36:59', '2026-06-07 09:36:59', '2026-06-07 09:36:59', NULL, '698f4b77d20b3c6d7a7df7741ec054d3', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P9YQ5V7WS6RFJNHW', 'status', 'backlog', 'done', NULL, '2026-06-07 09:51:32', '2026-06-07 09:51:32', '2026-06-07 09:51:32', NULL, '16c545fabac310e2c03c13575fd785f0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', 'status', 'backlog', 'ready', NULL, '2026-06-07 10:03:36', '2026-06-07 10:03:36', '2026-06-07 10:03:36', NULL, 'bac535db2b0406292ac10ba1c90d91af', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM40JE9MZZWCXC91THG', 'status', 'backlog', 'done', NULL, '2026-06-07 10:43:18', '2026-06-07 10:43:18', '2026-06-07 10:43:18', NULL, '5330cc7897890a6032fc222f792dc259', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', 'description', 'Add a Zed-style application menu integrated into the hat bar: File, Edit, Selection, View, Help/About. Rendered as custom Flutter widgets (no native menu bar — we own the chrome per D-7). Menus expose the same commands registered in the command palette with their keybindings shown inline. Submenus for View should include panel toggles, zoom, focus mode. About opens a modal with version, license, and links.', 'Add a Zed-style application menu integrated into the hat bar: File, Edit, Selection, View, Help/About. Rendered as custom Flutter widgets (no native menu bar — we own the chrome per D-7). Menus expose the same commands registered in the command palette with their keybindings shown inline. Submenus for View should include panel toggles, zoom, focus mode. About opens a modal with version, license, and links.
|
||
|
||
## Refinement (decisions, 2026-06-07)
|
||
|
||
**v1 scope:** File, View, Help/About only. Edit and Selection are deferred to follow-ups [[T-271]] (Edit) and [[T-272]] (Selection) — both blocked on this story, since they need focused-surface command routing.
|
||
- View submenu: panel toggles, zoom, focus mode.
|
||
- Help: About modal (version, license, links).
|
||
|
||
**Command mapping (hybrid):** A hand-authored menu tree defines curated placement — ordering, grouping, separators, and which commands sit where. Any registered command not explicitly placed auto-fills from the command registry (by category) into the matching submenu / an overflow section, so newly registered commands surface without manual wiring. Each item''s title + keybinding are pulled from the registry.
|
||
|
||
**Context behavior:** Items reflect the focused surface; inapplicable items render disabled (greyed), not hidden. Mainly exercised once Edit/Selection land, but View/File items honor it too where relevant.
|
||
|
||
**Keyboard / a11y:** Full keyboard support — Alt+mnemonic opens a menu, arrow-key navigation within, Enter activates, Esc / click-away closes. Must meet the a11y contract (keyboard nav + semantics).', NULL, '2026-06-07 10:43:22', '2026-06-07 10:43:22', '2026-06-07 10:43:22', NULL, '5c601de01c18e6a2173c503d0e0fb4c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', 'status', 'ready', 'done', NULL, '2026-06-07 15:40:06', '2026-06-07 15:40:06', '2026-06-07 15:40:06', NULL, 'a1ffa40ddf5216c02291ee63fb968969', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZGZ9GQ5W1MKV00Y4', 'status', 'backlog', 'ready', NULL, '2026-06-07 16:25:28', '2026-06-07 16:25:28', '2026-06-07 16:25:28', NULL, '0e2661a8a87c59ee3518bc5dc05bc934', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZGZ9GQ5W1MKV00Y4', 'status', 'ready', 'in_progress', NULL, '2026-06-07 17:22:06', '2026-06-07 17:22:06', '2026-06-07 17:22:06', NULL, 'f1faae99292a583bfc9024fd301f583b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71P14PAF7K4QE61G0', 'status', 'backlog', 'in_progress', NULL, '2026-06-07 17:23:39', '2026-06-07 17:23:39', '2026-06-07 17:23:39', NULL, '16d80211bbafa711b80b4cc2330ff3d2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71P14PAF7K4QE61G0', 'status', 'in_progress', 'done', NULL, '2026-06-07 17:41:11', '2026-06-07 17:41:11', '2026-06-07 17:41:11', NULL, 'b4cd35786f4d22bc1020dd0b02c2d372', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XXSERDHKR0EEH3N4', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 06:53:38', '2026-06-08 06:53:38', '2026-06-08 06:53:38', NULL, 'cff556a456f675bd41eb127ce02eb108', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W6VN98RQM6S22X28', 'parent_id', NULL, 'T-276', NULL, '2026-06-08 07:46:46', '2026-06-08 07:46:46', '2026-06-08 07:46:46', NULL, '66e1bb30391f9c6224bd944301fb844d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM55CV3VDYS88VBJZ60', 'parent_id', NULL, 'T-276', NULL, '2026-06-08 07:46:46', '2026-06-08 07:46:46', '2026-06-08 07:46:46', NULL, '7fc1b8a8ef580b47e634e6ee2b710ddd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'parent_id', NULL, 'T-276', NULL, '2026-06-08 07:46:46', '2026-06-08 07:46:46', '2026-06-08 07:46:46', NULL, 'be1e98024ab2f03928aec37d266040f1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'description', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work.', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work.
|
||
|
||
**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', NULL, '2026-06-08 07:53:32', '2026-06-08 07:53:32', '2026-06-08 07:53:32', NULL, '6a31f771212b13b5e38a8d3df74dc8a1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM60PAS5RJ55CZGPM1G', 'description', 'clide doesn''t surface pql ticket labels, so a rolling ''UI tweaks'' tracker has to be modeled as an epic (T-276) instead of a label — which is the cleaner primitive. Make labels first-class in the tickets UI so a label can BE the tracker.
|
||
|
||
Scope:
|
||
- Render a ticket''s labels on its card and detail view in the tickets panel (builtin/tickets) — small chips/pills, ui-design tokens, no hardcoded hex.
|
||
- A label navigator: filter/group the board by label (a sidepane affordance or a filter control on the tickets panel) so ''show me everything labelled ui-tweak'' is one click. Reuse the existing filter-box / MessageBus addressability (T-270) where it fits.
|
||
- Read labels via the pql wrapper only (D-3 — wrap, don''t duplicate); pql already supports + labels on records.
|
||
- CLI/UI parity (D-6): label add/remove + filter should have a clide verb counterpart.
|
||
|
||
Once this lands, loose UI/UX work + bugs can move from the T-276 tracker-epic to a label (e.g. ''ui-tweak''); note that migration as a follow-up, don''t auto-close T-276 (it''s marked never-close).
|
||
|
||
Relevant: lib/builtin/tickets/, lib/src/pql/ (pql wrapper), pql / label fields.', 'clide doesn''t surface pql ticket labels, so a rolling ''UI tweaks'' tracker has to be modeled as an epic (T-276) instead of a label — which is the cleaner primitive. Make labels first-class in the tickets UI so a label can BE the tracker.
|
||
|
||
Scope:
|
||
- Render a ticket''s labels on its card and detail view in the tickets panel (lib/builtin/tickets) — small chips/pills, ui-design tokens, no hardcoded hex.
|
||
- A label navigator: filter/group the board by label (a sidepane affordance or a filter control on the tickets panel) so ''show me everything labelled ui-tweak'' is one click. Reuse the existing filter-box / MessageBus addressability (T-270) where it fits.
|
||
- Read/write labels via the pql wrapper only (D-3 — wrap, don''t duplicate); pql already supports the ''ticket label'' verb and exposes labels on records.
|
||
- CLI/UI parity (D-6): label add/remove + filter should each have a clide verb counterpart.
|
||
|
||
Once this lands, loose UI/UX work + bugs can move from the T-276 tracker-epic to a label (e.g. ''ui-tweak''); file that migration as a follow-up — do NOT auto-close T-276 (it''s marked never-close).
|
||
|
||
Relevant: lib/builtin/tickets/, lib/src/pql/ (pql wrapper), and pql''s ''ticket label'' / ''ticket list'' label fields.', NULL, '2026-06-08 08:34:24', '2026-06-08 08:34:24', '2026-06-08 08:34:24', NULL, '6dec8b27eed6dc7ed86047c39a363912', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM60PAS5RJ55CZGPM1G', 'parent_id', NULL, 'T-276', NULL, '2026-06-08 08:34:47', '2026-06-08 08:34:47', '2026-06-08 08:34:47', NULL, '3d746f1dbcc965d98a7d04b7398c90c5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XXSERDHKR0EEH3N4', 'status', 'in_progress', 'done', NULL, '2026-06-08 08:45:23', '2026-06-08 08:45:23', '2026-06-08 08:45:23', NULL, '9a57241efcf29530ce0fb548b73b476c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 08:47:43', '2026-06-08 08:47:43', '2026-06-08 08:47:43', NULL, '6c4bf58a2d2ac4e46737b9e6cd5d5a9d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', 'status', 'in_progress', 'done', NULL, '2026-06-08 09:04:12', '2026-06-08 09:04:12', '2026-06-08 09:04:12', NULL, 'd5dded000a54cf6db5418b3525f967db', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYMMKFD5Z8WG9F60', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 09:07:02', '2026-06-08 09:07:02', '2026-06-08 09:07:02', NULL, 'e944f09acd2bd2b6d4ccb01b09e63f8b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYMMKFD5Z8WG9F60', 'status', 'in_progress', 'done', NULL, '2026-06-08 09:25:50', '2026-06-08 09:25:50', '2026-06-08 09:25:50', NULL, 'ec67ba8741c18305514d9e81f40fbbfb', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4Q1Q97CHA0XNK2G4W', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 09:27:49', '2026-06-08 09:27:49', '2026-06-08 09:27:49', NULL, 'd29fe012fff5678f99fe0917363fdfad', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4Q1Q97CHA0XNK2G4W', 'status', 'in_progress', 'done', NULL, '2026-06-08 10:09:14', '2026-06-08 10:09:14', '2026-06-08 10:09:14', NULL, '80ca7819edb9316a2f01c39249905376', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZGZ9GQ5W1MKV00Y4', 'status', 'in_progress', 'done', NULL, '2026-06-08 10:09:21', '2026-06-08 10:09:21', '2026-06-08 10:09:21', NULL, '751ea8ae1f3b8faef968556fb089d5c9', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM55CV3VDYS88VBJZ60', 'status', 'backlog', 'ready', NULL, '2026-06-08 10:26:11', '2026-06-08 10:26:11', '2026-06-08 10:26:11', NULL, '2be4726345e4db30d17f7285f08bd59d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YNEQM1WY25BV7TD8', 'description', NULL, 'Pre-existing (reproduces at base commit 3a78dfa, predates the T-267 conversation-rendering epic). The widget test `test/app_test.dart` › "Open Folder on a non-repo path surfaces the ''no git repo'' dialog" times out after 10 minutes; teardown is wedged on `_RawReceivePort._handleMessage`.
|
||
|
||
Bisected (each a 45s-timeout repro, all on this box):
|
||
- A bare `Process.run` inside `tester.runAsync` (no app, no extensions) hangs → `Process.run`-in-`runAsync` leaks its exit ReceivePort here.
|
||
- `pumpApp` + empty `runAsync`, and `pumpApp` + a 1.5s real delay → both PASS (boot + runAsync alone is fine).
|
||
- The full openFolder tap flow hangs even when project validation is stubbed to a synchronous, pure-Dart `.git` walk (no subprocess) AND `runAsync` is removed — so the wedge is not solely the git subprocess; something in the booted-app + extensions + open-folder command path holds a native port that teardown waits on forever.
|
||
|
||
Quarantined with `skip:` so the suite/gate stays green. Real fix: find the leaked native async resource (likely a Process/Isolate/FakeDaemonClient port reachable from the open-folder command or app boot under the test harness) and ensure it''s drained/cancelled before teardown — or drive the "no git repo" assertion without booting the resource. Then remove the skip.', NULL, '2026-06-08 11:29:33', '2026-06-08 11:29:33', '2026-06-08 11:29:33', NULL, '1f99f25c756004f2d368ef61b00c0da6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM55CV3VDYS88VBJZ60', 'status', 'ready', 'done', NULL, '2026-06-08 11:44:27', '2026-06-08 11:44:27', '2026-06-08 11:44:27', NULL, '45834ee4b421914db5fe8d7bfa72568d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59DG2WADJ9A0JFD1W', 'status', 'backlog', 'ready', NULL, '2026-06-08 11:47:56', '2026-06-08 11:47:56', '2026-06-08 11:47:56', NULL, '6602a5dca91f8ef4e762f326032767fc', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59DG2WADJ9A0JFD1W', 'status', 'ready', 'done', NULL, '2026-06-08 11:49:36', '2026-06-08 11:49:36', '2026-06-08 11:49:36', NULL, '08fa6a8e6fffcb665c51cbb7a8c93604', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6K1QRC911JMQ9C960', 'description', 'Surfaced 2026-06-08 while chasing a --resume hang in the Claude pane (T-274 diagnostic line). The specific corrupted-transcript repro may turn out to be a one-off, but the code trace found two real latent gaps that make a resume hang unrecoverable regardless of root cause.
|
||
|
||
(a) No timeout / no fallback on the init-event wait.
|
||
On spawn the orchestrator listens for the session_id from claude''s init event (StreamJsonSession sessionIdResolved -> session_orchestrator.dart ~257) with NO timeout. If ''claude --resume <id>'' never emits an init event (hangs), the listener never fires: the pane shows ''resumed · <id>'' + running indicator forever, the process is never killed, and there is no fallback to a fresh --session-id spawn. Unrecoverable without killing the process / restarting clide.
|
||
|
||
(b) Resume is decided by file existence, not resumable content.
|
||
claude_pane.dart ~279: ''final resume = await File(transcriptFile).exists();'' — resume=true purely because the .jsonl exists. A metadata-only transcript (only system / permission-mode / attachment records; parseTranscriptChunk().items returns []) yields resume=true with zero seeded items. So clide passes --resume against a functionally empty session, the diagnostic logs ''fresh session (no history)'' (it keys off seeded count, not the resume flag — itself misleading), and if that resume hangs there is no fallback per (a).
|
||
|
||
Acceptance:
|
||
1. If a --resume spawn yields no init event within a short timeout (e.g. 10-30s), fall back to a fresh --session-id spawn (or surface a recoverable error with a retry affordance) — the pane must never spin forever with no recovery.
|
||
2. Don''t pass --resume for a transcript that has no resumable conversation items: validate parsed item count (not just file existence) before choosing --resume vs --session-id, and/or detect-and-repair a metadata-only transcript.
|
||
3. The T-274 diagnostic log reflects the actual spawn mode (resume vs fresh), not just seeded-item count.
|
||
4. Tests: (i) fake process that never emits init -> pane falls back / surfaces error within the timeout; (ii) metadata-only transcript -> spawn chooses fresh, not --resume.
|
||
|
||
Cross-refs: T-274 (resumed-session status bar empty), T-167/T-185 (resume/fork session id capture), D-77, claude_pane.dart:279/300-308, session_orchestrator.dart:240/257.', 'Surfaced 2026-06-08 while chasing a --resume hang in the Claude pane (T-274 diagnostic line). The specific corrupted-transcript repro may turn out to be a one-off, but the code trace found two real latent gaps that make a resume hang unrecoverable regardless of root cause.
|
||
|
||
(a) No timeout / no fallback on the init-event wait.
|
||
On spawn the orchestrator listens for the session_id from claude''s init event (StreamJsonSession sessionIdResolved -> session_orchestrator.dart ~257) with NO timeout. If ''claude --resume <id>'' never emits an init event (hangs), the listener never fires: the pane shows ''resumed · <id>'' + running indicator forever, the process is never killed, and there is no fallback to a fresh --session-id spawn. Unrecoverable without killing the process / restarting clide.
|
||
|
||
(b) Resume is decided by file existence, not resumable content.
|
||
claude_pane.dart ~279: ''final resume = await File(transcriptFile).exists();'' — resume=true purely because the .jsonl exists. A metadata-only transcript (only system / permission-mode / attachment records; parseTranscriptChunk().items returns []) yields resume=true with zero seeded items. So clide passes --resume against a functionally empty session, the diagnostic logs ''fresh session (no history)'' (it keys off seeded count, not the resume flag — itself misleading), and if that resume hangs there is no fallback per (a).
|
||
|
||
Acceptance:
|
||
1. If a --resume spawn yields no init event within a short timeout (e.g. 10-30s), fall back to a fresh --session-id spawn (or surface a recoverable error with a retry affordance) — the pane must never spin forever with no recovery.
|
||
2. Don''t pass --resume for a transcript that has no resumable conversation items: validate parsed item count (not just file existence) before choosing --resume vs --session-id, and/or detect-and-repair a metadata-only transcript.
|
||
3. The T-274 diagnostic log reflects the actual spawn mode (resume vs fresh), not just seeded-item count.
|
||
4. Tests: (i) fake process that never emits init -> pane falls back / surfaces error within the timeout; (ii) metadata-only transcript -> spawn chooses fresh, not --resume.
|
||
|
||
Cross-refs: T-274 (resumed-session status bar empty), T-167/T-185 (resume/fork session id capture), D-77, claude_pane.dart:279/300-308, session_orchestrator.dart:240/257.
|
||
|
||
UPDATE 2026-06-08: the active hang did NOT reproduce — clide is running fine inside the 31b214bd primary session (this very session resumes cleanly). So the original break was a one-off (likely the single corrupted transcript), not a live resume bug. This ticket stands as defensive hardening only: the two gaps (no init-event timeout/fallback; resume keyed off file-exists not content) are real but latent — they''d only bite again if a resume genuinely stalls or a metadata-only transcript appears. Lowering to low priority.', NULL, '2026-06-08 13:44:57', '2026-06-08 13:44:57', '2026-06-08 13:44:57', NULL, '53d61af5561611d655fcee6eeb0ce208', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W2MWZ45YNK7TJGC4', 'status', 'backlog', 'ready', NULL, '2026-06-08 13:48:31', '2026-06-08 13:48:31', '2026-06-08 13:48:31', NULL, '2bf8f6c9f94877217f737d3354d4dbff', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6A6A56VFSVRRBKMP0', 'status', 'backlog', 'ready', NULL, '2026-06-08 13:48:42', '2026-06-08 13:48:42', '2026-06-08 13:48:42', NULL, 'c297bea78881a8738f24fb3313453294', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6A6A56VFSVRRBKMP0', 'status', 'ready', 'in_progress', NULL, '2026-06-08 13:49:32', '2026-06-08 13:49:32', '2026-06-08 13:49:32', NULL, '0d0c6f8d18e9d59100387427018c5f49', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W2MWZ45YNK7TJGC4', 'status', 'ready', 'in_progress', NULL, '2026-06-08 13:49:32', '2026-06-08 13:49:32', '2026-06-08 13:49:32', NULL, '440309f1ae2191f822fbcb79bd6dc9b2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'status', 'backlog', 'ready', NULL, '2026-06-08 14:12:26', '2026-06-08 14:12:26', '2026-06-08 14:12:26', NULL, '137d4e3cae7c30bc903e4eb059712c77', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6A6A56VFSVRRBKMP0', 'status', 'in_progress', 'done', NULL, '2026-06-08 14:12:46', '2026-06-08 14:12:46', '2026-06-08 14:12:46', NULL, '7880cf5180b7600fc4ca7673ceb6865d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KFHAS1WZFGBJ6JN4', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 14:55:51', '2026-06-08 14:55:51', '2026-06-08 14:55:51', NULL, '367a474aebf7e34a1cb5f2fad404113a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KFHAS1WZFGBJ6JN4', 'status', 'in_progress', 'done', NULL, '2026-06-08 14:57:15', '2026-06-08 14:57:15', '2026-06-08 14:57:15', NULL, '8a3454fab8ee7165408de4d35ea45880', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GZ55VD4SAZZ6XXP4', 'status', 'backlog', 'in_progress', NULL, '2026-06-08 15:22:35', '2026-06-08 15:22:35', '2026-06-08 15:22:35', NULL, '648f507b573971c2ce0dffab325f219b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GZ55VD4SAZZ6XXP4', 'status', 'in_progress', 'done', NULL, '2026-06-08 15:37:29', '2026-06-08 15:37:29', '2026-06-08 15:37:29', NULL, '9d9a58f785a610aa8bd4ce1850328f04', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W2MWZ45YNK7TJGC4', 'status', 'in_progress', 'done', NULL, '2026-06-08 15:42:01', '2026-06-08 15:42:01', '2026-06-08 15:42:01', NULL, '39d714d44f8a897420d248a9b245c868', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'status', 'ready', 'in_progress', NULL, '2026-06-08 15:45:13', '2026-06-08 15:45:13', '2026-06-08 15:45:13', NULL, '6e6b49a52ee640d74a171dcc8524bdcd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'parent_id', 'T-276', 'T-286', NULL, '2026-06-08 16:30:47', '2026-06-08 16:30:47', '2026-06-08 16:30:47', NULL, '6750d8892dd9b437fb7cafbed811fa90', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'status', 'in_progress', 'done', NULL, '2026-06-08 16:52:13', '2026-06-08 16:52:13', '2026-06-08 16:52:13', NULL, '2e092a257808514fe2b6e43888748024', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R93N5J6RNVTBRB28', 'status', 'backlog', 'ready', NULL, '2026-06-08 17:34:29', '2026-06-08 17:34:29', '2026-06-08 17:34:29', NULL, '55ba9ff47275418cf9238495c02d1dd0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R93N5J6RNVTBRB28', 'status', 'ready', 'in_progress', NULL, '2026-06-08 17:34:31', '2026-06-08 17:34:31', '2026-06-08 17:34:31', NULL, 'b568ce91cc9623a1ffc58c1874a36109', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'status', 'backlog', 'ready', NULL, '2026-06-08 17:37:13', '2026-06-08 17:37:13', '2026-06-08 17:37:13', NULL, 'f561a5bc9500193fbf0f74b89b4aa95f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4501EBPDFDH84RJ5G', 'description', 'Follow-up to the D-88 sweep. The primitive (ClideAnchoredOverlay + ClideMenu) shipped and the menu bar (T-286) + T-275 picker are migrated. The theme-picker migration was attempted and REVERTED after hitting two real blockers that affect all the remaining anchored surfaces; resolve these first, then migrate theme picker, @-mention typeahead, slash typeahead, and quick-open.
|
||
|
||
Blockers found (theme picker attempt):
|
||
1. Focus-capture race. ClideAnchoredOverlay.captureFocus does a post-frame _scope.requestFocus(); ClideMenu also autofocuses its own node. Depending on mount timing (e.g. an extra ListenableBuilder wrapper) the scope wins and STEALS focus from ClideMenu''s node, so arrow/enter never reach the menu (Esc still works only via the ancestor fallback). Menu bar happens to win the race; the theme popover loses. Fix the primitive''s focus model so content''s own focus node reliably ends up focused (e.g. don''t force scope focus; or make the scope delegate to the autofocus child deterministically), then re-verify menu bar + anchored tests.
|
||
2. Follower content is not reliably mouse-tappable in the widget-test harness. The shared harness() uses Overlay(canSizeOverlay) + a zero-size MediaQuery, so an anchored follower positions the panel off-screen (observed tap offsets like (680,-45) and (680,654)) and tester.tap misses. Also autoFlip reads MediaQuery.size (zero in harness) so it can''t flip. Mitigations: have autoFlip use View.physicalSize (more correct anyway); position content without the inner Align (the panel shrink-wraps); and in tests drive selection via keyboard (as menu_bar does) or provide a real viewport. Consider a test helper for anchored-overlay content.
|
||
|
||
Then migrate, one surface per commit, keeping each existing test green (theme_picker widget_test, team_chat_sidebar_test, claude_composer_test, quick_open_overlay_test). Quick-open is centered (no follower) and the weakest-fit — it only needs the lifecycle wrapper; migrate last or skip.
|
||
|
||
Refs: D-88, T-286, T-275 (done), menu bar (done, commit e1d51eb).', 'Follow-up to the D-88 sweep. The primitive (ClideAnchoredOverlay + ClideMenu) shipped and the menu bar (T-286) + T-275 picker are migrated. The theme-picker migration was attempted and REVERTED after hitting two real blockers that affect all the remaining anchored surfaces; resolve these first, then migrate theme picker, @-mention typeahead, slash typeahead, and quick-open.
|
||
|
||
Blockers found (theme picker attempt):
|
||
1. Focus-capture race. ClideAnchoredOverlay.captureFocus does a post-frame _scope.requestFocus(); ClideMenu also autofocuses its own node. Depending on mount timing (e.g. an extra ListenableBuilder wrapper) the scope wins and STEALS focus from ClideMenu''s node, so arrow/enter never reach the menu (Esc still works only via the ancestor fallback). Menu bar happens to win the race; the theme popover loses. Fix the primitive''s focus model so content''s own focus node reliably ends up focused (e.g. don''t force scope focus; or make the scope delegate to the autofocus child deterministically), then re-verify menu bar + anchored tests.
|
||
2. Follower content is not reliably mouse-tappable in the widget-test harness. The shared harness() uses Overlay(canSizeOverlay) + a zero-size MediaQuery, so an anchored follower positions the panel off-screen (observed tap offsets like (680,-45) and (680,654)) and tester.tap misses. Also autoFlip reads MediaQuery.size (zero in harness) so it can''t flip. Mitigations: have autoFlip use View.physicalSize (more correct anyway); position content without the inner Align (the panel shrink-wraps); and in tests drive selection via keyboard (as menu_bar does) or provide a real viewport. Consider a test helper for anchored-overlay content.
|
||
|
||
Then migrate, one surface per commit, keeping each existing test green (theme_picker widget_test, team_chat_sidebar_test, claude_composer_test, quick_open_overlay_test). Quick-open is centered (no follower) and the weakest-fit — it only needs the lifecycle wrapper; migrate last or skip.
|
||
|
||
Refs: D-88, T-286, T-275 (done), menu bar (done, commit e1d51eb).
|
||
|
||
RE-SCOPE (2026-06-08, per D-88 amendment): the goal is NOT ''migrate everything onto ClideMenu''. The shared base is ClideAnchoredOverlay; content matches the surface:
|
||
- ClideMenu stays scoped to selectable-list menus (menu bar + permission picker — done).
|
||
- Add a NEW shared content component ClideTypeahead for the slash + @ typeaheads (near-duplicate caret-anchored completion surfaces) — they share this, not ClideMenu. This dedups two hand-rolls.
|
||
- Quick-open keeps bespoke content (centred filter + fuzzy + two-column rows); uses ClideAnchoredOverlay base only. Weakest fit; last or skip.
|
||
- Theme picker: its own small toggle+list content on the base (or ClideMenu if it cleanly fits).
|
||
Order: (1) fix the base blockers — focus capture racing content autofocus, and follower content untappable in the canSizeOverlay/zero-MediaQuery test harness (use View size for autoFlip; add a test path for anchored content) — with tests; THEN (2) ClideTypeahead + migrate slash + @; (3) theme picker; (4) quick-open last.', NULL, '2026-06-08 20:12:31', '2026-06-08 20:12:31', '2026-06-08 20:12:31', NULL, 'cd71f1fce9071ab191adc7f2c573fb21', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4501EBPDFDH84RJ5G', 'description', 'Follow-up to the D-88 sweep. The primitive (ClideAnchoredOverlay + ClideMenu) shipped and the menu bar (T-286) + T-275 picker are migrated. The theme-picker migration was attempted and REVERTED after hitting two real blockers that affect all the remaining anchored surfaces; resolve these first, then migrate theme picker, @-mention typeahead, slash typeahead, and quick-open.
|
||
|
||
Blockers found (theme picker attempt):
|
||
1. Focus-capture race. ClideAnchoredOverlay.captureFocus does a post-frame _scope.requestFocus(); ClideMenu also autofocuses its own node. Depending on mount timing (e.g. an extra ListenableBuilder wrapper) the scope wins and STEALS focus from ClideMenu''s node, so arrow/enter never reach the menu (Esc still works only via the ancestor fallback). Menu bar happens to win the race; the theme popover loses. Fix the primitive''s focus model so content''s own focus node reliably ends up focused (e.g. don''t force scope focus; or make the scope delegate to the autofocus child deterministically), then re-verify menu bar + anchored tests.
|
||
2. Follower content is not reliably mouse-tappable in the widget-test harness. The shared harness() uses Overlay(canSizeOverlay) + a zero-size MediaQuery, so an anchored follower positions the panel off-screen (observed tap offsets like (680,-45) and (680,654)) and tester.tap misses. Also autoFlip reads MediaQuery.size (zero in harness) so it can''t flip. Mitigations: have autoFlip use View.physicalSize (more correct anyway); position content without the inner Align (the panel shrink-wraps); and in tests drive selection via keyboard (as menu_bar does) or provide a real viewport. Consider a test helper for anchored-overlay content.
|
||
|
||
Then migrate, one surface per commit, keeping each existing test green (theme_picker widget_test, team_chat_sidebar_test, claude_composer_test, quick_open_overlay_test). Quick-open is centered (no follower) and the weakest-fit — it only needs the lifecycle wrapper; migrate last or skip.
|
||
|
||
Refs: D-88, T-286, T-275 (done), menu bar (done, commit e1d51eb).
|
||
|
||
RE-SCOPE (2026-06-08, per D-88 amendment): the goal is NOT ''migrate everything onto ClideMenu''. The shared base is ClideAnchoredOverlay; content matches the surface:
|
||
- ClideMenu stays scoped to selectable-list menus (menu bar + permission picker — done).
|
||
- Add a NEW shared content component ClideTypeahead for the slash + @ typeaheads (near-duplicate caret-anchored completion surfaces) — they share this, not ClideMenu. This dedups two hand-rolls.
|
||
- Quick-open keeps bespoke content (centred filter + fuzzy + two-column rows); uses ClideAnchoredOverlay base only. Weakest fit; last or skip.
|
||
- Theme picker: its own small toggle+list content on the base (or ClideMenu if it cleanly fits).
|
||
Order: (1) fix the base blockers — focus capture racing content autofocus, and follower content untappable in the canSizeOverlay/zero-MediaQuery test harness (use View size for autoFlip; add a test path for anchored content) — with tests; THEN (2) ClideTypeahead + migrate slash + @; (3) theme picker; (4) quick-open last.', 'Follow-up to the D-88 sweep. The primitive (ClideAnchoredOverlay + ClideMenu) shipped and the menu bar (T-286) + T-275 picker are migrated. The theme-picker migration was attempted and REVERTED after hitting two real blockers that affect all the remaining anchored surfaces; resolve these first, then migrate theme picker, @-mention typeahead, slash typeahead, and quick-open.
|
||
|
||
Blockers found (theme picker attempt):
|
||
1. Focus-capture race. ClideAnchoredOverlay.captureFocus does a post-frame _scope.requestFocus(); ClideMenu also autofocuses its own node. Depending on mount timing (e.g. an extra ListenableBuilder wrapper) the scope wins and STEALS focus from ClideMenu''s node, so arrow/enter never reach the menu (Esc still works only via the ancestor fallback). Menu bar happens to win the race; the theme popover loses. Fix the primitive''s focus model so content''s own focus node reliably ends up focused (e.g. don''t force scope focus; or make the scope delegate to the autofocus child deterministically), then re-verify menu bar + anchored tests.
|
||
2. Follower content is not reliably mouse-tappable in the widget-test harness. The shared harness() uses Overlay(canSizeOverlay) + a zero-size MediaQuery, so an anchored follower positions the panel off-screen (observed tap offsets like (680,-45) and (680,654)) and tester.tap misses. Also autoFlip reads MediaQuery.size (zero in harness) so it can''t flip. Mitigations: have autoFlip use View.physicalSize (more correct anyway); position content without the inner Align (the panel shrink-wraps); and in tests drive selection via keyboard (as menu_bar does) or provide a real viewport. Consider a test helper for anchored-overlay content.
|
||
|
||
Then migrate, one surface per commit, keeping each existing test green (theme_picker widget_test, team_chat_sidebar_test, claude_composer_test, quick_open_overlay_test). Quick-open is centered (no follower) and the weakest-fit — it only needs the lifecycle wrapper; migrate last or skip.
|
||
|
||
Refs: D-88, T-286, T-275 (done), menu bar (done, commit e1d51eb).
|
||
|
||
RE-SCOPE (2026-06-08, per D-88 amendment): the goal is NOT ''migrate everything onto ClideMenu''. The shared base is ClideAnchoredOverlay; content matches the surface:
|
||
- ClideMenu stays scoped to selectable-list menus (menu bar + permission picker — done).
|
||
- Add a NEW shared content component ClideTypeahead for the slash + @ typeaheads (near-duplicate caret-anchored completion surfaces) — they share this, not ClideMenu. This dedups two hand-rolls.
|
||
- Quick-open keeps bespoke content (centred filter + fuzzy + two-column rows); uses ClideAnchoredOverlay base only. Weakest fit; last or skip.
|
||
- Theme picker: its own small toggle+list content on the base (or ClideMenu if it cleanly fits).
|
||
Order: (1) fix the base blockers — focus capture racing content autofocus, and follower content untappable in the canSizeOverlay/zero-MediaQuery test harness (use View size for autoFlip; add a test path for anchored content) — with tests; THEN (2) ClideTypeahead + migrate slash + @; (3) theme picker; (4) quick-open last.
|
||
|
||
DONE: Sweep complete. Base blockers fixed (View-size autoFlip; follower drops Align so it hit-tests; tests use anchoredHarness). Migrated: ClideTypeahead + slash + @-mention typeaheads (live ValueNotifier-bridged suggestions); theme picker onto ClideMenu (HC toggle = keepOpenOnSelect item). Quick-open deliberately NOT migrated — persistent centred widget sharing neither ClideMenu nor anchoring; rationale in D-88 closing amendment.', NULL, '2026-06-09 06:18:55', '2026-06-09 06:18:55', '2026-06-09 06:18:55', NULL, '820fe7f1d764b7707f531d4216578b3a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4501EBPDFDH84RJ5G', 'status', 'backlog', 'done', NULL, '2026-06-09 06:19:01', '2026-06-09 06:19:01', '2026-06-09 06:19:01', NULL, 'b9a0ef09f741bddcb1cfb52c17645f56', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R93N5J6RNVTBRB28', 'description', 'Extract a clide-owned anchored-popover + menu widget primitive (lib/widgets/, no Material) and migrate every anchored surface onto it. Nine surfaces hand-roll the same anchored-overlay + row-list + barrier + keyboard-nav pattern. Two layers: ClideAnchoredOverlay (positioning/lifecycle: LayerLink/follower or centered, barrier, Esc, focus capture, autoFlip) + ClideMenu / ClideMenuListController (rows + reusable nav). Modal DialogRouter pickers (session/project/branch) stay modal. Built on T-275''s permission-mode picker first, then migrate menu bar, theme picker, @-mention, slash typeahead, quick-open. See decision (architecture domain) + plan. Children: primitive, T-275 picker, one per migration.', 'Extract a clide-owned anchored-popover + menu widget primitive (lib/widgets/, no Material) and migrate every anchored surface onto it. Nine surfaces hand-roll the same anchored-overlay + row-list + barrier + keyboard-nav pattern. Two layers: ClideAnchoredOverlay (positioning/lifecycle: LayerLink/follower or centered, barrier, Esc, focus capture, autoFlip) + ClideMenu / ClideMenuListController (rows + reusable nav). Modal DialogRouter pickers (session/project/branch) stay modal. Built on T-275''s permission-mode picker first, then migrate menu bar, theme picker, @-mention, slash typeahead, quick-open. See decision (architecture domain) + plan. Children: primitive, T-275 picker, one per migration.
|
||
|
||
DONE: Primitive (ClideAnchoredOverlay + ClideMenu + ClideMenuListController + ClideTypeahead) shipped and exported. Migrated: T-275 permission picker, menu bar, theme picker (ClideMenu), @-mention + slash typeaheads (ClideTypeahead). Quick-open intentionally left bespoke (centred persistent widget, no shared shape) — see D-88 closing amendment. Modal session/project/branch pickers stay on DialogRouter by design.', NULL, '2026-06-09 06:19:33', '2026-06-09 06:19:33', '2026-06-09 06:19:33', NULL, 'c919a01c49bf8be33ba2e1cf5c3a7111', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R93N5J6RNVTBRB28', 'status', 'in_progress', 'done', NULL, '2026-06-09 06:19:36', '2026-06-09 06:19:36', '2026-06-09 06:19:36', NULL, '7f0dc7f8b224b31b585d5d2a49f88df6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50BPNDT50PM24ZVA4', 'status', 'backlog', 'ready', NULL, '2026-06-09 06:33:31', '2026-06-09 06:33:31', '2026-06-09 06:33:31', NULL, 'a9788029757cd116218c5a4f1f47c927', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'status', 'ready', 'in_progress', NULL, '2026-06-09 06:34:31', '2026-06-09 06:34:31', '2026-06-09 06:34:31', NULL, 'd179dce4eb30e804056b7a49596134c2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'description', 'When a file is opened in the editor, read .editorconfig from the workspace root and apply: indent_style, indent_size, max_line_length (ruler/wrap guide), end_of_line, trim_trailing_whitespace, insert_final_newline. Parse the INI format ourselves (small, no dep). Glob matching per the EditorConfig spec.', 'When a file is opened in the editor, read .editorconfig from the workspace root and apply: indent_style, indent_size, max_line_length (ruler/wrap guide), end_of_line, trim_trailing_whitespace, insert_final_newline. Parse the INI format ourselves (small, no dep). Glob matching per the EditorConfig spec.
|
||
|
||
DONE: The editor honours .editorconfig end-to-end. New source-agnostic EditorSettings model (editor_settings.dart) + composition seam (editor_settings_resolver.dart); .editorconfig demoted to a source (editorconfig.dart, own INI parser + glob matcher, root/nearest-wins precedence, no deps). Registry resolves on open and re-resolves open buffers when a .editorconfig is saved in-app (editor.settings-changed; a save hook, not an fs-watch). Editor: Tab/Shift+Tab indent + max_line_length ruler; save applies end_of_line/trim_trailing_whitespace/insert_final_newline. Follow-ups: T-290 (edit from settings panel), T-291 (external fs-watch). 100% coverage on new model/resolver/parser.', NULL, '2026-06-09 10:46:41', '2026-06-09 10:46:41', '2026-06-09 10:46:41', NULL, '88ea41d16ef29fe640e57f05dca084c2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'status', 'in_progress', 'done', NULL, '2026-06-09 10:46:41', '2026-06-09 10:46:41', '2026-06-09 10:46:41', NULL, '96346570173938ba2b7112497db1210e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50BPNDT50PM24ZVA4', 'status', 'ready', 'in_progress', NULL, '2026-06-09 14:59:45', '2026-06-09 14:59:45', '2026-06-09 14:59:45', NULL, 'af89fd8ae447a5052eaeb7b9375590cf', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50BPNDT50PM24ZVA4', 'status', 'in_progress', 'done', NULL, '2026-06-09 15:03:23', '2026-06-09 15:03:23', '2026-06-09 15:03:23', NULL, '60faf709c9ae2ac315fb9a5331fdcca7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50BPNDT50PM24ZVA4', 'description', 'Two issues in the Claude composer slash-command typeahead (T-152/T-153, lib/builtin/claude/src/slash_commands.dart + claude_composer.dart):
|
||
|
||
1. Typing a ''-'' character breaks the typeahead. Many command names contain hyphens (e.g. clear-context-style names, custom commands), but typing ''-'' appears to drop/empty the suggestion list or mis-parse the active query. Suspect activeSlashQuery''s token run or filterSlashCommands prefix matching not handling ''-'' (or the composer treating ''-'' as a boundary).
|
||
|
||
2. Tab to accept the highlighted suggestion responds flakily — sometimes it completes, sometimes nothing happens. Suspect a focus/key-handling race between the composer''s key handler and the typeahead overlay, or Tab being consumed by focus traversal before the accept intent fires.
|
||
|
||
Repro: open the composer, type ''/'' then a command fragment; (a) include a ''-'' and watch the list; (b) arrow-select an item and press Tab repeatedly.
|
||
|
||
Acceptance: ''-'' is treated as a normal command-name character (suggestions keep filtering through hyphens); Tab reliably completes the highlighted suggestion every time (insert via completeSlash). Add/extend unit tests in slash_commands_test.dart for hyphenated queries and a composer widget test for Tab-accept.', 'Two issues in the Claude composer slash-command typeahead (T-152/T-153, lib/builtin/claude/src/slash_commands.dart + claude_composer.dart):
|
||
|
||
1. Typing a ''-'' character breaks the typeahead. Many command names contain hyphens (e.g. clear-context-style names, custom commands), but typing ''-'' appears to drop/empty the suggestion list or mis-parse the active query. Suspect activeSlashQuery''s token run or filterSlashCommands prefix matching not handling ''-'' (or the composer treating ''-'' as a boundary).
|
||
|
||
2. Tab to accept the highlighted suggestion responds flakily — sometimes it completes, sometimes nothing happens. Suspect a focus/key-handling race between the composer''s key handler and the typeahead overlay, or Tab being consumed by focus traversal before the accept intent fires.
|
||
|
||
Repro: open the composer, type ''/'' then a command fragment; (a) include a ''-'' and watch the list; (b) arrow-select an item and press Tab repeatedly.
|
||
|
||
Acceptance: ''-'' is treated as a normal command-name character (suggestions keep filtering through hyphens); Tab reliably completes the highlighted suggestion every time (insert via completeSlash). Add/extend unit tests in slash_commands_test.dart for hyphenated queries and a composer widget test for Tab-accept.
|
||
|
||
RESOLVED by the D-88 ClideTypeahead migration (T-286), verified 2026-06-09. Root cause of both symptoms was the OLD hand-rolled overlay, not the pure helpers (activeSlashQuery/filterSlashCommands always handled ''-''): (1) the old overlay didn''t narrow live as you typed, so a ''-'' looked like it emptied/broke the list; (2) the old focus model raced Tab. Post-migration the popover narrows live (ValueNotifier) and the field keeps focus (captureFocus:false), so ''-'' filters normally and Tab reliably completes. Confirmed bare ''-'' is NOT a keybinding (zoom is ctrl/meta+minus). Added regression tests: hyphen cases in slash_commands_test; composer widget test typing through a hyphen + Tab-accept. Commit d8b9a41.', NULL, '2026-06-09 15:03:23', '2026-06-09 15:03:23', '2026-06-09 15:03:23', NULL, 'bce323a83d11cce9aa109b64b723a609', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'status', 'backlog', 'in_progress', NULL, '2026-06-09 15:03:37', '2026-06-09 15:03:37', '2026-06-09 15:03:37', NULL, '6e3a604a8718416923865ef53bec19bc', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'status', 'in_progress', 'done', NULL, '2026-06-09 15:09:05', '2026-06-09 15:09:05', '2026-06-09 15:09:05', NULL, '38a4f5c2f60bd3bcfabb99869c0deba0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'description', 'test/pty/session_test.dart:47 ''write sends keystrokes to child'' (and its sibling pty integration tests) intermittently fail when run in a large parallel pool — observed twice during T-29: once in a bare ''flutter test --coverage'' (whole suite, max parallelism + coverage instrumentation) and once mid pre-push gate (''+570 -1''). Passes reliably in isolation and via the gate''s SERIAL test-core pass (''dart test test/pty'', +571 ok), so it is not a logic bug.
|
||
|
||
Root cause: these tests spawn a real /bin/sh via NativePty and wait on the reader ISOLATE to deliver bytes. Under heavy CPU contention the reader isolate is starved and the 20s ioTimeout (test/helpers/timeouts.dart) elapses before the shell''s first byte / echo response is delivered. Already hardened once (T-108 swapped wall-clock sleeps for event-driven waits; first-byte Completer + 25ms-poll _waitForBuffer), but the isolate-scheduling assumption still breaks under load.
|
||
|
||
These tests are tagged ''pty'' and are meant to run in the gate''s serial pass. Fix options: (a) ensure the parallel coverage pass EXCLUDES tag:pty (run pty only in the serial pass) so they never compete for isolate scheduling — verify ci/test.sh / dart_test.yaml tag routing; (b) harden against isolate starvation (e.g. give the reader isolate priority, or assert on delivery via a more robust signal); (c) document that ''flutter test --coverage'' over the whole tree is not a supported invocation (use make test + the gate). Prefer (a). Refs: T-108, T-96 (reader-isolate hangs), T-192 (gate parallel/serial passes), D-23 (test pyramid).', 'test/pty/session_test.dart:47 ''write sends keystrokes to child'' (and its sibling pty integration tests) intermittently fail when run in a large parallel pool — observed twice during T-29: once in a bare ''flutter test --coverage'' (whole suite, max parallelism + coverage instrumentation) and once mid pre-push gate (''+570 -1''). Passes reliably in isolation and via the gate''s SERIAL test-core pass (''dart test test/pty'', +571 ok), so it is not a logic bug.
|
||
|
||
Root cause: these tests spawn a real /bin/sh via NativePty and wait on the reader ISOLATE to deliver bytes. Under heavy CPU contention the reader isolate is starved and the 20s ioTimeout (test/helpers/timeouts.dart) elapses before the shell''s first byte / echo response is delivered. Already hardened once (T-108 swapped wall-clock sleeps for event-driven waits; first-byte Completer + 25ms-poll _waitForBuffer), but the isolate-scheduling assumption still breaks under load.
|
||
|
||
These tests are tagged ''pty'' and are meant to run in the gate''s serial pass. Fix options: (a) ensure the parallel coverage pass EXCLUDES tag:pty (run pty only in the serial pass) so they never compete for isolate scheduling — verify ci/test.sh / dart_test.yaml tag routing; (b) harden against isolate starvation (e.g. give the reader isolate priority, or assert on delivery via a more robust signal); (c) document that ''flutter test --coverage'' over the whole tree is not a supported invocation (use make test + the gate). Prefer (a). Refs: T-108, T-96 (reader-isolate hangs), T-192 (gate parallel/serial passes), D-23 (test pyramid).
|
||
|
||
FIXED 2026-06-09 (commit 0231cb4). Root cause: ci/test_core.sh ran ''dart test test/ipc test/pty ... test/pql'' in the DEFAULT parallel pool, so test/pty''s real-PTY tests contended for fds+CPU with the other core suites and the reader isolate was starved. (ci/test.sh already isolated pty via --concurrency=1 --tags pty; test_core.sh was the gap.) My manual ''flutter test --coverage'' repro was a separate wrong-invocation artifact — the flutter runner can''t reliably deliver the PTY master fd, which is why the gate excludes pty from the flutter pool entirely.) Fix: test_core.sh now runs a serial ''--concurrency=1 --tags pty'' pass + a parallel ''--exclude-tags pty'' pass, mirroring ci/test.sh. Verified: pty pass +5 stable across repeated runs; full core +571 unchanged. Option (a) from the ticket.', NULL, '2026-06-09 15:09:05', '2026-06-09 15:09:05', '2026-06-09 15:09:05', NULL, '9182c7efd54567ce5dc158eb0d01d137', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'status', 'backlog', 'ready', NULL, '2026-06-09 15:10:57', '2026-06-09 15:10:57', '2026-06-09 15:10:57', NULL, 'c7f6e34ce3fd1a90a22d758f573f2676', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42WW0SY3XEXKE8PK8', 'status', 'backlog', 'ready', NULL, '2026-06-09 15:13:06', '2026-06-09 15:13:06', '2026-06-09 15:13:06', NULL, '0c5e9a67ba3131bef2e761a8db3cb753', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'status', 'backlog', 'ready', NULL, '2026-06-09 15:13:25', '2026-06-09 15:13:25', '2026-06-09 15:13:25', NULL, 'f4fbca5fcde6b9589ae5aad230b08a47', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'description', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.
|
||
|
||
CANCELLED as a duplicate of T-236 (2026-06-09). T-236 is the concrete spec for rendering pasted @path images inline in the Claude conversation; its scope now includes T-254''s path-as-caption nuance. Implement under T-236.', NULL, '2026-06-09 15:14:46', '2026-06-09 15:14:46', '2026-06-09 15:14:46', NULL, '6faa9d5cb777b2aa1d0a996c8f542c26', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'description', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).
|
||
|
||
MERGED T-254 (duplicate) into this ticket. Extra nuance carried over from T-254: keep the file path available (e.g. as a small caption/subtitle under the thumbnail, or via the lightbox) so it can still be copied/referenced. Reuse the existing inline image card + lightbox (T-252) rather than a new component.', NULL, '2026-06-09 15:14:46', '2026-06-09 15:14:46', '2026-06-09 15:14:46', NULL, '76b992a5d460e81ec313ae6154dea1f6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'status', 'ready', 'cancelled', NULL, '2026-06-09 15:14:46', '2026-06-09 15:14:46', '2026-06-09 15:14:46', NULL, 'de7af22be9beaa7c28d65a5fdde3ca8e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'status', 'cancelled', 'ready', NULL, '2026-06-09 15:15:18', '2026-06-09 15:15:18', '2026-06-09 15:15:18', NULL, '3478f18c0380519c915fa463bc12289f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'description', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).
|
||
|
||
MERGED T-254 (duplicate) into this ticket. Extra nuance carried over from T-254: keep the file path available (e.g. as a small caption/subtitle under the thumbnail, or via the lightbox) so it can still be copied/referenced. Reuse the existing inline image card + lightbox (T-252) rather than a new component.', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).
|
||
|
||
MERGED T-254 (duplicate) into this ticket. Extra nuance carried over from T-254: keep the file path available (e.g. as a small caption/subtitle under the thumbnail, or via the lightbox) so it can still be copied/referenced. Reuse the existing inline image card + lightbox (T-252) rather than a new component.
|
||
|
||
RETRACTED the earlier ''merged T-254'' note — T-236 and T-254 are distinct tickets (per user); disregard that note. T-236 stays scoped to its own description.', NULL, '2026-06-09 15:15:18', '2026-06-09 15:15:18', '2026-06-09 15:15:18', NULL, '5047d91e71c748a7a9c2e1ad29c636c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'description', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.
|
||
|
||
CANCELLED as a duplicate of T-236 (2026-06-09). T-236 is the concrete spec for rendering pasted @path images inline in the Claude conversation; its scope now includes T-254''s path-as-caption nuance. Implement under T-236.', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.
|
||
|
||
CANCELLED as a duplicate of T-236 (2026-06-09). T-236 is the concrete spec for rendering pasted @path images inline in the Claude conversation; its scope now includes T-254''s path-as-caption nuance. Implement under T-236.
|
||
|
||
RETRACTED the cancellation — NOT a duplicate of T-236 (per user, 2026-06-09). Both stay open. T-254 remains its own ticket.', NULL, '2026-06-09 15:15:18', '2026-06-09 15:15:18', '2026-06-09 15:15:18', NULL, '51620d0b6f97ab50c29fc12ad0a9632d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.', NULL, '2026-06-09 15:21:37', '2026-06-09 15:21:37', '2026-06-09 15:21:37', NULL, '674ce50eb5d3471f5c024942badb8c1a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).', NULL, '2026-06-09 15:24:00', '2026-06-09 15:24:00', '2026-06-09 15:24:00', NULL, 'bf8f7751e6379b8a3fc57cdab29566fc', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.', NULL, '2026-06-09 15:28:16', '2026-06-09 15:28:16', '2026-06-09 15:28:16', NULL, 'f415ce161a4fe67f8c24f4f6573a762a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.', NULL, '2026-06-09 15:29:08', '2026-06-09 15:29:08', '2026-06-09 15:29:08', NULL, '25d8d4bf9893db066dd16e5d06a10968', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'status', 'backlog', 'ready', NULL, '2026-06-09 15:29:11', '2026-06-09 15:29:11', '2026-06-09 15:29:11', NULL, 'a60a2682884dc0534ff386a4f029c97c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.', NULL, '2026-06-09 15:30:16', '2026-06-09 15:30:16', '2026-06-09 15:30:16', NULL, '240bef9edb3bf7d0d695673085f9498a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.', NULL, '2026-06-09 15:30:41', '2026-06-09 15:30:41', '2026-06-09 15:30:41', NULL, '817cb40d9b8f5d92a00e453d1747b7a2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.
|
||
|
||
GLYPH DECISION: use Phosphor caret-line icons (chevron + edge line), which read as "collapse to the edge":
|
||
- caret-line-left -> 0xe132 (CaretLineLeft)
|
||
- caret-line-right -> 0xe130 (CaretLineRight)
|
||
The glyphs already ship in assets/fonts/phosphor (codepoints.csv lines 150-151) — no new dependency. Add two consts to lib/widgets/src/icons/phosphor.dart (PhosphorIcons.caretLineLeft / caretLineRight) during implementation. Chevron-line direction flips per isCollapsed.', NULL, '2026-06-09 15:33:14', '2026-06-09 15:33:14', '2026-06-09 15:33:14', NULL, 'bef1363660f958fbda57d1a1ab2c3600', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.
|
||
|
||
GLYPH DECISION: use Phosphor caret-line icons (chevron + edge line), which read as "collapse to the edge":
|
||
- caret-line-left -> 0xe132 (CaretLineLeft)
|
||
- caret-line-right -> 0xe130 (CaretLineRight)
|
||
The glyphs already ship in assets/fonts/phosphor (codepoints.csv lines 150-151) — no new dependency. Add two consts to lib/widgets/src/icons/phosphor.dart (PhosphorIcons.caretLineLeft / caretLineRight) during implementation. Chevron-line direction flips per isCollapsed.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.
|
||
|
||
GLYPH DECISION: use Phosphor caret-line icons (chevron + edge line), which read as "collapse to the edge":
|
||
- caret-line-left -> 0xe132 (CaretLineLeft)
|
||
- caret-line-right -> 0xe130 (CaretLineRight)
|
||
The glyphs already ship in assets/fonts/phosphor (codepoints.csv lines 150-151) — no new dependency. Add two consts to lib/widgets/src/icons/phosphor.dart (PhosphorIcons.caretLineLeft / caretLineRight) during implementation. Chevron-line direction flips per isCollapsed.
|
||
|
||
RESOLUTION (2026-06-09): considered moving the re-open control into the center (pane edge or center status segment) to dodge the end-of-bar space contention; user chose to KEEP TOGGLES FIXED AT THE STATUS-BAR ENDS (best muscle memory). Final design = v3 mock: caret-line glyphs at fixed far-left/far-right status-bar cells (~24px reserved, side items shifted in), chevron-line flips per isCollapsed, firing existing sidebar.collapse / context.collapse. Design fully settled; ready to implement.', NULL, '2026-06-09 15:34:11', '2026-06-09 15:34:11', '2026-06-09 15:34:11', NULL, '194a6e362e707647b03610226991cdf1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42WW0SY3XEXKE8PK8', 'status', 'ready', 'done', NULL, '2026-06-09 15:36:15', '2026-06-09 15:36:15', '2026-06-09 15:36:15', NULL, '4daeb008d8c9b1f137995c7eef7d133d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42WW0SY3XEXKE8PK8', 'description', 'Picking a theme (status-bar switcher T-234, or the Settings modal) applies live but is never saved, so every reload resets to the first bundled theme. The choice should persist PER REPO.
|
||
|
||
Root cause: ThemeController.select() (lib/kernel/src/theme/controller.dart:43) only mutates _currentName + notifies — no persistence. It is constructed in facade.dart:160 as ThemeController(bundled: bundledThemes) with no initialName, so on boot it always falls back to bundled.first.
|
||
|
||
Infra already exists — no new store needed: SettingsStore (lib/kernel/src/settings.dart) has a PROJECT scope that persists to <repo>/.clide/settings.yaml via ''project.*'' keys, and ProjectManager.openProject() already calls settings.setProjectDir() on open (project.dart:130), which loads that file.
|
||
|
||
Proposed fix:
|
||
1. PERSIST on change — wire a listener on ThemeController (in facade.dart, where both theme + settings exist) that writes the current theme to settings whenever currentName changes; use project scope (''project.theme'') when a repo is open. currentName already encodes the high-contrast variant (resolveThemeName -> ''<base>-hc''), so the HC toggle persists for free.
|
||
2. RESTORE on load — the project dir is set AFTER boot, so restoration must hook project-open (ProjectManager.openProject / settings.setProjectDir), not just construction: when a project opens, read ''project.theme'' and, if present + known, theme.select() it. Guard ArgumentError for a theme that is no longer bundled (fall back to default, don''t throw).
|
||
|
||
Open question: app-scoped fallback. Persist project-scoped per the report; optionally also write ''app.theme'' as the global default so a brand-new repo inherits the last choice instead of bundled.first. Decide during implementation.
|
||
|
||
Acceptance: pick a theme (incl. the High-contrast toggle) -> restart -> same theme restored for that repo; two repos keep independent choices; with no repo open, the app default applies; a saved theme that no longer exists degrades to the default without throwing.
|
||
|
||
Tests: SettingsStore round-trips ''project.theme''; opening a project applies the saved theme to the controller; select() persists; unknown saved theme falls back. Refs: T-234 (theme switcher), T-288/D-88 (picker), SettingsStore, ProjectManager.', 'Picking a theme (status-bar switcher T-234, or the Settings modal) applies live but is never saved, so every reload resets to the first bundled theme. The choice should persist PER REPO.
|
||
|
||
Root cause: ThemeController.select() (lib/kernel/src/theme/controller.dart:43) only mutates _currentName + notifies — no persistence. It is constructed in facade.dart:160 as ThemeController(bundled: bundledThemes) with no initialName, so on boot it always falls back to bundled.first.
|
||
|
||
Infra already exists — no new store needed: SettingsStore (lib/kernel/src/settings.dart) has a PROJECT scope that persists to <repo>/.clide/settings.yaml via ''project.*'' keys, and ProjectManager.openProject() already calls settings.setProjectDir() on open (project.dart:130), which loads that file.
|
||
|
||
Proposed fix:
|
||
1. PERSIST on change — wire a listener on ThemeController (in facade.dart, where both theme + settings exist) that writes the current theme to settings whenever currentName changes; use project scope (''project.theme'') when a repo is open. currentName already encodes the high-contrast variant (resolveThemeName -> ''<base>-hc''), so the HC toggle persists for free.
|
||
2. RESTORE on load — the project dir is set AFTER boot, so restoration must hook project-open (ProjectManager.openProject / settings.setProjectDir), not just construction: when a project opens, read ''project.theme'' and, if present + known, theme.select() it. Guard ArgumentError for a theme that is no longer bundled (fall back to default, don''t throw).
|
||
|
||
Open question: app-scoped fallback. Persist project-scoped per the report; optionally also write ''app.theme'' as the global default so a brand-new repo inherits the last choice instead of bundled.first. Decide during implementation.
|
||
|
||
Acceptance: pick a theme (incl. the High-contrast toggle) -> restart -> same theme restored for that repo; two repos keep independent choices; with no repo open, the app default applies; a saved theme that no longer exists degrades to the default without throwing.
|
||
|
||
Tests: SettingsStore round-trips ''project.theme''; opening a project applies the saved theme to the controller; select() persists; unknown saved theme falls back. Refs: T-234 (theme switcher), T-288/D-88 (picker), SettingsStore, ProjectManager.
|
||
|
||
FIXED 2026-06-09 (this commit). Added lib/kernel/src/theme/theme_persistence.dart + wireThemePersistence() in facade. Decision on the open question: persist BOTH project.theme (per-repo, in .clide/settings.yaml) AND app.theme (global default) — so a themed repo keeps its choice, an unthemed/new repo inherits the last global choice, and the HC variant persists (name encodes -hc). Restore prefers project over app; unknown theme is ignored (no throw). Tests: test/kernel/src/theme_persistence_test.dart (6 cases — persist app/project, HC, restore-on-open, boot restore, unknown fallback). All green.', NULL, '2026-06-09 15:36:15', '2026-06-09 15:36:15', '2026-06-09 15:36:15', NULL, '767b65a07e73e6a0299fe19d7851b8c2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'description', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.
|
||
|
||
WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards:
|
||
- [3 edits] (the first edit run)
|
||
- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run)
|
||
- [7 edits] (the second edit run)
|
||
Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card.', NULL, '2026-06-09 15:45:12', '2026-06-09 15:45:12', '2026-06-09 15:45:12', NULL, 'b7535e024ee171cb46ae7bad08d23092', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'description', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.
|
||
|
||
WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards:
|
||
- [3 edits] (the first edit run)
|
||
- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run)
|
||
- [7 edits] (the second edit run)
|
||
Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card.', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.
|
||
|
||
WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards:
|
||
- [3 edits] (the first edit run)
|
||
- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run)
|
||
- [7 edits] (the second edit run)
|
||
Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card.
|
||
|
||
STATUS INDICATOR (live tick reuse): the bundled edits card carries ONE header status indicator that reuses the existing per-step success tick, driven by the latest edit''s state:
|
||
- edit in flight -> spinner
|
||
- edit completed -> success check (the current green tick)
|
||
- next edit starts -> back to spinner
|
||
- ...repeat, settling on check when the final edit in the run completes (or the error glyph if one fails).
|
||
So the collapsed card''s indicator animates spinner<->check as the run grows, rather than showing a static tick. Each individual edit keeps its own tick in the expanded list (unchanged); this is the aggregate indicator on the holder header/ticker. Mirror the same treatment for the existing "# steps" activity card if it doesn''t already do this.', NULL, '2026-06-09 15:46:02', '2026-06-09 15:46:02', '2026-06-09 15:46:02', NULL, 'ac4da1997b2c2cba25b78ed97ba9e9be', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'status', 'ready', 'done', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, '2453f53a89cda92cea0734929c9d1a81', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'description', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.
|
||
|
||
CANCELLED as a duplicate of T-236 (2026-06-09). T-236 is the concrete spec for rendering pasted @path images inline in the Claude conversation; its scope now includes T-254''s path-as-caption nuance. Implement under T-236.
|
||
|
||
RETRACTED the cancellation — NOT a duplicate of T-236 (per user, 2026-06-09). Both stay open. T-254 remains its own ticket.', 'When the user pastes an image, the conversation panel echoes back the file path as plain text (e.g. @/home/.../paste-<ts>.png). We now have an image viewer card — render the pasted image inline using that card instead of (or in addition to) the bare path.
|
||
|
||
## Behaviour
|
||
- Detect a pasted-image path in the conversation stream and render the image viewer card.
|
||
- Still surface the file path (e.g. as a caption / subtitle on the card) so it can be copied/referenced.
|
||
- Reuse the existing image viewer card component rather than building a new one.
|
||
|
||
## Notes
|
||
- Applies to the Claude conversation panel rendering path.
|
||
- Consider failure cases: missing/deleted file, non-image paste, very large images.
|
||
|
||
CANCELLED as a duplicate of T-236 (2026-06-09). T-236 is the concrete spec for rendering pasted @path images inline in the Claude conversation; its scope now includes T-254''s path-as-caption nuance. Implement under T-236.
|
||
|
||
RETRACTED the cancellation — NOT a duplicate of T-236 (per user, 2026-06-09). Both stay open. T-254 remains its own ticket.
|
||
|
||
DONE 2026-06-09 — satisfied by D-89 together with T-236. The lightbox-expansion + path-retention (filename a11y label / unchanged copyText) intent from T-254 is delivered by the shared ImageThumbnail → lightbox; the chosen presentation is inline (not a separate card) per the user. See T-236 / D-89 / commit.', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, '592144661bfd762b61789f05d958cdd4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'description', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).
|
||
|
||
MERGED T-254 (duplicate) into this ticket. Extra nuance carried over from T-254: keep the file path available (e.g. as a small caption/subtitle under the thumbnail, or via the lightbox) so it can still be copied/referenced. Reuse the existing inline image card + lightbox (T-252) rather than a new component.
|
||
|
||
RETRACTED the earlier ''merged T-254'' note — T-236 and T-254 are distinct tickets (per user); disregard that note. T-236 stays scoped to its own description.', 'When a user message contains a pasted-screenshot reference — an @<path> token pointing at an image (the composer''s ComposerAttachment.pathToken format, e.g. @/home/<user>/.cache/clide/pasted/paste-<ts>.png, see screenshot) — the conversation log renders it as the raw path string. Show it as an inline THUMBNAIL instead.
|
||
|
||
The composer already renders pre-send attachments as Image.file thumbnail chips (clipboard_paste.dart + claude_composer _chip); this carries that into the post-send message log (UserMessage rendering in conversation_view/conversation_card).
|
||
|
||
Scope:
|
||
- When rendering UserMessage text, detect @<path> tokens; for image paths (reuse the extension check behind _looksLikeImage: .png/.jpg/.jpeg/.gif/.webp/.bmp), render a bounded Image.file thumbnail in place of the bare token. Surrounding prose still renders normally; a message can carry multiple tokens.
|
||
- Click/activate the thumbnail to open the full-size image (a preview dialog). Keyboard-operable + a11y label (filename).
|
||
- Graceful fallback: a missing/unreadable file (pasted temp files can be cleaned up) degrades to a small placeholder or the path text via Image.file errorBuilder — never a crash/exception.
|
||
- Non-image @path tokens MAY render as a file chip (filename + icon) like the composer, but image thumbnails are the focus.
|
||
- Note: Image.file reads the path directly via dart:io, so this is NOT gated by the files.read allow-list (D-80) — the cache dir is outside the workspace and that''s fine for display.
|
||
- Display-only: the text actually sent to Claude is unchanged; this only affects rendering.
|
||
|
||
Acceptance:
|
||
1. A user message with an @<path> image token shows an inline thumbnail in the log (not the raw path).
|
||
2. Clicking/activating the thumbnail opens the full image.
|
||
3. A missing/unreadable referenced file degrades to a placeholder (or the path text), no exception.
|
||
4. Prose and any non-image @path tokens around it still render readably; multiple tokens in one message all resolve.
|
||
5. The message content delivered to Claude is unchanged (render-only). Relates to T-142 (paste attachments).
|
||
|
||
MERGED T-254 (duplicate) into this ticket. Extra nuance carried over from T-254: keep the file path available (e.g. as a small caption/subtitle under the thumbnail, or via the lightbox) so it can still be copied/referenced. Reuse the existing inline image card + lightbox (T-252) rather than a new component.
|
||
|
||
RETRACTED the earlier ''merged T-254'' note — T-236 and T-254 are distinct tickets (per user); disregard that note. T-236 stays scoped to its own description.
|
||
|
||
DONE 2026-06-09 — implemented as the hybrid (D-89): inline thumbnail in the message prose that opens the lightbox on click/Enter. New ImageThumbnail + openImageLightbox (lib/builtin/claude/src/image_thumbnail.dart); ClideMarkdown gained an onImageToken WidgetSpan seam. Missing file → placeholder; multiple tokens; non-image @path stays text; render-only (copyText unchanged). Composer chips reuse the same 44px thumbnail. Tests: image_thumbnail_test + conversation_view_test cases. Closes alongside T-254.', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, '8585bb4d9f6890c70777706d4225cef1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'status', 'ready', 'done', NULL, '2026-06-09 15:55:06', '2026-06-09 15:55:06', '2026-06-09 15:55:06', NULL, '862cc2b139b96265c8d8d693f581e943', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RXN6HW6XTEHKYQDG', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:01:58', '2026-06-09 16:01:58', '2026-06-09 16:01:58', NULL, '1caefc65216b108ebf53b512c21997a8', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:02:14', '2026-06-09 16:02:14', '2026-06-09 16:02:14', NULL, '1ccbef8e24e7315c9f907940a2a6db1c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'status', 'ready', 'done', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, '1ff5b1a09d01bbc6bf265c33dbdb8025', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'status', 'ready', 'done', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, '550373b90ef92ee9bf9b298f303f3bef', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RXN6HW6XTEHKYQDG', 'description', 'When the bottom input area changes height, the conversation view does not re-scroll to the new viewport bottom, so the last bit of content is left hidden behind the newly-sized box.
|
||
|
||
Trigger: anything that resizes the bottom interaction zone (D-78) — e.g. opening a permission dialog or the AskUserQuestion UI, which replace/expand the composer. The taller box shrinks the conversation viewport from the bottom, but the scroll offset isn''t adjusted, so content that was at the bottom edge is now occluded.
|
||
|
||
Expected: when the interaction zone grows, the conversation re-anchors so the previously-visible bottom content stays visible above the box (preserve bottom-anchoring / keep the tail in view). Symmetric on shrink — no leftover gap when the box collapses back.
|
||
|
||
Repro:
|
||
1. Scroll the Claude conversation to the bottom (tail in view).
|
||
2. Trigger a permission prompt or AskUserQuestion (interaction zone expands).
|
||
3. Observed: a strip of the last message/card is hidden behind the enlarged input box.
|
||
4. Expected: view scrolls so that content remains fully visible above the box.
|
||
|
||
Notes:
|
||
- Likely the scroll controller doesn''t react to the composer/interaction-zone height change (no re-scroll on viewport-inset/size change). Audit the conversation view''s scroll handling around interaction-zone show/hide (D-78) and on keyboard/box resize.
|
||
- Affects the Claude conversation panel.', 'When the bottom input area changes height, the conversation view does not re-scroll to the new viewport bottom, so the last bit of content is left hidden behind the newly-sized box.
|
||
|
||
Trigger: anything that resizes the bottom interaction zone (D-78) — e.g. opening a permission dialog or the AskUserQuestion UI, which replace/expand the composer. The taller box shrinks the conversation viewport from the bottom, but the scroll offset isn''t adjusted, so content that was at the bottom edge is now occluded.
|
||
|
||
Expected: when the interaction zone grows, the conversation re-anchors so the previously-visible bottom content stays visible above the box (preserve bottom-anchoring / keep the tail in view). Symmetric on shrink — no leftover gap when the box collapses back.
|
||
|
||
Repro:
|
||
1. Scroll the Claude conversation to the bottom (tail in view).
|
||
2. Trigger a permission prompt or AskUserQuestion (interaction zone expands).
|
||
3. Observed: a strip of the last message/card is hidden behind the enlarged input box.
|
||
4. Expected: view scrolls so that content remains fully visible above the box.
|
||
|
||
Notes:
|
||
- Likely the scroll controller doesn''t react to the composer/interaction-zone height change (no re-scroll on viewport-inset/size change). Audit the conversation view''s scroll handling around interaction-zone show/hide (D-78) and on keyboard/box resize.
|
||
- Affects the Claude conversation panel.
|
||
|
||
DONE 2026-06-09 (commit). conversation_view tracks at-bottom + a LayoutBuilder re-jumps to the tail when the viewport height changes (interaction-zone resize, D-78), only when pinned. Tests: conversation_scroll_test.', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, '8c8361c593bce055c595ee3d6eca594c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'description', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.
|
||
|
||
WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards:
|
||
- [3 edits] (the first edit run)
|
||
- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run)
|
||
- [7 edits] (the second edit run)
|
||
Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card.
|
||
|
||
STATUS INDICATOR (live tick reuse): the bundled edits card carries ONE header status indicator that reuses the existing per-step success tick, driven by the latest edit''s state:
|
||
- edit in flight -> spinner
|
||
- edit completed -> success check (the current green tick)
|
||
- next edit starts -> back to spinner
|
||
- ...repeat, settling on check when the final edit in the run completes (or the error glyph if one fails).
|
||
So the collapsed card''s indicator animates spinner<->check as the run grows, rather than showing a static tick. Each individual edit keeps its own tick in the expanded list (unchanged); this is the aggregate indicator on the holder header/ticker. Mirror the same treatment for the existing "# steps" activity card if it doesn''t already do this.', 'When Claude makes multiple subsequent edits to the SAME file, bundle them into a single collapsed holder card instead of rendering each edit as its own card — mirroring how we already fold meta/activity runs.
|
||
|
||
Behaviour:
|
||
- Detect a run of consecutive edits targeting the same file_path and group them into one ClideHolderCard (the shared container from T-266; see _ActivityCard in lib/builtin/claude/src/conversation_view.dart:681).
|
||
- Collapsed (default): one-line ticker of the latest edit + a count label.
|
||
- Show the count as "# edits" (e.g. "3 edits" / "1 edit"), NOT "# steps". The existing activity card uses stepLabel = ''$count steps'' at conversation_view.dart:683; this run wants an edits-flavoured label.
|
||
- Expanded: every individual edit, each with its full report — bundle, do NOT drop or summarise away any information. All per-edit detail must remain reachable on expand.
|
||
|
||
Notes:
|
||
- Reuse the existing ClideHolderCard / folding machinery rather than building a new card.
|
||
- Grouping breaks when the file_path changes or a non-edit step interleaves (consecutive-same-file only), matching the ''subsequent edits to the same file'' wording.
|
||
- Parity with the existing meta/activity folding (T-230) — same collapse/expand affordance, just an edits-labelled run.
|
||
|
||
WORKED EXAMPLE (from user screenshot, clide_markdown.dart run): the current stream renders 11 stacked cards — 3x "Edit clide_markdown.dart", then a folded "Read … 2 steps" holder, then 7x "Edit clide_markdown.dart". With this feature it collapses to THREE cards:
|
||
- [3 edits] (the first edit run)
|
||
- [2 steps] (the existing Read holder — unchanged; this is what splits the edit run)
|
||
- [7 edits] (the second edit run)
|
||
Confirms the split rule: an interleaving non-edit step (here the folded Read run) breaks the consecutive-same-file edit grouping into two separate edit cards. Same-file edits with nothing between them collapse into one "# edits" card.
|
||
|
||
STATUS INDICATOR (live tick reuse): the bundled edits card carries ONE header status indicator that reuses the existing per-step success tick, driven by the latest edit''s state:
|
||
- edit in flight -> spinner
|
||
- edit completed -> success check (the current green tick)
|
||
- next edit starts -> back to spinner
|
||
- ...repeat, settling on check when the final edit in the run completes (or the error glyph if one fails).
|
||
So the collapsed card''s indicator animates spinner<->check as the run grows, rather than showing a static tick. Each individual edit keeps its own tick in the expanded list (unchanged); this is the aggregate indicator on the holder header/ticker. Mirror the same treatment for the existing "# steps" activity card if it doesn''t already do this.
|
||
|
||
DONE 2026-06-09 (commit). coalesceEditRuns folds consecutive same-file edits into one ''# edits'' ClideHolderCard; aggregate live status via new ClideStatusIndicator (running/success/error) + ClideSpinner (logo-mark, 3D Y rotation, reduced-motion aware), shared with the activity card. Per user: spinner is a self-contained component (not built on ConversationCard''s mark) with an AnimatedSwitcher seam for a richer spinner→check transition later. Tests: activity_cluster_test (coalesce), conversation_view_test (edits card), clide_status_indicator_test.', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, 'd4aed0a994dbdc503811e7c309e96f7c', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RXN6HW6XTEHKYQDG', 'status', 'ready', 'done', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, 'ed7b38da5bfea63147029d903cc57728', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'description', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.
|
||
|
||
GLYPH DECISION: use Phosphor caret-line icons (chevron + edge line), which read as "collapse to the edge":
|
||
- caret-line-left -> 0xe132 (CaretLineLeft)
|
||
- caret-line-right -> 0xe130 (CaretLineRight)
|
||
The glyphs already ship in assets/fonts/phosphor (codepoints.csv lines 150-151) — no new dependency. Add two consts to lib/widgets/src/icons/phosphor.dart (PhosphorIcons.caretLineLeft / caretLineRight) during implementation. Chevron-line direction flips per isCollapsed.
|
||
|
||
RESOLUTION (2026-06-09): considered moving the re-open control into the center (pane edge or center status segment) to dodge the end-of-bar space contention; user chose to KEEP TOGGLES FIXED AT THE STATUS-BAR ENDS (best muscle memory). Final design = v3 mock: caret-line glyphs at fixed far-left/far-right status-bar cells (~24px reserved, side items shifted in), chevron-line flips per isCollapsed, firing existing sidebar.collapse / context.collapse. Design fully settled; ready to implement.', 'Design and add collapse/expand controls for the left sidebar and the right context pane, along the lines of the reference screenshot (green arrows mark the two intended affordance locations — bottom-left of the sidebar and bottom-right of the context pane).
|
||
|
||
Scope of this ticket: set up a Frame0 mock to talk through the design before implementing.
|
||
|
||
Open questions to resolve in the mock:
|
||
- Affordance placement: footer/status-bar anchored (as the screenshot arrows suggest) vs. pane-edge chevron.
|
||
- Collapsed state: fully hidden vs. thin rail with a re-expand handle.
|
||
- Iconography (chevron direction) and hover/active states.
|
||
- Whether sidebar and context pane share one control pattern (parity) or differ.
|
||
- Keyboard/CLI parity (D-6): each collapse action needs a clide verb.
|
||
|
||
Deliverable: Frame0 wireframe(s) of collapsed + expanded states for both panes, reviewed before any code.
|
||
|
||
DESIGN DIRECTION (settled): anchor both toggles on the OUTER EDGES of the center (Claude conversation) pane — one on the left edge controlling the sidebar, one on the right edge controlling the context pane. The control stays fixed on the center-pane edge whether the adjacent pane is open or collapsed, so a single button both collapses an open pane and re-opens a collapsed one (chevron flips direction). This avoids needing a separate "re-expand" handle on the collapsed pane.
|
||
|
||
Implications for the mock:
|
||
- Collapsed pane can be fully hidden (no thin rail needed) since the re-open control lives on the center edge.
|
||
- Sidebar and context pane share one mirrored control pattern (parity).
|
||
- Chevron direction reflects state: points outward to expand, inward to collapse.
|
||
|
||
IMPLEMENTATION NOTE: the collapse logic already exists — no new toggle behaviour needed. Commands `sidebar.collapse` (ctrl+shift+1) and `context.collapse` (ctrl+shift+3) are registered in lib/builtin/default_layout/src/extension.dart, exposed in the command palette + menubar, and call arrangement.toggleCollapsed(Slots.sidebar|contextPanel), returning isCollapsed (D-051, D-054).
|
||
|
||
So this ticket is scoped to the VISUAL AFFORDANCE only:
|
||
- Add the two edge-anchored toggle buttons on the center (Claude) pane''s outer edges.
|
||
- On click, invoke the existing `sidebar.collapse` / `context.collapse` commands (do NOT reimplement collapse).
|
||
- Read arrangement.isCollapsed(...) to flip the chevron direction per state.
|
||
- D-6 CLI/keyboard parity is already satisfied by the existing commands; this adds the mouse affordance.
|
||
|
||
Mock: docs/design/wireframes/hud/pane-collapse-toggles.{json,png} — State A (open) + State B (collapsed).
|
||
|
||
PLACEMENT REVISED: toggles do NOT float vertically-centered on the pane edges. They live in the BOTTOM STATUS BAR. Each toggle is horizontally pinned to the center pane''s left/right edge, so when a pane collapses the toggle slides along the status bar to that end (open: at the inner pane boundary; collapsed: at the far status-bar end — matching where the reference-screenshot arrows pointed). Still mirrored left/right for parity; chevron flips per isCollapsed. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
APPROVED (2026-06-09): pinned-to-edge status-bar placement confirmed by user. Design is settled; ready for implementation.
|
||
|
||
PLACEMENT FINAL (supersedes "pinned-to-edge"): toggles are FIXED at the far-left and far-right ends of the bottom status bar in every state. They do not slide with the pane edge. Position is constant (muscle memory; always where the reference-screenshot arrows pointed); only the chevron flips per isCollapsed. Left end = sidebar toggle, right end = context toggle. Mirrored for parity. Buttons invoke the existing sidebar.collapse / context.collapse commands.
|
||
|
||
Mock updated: docs/design/wireframes/hud/pane-collapse-toggles.{json,png}.
|
||
|
||
SPACE NOTE: the status bar already hosts content at both ends (left: branch / skills count; right: Output / terminal). Fixed-end toggles therefore share that space. Resolution: reserve the OUTERMOST ~24px cell at each end for the toggle and shift the existing status items inward by that width — a constant reservation, not a dynamic fight (the toggle never moves or grows). The mock reflects this: status text begins after the left toggle and ends before the right toggle.
|
||
|
||
GLYPH DECISION: use Phosphor caret-line icons (chevron + edge line), which read as "collapse to the edge":
|
||
- caret-line-left -> 0xe132 (CaretLineLeft)
|
||
- caret-line-right -> 0xe130 (CaretLineRight)
|
||
The glyphs already ship in assets/fonts/phosphor (codepoints.csv lines 150-151) — no new dependency. Add two consts to lib/widgets/src/icons/phosphor.dart (PhosphorIcons.caretLineLeft / caretLineRight) during implementation. Chevron-line direction flips per isCollapsed.
|
||
|
||
RESOLUTION (2026-06-09): considered moving the re-open control into the center (pane edge or center status segment) to dodge the end-of-bar space contention; user chose to KEEP TOGGLES FIXED AT THE STATUS-BAR ENDS (best muscle memory). Final design = v3 mock: caret-line glyphs at fixed far-left/far-right status-bar cells (~24px reserved, side items shifted in), chevron-line flips per isCollapsed, firing existing sidebar.collapse / context.collapse. Design fully settled; ready to implement.
|
||
|
||
DONE 2026-06-09 (commit). Fixed ~24px caret-line toggles bookend the status bar (StatusbarHost), flip chevron per arrangement.isCollapsed, fire existing sidebar.collapse/context.collapse. Visual affordance only. Tests: app_collapse_toggle_test + updated app_statusbar_test.', NULL, '2026-06-09 16:34:46', '2026-06-09 16:34:46', '2026-06-09 16:34:46', NULL, 'ef47660ba615407e22a179e30866d85a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:54:28', '2026-06-09 16:54:28', '2026-06-09 16:54:28', NULL, '7cfb52c0de285d0cf5230a72b27dfd35', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:55:49', '2026-06-09 16:55:49', '2026-06-09 16:55:49', NULL, 'f9fadc88a5e4ee925e7972f3389bcd16', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM53HQD6YV754GKBA60', 'status', 'backlog', 'done', NULL, '2026-06-09 16:56:55', '2026-06-09 16:56:55', '2026-06-09 16:56:55', NULL, 'fb4a56615b5e1e64c5cf6d12d9a3ee60', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AE2GEF6BXSGKRMGC', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:57:16', '2026-06-09 16:57:16', '2026-06-09 16:57:16', NULL, '8bba7559aeefd3fcdccacc7f7fa7a09b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BRWWPB4R06Q31XV0', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:57:23', '2026-06-09 16:57:23', '2026-06-09 16:57:23', NULL, 'ddb373332db2254e04fdda13cca96f94', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QJC039TMDF5JGMXC', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:57:35', '2026-06-09 16:57:35', '2026-06-09 16:57:35', NULL, '5501d1a65c21a3448d0ce1a1522cc90d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'status', 'backlog', 'ready', NULL, '2026-06-09 16:58:12', '2026-06-09 16:58:12', '2026-06-09 16:58:12', NULL, '6277911a72c6774b5692c9af3a2402c5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'status', 'ready', 'in_progress', NULL, '2026-06-09 17:06:47', '2026-06-09 17:06:47', '2026-06-09 17:06:47', NULL, '03f03880fba3cfc2ddb335cde60b2dfb', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'description', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool.
|
||
|
||
Why:
|
||
1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal).
|
||
2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane.
|
||
3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI.
|
||
|
||
Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget.
|
||
|
||
Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible.
|
||
|
||
Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned.
|
||
|
||
Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session.', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool.
|
||
|
||
Why:
|
||
1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal).
|
||
2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane.
|
||
3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI.
|
||
|
||
Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget.
|
||
|
||
Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible.
|
||
|
||
Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned.
|
||
|
||
Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session.
|
||
|
||
STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.', NULL, '2026-06-09 17:06:47', '2026-06-09 17:06:47', '2026-06-09 17:06:47', NULL, '1d1c8e343c2faf4f4ec24bd8abdf91c7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7D5DMYF5X92Q55CBW', 'description', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart.', 'Follow-up to T-230. The activity-card fold level is implemented as a switchable ConversationView.foldLevel parameter (FoldLevel none/tools/thinking/everything; groupConversation re-groups per level, unit-tested), but the live Claude pane currently uses the L1 default with no way for the user to change it. Finish acceptance #4: (1) read the fold level from a persisted user setting (via ctx.settings / the kernel settings service — see how other builtins read settings), defaulting to L1; (2) add a control to change it (e.g. a command ''claude.activity.fold-level'' cycling none->L1->L2->L3, and/or an entry in the Claude/settings UI); (3) thread it into the ConversationView built in claude_pane.dart (and team_panel_host.dart). The grouping + card + a11y are already done in activity_cluster.dart + conversation_view.dart.
|
||
|
||
DONE 2026-06-09. Persisted app-scoped setting kActivityFoldLevelKey (app.claude.activityFoldLevel); command claude.activity.fold-level cycles none→tools→thinking→everything (foldLevelFromName/nextFoldLevel in activity_cluster.dart); ClaudePane + team_panel_host read the setting and re-fold live via the settings notifier. Tests: foldLevelFromName/nextFoldLevel in activity_cluster_test.', NULL, '2026-06-09 17:16:33', '2026-06-09 17:16:33', '2026-06-09 17:16:33', NULL, 'b9f41312692ede5ad66e99200c785b91', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62FKQQD0B9B80PFY4', 'parent_id', 'T-132', NULL, NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, '1077ce81ca0eb879dcd1a6445d3de6e6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62FKQQD0B9B80PFY4', 'description', 'Show the account/subscription usage budget (5-hour + weekly limits, % used, reset times) in the Claude meta sidebar (T-141). BLOCKED: this data is not programmatically exposed under subscription (OAuth) auth as of claude 2.1.150 — verified empirically + via docs (2026-05-23). /usage is TUI-only (headless ''claude -p /usage'' returns only a one-liner); stats-cache.json has activity counts only; the stream-json rate_limit_event is undocumented + needs a billed turn; ''claude auth status --json'' shows plan only. A ''claude usage --json'' + a /v1/organizations/{org}/usage/subscription endpoint are an OPEN, unshipped feature request (GitHub anthropics/claude-code#44328). Revisit when #44328 ships or an API-key usage path exists. See project memory ''claude-usage-budget-not-exposed''.', 'Show the account/subscription usage budget (5-hour + weekly limits, % used, reset times) in the Claude meta sidebar (T-141). BLOCKED: this data is not programmatically exposed under subscription (OAuth) auth as of claude 2.1.150 — verified empirically + via docs (2026-05-23). /usage is TUI-only (headless ''claude -p /usage'' returns only a one-liner); stats-cache.json has activity counts only; the stream-json rate_limit_event is undocumented + needs a billed turn; ''claude auth status --json'' shows plan only. A ''claude usage --json'' + a /v1/organizations/{org}/usage/subscription endpoint are an OPEN, unshipped feature request (GitHub anthropics/claude-code#44328). Revisit when #44328 ships or an API-key usage path exists. See project memory ''claude-usage-budget-not-exposed''.
|
||
|
||
2026-06-09: detached from T-132 (which is otherwise complete) and made the RESOLVER ticket for Q-34 (how + when to surface the budget given upstream doesn''t expose it). Stays in the backlog; revisit when a viable data path lands (upstream claude usage --json / endpoint per anthropics/claude-code#44328, or an API-key usage path).', NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, '504b382920e7b07cafc1da32738625c6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7D5DMYF5X92Q55CBW', 'status', 'backlog', 'done', NULL, '2026-06-09 17:16:34', '2026-06-09 17:16:34', '2026-06-09 17:16:34', NULL, 'cbab9b537913971dba5e335d55928673', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'description', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool.
|
||
|
||
Why:
|
||
1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal).
|
||
2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane.
|
||
3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI.
|
||
|
||
Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget.
|
||
|
||
Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible.
|
||
|
||
Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned.
|
||
|
||
Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session.
|
||
|
||
STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.', 'Replace the terminal-based Claude pane with native rendering driven by Claude Code''s transcript JSONL (with cross-widget text selection), and surface experimental tmux agent teams (lead + teammates) as tiled native panels plus a team-meta sidebar. The terminal is retained only as a general IDE tool.
|
||
|
||
Why:
|
||
1. No cross-widget text selection/copy in clide''s native widgets today (regression vs a terminal).
|
||
2. PTY/TUI rendering is the fragile, OS-variant part of the Claude pane.
|
||
3. Claude Code''s experimental tmux team mode (lead + N teammates, each a tmux pane) is the flagship capability to surface as real GUI panels, not scraped TUI.
|
||
|
||
Direction: run Claude headless in tmux; render its conversation natively from the transcript JSONL; lead panel (left, with composer) + teammate tiles (right) in a responsive auto-wrap grid (1->2->3 cols); team-meta sidebar with roster + token budget.
|
||
|
||
Principle: Claude-centric first, CLI-first (D-6) a strong second. Accept isolated, version-pinned coupling to Claude Code internal contracts where it serves the Claude integration; preserve CLI/event surfaces where sensible.
|
||
|
||
Acceptance: all child tickets done; D-record landed; Claude pane renders natively with working select+copy and no terminal; a tmux team surfaces as lead + auto-wrapping teammate tiles + meta sidebar; all fragile CC-internals parsing isolated and version-pinned.
|
||
|
||
Source: plan iridescent-tinkering-umbrella (2026-05-22), grounded in an-idea.md + read-only validation of the transcript schema this session.
|
||
|
||
STATUS CORRECTED (2026-06-09): not superseded — substantially DELIVERED. 32/34 children done; the native Claude pane (transcript rendering, select/copy, no terminal), tmux team tiles + meta sidebar, composer, and D-75 all shipped. Re-flagged from ''ready'' (misleading — no startable work) to in_progress. Remaining, both non-blocking, kept under this epic: T-158 (team/account token budget — blocked on upstream /usage exposure) and T-235 (persist activity-card fold level, T-230 follow-up). Close when those land.
|
||
|
||
CLOSED 2026-06-09: all doable work delivered (native Claude pane, transcript rendering, select/copy, no terminal, tmux team tiles + meta sidebar, composer, activity-card folding incl. the persisted fold level T-235, D-75). The one blocked item — account/team token budget — was detached (T-158) and reframed as Q-34 (how + when to surface it given upstream doesn''t expose the data); T-158 is the resolver, parked in the backlog. Nothing actionable remains under this epic.', NULL, '2026-06-09 17:23:25', '2026-06-09 17:23:25', '2026-06-09 17:23:25', NULL, '749ec9a3224d1ae1c9661a37c05da4da', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'status', 'in_progress', 'done', NULL, '2026-06-09 17:23:25', '2026-06-09 17:23:25', '2026-06-09 17:23:25', NULL, 'eadab6091961f410df29809647ee6574', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'parent_id', NULL, 'T-276', NULL, '2026-06-09 20:02:53', '2026-06-09 20:02:53', '2026-06-09 20:02:53', NULL, '9a44220eef05245a83e53df6d32f9a13', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'description', NULL, 'Make file-path references rendered in the Claude conversation pane clickable: clicking a path opens that file in the editor.
|
||
|
||
**Behavior**
|
||
- Detect references to files in the workspace as the conversation markdown is rendered — bare paths (`lib/app.dart`), `file:line` forms (`lib/app.dart:42`, clickable per CLAUDE.md), and likely inline-code spans / markdown links pointing at repo paths.
|
||
- Resolve against the workspace root (CLIDE_WORKSPACE / git repo root). Only linkify paths that exist in the repo to avoid false positives on prose.
|
||
- On click, open in the editor — same path as `clide editor open <path>` — and jump to the line when a `:line` suffix is present.
|
||
|
||
**Notes / constraints**
|
||
- Markdown rendering is clide-owned custom CustomPaint/widgets (not a package), so detection + hit-testing lands in the conversation/markdown render path.
|
||
- Honors User/Claude parity (D-6): the click maps to the existing `clide editor open` verb.
|
||
- Open questions to settle during design: how aggressive path detection should be (existence check vs. heuristic), handling of non-existent / external paths, and visual affordance (underline/hover) for a linkified ref.', NULL, '2026-06-09 20:03:08', '2026-06-09 20:03:08', '2026-06-09 20:03:08', NULL, '5e7b2e6008889f0624ed204c97fc718b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6048VEGFDNEBHFMM4', 'parent_id', NULL, 'T-276', NULL, '2026-06-09 20:04:31', '2026-06-09 20:04:31', '2026-06-09 20:04:31', NULL, '818820d6c1dc1b3b8d0c60096d0025f4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6048VEGFDNEBHFMM4', 'description', NULL, 'When file paths to files inside the current repo appear in the Claude conversation, show them relative to the repo root rather than as absolute paths.
|
||
|
||
**DESIGN OPEN — needs discussion before implementation.** The exact presentation is undecided.
|
||
|
||
**Intent**
|
||
- Paths that resolve inside the workspace (CLIDE_WORKSPACE / git repo root) should read as repo-relative (e.g. `lib/app.dart` instead of `/var/mnt/data/projects/clide/lib/app.dart`).
|
||
- Goal is readability — strip the absolute prefix that''s noise for in-repo files.
|
||
|
||
**Open questions to settle in discussion**
|
||
- Visual treatment: silently rewrite the displayed text? show relative with the absolute available on hover/tooltip? a leading marker (e.g. `./` or a repo-root glyph)?
|
||
- Scope: only linkified/recognized refs (ties to T-300), or any path-looking token in the rendered output?
|
||
- Out-of-repo / absolute paths: leave untouched, or abbreviate (e.g. `~`)?
|
||
- Interaction with copy: does copying yield the relative or the original absolute path?
|
||
- Does this happen at render time only, or is the underlying text also normalized?
|
||
|
||
**Related**
|
||
- Pairs with T-300 (clickable file references) — same conversation/markdown render path; likely share path-detection + workspace-root resolution.', NULL, '2026-06-09 20:04:54', '2026-06-09 20:04:54', '2026-06-09 20:04:54', NULL, '8ecbd1bca77bd64222112c780fa216b4', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AE2GEF6BXSGKRMGC', 'description', NULL, 'VERIFIED DONE 2026-06-09: failures-only is the default reporter across all gate scripts — ci/test.sh (REPORTER var, used in every flutter/dart test invocation), ci/test_core.sh, ci/test_a11y.sh — each with a TEST_REPORTER=expanded escape hatch for debugging. No work remaining.', NULL, '2026-06-09 20:07:29', '2026-06-09 20:07:29', '2026-06-09 20:07:29', NULL, '29dd32588e4378ba0d0f4ba0ac104bfd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AE2GEF6BXSGKRMGC', 'status', 'ready', 'done', NULL, '2026-06-09 20:07:30', '2026-06-09 20:07:30', '2026-06-09 20:07:30', NULL, '23b91495413b8ba1b00291c6f22541d8', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'status', 'ready', 'done', NULL, '2026-06-09 20:17:04', '2026-06-09 20:17:04', '2026-06-09 20:17:04', NULL, '3c82a7b65e8d1c68ea241aa9a0a4e6dd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'description', 'The Claude conversation panel detects URLs and colorizes them, but they are not interactive. Make detected links clickable (a plain click, or control/cmd-click) so they are handed off to the OS URL opener.
|
||
|
||
## Behaviour
|
||
- Click (or ctrl/cmd-click) on a colorized link opens it via the OS default handler.
|
||
- Hover affordance (cursor change / underline) so it reads as clickable.
|
||
- Keep the existing colorization.
|
||
|
||
## Notes
|
||
- Honour user/Claude parity (D-6) where relevant.
|
||
- Use the platform URL launcher; avoid pulling in an opinionated package if a thin native/url_launcher shim already exists in the tree.
|
||
- Guard against non-http schemes / malformed URLs.', 'The Claude conversation panel detects URLs and colorizes them, but they are not interactive. Make detected links clickable (a plain click, or control/cmd-click) so they are handed off to the OS URL opener.
|
||
|
||
## Behaviour
|
||
- Click (or ctrl/cmd-click) on a colorized link opens it via the OS default handler.
|
||
- Hover affordance (cursor change / underline) so it reads as clickable.
|
||
- Keep the existing colorization.
|
||
|
||
## Notes
|
||
- Honour user/Claude parity (D-6) where relevant.
|
||
- Use the platform URL launcher; avoid pulling in an opinionated package if a thin native/url_launcher shim already exists in the tree.
|
||
- Guard against non-http schemes / malformed URLs.
|
||
|
||
DONE 2026-06-09. http(s) links in the conversation open via OsBridge.openURL on click (hover underline + pointer); non-http inert. ClideMarkdown gained an onLinkTap hook + _urlLinkSpan; the 3 inline-interaction callbacks were bundled into ClideMarkdownHooks (single threaded param) — also fixed links/images only working in some markdown contexts. Tests: clide_markdown_test.', NULL, '2026-06-09 20:17:04', '2026-06-09 20:17:04', '2026-06-09 20:17:04', NULL, 'bcf049444a57e41061b1b9adeeb7b058', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'parent_id', NULL, 'T-8', NULL, '2026-06-09 20:24:57', '2026-06-09 20:24:57', '2026-06-09 20:24:57', NULL, 'd7dfe7ec18b8657bf2ca96f2ca0d75f2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'description', NULL, 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
|
||
|
||
**Deliverable**
|
||
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
|
||
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
|
||
|
||
**Scope to frame (from T-8)**
|
||
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
|
||
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
|
||
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
|
||
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
|
||
|
||
**Open design questions for the wireframe to answer**
|
||
- Settings as a full-screen view, a pane/tab, or a modal?
|
||
- Per-project (`.clide/settings.yaml`) vs. user-global scope — shown together or switched?
|
||
- Search/filter across all settings.
|
||
- How schema-driven fields render labels, help text, defaults, and reset.
|
||
|
||
**Constraints**
|
||
- Follow clide visual language — pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
|
||
|
||
This is the design step; implementation of the actual settings UI is separate child work under T-8.', NULL, '2026-06-09 20:25:49', '2026-06-09 20:25:49', '2026-06-09 20:25:49', NULL, '1d0b97bd0ad807733f995ee05eb9d2f2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'description', 'On Linux desktop startup, two GLib-GIO-CRITICAL warnings fire to stderr:
|
||
|
||
GLib-GIO-CRITICAL: GFileInfo created without standard::size
|
||
file ../gio/gfileinfo.c: line 1865 (g_file_info_get_size): should not be reached
|
||
|
||
Repro: launch the app (make run / flutter run) on Linux. The pair fires early in boot — right after the primary pane binds its session and before the IPC server starts listening (observed ~0.3s apart, e.g. 15:53:15.570 and 15:53:15.832), on every launch.
|
||
|
||
Cause: some code path calls g_file_info_get_size() on a GFileInfo that was created/queried WITHOUT requesting the G_FILE_ATTRIBUTE_STANDARD_SIZE (''standard::size'') attribute. A grep of clide''s own code (lib/, linux/runner/, native/) finds no direct g_file_info / g_file_query_info usage, so it is most likely inside GTK/GLib itself or a Flutter Linux plugin''s file enumeration (icon/thumbnail/mime probe, path lookups), not clide Dart/C++. The GTK file-chooser in linux/runner/clide_app.cc is on-demand only, so it is not the trigger (the warnings fire at boot).
|
||
|
||
Impact: low — console noise at CRITICAL level; no observed functional breakage. But a size query that ''should not be reached'' may be reading a bogus/zero size somewhere worth confirming.
|
||
|
||
Investigation: run with G_DEBUG=fatal-warnings (or gdb break on g_log/g_logv) to capture the stack at the warning and identify the library/plugin frame; check whether a Flutter plugin (file_selector, path_provider, url_launcher) or GTK icon/mime loading is responsible. If upstream/GTK, document + suppress-from-our-side or pin; if a plugin, file upstream.
|
||
|
||
Env: Fedora, GTK Linux embedder, flutter run.', 'On Linux desktop startup, two GLib-GIO-CRITICAL warnings fire to stderr:
|
||
|
||
GLib-GIO-CRITICAL: GFileInfo created without standard::size
|
||
file ../gio/gfileinfo.c: line 1865 (g_file_info_get_size): should not be reached
|
||
|
||
Repro: launch the app (make run / flutter run) on Linux. The pair fires early in boot — right after the primary pane binds its session and before the IPC server starts listening (observed ~0.3s apart, e.g. 15:53:15.570 and 15:53:15.832), on every launch.
|
||
|
||
Cause: some code path calls g_file_info_get_size() on a GFileInfo that was created/queried WITHOUT requesting the G_FILE_ATTRIBUTE_STANDARD_SIZE (''standard::size'') attribute. A grep of clide''s own code (lib/, linux/runner/, native/) finds no direct g_file_info / g_file_query_info usage, so it is most likely inside GTK/GLib itself or a Flutter Linux plugin''s file enumeration (icon/thumbnail/mime probe, path lookups), not clide Dart/C++. The GTK file-chooser in linux/runner/clide_app.cc is on-demand only, so it is not the trigger (the warnings fire at boot).
|
||
|
||
Impact: low — console noise at CRITICAL level; no observed functional breakage. But a size query that ''should not be reached'' may be reading a bogus/zero size somewhere worth confirming.
|
||
|
||
Investigation: run with G_DEBUG=fatal-warnings (or gdb break on g_log/g_logv) to capture the stack at the warning and identify the library/plugin frame; check whether a Flutter plugin (file_selector, path_provider, url_launcher) or GTK icon/mime loading is responsible. If upstream/GTK, document + suppress-from-our-side or pin; if a plugin, file upstream.
|
||
|
||
Env: Fedora, GTK Linux embedder, flutter run.
|
||
|
||
UPDATE (2026-06-09): also fires MID-SESSION, not only at boot — contradicts the "fires early in boot, on every launch" framing above. Observed log: app booted 16:47:01, but the GLib-GIO-CRITICAL pair fired at 18:51:07.841 / 18:51:08.190 (~2h into the session), near pane/session activity. So the trigger is more likely a file-info code path tied to a user action or background file enumeration than pure startup. Re-scope the investigation to capture the stack when it fires mid-session (G_DEBUG=fatal-warnings / gdb break on g_log) rather than only at boot.', NULL, '2026-06-09 20:26:44', '2026-06-09 20:26:44', '2026-06-09 20:26:44', NULL, '4cdfa1f8240ffeba7dd710712405f482', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QJC039TMDF5JGMXC', 'status', 'ready', 'done', NULL, '2026-06-09 20:27:17', '2026-06-09 20:27:17', '2026-06-09 20:27:17', NULL, '894a8c8c0863f2abe160f610a2f85215', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QJC039TMDF5JGMXC', 'description', 'Muscle-memory gap from the Claude Code CLI: in clide''s interaction-zone prompts (D-78), the user reaches for number keys 1/2/3 to pick a button but has to click. Map number keys (1..N) to the visible buttons/options, and PREFIX each button label with its number for discoverability (CLI shows ''1. Allow / 2. ... / 3. No'').\n\nSurface: ToolPromptCard in lib/builtin/claude/src/prompt_card.dart. Two modes:\n- PERMISSION (_permission, ~line 119): buttons Allow (primary) / ''Allow & don''t ask again'' (only when permissionSuggestions present) / Deny. Map 1=Allow, 2=''Allow & don''t ask again'' (when shown) else Deny, 3=Deny when the middle one is shown. Match the CLI''s ordering/numbering.\n- ASKUSERQUESTION (_question / _optButton, ~line 280/294): each option per question gets a number (1..N) that selects (single) or toggles (multiSelect) it; keep Enter for Submit/Next and Esc/back as-is. Prefix option labels with the number alongside the existing radio/checkbox glyph (the ''○/●'' in _optButton).\n\nKEY CAVEAT (do this right): the prompt card hosts a free-text note field (_NoteField) and AskUserQuestion ''Other'' free-text. Number keys MUST NOT be captured while focus is in a text field (otherwise typing ''1'' triggers a button). Gate the shortcut on focus not being in an editable, i.e. intercept at the prompt''s Focus/FocusScope and bail when a text field has focus — same consumer-interception discipline as D-82 (Vim) / the editor. Keep the ClideButtons (clicks + AT) intact; the number key is an additional accelerator. Accessibility: include the number in the button''s semantics label.\n\nAcceptance: with a permission prompt or AskUserQuestion open and focus not in a text field, pressing 1/2/3 (and up to N for question options) activates the matching button/option; labels are number-prefixed; typing in a note/Other field is unaffected; mouse + screen-reader paths still work.
|
||
|
||
Refinement (2026-06-05, user): match the Claude Code CLI''s actual behavior for the text-field interaction. The number key triggers the button/option shortcut when it would be the FIRST character typed -- i.e. focus is NOT in a text field, OR focus IS in the note/Other field but that field is currently EMPTY. Once the field has any content, digits type normally (no shortcut). This supersedes the earlier ''never capture digits while a text field has focus'' gate: it''s better because focus often defaults into the (empty) note field, so the shortcut still fires there (muscle-memory case) while a digit mid-note still types. Implementation: intercept the digit at the prompt focus scope; consume+activate only when the focused editable (if any) is empty, else let it through to type. The CLI exhibits this exact ''footgun'' (digit-as-first-char in an empty field acts as the choice) and we intentionally mirror it.', 'Muscle-memory gap from the Claude Code CLI: in clide''s interaction-zone prompts (D-78), the user reaches for number keys 1/2/3 to pick a button but has to click. Map number keys (1..N) to the visible buttons/options, and PREFIX each button label with its number for discoverability (CLI shows ''1. Allow / 2. ... / 3. No'').\n\nSurface: ToolPromptCard in lib/builtin/claude/src/prompt_card.dart. Two modes:\n- PERMISSION (_permission, ~line 119): buttons Allow (primary) / ''Allow & don''t ask again'' (only when permissionSuggestions present) / Deny. Map 1=Allow, 2=''Allow & don''t ask again'' (when shown) else Deny, 3=Deny when the middle one is shown. Match the CLI''s ordering/numbering.\n- ASKUSERQUESTION (_question / _optButton, ~line 280/294): each option per question gets a number (1..N) that selects (single) or toggles (multiSelect) it; keep Enter for Submit/Next and Esc/back as-is. Prefix option labels with the number alongside the existing radio/checkbox glyph (the ''○/●'' in _optButton).\n\nKEY CAVEAT (do this right): the prompt card hosts a free-text note field (_NoteField) and AskUserQuestion ''Other'' free-text. Number keys MUST NOT be captured while focus is in a text field (otherwise typing ''1'' triggers a button). Gate the shortcut on focus not being in an editable, i.e. intercept at the prompt''s Focus/FocusScope and bail when a text field has focus — same consumer-interception discipline as D-82 (Vim) / the editor. Keep the ClideButtons (clicks + AT) intact; the number key is an additional accelerator. Accessibility: include the number in the button''s semantics label.\n\nAcceptance: with a permission prompt or AskUserQuestion open and focus not in a text field, pressing 1/2/3 (and up to N for question options) activates the matching button/option; labels are number-prefixed; typing in a note/Other field is unaffected; mouse + screen-reader paths still work.
|
||
|
||
Refinement (2026-06-05, user): match the Claude Code CLI''s actual behavior for the text-field interaction. The number key triggers the button/option shortcut when it would be the FIRST character typed -- i.e. focus is NOT in a text field, OR focus IS in the note/Other field but that field is currently EMPTY. Once the field has any content, digits type normally (no shortcut). This supersedes the earlier ''never capture digits while a text field has focus'' gate: it''s better because focus often defaults into the (empty) note field, so the shortcut still fires there (muscle-memory case) while a digit mid-note still types. Implementation: intercept the digit at the prompt focus scope; consume+activate only when the focused editable (if any) is empty, else let it through to type. The CLI exhibits this exact ''footgun'' (digit-as-first-char in an empty field acts as the choice) and we intentionally mirror it.
|
||
|
||
DONE 2026-06-09. ToolPromptCard: number keys pick the matching button/option (labels prefixed 1./2./3.), Enter confirms the primary action. Permission 1=Allow / 2=Allow&remember(if offered) else Deny / 3=Deny; AskUserQuestion 1..N select+toggle options + Other. Card autofocuses; _onKey self-guards on hasPrimaryFocus so a focused note field types digits normally. Permission/option actions shared between buttons + keys. Tests: prompt_card_test number-key group + updated label finders.', NULL, '2026-06-09 20:27:17', '2026-06-09 20:27:17', '2026-06-09 20:27:17', NULL, 'c05ca703a5a99d070e1eca42d853370f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'status', 'ready', 'done', NULL, '2026-06-09 20:34:30', '2026-06-09 20:34:30', '2026-06-09 20:34:30', NULL, '6dd8b5ff2ee01ab1eac6aa74c08d4186', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'description', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end.', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end.
|
||
|
||
DONE 2026-06-09. Added the reusable setSurfaceSize(tester, width) helper to test/helpers/widget_harness.dart (the foundation the ticket asked for). Ultrawide (3440) cases added on the flagged width-sensitive surfaces: ClideMarquee (was narrow-only — now verified static when a line fits a wide slot) and the quick-open palette (verified width-capped, not stretched). Status bar already covers 600+3440 (T-239). AUDIT NOTES on the rest: overlays/pickers use fixed-width panels (ClideMenu/quick-open 480, anchored popovers) so they''re inherently capped/centered; tab strips (MultitabPane) and status items overflow only when NARROW (a small-screen concern, Q-26), not wide; RootLayout slots are explicit fixed/flex widths with drag-resize. The helper is now in place for any future width-sensitive widget. Not exhaustive by design (audit + reusable tooling + the high-risk surfaces).', NULL, '2026-06-09 20:34:30', '2026-06-09 20:34:30', '2026-06-09 20:34:30', NULL, 'f81c453978dfa298dfb66a17a39a6264', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BRWWPB4R06Q31XV0', 'status', 'ready', 'done', NULL, '2026-06-09 20:41:22', '2026-06-09 20:41:22', '2026-06-09 20:41:22', NULL, '9a195bdb92b08e8a05c5ea8ab0792278', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BRWWPB4R06Q31XV0', 'description', 'Register clide:// protocol handler. Support clide://open?path=/repo/file.md&line=42 for external tools to open files at specific locations. Useful for CI links, error reports, and cross-tool integration.', 'Register clide:// protocol handler. Support clide://open?path=/repo/file.md&line=42 for external tools to open files at specific locations. Useful for CI links, error reports, and cross-tool integration.
|
||
|
||
DONE (Linux) 2026-06-09. clide://open?path=&line= → editor.open via parseArgv, routed through the existing CLI→IPC path into the running window (single-instance for free). Parser validates action/path/line; tests in argv_to_request_test. Scheme registered: linux/clide.desktop (x-scheme-handler/clide, Exec already %U) + macOS Info.plist CFBundleURLTypes. Linux works end-to-end. macOS URL DELIVERY (AppDelegate openURLs → method channel → Dart) deferred to T-303 — needs a macOS machine to verify, not shipped blind.', NULL, '2026-06-09 20:41:22', '2026-06-09 20:41:22', '2026-06-09 20:41:22', NULL, 'ca04430461aeac7583a044f10aa57469', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BRWWPB4R06Q31XV0', 'description', 'Register clide:// protocol handler. Support clide://open?path=/repo/file.md&line=42 for external tools to open files at specific locations. Useful for CI links, error reports, and cross-tool integration.
|
||
|
||
DONE (Linux) 2026-06-09. clide://open?path=&line= → editor.open via parseArgv, routed through the existing CLI→IPC path into the running window (single-instance for free). Parser validates action/path/line; tests in argv_to_request_test. Scheme registered: linux/clide.desktop (x-scheme-handler/clide, Exec already %U) + macOS Info.plist CFBundleURLTypes. Linux works end-to-end. macOS URL DELIVERY (AppDelegate openURLs → method channel → Dart) deferred to T-303 — needs a macOS machine to verify, not shipped blind.', 'Register clide:// protocol handler. Support clide://open?path=/repo/file.md&line=42 for external tools to open files at specific locations. Useful for CI links, error reports, and cross-tool integration.
|
||
|
||
DONE (Linux) 2026-06-09. clide://open?path=&line= → editor.open via parseArgv, routed through the existing CLI→IPC path into the running window (single-instance for free). Parser validates action/path/line; tests in argv_to_request_test. Scheme registered: linux/clide.desktop (x-scheme-handler/clide, Exec already %U) + macOS Info.plist CFBundleURLTypes. Linux works end-to-end. macOS URL DELIVERY (AppDelegate openURLs → method channel → Dart) deferred to T-303 — needs a macOS machine to verify, not shipped blind.
|
||
|
||
SECURITY REDESIGN 2026-06-09 (D-90): per user, the clide:// surface is now paranoid. The link no longer translates to a command in parseArgv — it routes the raw URL to a new builtin.deeplink handler gated by a DEFAULT-DENY allowlist (kDeepLinkSafeActions = {open} only — run/git/write/passthrough rejected) AND a mandatory confirmation prompt (''an external link wants to: … allow?'') before any action. The generic CLI-passthrough idea is deliberately NOT shipped (it would make a webpage a remote control); each new action is an explicit allowlist + handler addition. Tests: deep_link_test (allowlist boundary), extension_test (gating).', NULL, '2026-06-09 20:57:53', '2026-06-09 20:57:53', '2026-06-09 20:57:53', NULL, 'aee84cbde5ade207d02379c497eed4ed', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'status', 'backlog', 'ready', NULL, '2026-06-09 21:04:10', '2026-06-09 21:04:10', '2026-06-09 21:04:10', NULL, '9004651246035582806f204f61b20e91', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'status', 'ready', 'in_progress', NULL, '2026-06-09 21:04:33', '2026-06-09 21:04:33', '2026-06-09 21:04:33', NULL, 'bff9123dd5b8658d12afdf3d87bfc71f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'description', 'On Linux desktop startup, two GLib-GIO-CRITICAL warnings fire to stderr:
|
||
|
||
GLib-GIO-CRITICAL: GFileInfo created without standard::size
|
||
file ../gio/gfileinfo.c: line 1865 (g_file_info_get_size): should not be reached
|
||
|
||
Repro: launch the app (make run / flutter run) on Linux. The pair fires early in boot — right after the primary pane binds its session and before the IPC server starts listening (observed ~0.3s apart, e.g. 15:53:15.570 and 15:53:15.832), on every launch.
|
||
|
||
Cause: some code path calls g_file_info_get_size() on a GFileInfo that was created/queried WITHOUT requesting the G_FILE_ATTRIBUTE_STANDARD_SIZE (''standard::size'') attribute. A grep of clide''s own code (lib/, linux/runner/, native/) finds no direct g_file_info / g_file_query_info usage, so it is most likely inside GTK/GLib itself or a Flutter Linux plugin''s file enumeration (icon/thumbnail/mime probe, path lookups), not clide Dart/C++. The GTK file-chooser in linux/runner/clide_app.cc is on-demand only, so it is not the trigger (the warnings fire at boot).
|
||
|
||
Impact: low — console noise at CRITICAL level; no observed functional breakage. But a size query that ''should not be reached'' may be reading a bogus/zero size somewhere worth confirming.
|
||
|
||
Investigation: run with G_DEBUG=fatal-warnings (or gdb break on g_log/g_logv) to capture the stack at the warning and identify the library/plugin frame; check whether a Flutter plugin (file_selector, path_provider, url_launcher) or GTK icon/mime loading is responsible. If upstream/GTK, document + suppress-from-our-side or pin; if a plugin, file upstream.
|
||
|
||
Env: Fedora, GTK Linux embedder, flutter run.
|
||
|
||
UPDATE (2026-06-09): also fires MID-SESSION, not only at boot — contradicts the "fires early in boot, on every launch" framing above. Observed log: app booted 16:47:01, but the GLib-GIO-CRITICAL pair fired at 18:51:07.841 / 18:51:08.190 (~2h into the session), near pane/session activity. So the trigger is more likely a file-info code path tied to a user action or background file enumeration than pure startup. Re-scope the investigation to capture the stack when it fires mid-session (G_DEBUG=fatal-warnings / gdb break on g_log) rather than only at boot.', 'On Linux desktop startup, two GLib-GIO-CRITICAL warnings fire to stderr:
|
||
|
||
GLib-GIO-CRITICAL: GFileInfo created without standard::size
|
||
file ../gio/gfileinfo.c: line 1865 (g_file_info_get_size): should not be reached
|
||
|
||
Repro: launch the app (make run / flutter run) on Linux. The pair fires early in boot — right after the primary pane binds its session and before the IPC server starts listening (observed ~0.3s apart, e.g. 15:53:15.570 and 15:53:15.832), on every launch.
|
||
|
||
Cause: some code path calls g_file_info_get_size() on a GFileInfo that was created/queried WITHOUT requesting the G_FILE_ATTRIBUTE_STANDARD_SIZE (''standard::size'') attribute. A grep of clide''s own code (lib/, linux/runner/, native/) finds no direct g_file_info / g_file_query_info usage, so it is most likely inside GTK/GLib itself or a Flutter Linux plugin''s file enumeration (icon/thumbnail/mime probe, path lookups), not clide Dart/C++. The GTK file-chooser in linux/runner/clide_app.cc is on-demand only, so it is not the trigger (the warnings fire at boot).
|
||
|
||
Impact: low — console noise at CRITICAL level; no observed functional breakage. But a size query that ''should not be reached'' may be reading a bogus/zero size somewhere worth confirming.
|
||
|
||
Investigation: run with G_DEBUG=fatal-warnings (or gdb break on g_log/g_logv) to capture the stack at the warning and identify the library/plugin frame; check whether a Flutter plugin (file_selector, path_provider, url_launcher) or GTK icon/mime loading is responsible. If upstream/GTK, document + suppress-from-our-side or pin; if a plugin, file upstream.
|
||
|
||
Env: Fedora, GTK Linux embedder, flutter run.
|
||
|
||
UPDATE (2026-06-09): also fires MID-SESSION, not only at boot — contradicts the "fires early in boot, on every launch" framing above. Observed log: app booted 16:47:01, but the GLib-GIO-CRITICAL pair fired at 18:51:07.841 / 18:51:08.190 (~2h into the session), near pane/session activity. So the trigger is more likely a file-info code path tied to a user action or background file enumeration than pure startup. Re-scope the investigation to capture the stack when it fires mid-session (G_DEBUG=fatal-warnings / gdb break on g_log) rather than only at boot.
|
||
|
||
RESOLVED (2026-06-09).
|
||
|
||
Root cause CONFIRMED by reproducer: the Open Workspace folder picker. `gtk_file_chooser_dialog_new(..., SELECT_FOLDER, ...)` at linux/runner/clide_app.cc:222 builds a GtkPlacesSidebar that enumerates bookmarks/recent and internally creates GFileInfo objects WITHOUT G_FILE_ATTRIBUTE_STANDARD_SIZE, then calls g_file_info_get_size() on them — GTK''s own bug. A standalone repro (gtk3, SELECT_FOLDER dialog, auto-cancel) emitted the exact warning pair ~0.3s apart, matching the user''s mid-session log (18:51:07.841 / 18:51:08.190). The original "fires at boot" framing was wrong; the picker is opened on demand. Ruled OUT the T-138 clipboard channel: gtk_clipboard_wait_for_uris/wait_for_image emit nothing for text/image/file-uri clipboard states under G_DEBUG=fatal-warnings.
|
||
|
||
No public GTK API exists to fix at source (the sidebar''s enumeration is private; can''t inject the missing attribute).
|
||
|
||
Fix (both, per user direction):
|
||
1. Switched pickDirectory to GtkFileChooserNative — portal-backed, runs out-of-process in sandboxed/Flatpak builds so the noise never enters our process; nicer Wayland picker. (Verified: outside a sandbox GtkFileChooserNative falls back to the in-process chooser and STILL warns on this box — portal is only used under Flatpak/snap or GTK_USE_PORTAL=1.)
|
||
2. Added clide_gio_log_filter in clide_app.cc, installed via g_log_set_handler("GLib-GIO", CRITICAL, ...) in clide_app_startup. Drops ONLY messages containing "g_file_info_get_size" or "without standard::size"; forwards every other GLib-GIO log to the default handler. This is the universal fix (works in every run mode).
|
||
|
||
Verification: standalone reproducer with both the native chooser AND the filter — size warnings gone, an unrelated GLib-GIO CRITICAL still passes through (filter is scoped, not a blanket mute). `make build-linux` green. CHANGELOG updated under Fixed.', NULL, '2026-06-09 21:21:04', '2026-06-09 21:21:04', '2026-06-09 21:21:04', NULL, '0b6444210390483f0e12a042efd5e3f1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'status', 'in_progress', 'done', NULL, '2026-06-09 21:21:08', '2026-06-09 21:21:08', '2026-06-09 21:21:08', NULL, 'd22822fc67f1dc3e1d63c924b6fe91fd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50S6WRD81V3Z0NH1M', 'status', 'backlog', 'ready', NULL, '2026-06-09 21:21:30', '2026-06-09 21:21:30', '2026-06-09 21:21:30', NULL, '80c5f5e2ca71bc6146c769b93cfdc8e9', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50S6WRD81V3Z0NH1M', 'status', 'ready', 'in_progress', NULL, '2026-06-09 21:22:26', '2026-06-09 21:22:26', '2026-06-09 21:22:26', NULL, '8c699e741fba562a14ec7fe5ebd4695e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'parent_id', NULL, 'T-276', NULL, '2026-06-09 21:22:31', '2026-06-09 21:22:31', '2026-06-09 21:22:31', NULL, 'b70cdfeb0ac73677be0a6d190ee88376', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'description', NULL, 'The logo-mark spinner shown on in-progress activity/holder cards in the Claude conversation is too small to read as a spinner — it reads as a static speck. Enlarge it so the running state is legible at a glance.
|
||
|
||
**Where**
|
||
- `ClideSpinner` (lib/widgets/src/clide_spinner.dart) — defaults to size 14; renders the logo SVG at width/height = size.
|
||
- `ClideStatusIndicator` (lib/widgets/src/clide_status_indicator.dart) — default size 14; maps running→ClideSpinner, success→check, error→cross at the same size.
|
||
- Call sites: holder_card.dart:117 and :199 pass `size: 12` — the small value the user is seeing.
|
||
|
||
**Direction (settle in review)**
|
||
- Bump the spinner size on the activity cards (the `size: 12` call sites, and/or the indicator default) to something clearly legible — pull a concrete value from the ui-design control-geometry tokens rather than a magic number.
|
||
- Keep the running spinner, success check, and error cross visually balanced at the new size (they share `size`), so the card doesn''t jump when the state settles.
|
||
- Check the other ClideSpinner/StatusIndicator consumers (status surfaces) so the bump doesn''t bloat unrelated spots — may warrant sizing the cards explicitly rather than changing the shared default.
|
||
|
||
**Acceptance**
|
||
- The in-progress spinner on conversation activity cards is comfortably distinguishable as a spinning indicator; success/error glyphs stay aligned at the same footprint.', NULL, '2026-06-09 21:22:44', '2026-06-09 21:22:44', '2026-06-09 21:22:44', NULL, '71233a6425b5734391c4d315579d2c64', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YNEQM1WY25BV7TD8', 'status', 'backlog', 'ready', NULL, '2026-06-09 21:32:29', '2026-06-09 21:32:29', '2026-06-09 21:32:29', NULL, 'b6690849ecbdddf638c67dfa82192a3f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50S6WRD81V3Z0NH1M', 'status', 'in_progress', 'done', NULL, '2026-06-09 21:36:05', '2026-06-09 21:36:05', '2026-06-09 21:36:05', NULL, 'c494cd6008aa55db47c5f372ab52c5b2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YNEQM1WY25BV7TD8', 'status', 'ready', 'done', NULL, '2026-06-09 21:42:59', '2026-06-09 21:42:59', '2026-06-09 21:42:59', NULL, '73d904ce53c8f0d5adbb6e724d47f885', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'description', 'Group/tool collapser cards (ClideHolderCard vs ConversationCard) handle collapse + visuals inconsistently; extract one shared collapser primitive with a color property, fixed-width counter slot, and the status icon hard against the card edge.', 'Group/tool collapser cards (ClideHolderCard vs ConversationCard) handle collapse + visuals inconsistently; extract one shared collapser primitive with a color property, fixed-width counter slot, and the status icon hard against the card edge.
|
||
|
||
## Bug / discrepancy
|
||
|
||
There are two collapser-card families with inconsistent collapse handling and
|
||
"card within card" chrome:
|
||
|
||
- **`ClideHolderCard`** (lib/builtin/claude/src/holder_card.dart, T-266) — the
|
||
group container for Activity (`_ActivityCard`) and Edits (`_EditRunCard`).
|
||
Collapse = ticker row + frame background + a header caret. Header order:
|
||
`[chevron] summary … [status] [count]`. Status indicator sits INBOARD, left
|
||
of the count label. Border/text hardcoded to `panelBorder` / `globalTextMuted`
|
||
(no color knob).
|
||
- **`ConversationCard`** (lib/builtin/claude/src/conversation_card.dart, T-262)
|
||
— the per-tool merged card. Collapse = a single leading caret only (no
|
||
background/ticker). Header order: `[caret] label summary/spacer [status]
|
||
[actions]`. Has `borderColor` + `accent`.
|
||
|
||
So the toggle is attached differently, the status mark sits in a different
|
||
place, the counter has no fixed slot, and color customization exists on one but
|
||
not the other. They should grab the SAME widget (each its own instance — NOT
|
||
collapsing across types).
|
||
|
||
## Proposal — one `ClideCollapserCard` primitive
|
||
|
||
Both families render through a single collapser primitive. Requirements:
|
||
|
||
1. **`color` property** drives the border AND the label/text color, so each
|
||
instance keeps its visual identity (activity = muted, edits = accent,
|
||
error = red, sub-agent, …) through one widget. Default = panelBorder/muted.
|
||
2. **Fixed-width counter slot** — the `N steps` / `N edits` count lives in a
|
||
right-aligned fixed-width slot so it (and the trailing status icon) never
|
||
shift as the number grows or the status appears/changes.
|
||
3. **Status icon hard against the card edge** — flip the current
|
||
`[count][status]` to `[status]` against the right edge with the counter
|
||
inboard of it. The chevron already hugs the LEFT edge. Rule: both icons hug
|
||
the card edges; text sits inboard.
|
||
4. **Consistent collapse/expand attachment** across both uses (keep the
|
||
group-container background-tap-to-collapse for the tail-follow case, D-78,
|
||
but the visible control + header layout are identical).
|
||
5. Status glyphs: `ClideRunStatus` running (logo-mark spinner ◐) / success ✓ /
|
||
error ✕, reusing ClideStatusIndicator.
|
||
|
||
Nested sub-cards render the SAME primitive (card-within-card consistency).
|
||
|
||
## Scope
|
||
|
||
- New `ClideCollapserCard` in lib/widgets/src/ (exported from widgets.dart).
|
||
- Migrate `ClideHolderCard` (Activity/Edits) and `ConversationCard` (merged
|
||
tool card) onto it; keep per-type content/labels.
|
||
- Hold visual fidelity via goldens (conversation_card_goldens_test,
|
||
ClideHolderCard golden) + a11y (expanded/collapsed semantics, focusable
|
||
toggle).
|
||
|
||
## Wireframe
|
||
|
||
docs/design/wireframes/cards/collapser-card.json (+ .png) — collapsed (3 color
|
||
variants), expanded with nested sub-cards, annotated for the counter slot +
|
||
edge-anchored icons.', NULL, '2026-06-10 08:16:05', '2026-06-10 08:16:05', '2026-06-10 08:16:05', NULL, 'fe6c300aa083f7faa80d3d78bd1ba0bf', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'description', 'Group/tool collapser cards (ClideHolderCard vs ConversationCard) handle collapse + visuals inconsistently; extract one shared collapser primitive with a color property, fixed-width counter slot, and the status icon hard against the card edge.
|
||
|
||
## Bug / discrepancy
|
||
|
||
There are two collapser-card families with inconsistent collapse handling and
|
||
"card within card" chrome:
|
||
|
||
- **`ClideHolderCard`** (lib/builtin/claude/src/holder_card.dart, T-266) — the
|
||
group container for Activity (`_ActivityCard`) and Edits (`_EditRunCard`).
|
||
Collapse = ticker row + frame background + a header caret. Header order:
|
||
`[chevron] summary … [status] [count]`. Status indicator sits INBOARD, left
|
||
of the count label. Border/text hardcoded to `panelBorder` / `globalTextMuted`
|
||
(no color knob).
|
||
- **`ConversationCard`** (lib/builtin/claude/src/conversation_card.dart, T-262)
|
||
— the per-tool merged card. Collapse = a single leading caret only (no
|
||
background/ticker). Header order: `[caret] label summary/spacer [status]
|
||
[actions]`. Has `borderColor` + `accent`.
|
||
|
||
So the toggle is attached differently, the status mark sits in a different
|
||
place, the counter has no fixed slot, and color customization exists on one but
|
||
not the other. They should grab the SAME widget (each its own instance — NOT
|
||
collapsing across types).
|
||
|
||
## Proposal — one `ClideCollapserCard` primitive
|
||
|
||
Both families render through a single collapser primitive. Requirements:
|
||
|
||
1. **`color` property** drives the border AND the label/text color, so each
|
||
instance keeps its visual identity (activity = muted, edits = accent,
|
||
error = red, sub-agent, …) through one widget. Default = panelBorder/muted.
|
||
2. **Fixed-width counter slot** — the `N steps` / `N edits` count lives in a
|
||
right-aligned fixed-width slot so it (and the trailing status icon) never
|
||
shift as the number grows or the status appears/changes.
|
||
3. **Status icon hard against the card edge** — flip the current
|
||
`[count][status]` to `[status]` against the right edge with the counter
|
||
inboard of it. The chevron already hugs the LEFT edge. Rule: both icons hug
|
||
the card edges; text sits inboard.
|
||
4. **Consistent collapse/expand attachment** across both uses (keep the
|
||
group-container background-tap-to-collapse for the tail-follow case, D-78,
|
||
but the visible control + header layout are identical).
|
||
5. Status glyphs: `ClideRunStatus` running (logo-mark spinner ◐) / success ✓ /
|
||
error ✕, reusing ClideStatusIndicator.
|
||
|
||
Nested sub-cards render the SAME primitive (card-within-card consistency).
|
||
|
||
## Scope
|
||
|
||
- New `ClideCollapserCard` in lib/widgets/src/ (exported from widgets.dart).
|
||
- Migrate `ClideHolderCard` (Activity/Edits) and `ConversationCard` (merged
|
||
tool card) onto it; keep per-type content/labels.
|
||
- Hold visual fidelity via goldens (conversation_card_goldens_test,
|
||
ClideHolderCard golden) + a11y (expanded/collapsed semantics, focusable
|
||
toggle).
|
||
|
||
## Wireframe
|
||
|
||
docs/design/wireframes/cards/collapser-card.json (+ .png) — collapsed (3 color
|
||
variants), expanded with nested sub-cards, annotated for the counter slot +
|
||
edge-anchored icons.', 'Unify the conversation-panel collapsible cards onto one ClideCollapserCard primitive, and share card spacing across all card categories. Supersedes ClideHolderCard (T-266) and the collapse logic in ConversationCard (T-262) for the tool path.
|
||
|
||
## Conversation-panel card model (agreed 2026-06-10)
|
||
|
||
### Shared spacing — constants, NOT a shared wrapper
|
||
All card categories share a small set of spacing CONSTANTS (inter-card bottom gap, inner padding, corner radius). NOT a forced common wrapper widget — each category is its own widget; they just pull the same spacing tokens so the stream reads as one consistent rhythm.
|
||
|
||
### Three card categories
|
||
1. **Dialog cards** — carry the side stripe marking who is speaking (user / Claude / agent). Prose / attribution. Not collapsible. (Today: ConversationCard stripe variant.)
|
||
2. **Simple cards** — a single item shown fully open in the stream, never collapses (e.g. the image-show card; more to come). Standalone display: no chevron, no status chrome.
|
||
3. **Collapsibles** — the unified collapser. Covers edits, bash, task updates, runs — every tool use. Behaves like the bash card should:
|
||
- The whole card is clickable to collapse/expand.
|
||
- Collapsed: title = the echoed last content line (like bash now) + an item count + aggregate status (spinner / check / cross).
|
||
- Expanded: an inner canvas holding the nested item card(s), each item in its own inner card.
|
||
- A single item still gets its own inner card inside the collapser when open, and pushes its status / count / last-line up to the collapser header.
|
||
- Inner item cards ALSO show their own per-item status (check / cross / spinner) when expanded; the collapser header carries the aggregate.
|
||
- Chrome (per the wireframe): `color` (outer border + chevron / label / text), fixed-width counter slot, status icon hard against the right edge, chevron hard against the left edge.
|
||
|
||
## Scope
|
||
- New `ClideCollapserCard` in lib/widgets/ (exported from widgets.dart) — the category-3 primitive: a list of 1..N inner item cards, collapsed ticker <-> expanded inner canvas, color / fixed-counter / edge-status / edge-chevron chrome, background + caret toggle (D-78 tail-follow), aggregate status + count + echoed-title computed from the items.
|
||
- Shared card-spacing constants consumed by all three categories.
|
||
- ALL tool uses render as collapsers — single ones as a 1-item list (Bash, Read, Edit, Task, edits runs, activity runs, etc.).
|
||
- Inner item cards: content + their own per-item status; no own collapse; no stripe.
|
||
- Dialog cards (1) and simple cards (2) are NOT pulled into the collapser — they only adopt the shared spacing constants (keep stripe / inner config).
|
||
|
||
## Verify
|
||
- Goldens regenerated (holder_card, conversation_card_merged) + a11y (expanded / collapsed semantics, focusable toggle).
|
||
- Wireframe: docs/design/wireframes/cards/collapser-card.{json,png}.
|
||
- After landing: update the ui-design skill''s conversation-panel guidance to describe the three card categories + the collapser.
|
||
', NULL, '2026-06-10 08:43:03', '2026-06-10 08:43:03', '2026-06-10 08:43:03', NULL, '15f42c1d62be96cf45d583bf3ac704cd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42WW0SY3XEXKE8PK8', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, '248034327b78f2dec7d43564ccc01b18', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'status', 'in_progress', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, '26b3a60618574a2ef4e947cc1e4a221e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'c51669ac4ba92aed8a72728bf08ea6e0', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'd5c50f178f76cccba89ad44a62af29de', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'dd13cc3149df6c1dc2d27e53fce878c1', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'status', 'in_progress', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'f4a1697c1c839dc92d6bedc2cd86d997', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'fe6d00feb488f12fd0931852b2e66236', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 09:13:08', '2026-06-10 09:13:08', '2026-06-10 09:13:08', NULL, '56fbf20da154b3465d3d2e7dcc87dfc9', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'status', 'in_progress', 'done', NULL, '2026-06-10 09:39:52', '2026-06-10 09:39:52', '2026-06-10 09:39:52', NULL, '5f059718e9cf548a13a9d141970ccd2e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB170DJA02EH2E8HMH7X7SS4', 'status', 'backlog', 'done', NULL, '2026-06-10 09:41:52', '2026-06-10 09:41:52', '2026-06-10 09:41:52', NULL, '67000e62833f645575e2b557a711d292', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'status', 'backlog', 'ready', NULL, '2026-06-10 09:42:26', '2026-06-10 09:42:26', '2026-06-10 09:42:26', NULL, '4dc40a53e7b803afd26d2334adc708b6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'description', 'Add a fourth option to the Claude permission prompt card (lib/builtin/claude/src/prompt_card.dart) alongside Allow / Allow & don''t ask again / Deny.
|
||
|
||
WHAT IT DOES
|
||
A deny that carries a preformatted follow-up note telling Claude the action was too complex for the permission system and to retry in a simpler format. Implemented as a _permDeny variant that passes a fixed note into DenyTool (today _permDeny uses _permNote() ?? ''Denied by the user.'' at prompt_card.dart:202). The user''s own note field, if filled, should still be respected — append it to / combine with the preformatted text rather than discard it.
|
||
|
||
LABEL: ''Deny & simplify'' (working label; placed after Deny).
|
||
TOOLTIP: ClideButton already supports (clide_button.dart:26) — add a mouseover explaining the behavior, e.g. ''Deny and ask Claude to retry this action in a simpler format — complex interactions don''t work well with the permission system.''
|
||
|
||
PREFORMATTED DENY NOTE (workshop wording, starting point):
|
||
"Denied — this action is too complex for the permission system to approve cleanly. Please retry with a simpler, more granular approach (break it into smaller steps or use a plainer command) to avoid this permission prompt. This is a one-off for THIS action only: do not add a memory and do not change permission settings — just reformulate and try again."
|
||
The ''do not add a memory / do not change permission settings'' clause is deliberate: without it Claude tends to ''fix'' the permission system (writing memories, rewriting permission config), which means continuous fiddling with a surface we don''t want it touching.
|
||
|
||
NUMBER-KEY SLOT
|
||
_activateNumber (prompt_card.dart:161-164) maps 1=Allow, 2=Allow&remember (when canRemember), 3/2=Deny. Add the new option as the next index (4 when canRemember, else 3). Keep Deny as its own option; the new one is additive. Update the digit/numpad shortcut mapping accordingly (see also T-310 numpad parity).
|
||
|
||
DESIGN NOTE — escalation behavior
|
||
The deny note enters the conversation and stays in context, so repeated use within one session compounds (Claude leans progressively harder toward simpler formats). That''s largely the intended escalating pressure, but the note is phrased as a one-shot ''retry THIS action'' rather than a standing rule to limit over-correction. Worth watching in testing whether repeated denials over-bias toward trivial formats.
|
||
|
||
ACCEPTANCE
|
||
- A fourth button ''Deny & simplify'' appears on the permission card with a tooltip.
|
||
- Activating it resolves the prompt as a deny whose note is the preformatted retry-simpler text (with the user''s typed note appended when present).
|
||
- The note explicitly tells Claude not to add a memory or change permission settings.
|
||
- Number-row and numpad digit shortcuts address the new option in the correct slot.
|
||
- Widget test covers the new button resolving to a DenyTool with the expected note.', 'Add a fourth option to the Claude permission prompt card (lib/builtin/claude/src/prompt_card.dart) alongside Allow / Allow & don''t ask again / Deny.
|
||
|
||
WHAT IT DOES
|
||
A deny that carries a preformatted follow-up note telling Claude the action was too complex for the permission system and to retry in a simpler format. Implemented as a _permDeny variant that passes a fixed note into DenyTool (today _permDeny uses _permNote() ?? ''Denied by the user.'' at prompt_card.dart:202). The user''s own note field, if filled, should still be respected — append it to / combine with the preformatted text rather than discard it.
|
||
|
||
LABEL: ''Deny & simplify'' (working label; placed after Deny).
|
||
TOOLTIP: ClideButton already supports (clide_button.dart:26) — add a mouseover explaining the behavior, e.g. ''Deny and ask Claude to retry this action in a simpler format — complex interactions don''t work well with the permission system.''
|
||
|
||
PREFORMATTED DENY NOTE (workshop wording, starting point):
|
||
"Denied — this action is too complex for the permission system to approve cleanly. Please retry with a simpler, more granular approach (break it into smaller steps or use a plainer command) to avoid this permission prompt. This is a one-off for THIS action only: do not add a memory and do not change permission settings — just reformulate and try again."
|
||
The ''do not add a memory / do not change permission settings'' clause is deliberate: without it Claude tends to ''fix'' the permission system (writing memories, rewriting permission config), which means continuous fiddling with a surface we don''t want it touching.
|
||
|
||
NUMBER-KEY SLOT
|
||
_activateNumber (prompt_card.dart:161-164) maps 1=Allow, 2=Allow&remember (when canRemember), 3/2=Deny. Add the new option as the next index (4 when canRemember, else 3). Keep Deny as its own option; the new one is additive. Update the digit/numpad shortcut mapping accordingly (see also T-310 numpad parity).
|
||
|
||
DESIGN NOTE — escalation behavior
|
||
The deny note enters the conversation and stays in context, so repeated use within one session compounds (Claude leans progressively harder toward simpler formats). That''s largely the intended escalating pressure, but the note is phrased as a one-shot ''retry THIS action'' rather than a standing rule to limit over-correction. Worth watching in testing whether repeated denials over-bias toward trivial formats.
|
||
|
||
ACCEPTANCE
|
||
- A fourth button ''Deny & simplify'' appears on the permission card with a tooltip.
|
||
- Activating it resolves the prompt as a deny whose note is the preformatted retry-simpler text (with the user''s typed note appended when present).
|
||
- The note explicitly tells Claude not to add a memory or change permission settings.
|
||
- Number-row and numpad digit shortcuts address the new option in the correct slot.
|
||
- Widget test covers the new button resolving to a DenyTool with the expected note.
|
||
|
||
CLARIFICATION (the TOOLTIP line above lost a word to shell escaping): ClideButton already exposes a tooltip parameter (clide_button.dart:17,26,42) — pass tooltip on the new button for the mouseover; no widget change needed.', NULL, '2026-06-10 09:56:24', '2026-06-10 09:56:24', '2026-06-10 09:56:24', NULL, 'f4fa8bc5799ef68d14512e128c03d1ba', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:01:09', '2026-06-10 10:01:09', '2026-06-10 10:01:09', NULL, '7ed94c6ae98bb9f7a2e3a843512c47bd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1SJKA7KRCQMZE41SPQG780', 'status', 'ready', 'done', NULL, '2026-06-10 10:07:19', '2026-06-10 10:07:19', '2026-06-10 10:07:19', NULL, '15b320f72bf2a7bec603482d4b980bf6', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:08:59', '2026-06-10 10:08:59', '2026-06-10 10:08:59', NULL, 'e240ed5c41e787235e004fc193df9163', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16YM1Y910T07Y1Y22MGTEM', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:09:11', '2026-06-10 10:09:11', '2026-06-10 10:09:11', NULL, '4cc7018105565dc1ab774296d7bdf4ac', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'description', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
|
||
|
||
**Deliverable**
|
||
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
|
||
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
|
||
|
||
**Scope to frame (from T-8)**
|
||
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
|
||
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
|
||
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
|
||
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
|
||
|
||
**Open design questions for the wireframe to answer**
|
||
- Settings as a full-screen view, a pane/tab, or a modal?
|
||
- Per-project (`.clide/settings.yaml`) vs. user-global scope — shown together or switched?
|
||
- Search/filter across all settings.
|
||
- How schema-driven fields render labels, help text, defaults, and reset.
|
||
|
||
**Constraints**
|
||
- Follow clide visual language — pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
|
||
|
||
This is the design step; implementation of the actual settings UI is separate child work under T-8.', 'Design the app''s settings screen as a Frame0 wireframe before building `builtin.settings-ui` (see Tier 6 epic T-8). Output is a wireframe to align on layout/IA, not implementation.
|
||
|
||
**Deliverable**
|
||
- Frame0 wireframe authored via the frame0-wireframe skill: local JSON source-of-truth synced to Frame0, exported for review.
|
||
- Covers the settings screen shell + at least one fully-rendered category so the form-field patterns are concrete.
|
||
|
||
**Scope to frame (from T-8)**
|
||
- Schema-driven panel: form fields keyed off the schema each subsystem registers against the kernel SettingsStore; edits write back to `.clide/settings.yaml`.
|
||
- Navigation/IA: how categories are grouped and selected (sidebar list? sections? search?).
|
||
- Field types to mock: toggle, enum/select (e.g. keymap preset), text/number, and a ''opens external file'' affordance (e.g. editor `.editorconfig` per T-290).
|
||
- Known consumers to account for: keymap preset switching (T-115/T-64/T-65/T-66), editor settings (T-290), activity fold level (T-183), theme picker (Tier 6 theming UI).
|
||
|
||
**Open design questions for the wireframe to answer**
|
||
- Settings as a full-screen view, a pane/tab, or a modal?
|
||
- Per-project (`.clide/settings.yaml`) vs. user-global scope — shown together or switched?
|
||
- Search/filter across all settings.
|
||
- How schema-driven fields render labels, help text, defaults, and reset.
|
||
|
||
**Constraints**
|
||
- Follow clide visual language — pull theme tokens / control geometry from the ui-design skill so the wireframe maps cleanly to real widgets (no Material/Cupertino).
|
||
|
||
This is the design step; implementation of the actual settings UI is separate child work under T-8.
|
||
|
||
## Scope-tag icon decision (review, 2026-06-10)
|
||
|
||
The per-field scope tag is an ICON, not a text label (space + cognitive load).
|
||
Chosen trio (location→global reach ladder), keeping the colour coding + a
|
||
tooltip with the word for clarity/a11y; tapping opens the scope menu:
|
||
|
||
- Project (this repo) = `folder` 0xe24a (teal) — already wired
|
||
- Always (all clide) = `globe` 0xe288 (amber) — add const
|
||
- Default (unset) = `circle-dashed` 0xe602 (grey) — add const
|
||
|
||
Alternatives considered: house (warmer home↔world pair), user-circle (mirrors
|
||
the real .clide vs ~/.clide files), gear (collides with "settings"). Note that
|
||
every Phosphor glyph already renders via PhosphorIconPainter(0xNNNN); only named
|
||
consts need adding. Preview/picking is blocked on a native glyph card (see the
|
||
new ticket) — goldens render the font as Ahem boxes.', NULL, '2026-06-10 10:20:48', '2026-06-10 10:20:48', '2026-06-10 10:20:48', NULL, '14f95d56a8f037f285dc4cbeb1ae7504', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders Phosphor glyphs by name/codepoint, so icons can be previewed and picked in the live pane.', 'A conversation-pane card that renders Phosphor glyphs by name/codepoint, so icons can be previewed and picked in the live pane.
|
||
|
||
## Why
|
||
|
||
Picking icons (e.g. the settings scope tags, T-302) needs to SEE real glyphs.
|
||
Frame0 can''t render Phosphor (private-use codepoints, no font) and golden tests
|
||
render the font as Ahem boxes (only painter_bold_metrics loads a real font), so
|
||
preview only works where the app has the font — a native card is the vehicle.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a
|
||
grid of Phosphor glyphs with their name + codepoint, driven by the clide CLI —
|
||
e.g. `clide icon show <name|0xNNNN ...>` or `clide glyphs [filter]` (D-6 parity).
|
||
Uses the already-bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
## Notes / scope
|
||
|
||
- Every glyph already renders via `PhosphorIconPainter(0xNNNN)`; the 49 named
|
||
consts in lib/widgets/src/icons/phosphor.dart are curated sugar. We do NOT
|
||
need to bulk-add all ~1512 consts for availability.
|
||
- OPTIONAL behind this card: generate the full name→codepoint set (from
|
||
.claude/skills/ui-design/references/phosphor-glyphs.md, 1512 entries) so glyphs
|
||
are discoverable by name — but only if the card makes them browsable; weigh
|
||
against clide''s curated/minimal philosophy.
|
||
- The card is display-only (D-78); a click could copy the codepoint/name.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302).', NULL, '2026-06-10 10:21:09', '2026-06-10 10:21:09', '2026-06-10 10:21:09', NULL, '1ea34a81269e81267f69a993eabf4002', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'status', 'ready', 'done', NULL, '2026-06-10 10:21:48', '2026-06-10 10:21:48', '2026-06-10 10:21:48', NULL, '37b28095fb8c93b3e3319402eae91173', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:29:08', '2026-06-10 10:29:08', '2026-06-10 10:29:08', NULL, '7c78be9c5f8508b9e8ec788b0f34d95a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'status', 'backlog', 'ready', NULL, '2026-06-10 10:38:06', '2026-06-10 10:38:06', '2026-06-10 10:38:06', NULL, '9ea7fb36b62a72a630cbe9c58a54521d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders Phosphor glyphs by name/codepoint, so icons can be previewed and picked in the live pane.
|
||
|
||
## Why
|
||
|
||
Picking icons (e.g. the settings scope tags, T-302) needs to SEE real glyphs.
|
||
Frame0 can''t render Phosphor (private-use codepoints, no font) and golden tests
|
||
render the font as Ahem boxes (only painter_bold_metrics loads a real font), so
|
||
preview only works where the app has the font — a native card is the vehicle.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a
|
||
grid of Phosphor glyphs with their name + codepoint, driven by the clide CLI —
|
||
e.g. `clide icon show <name|0xNNNN ...>` or `clide glyphs [filter]` (D-6 parity).
|
||
Uses the already-bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
## Notes / scope
|
||
|
||
- Every glyph already renders via `PhosphorIconPainter(0xNNNN)`; the 49 named
|
||
consts in lib/widgets/src/icons/phosphor.dart are curated sugar. We do NOT
|
||
need to bulk-add all ~1512 consts for availability.
|
||
- OPTIONAL behind this card: generate the full name→codepoint set (from
|
||
.claude/skills/ui-design/references/phosphor-glyphs.md, 1512 entries) so glyphs
|
||
are discoverable by name — but only if the card makes them browsable; weigh
|
||
against clide''s curated/minimal philosophy.
|
||
- The card is display-only (D-78); a click could copy the codepoint/name.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302).', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant scale is the inline type scale in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive sizing off those tokens (not bare numbers) so the row tracks the scale if it changes; the hero size can be a named large constant. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only vs. selectable (D-78 — key boundary)
|
||
|
||
The card itself is a CONVERSATION WIDGET and stays display-only (D-78): it shows the labelled icon options and a click may copy the codepoint/name, but it does NOT resolve a selection inline. When the offer needs to be an actual PICK that returns a choice, that selection belongs in the interaction zone (AskUserQuestion-style option list that replaces the composer), NOT inline in the transcript — see the interaction-zone rule. So scope this ticket to the display/offer card; an interaction-zone icon picker (or feeding these entries into an AskUserQuestion option list) is a follow-up, not part of this card. Flag which path we want before building the selectable variant.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a large hero plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); the description records that a selectable picker routes through the interaction zone as a separate follow-up.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:41:56', '2026-06-10 10:41:56', '2026-06-10 10:41:56', NULL, 'cd2ddb517ae86b0e0751a599a99c3acd', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'title', 'Native Phosphor glyph preview card (clide icon show)', 'Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)', NULL, '2026-06-10 10:41:56', '2026-06-10 10:41:56', '2026-06-10 10:41:56', NULL, 'fd46733997b9f01126da8e45a3f2a52a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant scale is the inline type scale in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive sizing off those tokens (not bare numbers) so the row tracks the scale if it changes; the hero size can be a named large constant. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only vs. selectable (D-78 — key boundary)
|
||
|
||
The card itself is a CONVERSATION WIDGET and stays display-only (D-78): it shows the labelled icon options and a click may copy the codepoint/name, but it does NOT resolve a selection inline. When the offer needs to be an actual PICK that returns a choice, that selection belongs in the interaction zone (AskUserQuestion-style option list that replaces the composer), NOT inline in the transcript — see the interaction-zone rule. So scope this ticket to the display/offer card; an interaction-zone icon picker (or feeding these entries into an AskUserQuestion option list) is a follow-up, not part of this card. Flag which path we want before building the selectable variant.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a large hero plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); the description records that a selectable picker routes through the interaction zone as a separate follow-up.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering at 48px so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. The 48px hero is larger than any existing type token (welcome banner is 52, dialog title 16), so add a named preview constant for it (e.g. clideIconPreviewHero = 48) rather than a bare 48 — keep the scale coherent. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a 48px hero (new named constant, e.g. clideIconPreviewHero) plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:43:01', '2026-06-10 10:43:01', '2026-06-10 10:43:01', NULL, 'fdb996091e37ea8551ea0b002afb0291', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering at 48px so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. The 48px hero is larger than any existing type token (welcome banner is 52, dialog title 16), so add a named preview constant for it (e.g. clideIconPreviewHero = 48) rather than a bare 48 — keep the scale coherent. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a 48px hero (new named constant, e.g. clideIconPreviewHero) plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. For the hero, REUSE the existing clideFontWelcomeBanner (52) token rather than adding a new constant — it''s already the app''s named oversized size; no new token needed. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a hero at the existing clideFontWelcomeBanner (52) token plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:44:06', '2026-06-10 10:44:06', '2026-06-10 10:44:06', NULL, 'a55e979e042abaf7b0758b5dbba183f7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'status', 'ready', 'done', NULL, '2026-06-10 10:46:25', '2026-06-10 10:46:25', '2026-06-10 10:46:25', NULL, '4f76b8c659b36db90ac957a42c610d6f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. For the hero, REUSE the existing clideFontWelcomeBanner (52) token rather than adding a new constant — it''s already the app''s named oversized size; no new token needed. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape
|
||
|
||
- Trivial case stays terse, mirroring image show: `clide icon show <name|0xNNNN> [<name> ...]` renders the bare glyph(s) with name + codepoint.
|
||
- Labelled/described case takes a structured payload (per entry has up to three fields, so flags get unwieldy): a JSON array via `--stdin`/`--file`, each item `{"icon": "gear", "label": "Settings", "description": "global scope"}` where label and description are optional. icon resolves by kebab-case name (PhosphorIcons.byName) or 0xNNNN codepoint.
|
||
- One card per invocation; the entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `clide icon show` accepts multiple icons in one call and renders them in a single conversation card.
|
||
- A structured (JSON) input lets each icon carry an optional label and optional description, both rendered alongside the glyph.
|
||
- Each icon renders at multiple sizes: a hero at the existing clideFontWelcomeBanner (52) token plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. For the hero, REUSE the existing clideFontWelcomeBanner (52) token rather than adding a new constant — it''s already the app''s named oversized size; no new token needed. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape + IPC wiring (mirror image.show exactly)
|
||
|
||
Follow the image-card template (lib/src/daemon/image_commands.dart) end to end — it is the proven D-6 parity pattern:
|
||
|
||
- REGISTRATION: a dotted `icon.show` command on DaemonDispatcher (invoked as `clide icon show`), declared with a CommandSchema — positional + per-arg ArgSpec — exactly like image.show''s `{positional: [''path''], args: {...}}`. Handler stays Flutter-free so it runs under `dart test`.
|
||
- BARE PREVIEW (variadic): one or more icons as positionals via ArgType.stringList — `clide icon show gear folder gauge` — each a kebab-case name (resolved by PhosphorIcons.byName) or a 0xNNNN codepoint. stringList is already supported by the schema (lib/src/ipc/command_schema.dart) and the argv parser, so no new CLI plumbing.
|
||
- LABELLED/DESCRIBED entries: a `--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
|
||
- RENDER PATH: validate + resolve icon names in the handler (inject a resolver the way image.show injects ImagePathResolver, so headless/dart-test stays filesystem-free), then publish on a dedicated MessageBus channel — e.g. `iconShowChannel = ''icon''`, peer of `imageShowChannel = ''image''` — captured post-boot in main.dart; the Claude extension subscribes to that literal and injects the card into the primary session''s conversation log. Honest failure (IpcError userError/notFound) on an unknown glyph name or a malformed/missing --file, and on no live UI bus (headless), mirroring image.show.
|
||
- One card per invocation; entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `icon.show` is registered on DaemonDispatcher with a CommandSchema and invoked as `clide icon show`, mirroring image.show; the handler is Flutter-free and publishes on an `icon` MessageBus channel injected by the Claude extension.
|
||
- `clide icon show <name> [<name> ...]` accepts multiple icons (variadic stringList positionals) in one call and renders them in a single conversation card.
|
||
- A `--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
|
||
- Each icon renders at multiple sizes: a hero at the existing clideFontWelcomeBanner (52) token plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', NULL, '2026-06-10 10:49:59', '2026-06-10 10:49:59', '2026-06-10 10:49:59', NULL, '071db34b13832c72d0762fb6f47d40f5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'description', NULL, 'Add a stdin path to clide''s CLI so a command can receive a JSON payload piped in — `… | clide icon show --stdin`, `cat meta.json | clide image show foo.png --stdin` — instead of only positionals/flags or a `--file`.
|
||
|
||
## Why
|
||
|
||
Structured commands (the labelled icon-card entries in T-313, image annotation metadata in T-316) want a JSON payload that''s awkward to express as flags. Today clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals, --flags, and `-- passthrough`; there is no stdin path. T-313 therefore falls back to a `--file <path.json>` flag. A `--stdin` convention is the ergonomic peer of `--file` for piping, and is shared infra both icon.show and image.show consume.
|
||
|
||
## Where the work lives
|
||
|
||
clide''s IPC server runs in-process and the `clide` CLI is a thin client that serialises argv into an IpcRequest over CLIDE_SOCK. So stdin must be slurped CLIENT-SIDE (in lib/src/cli/, around argv_to_request.dart / argv_dispatch.dart) and folded into the request before it is sent — the in-process handler never sees the real stdin. Decide how it surfaces in the envelope: e.g. a reserved `stdin`/`payload` field on IpcRequest, or a synthesised arg the CommandSchema can opt into (an ArgSpec flag like `acceptsStdin`, mirroring how ArgType.stringList is declared in lib/src/ipc/command_schema.dart).
|
||
|
||
## Scope / decisions
|
||
|
||
- Generic infra, not icon/image specific — once landed, any command opts in via its CommandSchema.
|
||
- Keep `--file` working; --stdin and --file should be mutually exclusive (error if both given) or layered with a defined precedence.
|
||
- Text/JSON payloads only to start; define a size cap and a clear error when --stdin is passed but stdin is empty/not a pipe (don''t hang waiting on a TTY).
|
||
- Honest IpcError (userError) on malformed JSON, surfaced like image.show''s other validation failures.
|
||
- D-6 parity: document the stdin convention alongside the other CLI verbs.
|
||
|
||
## Acceptance
|
||
|
||
- A command can declare (via CommandSchema) that it accepts a stdin payload; piping JSON in populates the IpcRequest with that payload.
|
||
- `clide icon show --stdin` (T-313) and `clide image show <path> --stdin` (T-316) both consume it.
|
||
- --stdin + --file together is a clear user error; --stdin with no piped input fails fast, never hangs on a TTY.
|
||
- Malformed JSON returns a userError with a helpful message.
|
||
|
||
Unblocks the piped-JSON variants of T-313 (icon entries) and T-316 (image annotations); both can also ship with --file independently of this.', NULL, '2026-06-10 10:55:30', '2026-06-10 10:55:30', '2026-06-10 10:55:30', NULL, '60b96ae9749305f70e7ca44457cf9e2b', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', 'description', NULL, 'Give `clide image show` the same structured-metadata plumbing the icon card gets (T-313), so an image driven into the conversation can carry ANNOTATIONS — not just a single one-line caption.
|
||
|
||
## Why
|
||
|
||
Today image.show (lib/src/daemon/image_commands.dart) takes a `path` positional plus an optional `--caption` string, and ImageMessage (lib/builtin/claude/src/transcript_reader.dart:160) holds only `path` + optional one-line `caption`. When showing a screenshot/wireframe to discuss, one line is thin — we want to attach a title/label, a longer description, and potentially captioned markers (callouts on specific spots). This mirrors the per-entry label+description the icon card introduces.
|
||
|
||
## Deliverable
|
||
|
||
Extend image.show to accept a JSON metadata payload (via the new stdin plumbing T-315 and/or a `--file <path.json>` flag, same as T-313''s icon entries) describing the image''s annotations, e.g.:
|
||
|
||
{ "path": "docs/shot.png", "label": "HUD v3", "description": "note the cramped status row", "caption": "before the fix" }
|
||
|
||
Extend ImageMessage + the image card to render the richer metadata. Keep the existing `clide image show <path> --caption "…"` form working unchanged (back-compat); the JSON payload is additive.
|
||
|
||
## Open decision — text vs visual annotation
|
||
|
||
''Annotated'' could mean (a) richer TEXT metadata shown around the image (label + description + caption), or (b) VISUAL overlays drawn ON the image (markers/arrows/numbered callouts at coordinates). Start with (a) — straightforward extension of the current card. (b) is a bigger, clide-owned CustomPaint job (markers:[{x,y,label}] painted over the image); feasible since the image card is our own rendering, but scope it as a follow-up unless we decide we need it now. Flag which we want before building markers.
|
||
|
||
## Dependencies
|
||
|
||
- Pairs with T-315 (stdin JSON plumbing) for the piped form; can land with `--file` alone if T-315 isn''t ready.
|
||
- Parallel to T-313 (icon card) — same --file/--stdin metadata pattern, same Flutter-free handler + MessageBus publish path.
|
||
|
||
## Acceptance
|
||
|
||
- `clide image show` accepts a JSON metadata payload (via --file, and via --stdin once T-315 lands) carrying at least label + description alongside the existing caption.
|
||
- ImageMessage + the image card render the added metadata.
|
||
- The existing `image show <path> [--caption]` form is unchanged.
|
||
- Malformed payload / unknown fields fail with a clear userError, mirroring image.show''s current validation.
|
||
- Visual marker overlays are explicitly noted as a separate follow-up unless pulled in by decision.', NULL, '2026-06-10 10:55:34', '2026-06-10 10:55:34', '2026-06-10 10:55:34', NULL, '8ff1b92539fd1e66d249a8ebb437197d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'description', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. For the hero, REUSE the existing clideFontWelcomeBanner (52) token rather than adding a new constant — it''s already the app''s named oversized size; no new token needed. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape + IPC wiring (mirror image.show exactly)
|
||
|
||
Follow the image-card template (lib/src/daemon/image_commands.dart) end to end — it is the proven D-6 parity pattern:
|
||
|
||
- REGISTRATION: a dotted `icon.show` command on DaemonDispatcher (invoked as `clide icon show`), declared with a CommandSchema — positional + per-arg ArgSpec — exactly like image.show''s `{positional: [''path''], args: {...}}`. Handler stays Flutter-free so it runs under `dart test`.
|
||
- BARE PREVIEW (variadic): one or more icons as positionals via ArgType.stringList — `clide icon show gear folder gauge` — each a kebab-case name (resolved by PhosphorIcons.byName) or a 0xNNNN codepoint. stringList is already supported by the schema (lib/src/ipc/command_schema.dart) and the argv parser, so no new CLI plumbing.
|
||
- LABELLED/DESCRIBED entries: a `--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
|
||
- RENDER PATH: validate + resolve icon names in the handler (inject a resolver the way image.show injects ImagePathResolver, so headless/dart-test stays filesystem-free), then publish on a dedicated MessageBus channel — e.g. `iconShowChannel = ''icon''`, peer of `imageShowChannel = ''image''` — captured post-boot in main.dart; the Claude extension subscribes to that literal and injects the card into the primary session''s conversation log. Honest failure (IpcError userError/notFound) on an unknown glyph name or a malformed/missing --file, and on no live UI bus (headless), mirroring image.show.
|
||
- One card per invocation; entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `icon.show` is registered on DaemonDispatcher with a CommandSchema and invoked as `clide icon show`, mirroring image.show; the handler is Flutter-free and publishes on an `icon` MessageBus channel injected by the Claude extension.
|
||
- `clide icon show <name> [<name> ...]` accepts multiple icons (variadic stringList positionals) in one call and renders them in a single conversation card.
|
||
- A `--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
|
||
- Each icon renders at multiple sizes: a hero at the existing clideFontWelcomeBanner (52) token plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
|
||
|
||
## Why
|
||
|
||
Picking icons needs to SEE real glyphs side by side with what each one means. Frame0 can''t render Phosphor (private-use codepoints, no font) and goldens render the font as Ahem boxes, so preview only works where the app has the font — a native card is the vehicle. Beyond a bare grid, real icon decisions are ''which of these N icons, for these N meanings'' — so each entry wants an optional label (what we''d call it) and an optional description (what it represents), turning the card into an offer/choice list.
|
||
|
||
## Deliverable
|
||
|
||
A conversation-pane card (peer of the image card, T-249/T-252) that renders a list/grid of entries, each entry = glyph + optional label + optional description, driven by the clide CLI (D-6 parity). Uses the bundled Phosphor.ttf via PhosphorIconPainter.
|
||
|
||
### Multi-size rendering (per entry)
|
||
|
||
Each icon is shown at SEVERAL sizes, not one: (1) a large hero rendering so the glyph''s detail is clearly legible, and (2) a sample at each font-size token the app actually uses inline, so you can judge how the glyph reads at real UI sizes. The relevant inline scale is in lib/widgets/src/typography.dart: clideFontBadge (11), clideFontSmall (12), clideFontMeta (13), clideFontCaption/clideFontMono (14), clideFontBody (15). Drive the inline samples off those tokens (not bare numbers) so the row tracks the scale if it changes. For the hero, REUSE the existing clideFontWelcomeBanner (52) token rather than adding a new constant — it''s already the app''s named oversized size; no new token needed. Lay the size samples out in a single row/strip per entry, smallest to largest, labelled with the token/px so a reviewer sees exactly where each size lands.
|
||
|
||
### CLI shape + IPC wiring (mirror image.show exactly)
|
||
|
||
Follow the image-card template (lib/src/daemon/image_commands.dart) end to end — it is the proven D-6 parity pattern:
|
||
|
||
- REGISTRATION: a dotted `icon.show` command on DaemonDispatcher (invoked as `clide icon show`), declared with a CommandSchema — positional + per-arg ArgSpec — exactly like image.show''s `{positional: [''path''], args: {...}}`. Handler stays Flutter-free so it runs under `dart test`.
|
||
- BARE PREVIEW (variadic): one or more icons as positionals via ArgType.stringList — `clide icon show gear folder gauge` — each a kebab-case name (resolved by PhosphorIcons.byName) or a 0xNNNN codepoint. stringList is already supported by the schema (lib/src/ipc/command_schema.dart) and the argv parser, so no new CLI plumbing.
|
||
- LABELLED/DESCRIBED entries: a `--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
|
||
- RENDER PATH: validate + resolve icon names in the handler (inject a resolver the way image.show injects ImagePathResolver, so headless/dart-test stays filesystem-free), then publish on a dedicated MessageBus channel — e.g. `iconShowChannel = ''icon''`, peer of `imageShowChannel = ''image''` — captured post-boot in main.dart; the Claude extension subscribes to that literal and injects the card into the primary session''s conversation log. Honest failure (IpcError userError/notFound) on an unknown glyph name or a malformed/missing --file, and on no live UI bus (headless), mirroring image.show.
|
||
- One card per invocation; entries render as rows (or a grid when label/description are absent).
|
||
|
||
## Display-only card + interaction-zone selection (D-78 — decided)
|
||
|
||
DECIDED: the display card is display-only; the SELECTION happens in the convo box (interaction zone), not on the card. The card renders the labelled icon options for the user to SEE; when a pick is needed, Claude offers a matching choice list in the interaction zone (AskUserQuestion-style options that replace the composer), and the user selects there. This keeps conversation widgets display-only per D-78 and the interaction-zone rule.
|
||
|
||
The LABEL is the bridge between the two surfaces: Claude attaches a label to each icon on the display card, then offers the SAME labels as the options in the interaction-zone choice list — so ''I pick Settings'' in the convo box maps unambiguously back to the glyph the user saw on the card. That''s why per-entry labels are first-class here: they exist to facilitate this show-then-pick flow, with the description giving the extra context that doesn''t fit a one-word option. The card may still copy a codepoint/name on click (a convenience), but it never resolves the choice itself.
|
||
|
||
## Notes / scope
|
||
|
||
- Name->codepoint resolution ALREADY EXISTS: lib/widgets/src/icons/phosphor_glyphs.g.dart (generated, 1512 glyphs) + PhosphorIcons.byName. The earlier ''OPTIONAL: generate the full set'' caveat is resolved — every named glyph is already resolvable; the 49 curated consts in phosphor.dart remain curated sugar.
|
||
- Every glyph already renders via PhosphorIconPainter(0xNNNN).
|
||
- label and description are both optional per entry; an entry with neither degrades to the bare-preview look.
|
||
- Surfaced 2026-06-10 while choosing settings scope icons (T-302); refined 2026-06-10 to cover multi-icon labelled offer/choice lists.
|
||
|
||
## Acceptance
|
||
|
||
- `icon.show` is registered on DaemonDispatcher with a CommandSchema and invoked as `clide icon show`, mirroring image.show; the handler is Flutter-free and publishes on an `icon` MessageBus channel injected by the Claude extension.
|
||
- `clide icon show <name> [<name> ...]` accepts multiple icons (variadic stringList positionals) in one call and renders them in a single conversation card.
|
||
- A `--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
|
||
- Each icon renders at multiple sizes: a hero at the existing clideFontWelcomeBanner (52) token plus one sample at each inline font-size token (badge 11 -> body 15), sized off the typography tokens and labelled so the reviewer sees legibility at real UI sizes.
|
||
- The card is display-only (no inline selection); selection happens in the interaction zone (convo box) via a Claude-offered choice list whose options reuse the per-icon labels from the card.
|
||
- Unknown/invalid icon names fail with a clear user error, not a blank glyph.
|
||
|
||
FOLLOW-UPS: the piped-JSON (--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.', NULL, '2026-06-10 10:55:49', '2026-06-10 10:55:49', '2026-06-10 10:55:49', NULL, 'b75962f3fb5da0a8c44f7e059d7035b2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EDCBYRBDSV9V1PJ1KE3CM', 'description', NULL, 'Epic for the unified conversation drawing card — ONE clide-owned canvas renderer driven by a JSON document, replacing the trend of one-off conversation renderers (image card, icon card, …). See decision D-91.
|
||
|
||
## The model
|
||
|
||
Think HTML `<canvas>`, NOT Obsidian''s `.canvas` (Obsidian naming its feature ''canvas'' doesn''t make its schema our pattern). The card''s JSON can express raw PRIMITIVES (rects/lines/text at coordinates — ''draw a box at x,y, a line from x,y to x,y'' works), but in the common case it runs in TEMPLATE mode: the JSON names a predefined component and the card grabs it. Templates lower onto the same primitive scene the raw API accepts (hybrid: scene-graph core + high-level block sugar).
|
||
|
||
Per drawn object there is an optional label + description widget rendered BENEATH it, shown only when those fields are present in the JSON.
|
||
|
||
The card is DISPLAY-ONLY (D-78) — selection/interaction happens in the interaction zone, never on the card. The labels are the bridge to interaction-zone choice lists (see T-313).
|
||
|
||
## Template set (expand as we go)
|
||
|
||
- image — display the file, lightbox on click
|
||
- icon — the multi-size glyph set (T-313)
|
||
- compare-images / before-after — two (or more) paths side by side, each with its label/description
|
||
- svg — render an SVG file
|
||
- graph — render a graph
|
||
- …more added incrementally; ''we will expand the templates as we go''
|
||
|
||
## Reuse goal
|
||
|
||
The same renderer is intended for reuse as the `.canvas` VIEWER elsewhere: a `.canvas` file is converted into this JSON (not the card adopting Obsidian''s schema natively). Narrows Q-4.
|
||
|
||
## Input
|
||
|
||
Driven via the clide CLI (D-6 parity), consuming the JSON-payload plumbing — `--file` and the stdin path (T-315).
|
||
|
||
## Children / related
|
||
|
||
- Core canvas engine (primitives + template dispatch + per-object label/description) — foundational.
|
||
- Per-template tickets: image, icon (T-313), compare/before-after, svg, graph.
|
||
- T-315 — stdin/--file JSON input plumbing (input channel).
|
||
- T-316 — image annotation metadata (folds in as the image/compare template''s label/description/markers).
|
||
- `.canvas` -> JSON converter / reuse (deferred).
|
||
- Existing image card (T-249/T-252): LEFT AS-IS for now; migrated onto the canvas once the canvas layer lands.
|
||
|
||
## Scope note
|
||
|
||
This is a real renderer with a schema, not a one-off widget. The schema must stay coherent across the primitive and template layers as templates accrue (D-91 cost). Start with a true canvas + the first templates; grow the template set over time.', NULL, '2026-06-10 11:10:59', '2026-06-10 11:10:59', '2026-06-10 11:10:59', NULL, '7892f2cc2259a7e3cf3d50ceb1c93358', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', 'parent_id', 'T-276', 'T-317', NULL, '2026-06-10 11:11:10', '2026-06-10 11:11:10', '2026-06-10 11:11:10', NULL, '245a2639738736df805e3e3eaa7cbb96', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'parent_id', 'T-276', 'T-317', NULL, '2026-06-10 11:11:10', '2026-06-10 11:11:10', '2026-06-10 11:11:10', NULL, '994f7c1bd8f3f205dc7183cccf7d4329', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'parent_id', 'T-276', 'T-317', NULL, '2026-06-10 11:11:10', '2026-06-10 11:11:10', '2026-06-10 11:11:10', NULL, 'e0ba90285dafac31357bb999d8e12c36', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16YM1Y910T07Y1Y22MGTEM', 'status', 'ready', 'done', NULL, '2026-06-10 11:16:41', '2026-06-10 11:16:41', '2026-06-10 11:16:41', NULL, '9873b3f3b5d107be1146224ac48b9b45', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7162Z26FDWXG35VH0', 'description', '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).', '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).
|
||
|
||
MERGED (2026-06-10) into the unified canvas epic T-317 (decision D-91). The canvas rendering work consolidates onto one clide-owned canvas renderer driven by JSON; .canvas becomes an import format converted into that JSON rather than a native schema. T-7''s scope is preserved as children of T-317: canvas pane -> T-322, graph view -> T-323. This epic is cancelled as superseded; track the work under T-317.', NULL, '2026-06-10 11:17:31', '2026-06-10 11:17:31', '2026-06-10 11:17:31', NULL, 'e7d70c5b631bd6109f1b83b55c857c20', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67JSC5RKS6M9182KG', 'parent_id', 'T-7', 'T-276', NULL, '2026-06-10 11:18:12', '2026-06-10 11:18:12', '2026-06-10 11:18:12', NULL, 'df23887d88f2a45e8ad12a72a4b27407', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7162Z26FDWXG35VH0', 'status', 'backlog', 'cancelled', NULL, '2026-06-10 11:18:15', '2026-06-10 11:18:15', '2026-06-10 11:18:15', NULL, '64906dcc15f8f1b32034640a7e9aab82', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'status', 'ready', 'done', NULL, '2026-06-10 11:25:58', '2026-06-10 11:25:58', '2026-06-10 11:25:58', NULL, '678a32ea96db32f51581309dc2af1b6d', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'status', 'ready', 'backlog', NULL, '2026-06-10 11:29:04', '2026-06-10 11:29:04', '2026-06-10 11:29:04', NULL, 'fb25a3680bd8ffd0bc2089a6ab33820a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2J2HWD66QAFDDRRWS5NM48', 'status', 'backlog', 'ready', NULL, '2026-06-10 11:29:10', '2026-06-10 11:29:10', '2026-06-10 11:29:10', NULL, '43ad1db5744fc3bc86913f79cd007f42', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'status', 'backlog', 'ready', NULL, '2026-06-10 11:33:18', '2026-06-10 11:33:18', '2026-06-10 11:33:18', NULL, 'fba9411887602148dab1b2b4ad8647b7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'status', 'backlog', 'ready', NULL, '2026-06-10 11:33:26', '2026-06-10 11:33:26', '2026-06-10 11:33:26', NULL, 'f280d9b2793130900d5e470c4cb6c088', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'status', 'backlog', 'ready', NULL, '2026-06-10 11:33:35', '2026-06-10 11:33:35', '2026-06-10 11:33:35', NULL, 'bb110c716ec26f37ed4ece36b582d4d5', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2J2HWD66QAFDDRRWS5NM48', 'status', 'ready', 'done', NULL, '2026-06-10 11:36:35', '2026-06-10 11:36:35', '2026-06-10 11:36:35', NULL, '376bbab713452158f399f02429870546', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'description', 'The logo-mark spinner shown on in-progress activity/holder cards in the Claude conversation is too small to read as a spinner — it reads as a static speck. Enlarge it so the running state is legible at a glance.
|
||
|
||
**Where**
|
||
- `ClideSpinner` (lib/widgets/src/clide_spinner.dart) — defaults to size 14; renders the logo SVG at width/height = size.
|
||
- `ClideStatusIndicator` (lib/widgets/src/clide_status_indicator.dart) — default size 14; maps running→ClideSpinner, success→check, error→cross at the same size.
|
||
- Call sites: holder_card.dart:117 and :199 pass `size: 12` — the small value the user is seeing.
|
||
|
||
**Direction (settle in review)**
|
||
- Bump the spinner size on the activity cards (the `size: 12` call sites, and/or the indicator default) to something clearly legible — pull a concrete value from the ui-design control-geometry tokens rather than a magic number.
|
||
- Keep the running spinner, success check, and error cross visually balanced at the new size (they share `size`), so the card doesn''t jump when the state settles.
|
||
- Check the other ClideSpinner/StatusIndicator consumers (status surfaces) so the bump doesn''t bloat unrelated spots — may warrant sizing the cards explicitly rather than changing the shared default.
|
||
|
||
**Acceptance**
|
||
- The in-progress spinner on conversation activity cards is comfortably distinguishable as a spinning indicator; success/error glyphs stay aligned at the same footprint.', 'The logo-mark spinner shown on in-progress activity/holder cards in the Claude conversation is too small to read as a spinner — it reads as a static speck. Enlarge it so the running state is legible at a glance.
|
||
|
||
**Where**
|
||
- `ClideSpinner` (lib/widgets/src/clide_spinner.dart) — defaults to size 14; renders the logo SVG at width/height = size.
|
||
- `ClideStatusIndicator` (lib/widgets/src/clide_status_indicator.dart) — default size 14; maps running→ClideSpinner, success→check, error→cross at the same size.
|
||
- Call sites: holder_card.dart:117 and :199 pass `size: 12` — the small value the user is seeing.
|
||
|
||
**Direction (settle in review)**
|
||
- Bump the spinner size on the activity cards (the `size: 12` call sites, and/or the indicator default) to something clearly legible — pull a concrete value from the ui-design control-geometry tokens rather than a magic number.
|
||
- Keep the running spinner, success check, and error cross visually balanced at the new size (they share `size`), so the card doesn''t jump when the state settles.
|
||
- Check the other ClideSpinner/StatusIndicator consumers (status surfaces) so the bump doesn''t bloat unrelated spots — may warrant sizing the cards explicitly rather than changing the shared default.
|
||
|
||
**Acceptance**
|
||
- The in-progress spinner on conversation activity cards is comfortably distinguishable as a spinning indicator; success/error glyphs stay aligned at the same footprint.
|
||
|
||
**Initial trial**
|
||
- For the first cut, double the current size: the `size: 12` activity-card call sites go to `size: 24`. Trial that footprint, then settle the final value in review against the control-geometry tokens.', NULL, '2026-06-10 11:55:15', '2026-06-10 11:55:15', '2026-06-10 11:55:15', NULL, '3be7c3ff633afbc67c91dd3f97c27e6f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'status', 'ready', 'done', NULL, '2026-06-10 12:05:17', '2026-06-10 12:05:17', '2026-06-10 12:05:17', NULL, 'a19ce81190caca50c2628a98835d41a3', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'parent_id', NULL, 'T-276', NULL, '2026-06-10 12:05:21', '2026-06-10 12:05:21', '2026-06-10 12:05:21', NULL, 'fefee4b227754f03c985841fb869a346', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'description', NULL, 'AnimatedSwitcher in ClideStatusIndicator throws "Duplicate keys found" (Stack has multiple children with key [<''running''>]) during normal app run, cascading into a flood of follow-on errors ("Tried to build dirty widget in the wrong build scope", "debugNeedsLayout is not true", "ScrollController attached to multiple scroll views", etc).
|
||
|
||
Location: lib/widgets/src/clide_status_indicator.dart:37 (AnimatedSwitcher at build()).
|
||
|
||
Root cause: each status maps to a child with a fixed ValueKey (''running'' / ''success'' / ''error''). AnimatedSwitcher cross-fades the outgoing and incoming child inside a Stack for its 200ms duration. When the status flips back to a value whose previous child is still animating out (e.g. running -> success -> running within 200ms, or repeated running rebuilds), the still-exiting child and the new child both carry ValueKey(''running'') and collide in the Stack -> duplicate-key assertion. The downstream exceptions are the framework unwinding from the failed build.
|
||
|
||
Repro: observed live during `make run` with two Claude panes bound (primary + secondary-1); status indicators flipping quickly trigger it.
|
||
|
||
Fix direction: the ValueKey must be unique per indicator instance, not just per status, so two instances (or an in-flight transition) never share a key. Options: key by status combined with a stable per-widget id, or drop the const keys and let AnimatedSwitcher key on child type. Add a widget test that rapidly toggles status within the switch duration and pumps mid-transition to guard the regression.', NULL, '2026-06-10 12:05:39', '2026-06-10 12:05:39', '2026-06-10 12:05:39', NULL, '540deffcc7de142a4732a92b5f1dc5be', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'priority', 'medium', 'high', NULL, '2026-06-10 12:05:39', '2026-06-10 12:05:39', '2026-06-10 12:05:39', NULL, 'e845355200ef8aaabc986d577724d39e', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'status', 'backlog', 'ready', NULL, '2026-06-10 12:07:39', '2026-06-10 12:07:39', '2026-06-10 12:07:39', NULL, 'b013a394e376245d8248d776021e0314', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'backlog', 'ready', NULL, '2026-06-10 12:08:14', '2026-06-10 12:08:14', '2026-06-10 12:08:14', NULL, 'f56fda6382cbec3561ef644954c04f43', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'ready', 'backlog', NULL, '2026-06-10 12:08:18', '2026-06-10 12:08:18', '2026-06-10 12:08:18', NULL, '12eee2948e8f79732b40384a40429e14', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2W4G9K8ZF782W7H2TM5XA8', 'status', 'backlog', 'ready', NULL, '2026-06-10 12:12:30', '2026-06-10 12:12:30', '2026-06-10 12:12:30', NULL, '6b9aaf9bd7ec3ad4ad3b95d1468ff9cf', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'status', 'ready', 'done', NULL, '2026-06-10 12:14:00', '2026-06-10 12:14:00', '2026-06-10 12:14:00', NULL, 'd90deec4e27b4298c6f7280338599c01', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'status', 'ready', 'done', NULL, '2026-06-10 12:31:23', '2026-06-10 12:31:23', '2026-06-10 12:31:23', NULL, 'dbac53eef0b8d58dbb5f00a2f77ee52f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'status', 'done', 'done', NULL, '2026-06-10 12:32:07', '2026-06-10 12:32:07', '2026-06-10 12:32:07', NULL, 'ea979d683072b321ce6eae3dbbbf6c31', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2W4G9K8ZF782W7H2TM5XA8', 'status', 'ready', 'done', NULL, '2026-06-10 12:51:50', '2026-06-10 12:51:50', '2026-06-10 12:51:50', NULL, 'a8a700b9b92afcb998b5c2e69660e1b2', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1S7613SYF0M9XQT5JNWM40', 'status', 'backlog', 'ready', NULL, '2026-06-10 12:56:38', '2026-06-10 12:56:38', '2026-06-10 12:56:38', NULL, '0c7b9f022143b733461ca7b1b715c435', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB37JZSFZKWPK9PDFYJY2YC0', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:00:19', '2026-06-10 13:00:19', '2026-06-10 13:00:19', NULL, 'a394b4a8b72a7bc99c7fc1a1ac6a2f19', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'status', 'ready', 'done', NULL, '2026-06-10 13:00:29', '2026-06-10 13:00:29', '2026-06-10 13:00:29', NULL, '25246286c46b36a114817316658049c7', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1S7613SYF0M9XQT5JNWM40', 'status', 'ready', 'done', NULL, '2026-06-10 13:15:15', '2026-06-10 13:15:15', '2026-06-10 13:15:15', NULL, '9cea01c55347a9c347bd195d322dd004', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB37JZSFZKWPK9PDFYJY2YC0', 'status', 'ready', 'done', NULL, '2026-06-10 13:18:57', '2026-06-10 13:18:57', '2026-06-10 13:18:57', NULL, '02a8ea1b2623be255101f56c077f63ee', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:24:15', '2026-06-10 13:24:15', '2026-06-10 13:24:15', NULL, '85d0441183cc6f64fb1a956522a60f4a', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'ready', 'in_progress', NULL, '2026-06-10 13:41:17', '2026-06-10 13:41:17', '2026-06-10 13:41:17', NULL, '14fa1745f31d356412fe910180183a53', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:53:28', '2026-06-10 13:53:28', '2026-06-10 13:53:28', NULL, 'addfc7d2a002212e979b3d7237cf37ce', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3KS499THAD899M0NWN7E3R', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:53:55', '2026-06-10 13:53:55', '2026-06-10 13:53:55', NULL, '72773d8d91af176305bd6c1b7d5d65be', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'status', 'backlog', 'ready', NULL, '2026-06-10 13:54:02', '2026-06-10 13:54:02', '2026-06-10 13:54:02', NULL, '5cb488441033ac5a781d4b7d91dbda6f', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'status', 'in_progress', 'done', NULL, '2026-06-10 13:55:30', '2026-06-10 13:55:30', '2026-06-10 13:55:30', NULL, 'f618ee8a3a33dabfa822bd0cebae4e03', 2) ON CONFLICT(hash) DO NOTHING;
|
||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'status', 'ready', 'done', NULL, '2026-06-10 14:03:10', '2026-06-10 14:03:10', '2026-06-10 14:03:10', NULL, '96365bc3ef7dbf505448be2dad16e4b6', 2) ON CONFLICT(hash) DO NOTHING;
|