From 7a98cbae4b0fe3fd31b39aacb4b2ae2ece752f95 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 10 Jun 2026 13:16:41 +0200 Subject: [PATCH] frame the context / thinking / agent-prompt meta cards (T-306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injected context block and the thinking / agent-prompt blocks rendered as frameless `bare` cards, reading as unfinished `> context …` rows next to the framed tool cards. Switch them to the bordered variant — same panel border + left chevron + label as the surrounding cards — while keeping the D-78 de-emphasis (muted accent, collapsed by default, first-line summary; thinking gains a summary for parity). Adds a conversation_card_meta golden. Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_deps/2026-06.sql | 5 + .pql/changelog/ticket_history/2026-06.sql | 43 +++++ .pql/changelog/ticket_idmap/2026-06.sql | 5 + .pql/changelog/tickets/2026-06.sql | 154 ++++++++++++++++++ CHANGELOG.md | 6 + governance/decisions/architecture.md | 10 ++ governance/questions/architecture.md | 3 +- lib/builtin/claude/src/conversation_view.dart | 9 +- .../conversation_card_goldens_test.dart | 54 ++++++ .../goldens/linux/conversation_card_meta.png | Bin 0 -> 27469 bytes 10 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 test/goldens/goldens/linux/conversation_card_meta.png diff --git a/.pql/changelog/ticket_deps/2026-06.sql b/.pql/changelog/ticket_deps/2026-06.sql index af601ed2..d5d4b7e5 100644 --- a/.pql/changelog/ticket_deps/2026-06.sql +++ b/.pql/changelog/ticket_deps/2026-06.sql @@ -9,3 +9,8 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', '06FB0TNQM5YYMMKFD5Z8WG9F60', '2026-06-07 08:49:16', '2026-06-07 08:49:16', NULL, '11d94aab7a635da1ffb610aa2ade2061', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', '06FB0TNQM4XBRZJ6DEHFWREWC0', '2026-06-07 10:42:46', '2026-06-07 10:42:46', NULL, 'daa1e2dd097721230deca67500c1c00d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', '06FB0TNQM67PDXAFTPPW7B1WDR', '2026-06-07 10:42:50', '2026-06-07 10:42:50', NULL, '50bb0fcfafb5aacdd747bd9fcd6a3ea6', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-10 11:12:11', NULL, 'e0876fe73623c4063849a13f99d552f5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-10 11:12:14', NULL, 'cd39602b20a0681e3cca2dfee38d1734', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ESCR4V6V07CRH18FBCDN8', '2026-06-10 11:12:18', '2026-06-10 11:12:18', NULL, 'c74a2c5d562157c24c11c8fa5d04799e', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ETJQP0CT6X7W3CWZ6NS9G', '2026-06-10 11:12:20', '2026-06-10 11:12:20', NULL, 'cb20764a77d2a15290ed61a186542095', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); +INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-10 11:12:25', '2026-06-10 11:12:25', NULL, 'dd9434426790edcaa556221f74c431ba', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash); diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index af344c90..6ce670ab 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -3042,3 +3042,46 @@ The LABEL is the bridge between the two surfaces: Claude attaches a label to eac - 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 ``, 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; diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 9008a5dd..7eb8efd6 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -141,3 +141,8 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'T-314', '2026-06-10 10:24:29', '2026-06-10 10:24:29', NULL, 'f8b89421a13f620c2a0e76bd780db5a2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'T-315', '2026-06-10 10:52:34', '2026-06-10 10:52:34', NULL, 'd0521ab80265d66c99c803a8714e8f02', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', 'T-316', '2026-06-10 10:52:36', '2026-06-10 10:52:36', NULL, '5beb005b263eb4c5bb10ffc6e2fc0100', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EDCBYRBDSV9V1PJ1KE3CM', 'T-317', '2026-06-10 11:10:07', '2026-06-10 11:10:07', NULL, '4d4a02ab95987e3ebb134a2f0173b4a1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'T-318', '2026-06-10 11:11:40', '2026-06-10 11:11:40', NULL, '31820aadd1f19f9dd0cf931dae565abc', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'T-319', '2026-06-10 11:11:45', '2026-06-10 11:11:45', NULL, '12c5a57fb83444410cca98a802e9ba6f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'T-320', '2026-06-10 11:11:55', '2026-06-10 11:11:55', NULL, '2266e362f144e4fd97403e1d1abe3eb4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EV29HSK6EJ5VF50R87VC4', 'T-321', '2026-06-10 11:11:59', '2026-06-10 11:11:59', NULL, 'd48b6bd9f0c8289c95448dda6865d666', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 15ecd666..d5265dcd 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -1866,3 +1866,157 @@ The LABEL is the bridge between the two surfaces: Claude attaches a label to eac - 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.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 10:55:49', NULL, 'e2a3afff7620d41cf75d0b55c90543df', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EDCBYRBDSV9V1PJ1KE3CM', 'epic', NULL, 'Unified conversation drawing card (canvas renderer)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:10:07', '2026-06-10 11:10:07', NULL, 'f62f2ff781a057083ca0dc6855824778', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EDCBYRBDSV9V1PJ1KE3CM', 'epic', NULL, 'Unified conversation drawing card (canvas renderer)', '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 ``, 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.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:10:07', '2026-06-10 11:10:59', NULL, 'bc20218bb8e27a91cd16929d5b99352d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)', '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 ` 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 [ ...]` accepts multiple icons (variadic stringList positionals) in one call and renders them in a single conversation card. +- A `--file ` 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.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-10 11:11:10', NULL, '55c15feee383840b59617058ed513421', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'clide CLI: accept a JSON payload on stdin for structured commands', '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 ` 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 --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.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:52:34', '2026-06-10 11:11:10', NULL, '69251007038985decbc3e18585f2d7b1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'clide image show: JSON metadata payload so images can be annotated (label/description/markers)', '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 ` 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 --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 [--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.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:52:36', '2026-06-10 11:11:10', NULL, 'ab80fa0823568f0d159b6a9d162114b2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'story', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card core: canvas engine + JSON schema + template dispatch', 'Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-10 11:11:40', NULL, '73af1f59dfa92fcb13073cd31a73e2f4', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: compare-images / before-after', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:45', '2026-06-10 11:11:45', NULL, '5d2d21c180849ce0bb652d79855c40af', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: SVG render', 'Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).', 'backlog', 'low', NULL, NULL, NULL, '2026-06-10 11:11:55', '2026-06-10 11:11:55', NULL, 'aee93f5693c771bedec77e288a2784b1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EV29HSK6EJ5VF50R87VC4', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: graph render', 'Template for the unified drawing card (T-317, D-91): render a graph passed in the JSON. Reuses clide''s native graph rendering (CustomPaint; the planned graph subsystem, D-46) rather than a new renderer. Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318); coordinate with the graph subsystem so the card embeds it rather than forking it.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-10 11:11:59', '2026-06-10 11:11:59', NULL, 'd21ba3f471f6518d5775008897e67416', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16YM1Y910T07Y1Y22MGTEM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Card the ''context'' conversation block like the rest of the cards', 'The injected "context" block (skill-load / command-expansion / system-reminder messages) renders as a frameless `ConversationCardVariant.bare` card in `lib/builtin/claude/src/conversation_view.dart:410-419`, so it reads as a bare `> context …` row wedged between the fully-framed Skill and Bash tool cards (see pasted screenshot). It looks unfinished next to the carded tool calls. + +Make the context block follow the standard collapser-card pattern established for all group/tool cards in the `collapser-card` wireframe (docs/design/wireframes/cards/): a proper card frame with the chevron toggle hard against the left edge, label, and the fixed-width right-aligned counter/status slot — so it is visually consistent with the Skill/Bash/Activity cards around it. + +Keep the de-emphasis that D-78 calls for (muted accent, collapsed-by-default, first-line summary) — this is about giving it the same *frame* as the other cards, not the blue "you" accent. The same treatment likely applies to the sibling bare cards that share this branch (agent prompt, thinking) for consistency; scope to context first and note whether thinking/agent-prompt should follow. + +Acceptance: the context row sits in a framed card matching the collapser-card wireframe geometry, still muted and collapsed by default, with the toggle and counter/status slot aligned to the other cards. Update/extend the relevant golden(s).', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 08:17:42', '2026-06-10 11:16:41', NULL, '5b583df100b64dbebe19c235f32e0541', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index d7bde4e9..d480e1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -256,6 +256,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- **The `context` / `thinking` / agent-prompt blocks are now carded like the + rest.** These muted meta blocks used to render frameless, reading as + unfinished `> context …` rows wedged between the framed tool cards. They now + sit in a proper bordered card — still muted, collapsed by default, with a + first-line summary and a left chevron toggle — matching the surrounding + cards. (T-306) - **Numpad digits now pick permission/question options too.** The prompt card's number-key shortcuts only matched the top number row; numpad `1`-`9` now map to the same 1-9 selection, so the keypad works for Allow/Deny and question options. diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 1e7959b0..4e2c906a 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -460,3 +460,13 @@ Core, rendering, IPC, kernel, panel manager. - **Raised by:** 2026-06-09 — user, on reviewing the T-56 passthrough: "put a heavy blocklist with do-you-want-this prompts on it (a paranoid allowlist)." Spotted that routing clide:// through the CLI path is "incredibly extensible" and that extensibility is exactly the risk for an external vector. --- + +### D-91: Unified conversation drawing card backed by a canvas renderer +- **Date:** 2026-06-10 +- **Decision:** The conversation pane gets ONE drawing card — a clide-owned canvas renderer driven by a JSON document — instead of a growing set of one-off renderers (image card, icon card, …). The mental model is the **HTML ``**, not Obsidian's `.canvas` (Obsidian naming its feature "canvas" does not make its schema our pattern). The card's JSON can express raw **primitives** (rects/lines/text/etc. 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 — `image` (display + lightbox-on-click), `icon` (the multi-size glyph set, D-78/T-313), `compare-images` / before-after (two paths side by side), and — added incrementally — `svg`, `graph`, and more. Every drawn object may carry an optional **label + description**, rendered as a widget beneath it only when those fields are present in the JSON. Templates lower onto the same primitive scene the raw API accepts (hybrid: scene-graph core + high-level block sugar). The card stays **display-only** per [D-78] — selection/interaction happens in the interaction zone, not on the card. The same renderer is intended for **reuse as the `.canvas` viewer** elsewhere: a `.canvas` file is converted into this JSON rather than the card adopting Obsidian's schema natively. The already-shipped image card (T-249/T-252) is left in place for now and migrated onto this canvas once the canvas layer lands. +- **Rationale:** Each new "show X in the conversation" need was trending toward another bespoke message type + widget + CLI verb. A single canvas that dispatches templates (and falls back to primitives) collapses that into one surface to build, test, and drive — new visuals become a new template/JSON shape, not a new renderer. Anchoring on HTML `` rather than `.canvas` keeps the model general (arbitrary drawing) instead of bending features to a third-party document schema, while still letting us ingest `.canvas` via conversion. Hybrid (templates over primitives) is the pragmatic middle: the common cards ship without hand-placing coordinates, but nothing is locked out of the primitive layer. Honors "own the rendering stack" — canvas is explicitly a clide-owned `CustomPaint`, not a packaged widget. +- **Cost:** A real renderer with a JSON schema is more up-front design than another one-off widget, and the schema must stay coherent across the primitive and template layers as templates accrue. Templates expand over time ("we will expand the templates as we go") so the schema is deliberately open-ended; that flexibility is a standing maintenance surface. The `.canvas`→JSON converter is additional scope, deferred. Migrating the shipped image card is follow-up churn on working code. +- **Relationship:** Narrows [Q-4](../questions/architecture.md#q-4-canvas-schema-compatibility-with-obsidian) — clide's canvas is its own HTML-canvas-inspired JSON; Obsidian `.canvas` is an *import* format via conversion, not the native schema. Consumes the stdin/`--file` JSON input plumbing (T-315). Subsumes the standalone icon card (T-313) and image-annotation work (T-316) as templates of this card. +- **Raised by:** 2026-06-10 — user, while refining the icon-preview card (T-313): "make it all into one drawing card that receives a json input and selects based on the context inside the json what to draw … pull the entire thing closer to a dynamic canvas than a bunch of one-off renderers." Clarified the model is HTML `` (not Obsidian's), templates-over-primitives, per-object label/description, and reuse as the `.canvas` renderer; before/after comparisons, SVGs, icons, and graphs all become things you send into the card. + +--- diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md index a0466b3c..5c58a561 100644 --- a/governance/questions/architecture.md +++ b/governance/questions/architecture.md @@ -29,9 +29,10 @@ ticket persistence. - **Source:** ADR 0006 (migrated to [D-6](architecture.md)). ### Q-4: `.canvas` schema compatibility with Obsidian -- **Status:** Open +- **Status:** Open — narrowed by [D-91](../decisions/architecture.md#d-91-unified-conversation-drawing-card-backed-by-a-canvas-renderer) - **Question:** Clide's canvas (Tier 5) should read/write something — either Obsidian's `.canvas` JSON schema verbatim, a compatible-ish superset, or our own format. Each has trade-offs. - **Context:** Obsidian's canvas users might want their canvases portable; conversely, bending to Obsidian's schema constrains our canvas features. +- **Narrowed (2026-06-10):** D-91 sets the direction — clide's canvas uses its OWN HTML-``-inspired JSON (the unified drawing-card schema), not Obsidian's `.canvas` natively. Obsidian `.canvas` is treated as an **import** format, ingested by converting it into clide's JSON, so the same renderer can display it. What remains open here: the exact converter mapping and how lossless/round-trippable `.canvas` interop needs to be. - **Source:** CLAUDE.md "Open questions" footer. ### Q-5: IPC wire-format stability + `schema_version:` diff --git a/lib/builtin/claude/src/conversation_view.dart b/lib/builtin/claude/src/conversation_view.dart index 5f2997d4..a422b3e2 100644 --- a/lib/builtin/claude/src/conversation_view.dart +++ b/lib/builtin/claude/src/conversation_view.dart @@ -419,7 +419,10 @@ class _ConversationTurn extends StatelessWidget { // sidechain prompt here is an orphan one (its Agent card couldn't be // resolved) — folded prompts are suppressed upstream (T-263). UserMessage() when i.injected || i.isSidechain => ConversationCard( - variant: ConversationCardVariant.bare, + // Framed like every other card (T-306) — just muted + collapsed, not + // the blue "you" accent (D-78); bare read as unfinished next to the + // carded tool calls. + variant: ConversationCardVariant.bordered, accent: tokens.globalTextMuted, label: i.isSidechain ? 'agent prompt' : 'context', copyText: i.text, @@ -454,12 +457,14 @@ class _ConversationTurn extends StatelessWidget { body: ClideMarkdown(i.text, onRecordTap: (id) => _openRecord(context, id), onLinkTap: (url) => _openUrl(context, url)), ), AssistantThinkingMessage() => ConversationCard( - variant: ConversationCardVariant.bare, + // Framed + muted like the context card (T-306). + variant: ConversationCardVariant.bordered, accent: tokens.globalTextMuted, label: i.isSidechain ? 'agent thinking' : 'thinking', copyText: i.thinking, collapsible: true, collapsedByDefault: true, + collapsedSummary: _firstLine(i.thinking), margin: _childMargin, body: ClideText(i.thinking, muted: true, fontSize: clideFontMeta), ), diff --git a/test/goldens/conversation_card_goldens_test.dart b/test/goldens/conversation_card_goldens_test.dart index fa15e088..73ba1a18 100644 --- a/test/goldens/conversation_card_goldens_test.dart +++ b/test/goldens/conversation_card_goldens_test.dart @@ -122,6 +122,60 @@ void main() { ], ), ); + + goldenTest( + 'ConversationCard meta cards (T-306): context / thinking are framed + muted', + fileName: 'conversation_card_meta', + builder: () => GoldenTestGroup( + columns: 1, + children: [ + GoldenTestScenario( + name: 'context — framed, muted, collapsed with first-line summary', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF6A7280), + label: 'context', + collapsible: true, + collapsedByDefault: true, + collapsedSummary: 'Base directory for this skill: /var/mnt/…', + body: Text('Base directory for this skill: /var/mnt/data/projects/clide', textDirection: TextDirection.ltr), + ), + ), + ), + GoldenTestScenario( + name: 'thinking — framed, muted, collapsed with first-line summary', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF6A7280), + label: 'thinking', + collapsible: true, + collapsedByDefault: true, + collapsedSummary: 'Let me check the layout chain first…', + body: Text('Let me check the layout chain first…', textDirection: TextDirection.ltr), + ), + ), + ), + GoldenTestScenario( + name: 'context — expanded body', + child: _wrap( + f, + const ConversationCard( + variant: ConversationCardVariant.bordered, + accent: Color(0xFF6A7280), + label: 'context', + collapsible: true, + collapsedByDefault: false, + body: Text('Base directory for this skill: /var/mnt/data/projects/clide', textDirection: TextDirection.ltr), + ), + ), + ), + ], + ), + ); } Widget _wrap(KernelFixture f, Widget child) => SizedBox( diff --git a/test/goldens/goldens/linux/conversation_card_meta.png b/test/goldens/goldens/linux/conversation_card_meta.png new file mode 100644 index 0000000000000000000000000000000000000000..22f863a7974113d748303e5fc83327dc332b2a57 GIT binary patch literal 27469 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 LU9L*TJnVk~EWhYA literal 0 HcmV?d00001