diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql
index 341443da..8dcf76b0 100644
--- a/.pql/changelog/ticket_history/2026-07.sql
+++ b/.pql/changelog/ticket_history/2026-07.sql
@@ -52,3 +52,14 @@ EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation r
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''Set model to claude-fable-5[1m] (claude-fable-5)'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.
SCOPE (do not narrow): the local-command-stdout rendering defect above is ONE symptom, not the whole ticket. The core problem is that after switching model or effort the user has no reliable, well-placed signal that it took effect — so this stays a broad investigate-and-improve of the switch/toggle trust UX, with the render bug as just the first concrete instance. Other symptoms/questions to investigate (non-exhaustive): does the control itself (Config tab picker / effort control / any menu) visibly update to the NEW active value after a switch, or does it look unchanged? is there ANY acknowledgement at the point of interaction, or only (mangled) output buried in the transcript far from where the user clicked? do model vs effort behave consistently, or differently? can clide''s shown ''active'' value drift from what actually took effect (e.g. a switch that silently no-ops)? is the confirmation positioned where the eye already is? The deliverable is: across all these controls, a consistent and unmistakable ''this is now active'' — fixing the render path is necessary but not sufficient. Enumerate the full symptom set during investigation before designing the fix.', NULL, '2026-07-02 20:41:13', '2026-07-02 20:41:13.402', '2026-07-02 20:41:13.402', NULL, '1c7d99c842cc857d5978d91e5b23ab8b', 2) ON CONFLICT(hash) DO NOTHING;
+INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'description', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
+
+SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
+
+PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
+
+SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
+
+PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
+
+PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).', NULL, '2026-07-02 21:22:59', '2026-07-02 21:22:59.395', '2026-07-02 21:22:59.395', NULL, '6b17245b0d3d39a0d61bc90a2a0a034b', 2) ON CONFLICT(hash) DO NOTHING;
diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql
index 8f9d946d..8b3b1803 100644
--- a/.pql/changelog/tickets/2026-07.sql
+++ b/.pql/changelog/tickets/2026-07.sql
@@ -51,3 +51,10 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
EVIDENCE + ROOT CAUSE (2026-07-02, from a /model screenshot). The confirmation renders as a raw, unstyled line: literally ''Set model to claude-fable-5[1m] (claude-fable-5)'', shown under the ''you'' (user) speaker stripe. Three distinct defects in that one line: (a) the CLI''s wrapper tag is displayed verbatim instead of being unwrapped; (b) an ANSI SGR bold code leaks through as literal ''[1m]'' (a \x1b[1m) — slash-command stdout is not ANSI-stripped; (c) it is attributed to the USER speaker, so a system/command acknowledgement masquerades as something the user typed. ROOT CAUSE: clide has NO handling for local-command-stdout at all (grep in lib/ = zero hits) — the slash-command result falls through and is rendered as plain user prose. ENTRY POINTS for the fix: inbound message parse / role attribution in lib/builtin/claude/src/transcript_reader.dart; existing ANSI-strip refs in transcript_reader.dart + claude_composer.dart. IMPROVE: detect local-command-stdout content, unwrap the tag + strip ANSI, and render it as a distinct, clearly-positioned system/command-acknowledgement (NOT the user stripe) so a /model or /effort change is an unmistakable confirmation. Affects all slash-command stdout (model, effort, etc.), so fix at the render/parse path, not per-command.
SCOPE (do not narrow): the local-command-stdout rendering defect above is ONE symptom, not the whole ticket. The core problem is that after switching model or effort the user has no reliable, well-placed signal that it took effect — so this stays a broad investigate-and-improve of the switch/toggle trust UX, with the render bug as just the first concrete instance. Other symptoms/questions to investigate (non-exhaustive): does the control itself (Config tab picker / effort control / any menu) visibly update to the NEW active value after a switch, or does it look unchanged? is there ANY acknowledgement at the point of interaction, or only (mangled) output buried in the transcript far from where the user clicked? do model vs effort behave consistently, or differently? can clide''s shown ''active'' value drift from what actually took effect (e.g. a switch that silently no-ops)? is the confirmation positioned where the eye already is? The deliverable is: across all these controls, a consistent and unmistakable ''this is now active'' — fixing the render path is necessary but not sufficient. Enumerate the full symptom set during investigation before designing the fix.', 'backlog', 'medium', NULL, NULL, NULL, '2026-07-02 20:31:02.783', '2026-07-02 20:41:13.402', NULL, '9c7b3632bd0846533a1b65cc12b8145a', 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 ('06FB2G1WD1839Z90AQ5C0BHNV4', 'story', '06FHAX7FV5KWGZQ31617R63W94', 'Tier-5 canvas pane (builtin.canvas)', 'Tier-5 canvas PANE, absorbed from the former T-7 into the unified canvas epic (T-317, decision D-91). A full workspace pane that renders .canvas files by CONVERTING them into the drawing-card JSON (per D-91: .canvas is an import format, not a native schema) and painting via the shared canvas renderer (T-318). Scope: nodes (note, text, group, image) + edges + layout state; pan/zoom; node selection, drag, resize; edit affordances (add note from file picker, add text node, draw edge between nodes); persist layout back to the .canvas file on disk. Uses MultitabPane (T-83) for tabs and its own slot per D-47; panels are extension-shaped (D-17). Depends on the core renderer (T-318). NOTE: unlike the conversation drawing card (display-only, D-78), this pane is interactive/editable — it is a pane, not a conversation widget.
+
+SCOPE NOTE (2026-06-29): currently a 17-line stub. This is a from-scratch interactive Tier-5 pane (parse .canvas -> drawing-card JSON, CustomPaint render, pan/zoom, node select/drag/resize, edit affordances, persist .canvas) — a multi-session feature, NOT template-class work. The drawing-card TEMPLATE half of epic T-317 is now complete (svg/d2/image/icon/compare/graph + stdin).
+
+PROGRESS (2026-07-02): two foundation slices built + tested (from the 17-line stub). (1) JSONCanvas parser/model — lib/src/canvas/json_canvas.dart (2eb5d0f1): typed CanvasDoc (text/file/link/group nodes + edges with sides/ends/color/label), parse + toJson round-trip, skips unknown/malformed; pure Dart, 8 dart-test cases. (2) CustomPaint renderer — lib/builtin/canvas/src/canvas_painter.dart (035a37a1): CanvasBounds + pan/zoom CanvasViewport, group frames + edges (arrowheads) + node cards; node colours from the file (Obsidian presets/hex, NOT clide tokens per arbitrary-content rule), chrome themed; 7 tests (pixel-probe + viewport + colour). DECISION NOTE: the interactive pane paints the CanvasDoc model directly via CustomPaint (hit-testable, cheap re-paint) rather than re-lowering to SVG each frame — the ticket''s ''convert to drawing-card JSON'' path (D-91 SVG substrate) is for the display-only viewer; an editable pane needs direct model painting. Mirrors the graph pane. REMAINING (multi-session): interactive CanvasView (hit-test nodes + pan/zoom + click-select), pane shell (load .canvas from disk, workspace slot D-47, MultitabPane), CanvasExtension registration + open-.canvas routing, then edit affordances (add node/edge, drag, resize) + persist back to disk.
+
+PROGRESS (2026-07-02, cont.): third foundation slice landed — interactive CanvasView (lib/builtin/canvas/src/canvas_view.dart, 7700fc7a): hitTestCanvasNode (topmost node under a point; cards beat the group frames behind, via the painter''s own viewport) + scroll-zoom + drag-pan + click-select with a focus ring; 6 tests. The canvas rendering + interaction FOUNDATION is now complete + tested (parser 2eb5d0f1, renderer 035a37a1, view 7700fc7a — 21 canvas tests), mirroring where T-323''s foundation was before its integration phase. NOT yet reachable. REACHABLE-INTEGRATION PHASE (next, fresh session): (a) pane shell — a StatefulWidget that loads a .canvas from disk (files.read/editor.open path), holds the CanvasDoc, embeds CanvasView; (b) CanvasExtension registration as a workspace pane (own slot D-47, MultitabPane T-83); (c) FILE-OPEN ROUTING — how opening a .canvas lands in this pane instead of the text editor: NOTE the TabContribution.fileGlobs field is DECLARED but has ZERO consumers in lib/, so this routing must be built (wire fileGlobs, or have the editor delegate by extension, or a command + workspace tab); (d) i18n + tests. THEN the edit phase: add node/text/edge affordances, node drag + resize, and persist the CanvasDoc back to the .canvas file (encode() already exists).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:17:17', '2026-07-02 21:22:59.395', NULL, '6ba2a2c1892dbf56c582f83bd9753314', 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);