From e9f394ff0f650b22dbe6020790fdfbe48f0d178f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 28 Jun 2026 18:01:45 +0200 Subject: [PATCH] =?UTF-8?q?docs(governance):=20D-103=20=E2=80=94=20drawing?= =?UTF-8?q?-card=20primitive=20layer=20is=20SVG=20(amends=20D-91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified drawing card's primitive layer is SVG, not a bespoke {type:"rect"} JSON vocabulary; the clide-owned CustomPaint SVG renderer (T-320) is the engine the rest builds on. Templates lower to SVG; a thin Flutter overlay carries label/description captions + lightbox, anchored via data-* attributes; the graph template stays a live widget. Inverts the former T-320 → T-318 dependency. Raised by the user: "if we are close to svg, are we not better off extending svg instead?" Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 376 ++++++++++++++++++++++ .pql/changelog/tickets/2026-06.sql | 204 ++++++++++++ governance/README.md | 1 + governance/decisions/architecture.md | 9 + 4 files changed, 590 insertions(+) diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index db53768c..37fc508c 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -7689,3 +7689,379 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang Mechanism shipped 2026-06-28: ''make dugite-check'' (ci/check_dugite_version.sh) compares the Makefile DUGITE_VERSION pin against the latest desktop/dugite-native release and flags CVE/security mentions in the notes. D-59''s cost line + a Makefile comment now point at it (closes the clean-house RULE-SUNSET-WITHOUT-TICKET flag). FIRST CHECK (2026-06-28): bundled v2.53.0-3 == latest v2.53.0-3 — up to date, no CVE flagged, no bump needed. RECURRING — do not close. Next check due ~2026-09-28 (quarterly), or immediately on a git/dugite security advisory (subscribe: github.com/git/git/security/advisories, github.com/desktop/dugite-native/security/advisories). Bump = D-63 record + T-25 (CI machine); ''scheduled'' CI cron deferred until CI exists (T-25/T-491).', NULL, '2026-06-28 13:12:29', '2026-06-28 13:12:29.257', '2026-06-28 13:12:29.257', NULL, '46ef38531a96b893f70de886f19187cd', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7Y0DFP66HSP073XHC', 'status', 'in_progress', 'backlog', NULL, '2026-06-28 13:12:29', '2026-06-28 13:12:29.289', '2026-06-28 13:12:29.289', NULL, 'aa8e667ed39d133c1bd3ca547d5a9acf', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO 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', '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.', '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. + +Deferred 2026-06-28 (user): blocked on its consumers T-316 + T-313 so it only resurfaces if one actually wants the piping UX. Standalone --stdin infra isn''t worth a build cycle now — both consumers can ship with --file (per this ticket), and structured JSON also flows natively through the MCP tool surface (D-86). Build it lazily inside whichever consumer first needs piping, if ever.', NULL, '2026-06-28 13:28:49', '2026-06-28 13:28:49.359', '2026-06-28 13:28:49.359', NULL, '890b0e8efb157c36d1acc26d22876b81', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO 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 ` 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.', '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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png.', NULL, '2026-06-28 13:43:39', '2026-06-28 13:43:39.648', '2026-06-28 13:43:39.648', NULL, '67c98f21d0dea3be7327403295906b7e', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO 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 ` 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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png.', '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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png. + +Size set FINALIZED (2026-06-28): one continuous sample strip — 10, 11, 12, 13, 14, 15, 18, 20, 24, 32, 48 px — no clide-vs-common visual split. Hero stays 52. Supersedes the grouped framing in the prior note.', NULL, '2026-06-28 13:47:44', '2026-06-28 13:47:44.006', '2026-06-28 13:47:44.006', NULL, '89902262b44651cef22839749396391a', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO 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 ` 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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png. + +Size set FINALIZED (2026-06-28): one continuous sample strip — 10, 11, 12, 13, 14, 15, 18, 20, 24, 32, 48 px — no clide-vs-common visual split. Hero stays 52. Supersedes the grouped framing in the prior note.', '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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png. + +Size set FINALIZED (2026-06-28): one continuous sample strip — 10, 11, 12, 13, 14, 15, 18, 20, 24, 32, 48 px — no clide-vs-common visual split. Hero stays 52. Supersedes the grouped framing in the prior note. + +COLOR (2026-06-28, folded into payload spec): add an OPTIONAL per-entry ''color'' to the --file JSON, e.g. {"icon":"gear","label":"Settings","color":"#e2b714"}. Value is an ARBITRARY color — hex #rrggbb / #rrggbbaa (CSS-style names acceptable) — passed straight to PhosphorIconPainter. NOT a clide SurfaceTokens theme token: the glyph is content for whatever project we''re working on (its own palette), and the D-7 token discipline governs clide''s own chrome, not rendered content. Optional card-level default ''color''; a glyph with none falls back to the card''s default foreground. Unknown/malformed color → the same honest userError as a bad glyph name. Mirrors how arbitrary colors already flow through the image/svg cards.', NULL, '2026-06-28 14:00:47', '2026-06-28 14:00:47.466', '2026-06-28 14:00:47.466', NULL, '9d27fb1aca3e4b48131db84d55c7d2b1', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGX26B0NAC9WRJMVB0QE6CV8', 'description', NULL, 'A conversation-pane drawing card (peer of the SVG card T-320) that renders a D2 diagram. SEPARATE card type from SVG, but the ONLY difference is a compile step in front: D2 source → compile to SVG → render through the SAME SVG widget (T-320). Blocked on T-320 for that reason. + +CLI/parity (D-6): driven via the drawing-card dispatch (T-318) — ''clide draw --file pipeline.d2'' (type inferred from .d2), or template:"d2" with inline source in the drawing-card JSON. Honest userError on a d2 compile failure (bad source), mirroring image/svg validation. + +UX: the rendered diagram LEADS; the d2 source folds into a collapsed ''view d2 source'' disclosure on the card (ClideCollapserCard pattern, T-305) so the code doesn''t dominate. Display-only per D-78. + +Open: D2 compiler dependency — d2 is Go (there is a d2-diagram skill). Decide whether to shell out to a d2 binary (supporter-tool pattern, peer of pql) or vendor it; flag before building. Wireframe: docs/design/wireframes/conversation/d2-render-card.png.', NULL, '2026-06-28 14:01:24', '2026-06-28 14:01:24.709', '2026-06-28 14:01:24.709', NULL, '0dfd716104a3b7defb4155f932f75c4c', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'description', '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.', '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. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.', NULL, '2026-06-28 15:20:03', '2026-06-28 15:20:03.628', '2026-06-28 15:20:03.628', NULL, 'a0c15cfea906b9896b19516bf3859bea', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'description', '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).', '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). + +PROMOTED TO ENGINE (D-103, 2026-06-28): this is now the drawing-card''s rendering FOUNDATION, not one template among many. Build the clide-owned CustomPaint SVG renderer over a BOUNDED subset (rect/line/polyline/polygon/circle/ellipse/path, text incl. the Phosphor font, image, g + transform, basic fill/stroke/opacity/rx). T-318 (envelope + dispatch + Flutter overlay) and every template lower onto this; d2/graphviz/mermaid output renders for free. No longer blocked by T-318 (dependency inverted). Open: pin the subset to what our own templates + d2/graphviz emit. Schema: docs/design/drawing-card-schema.md.', NULL, '2026-06-28 15:30:27', '2026-06-28 15:30:27.974', '2026-06-28 15:30:27.974', NULL, '936e0475a51e0881fb775116c75bbc14', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'description', '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. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.', '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. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.', NULL, '2026-06-28 15:30:33', '2026-06-28 15:30:33.771', '2026-06-28 15:30:33.771', NULL, '6298b402fc49547c7bdba1f3d5626bed', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index c4214254..000f6cb6 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -9723,3 +9723,207 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat **Out of scope:** the rebuild itself is T-25 (CI reproducible build for libtree-sitter.so) — same shape applies to dugite. This ticket is the calendar; T-25 is the machine. Mechanism shipped 2026-06-28: ''make dugite-check'' (ci/check_dugite_version.sh) compares the Makefile DUGITE_VERSION pin against the latest desktop/dugite-native release and flags CVE/security mentions in the notes. D-59''s cost line + a Makefile comment now point at it (closes the clean-house RULE-SUNSET-WITHOUT-TICKET flag). FIRST CHECK (2026-06-28): bundled v2.53.0-3 == latest v2.53.0-3 — up to date, no CVE flagged, no bump needed. RECURRING — do not close. Next check due ~2026-09-28 (quarterly), or immediately on a git/dugite security advisory (subscribe: github.com/git/git/security/advisories, github.com/desktop/dugite-native/security/advisories). Bump = D-63 record + T-25 (CI machine); ''scheduled'' CI cron deferred until CI exists (T-25/T-491).', 'backlog', 'low', NULL, NULL, 'D-59', '2026-05-06 14:34:35', '2026-06-28 13:12:29.289', NULL, '0ef1067dc16303cce58e4b0fb485fa1f', 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. + +Deferred 2026-06-28 (user): blocked on its consumers T-316 + T-313 so it only resurfaces if one actually wants the piping UX. Standalone --stdin infra isn''t worth a build cycle now — both consumers can ship with --file (per this ticket), and structured JSON also flows natively through the MCP tool surface (D-86). Build it lazily inside whichever consumer first needs piping, if ever.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:52:34', '2026-06-28 13:28:49.359', NULL, 'cc06f073d3f02afba4ad728c364da0b8', 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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-28 13:43:39.648', NULL, '1bd3db3360aa4145ca351b10a829141f', 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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png. + +Size set FINALIZED (2026-06-28): one continuous sample strip — 10, 11, 12, 13, 14, 15, 18, 20, 24, 32, 48 px — no clide-vs-common visual split. Hero stays 52. Supersedes the grouped framing in the prior note.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-28 13:47:44.006', NULL, '8bb45b8217291cc6d2f974896fcc4a27', 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. + +DESIGN REFINEMENT (2026-06-28, wireframe review): broaden the per-entry size-sample strip beyond clide''s inline typography tokens. clide icon show is a general-purpose glyph previewer, not bound to clide''s own UI, so the strip renders BOTH clide inline tokens (11–15: badge/small/meta/caption/body) AND common icon sizes (18, 20, 24, 32, 48). Hero stays 52 (clideFontWelcomeBanner). Update the acceptance bullet accordingly: ''a sample at each inline token (11–15) PLUS common sizes 18/20/24/32/48'', driven off named tokens where they exist and explicit px otherwise. Wireframe: docs/design/wireframes/conversation/icon-glyph-card.png. + +Size set FINALIZED (2026-06-28): one continuous sample strip — 10, 11, 12, 13, 14, 15, 18, 20, 24, 32, 48 px — no clide-vs-common visual split. Hero stays 52. Supersedes the grouped framing in the prior note. + +COLOR (2026-06-28, folded into payload spec): add an OPTIONAL per-entry ''color'' to the --file JSON, e.g. {"icon":"gear","label":"Settings","color":"#e2b714"}. Value is an ARBITRARY color — hex #rrggbb / #rrggbbaa (CSS-style names acceptable) — passed straight to PhosphorIconPainter. NOT a clide SurfaceTokens theme token: the glyph is content for whatever project we''re working on (its own palette), and the D-7 token discipline governs clide''s own chrome, not rendered content. Optional card-level default ''color''; a glyph with none falls back to the card''s default foreground. Unknown/malformed color → the same honest userError as a bad glyph name. Mirrors how arbitrary colors already flow through the image/svg cards.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-28 14:00:47.466', NULL, 'ed52cbbebf02660ef217c7f948ca66db', 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 ('06FGX26B0NAC9WRJMVB0QE6CV8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'D2 diagram card — compile d2 source to SVG, reuse the SVG render widget', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-28 14:01:13.733', '2026-06-28 14:01:13.733', NULL, 'a5abd32f6dbe24a6e10ad52edfa568d7', 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 ('06FGX26B0NAC9WRJMVB0QE6CV8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'D2 diagram card — compile d2 source to SVG, reuse the SVG render widget', 'A conversation-pane drawing card (peer of the SVG card T-320) that renders a D2 diagram. SEPARATE card type from SVG, but the ONLY difference is a compile step in front: D2 source → compile to SVG → render through the SAME SVG widget (T-320). Blocked on T-320 for that reason. + +CLI/parity (D-6): driven via the drawing-card dispatch (T-318) — ''clide draw --file pipeline.d2'' (type inferred from .d2), or template:"d2" with inline source in the drawing-card JSON. Honest userError on a d2 compile failure (bad source), mirroring image/svg validation. + +UX: the rendered diagram LEADS; the d2 source folds into a collapsed ''view d2 source'' disclosure on the card (ClideCollapserCard pattern, T-305) so the code doesn''t dominate. Display-only per D-78. + +Open: D2 compiler dependency — d2 is Go (there is a d2-diagram skill). Decide whether to shell out to a d2 binary (supporter-tool pattern, peer of pql) or vendor it; flag before building. Wireframe: docs/design/wireframes/conversation/d2-render-card.png.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-28 14:01:13.733', '2026-06-28 14:01:24.709', NULL, 'c51387a44d70061be160ab331513db27', 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. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 15:20:03.628', NULL, '70c4bf1a4c0c8917d4f7830642bb1713', 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). + +PROMOTED TO ENGINE (D-103, 2026-06-28): this is now the drawing-card''s rendering FOUNDATION, not one template among many. Build the clide-owned CustomPaint SVG renderer over a BOUNDED subset (rect/line/polyline/polygon/circle/ellipse/path, text incl. the Phosphor font, image, g + transform, basic fill/stroke/opacity/rx). T-318 (envelope + dispatch + Flutter overlay) and every template lower onto this; d2/graphviz/mermaid output renders for free. No longer blocked by T-318 (dependency inverted). Open: pin the subset to what our own templates + d2/graphviz emit. Schema: docs/design/drawing-card-schema.md.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-10 11:11:55', '2026-06-28 15:30:27.974', NULL, '31922dc79d19a564325ab3e30bd2927b', 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. + +SCHEMA DRAFTED (2026-06-28): docs/design/drawing-card-schema.md — declarative JSON scene-graph (document envelope, primitive types rect/line/text/glyph/image, template envelope, shared label/description, arbitrary-hex color, CLI ''clide draw --file'', error contract). Refined from the T-317 wireframe set. + +MODEL CLARIFICATION (user): we build clide''s OWN native Flutter CustomPaint interpreting a declarative scene-graph document. We do NOT port the HTML Canvas 2D API — ''HTML canvas'' in D-91 is only the mental model (a general drawing surface, chosen to reject Obsidian''s .canvas schema), not an API to implement. The declarative model is closer to SVG/a retained scene-graph than to canvas''s imperative 2D context. + +RESCOPED (D-103, 2026-06-28): the primitive layer is now SVG and the SVG renderer (T-320) is the engine — so T-318 is NO LONGER a primitive renderer. T-318 = the document envelope ({template? | svg/svgPath}, card metadata), the template DISPATCH, and the clide FLUTTER OVERLAY: per-object label/description caption widgets + lightbox affordance, anchored to SVG elements via data-label / data-description / data-lightbox. Now blocked by T-320 (engine first). Schema: docs/design/drawing-card-schema.md.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:40', '2026-06-28 15:30:33.771', NULL, '98e798ae1f9749ff4d6895b5ad15cbd9', 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/governance/README.md b/governance/README.md index 9f577354..237b844c 100644 --- a/governance/README.md +++ b/governance/README.md @@ -144,6 +144,7 @@ You might also want, project-permitting: - [D-100: Fence `dart:ffi` behind conditional imports + web stubs to keep the web/WASM target compiling](decisions/tooling.md#d-100-fence-dartffi-behind-conditional-imports--web-stubs-to-keep-the-webwasm-target-compiling) — _tooling_ - [D-101: ClideSettings — one live-preferences access facade](decisions/architecture.md#d-101-clidesettings--one-live-preferences-access-facade) — _architecture_ - [D-102: i18n routing — ext-id namespaces, `core` catalog, ClideSettings.i18n facade, contribution keys](decisions/accessibility.md#d-102-i18n-routing--ext-id-namespaces-core-catalog-clidesettingsi18n-facade-contribution-keys) — _accessibility_ +- [D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine](decisions/architecture.md#d-103-drawing-card-primitive-layer-is-svg-the-svg-renderer-is-the-engine) — _architecture_ ## Open questions diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index d224d4e3..d36baa66 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -542,4 +542,13 @@ Core, rendering, IPC, kernel, panel manager. - **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`. - **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many." +### D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine +- **Date:** 2026-06-28 +- **Decision:** The unified drawing card's primitive / scene-graph layer is **SVG**, not a bespoke `{type:"rect",…}` JSON vocabulary. The clide-owned `CustomPaint` SVG renderer (T-320) **is** the card's rendering engine — the foundation the rest builds on, not one template among many. The low-level escape hatch is "send SVG" (`template:"svg"` with inline `source`, or a `.svg` path); the high-level **templates** (image / icon / compare / d2) stay as JSON sugar but **lower to SVG** rather than to a custom primitive scene. A thin **Flutter overlay** renders the clide chrome that is *not* content — the per-object label/description caption widgets and the lightbox affordance — layered over the rendered SVG, anchored to elements via `data-label` / `data-description` / `data-lightbox` attributes. The **graph template is the exception:** it embeds the interactive native graph subsystem ([D-46], T-323) as a live widget rather than lowering to static SVG. Driven via `clide draw --file` (D-6); Flutter-free handler → `draw` MessageBus channel → Claude-extension injection. +- **Rationale:** SVG already *is* a declarative, standard scene-graph with precisely the primitives [D-91] wanted (rect/line/text/image/path at coordinates, transforms). Since a clide-owned SVG renderer is being built for the `svg` template regardless, inventing a parallel primitive JSON + a second renderer duplicates the work for one job. Making SVG the substrate collapses two renderers and two schemas into one, makes external SVG / graphviz / mermaid / d2 output renderable for free, and keeps the `.canvas`-viewer reuse ([D-91]) on the same path. Owning the SVG painter (`CustomPaint`, no package) honors "own the rendering stack" — SVG here is a document *format*, not a third-party renderer, and explicitly **not** the imperative HTML Canvas 2D API ("HTML ``" in D-91 was a mental model to reject Obsidian's `.canvas` schema, never an API to port). +- **Cost:** The SVG-subset scope becomes the card's central design surface (the substrate, not one template) — but bounded, since the only SVG that must render is what clide's own templates + d2/graphviz emit; clide controls both ends. Re-sequences epic T-317: the SVG renderer (T-320) lands **before** the core envelope (T-318), inverting the prior `T-320 → T-318` dependency. The card is a hybrid (SVG content + Flutter overlay), so captions/interaction are not expressible in the document SVG itself. +- **Amends [D-91]:** D-91's "raw primitives (rects/lines/text at coordinates)" are now SVG elements; "templates lower onto the same primitive scene" becomes "templates lower to SVG"; the renderer foundation is the SVG painter (T-320), not a separate primitive engine inside T-318 (which becomes the document envelope + template dispatch + the Flutter overlay). +- **Cross-reference:** [D-91](#d-91-unified-conversation-drawing-card-backed-by-a-canvas-renderer), [D-78], [D-46], T-317 (epic), T-318 (envelope/dispatch/overlay), T-320 (SVG engine), T-494 (d2→svg), T-313/T-316/T-319 (templates), T-321/T-323 (graph). Schema: `docs/design/drawing-card-schema.md`. +- **Raised by:** 2026-06-28 — user, during the drawing-card schema draft: "if we are close to svg, are we not better off extending svg instead?" Confirmed the primitive layer should be SVG with templates lowering to it, a Flutter overlay for captions/interaction, and graph as the live-widget exception. + ---