feat(canvas): design kit, starter layouts and the canvas skill — a canvas looks designed without CSS (abilityai/trinity-enterprise#537) - #2566
Merged
Conversation
… agent's canvas looks designed without touching CSS (abilityai/trinity-enterprise#537) The kit: `components/canvas/CanvasKit.vue` — one unscoped, `.canvas-kit`- prefixed, token-only stylesheet (an SFC so the raw-colour ratchet walks it; `theme()` colours with `.dark` overrides; `@container` collapse, never a viewport query, because the Portal rail is ~300px wide on a desktop) with a small class vocabulary — cards, 2/3/4-up grids, section headers, KPI tiles, tables, callouts, chips, figures. The v-html twin of the primitives, held to the same tokens, recorded in design-system.md as the one sanctioned exception to primitives-first. Every canvas surface renders blocks inside it. The sanitiser admits the kit and nothing else, on the canvas: `html` blocks go through `sanitizeCanvasHtml`, markdown prose through `renderCanvasMarkdown` (`CanvasProse.vue`) — the ONE DOMPurify instance with a per-call `canvasKit` config flag the existing hook reads from its third argument (no module state to leak). Only exact `KIT_CLASSES` members survive on `class`, only a bounded `width`/`max-width` on `style`, and `id` is dropped. Canvas-scoped because chat and report markdown depend on the code-block decorator's classes. Found on the way and fixed app-wide: DOMPurify's default tag list admits the `<style>` ELEMENT, and a body `<style>` is document-global — an agent message, report or canvas block could restyle the whole page, a customer's Workspace included. `FORBID_TAGS: ['style']` on every markdown/html path; `sanitizeSvg` is the named exception for mermaid's id-scoped stylesheet, and the mermaid config now lists `themeCSS` as a secure key so a directive cannot carry CSS. Starter layouts: `template` ∈ dashboard | report | brief | status-board is a nullable column on the row (SQLite `agent_canvases_template` + Alembic 0054); `slot` is a key on the block so it travels through `patch_canvas`. A layout never hides a block — unslotted blocks render after it, nothing slotted means stacked, empty regions are not rendered; an unknown slot is kept, an unknown template refused by name. Every writer carries the template (set, patch, the voice verbs). The MCP tool advertises both; the platform prompt gains one compact worked example plus the layouts and the class list (cap raised 2,700 → 3,400, deliberately); the `canvas` library skill carries the full reference with three worked examples. Caught by the live write, not the unit suite: the router dropped `template` between a valid model and a storing service. Fixed and pinned by a router-forwarding test; the class is in learnings.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CgzGLnmPrQk6q5LqZCRru
vybe
approved these changes
Sep 7, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: dual-track migration (SQLite + Alembic 0054, single head incl. after merge with dev), security scans clean, 228 touched backend tests pass locally, all 27 CI checks green. Cross-tracker issue ent#537 needs a manual status-in-dev bump after merge.
vybe
added a commit
that referenced
this pull request
Sep 7, 2026
…sion off 0055_portal_session_main_chat as 0056 dev renamed the Main-chat revision 0054 → 0055 (#2558 landed after #2561's 0053 and #2566's 0054), so this branch's 0055_schedule_workspace_delivery chained off a parent that no longer exists and the merge left a stale 0054_portal_session_main_chat.py behind — two heads, zero revisions applied. Renumbered to 0056 off 0055_portal_session_main_chat, stale file dropped, docs and the ent#498 revision-pin test updated. check_alembic_heads: 1 head. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LaBkiyfRkYkmk4iMJkHRdL
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
components/canvas/CanvasKit.vue: one unscoped,.canvas-kit-prefixed, token-only stylesheet (theme()colours +.darkoverrides, zero raw colours,@containercollapse so the ~300px Portal rail works) withck-card,ck-grid-2/3/4,ck-section,ck-kpi,ck-table,ck-callout,ck-chip,ck-figure. Recorded indesign-system.mdas the v-html twin of the primitives — the one sanctioned exception to primitives-first. Applied on every canvas surface throughCanvasPanel.sanitizeCanvasHtml/renderCanvasMarkdownrun the one DOMPurify instance with a per-callcanvasKitconfig flag (read by the hook's third argument — no module state). Only exactKIT_CLASSESmembers survive onclass, only boundedwidth/max-widthonstyle,idis dropped. The kit root scrolls its own overflow.<style>forbidden everywhere — DOMPurify's default admits the element and a body<style>is document-global, so before this any agent markdown/html could restyle the whole page (arostercanvas reaches a customer's Workspace).FORBID_TAGS: ['style']on every path;sanitizeSvgis the named mermaid exception, andMERMAID_CONFIG.securedeletesthemeCSSfrom directives.template∈dashboard | report | brief | status-boardas a nullable column (dual-track: SQLiteagent_canvases_template+ Alembic0054),sloton the block. A layout never hides a block. Every writer carries the template (set, patch, voice verbs); MCPset_canvasgainstemplate, blocks gainslot.### Your Canvasgains one compact worked example, the four layouts and the class list (cap 2,700 → 3,400, stated in the test); thecanvaslibrary skill is authored and validated in the trinity-skills clone (push is operator-gated, see below).Changes
Backend:
models.py,services/canvas_service.py,services/canvas_blocks.py,db/canvas.py,db/schema.py,db/tables.py,db/migrations.py,migrations/versions/0054_agent_canvases_template.py,routers/canvas.py,services/gemini_voice.py,services/platform_prompt_service.py.Frontend:
components/canvas/{CanvasKit.vue, CanvasProse.vue, canvasLayouts.js, CanvasPanel.vue, CanvasBlock.vue, CanvasMarkdown.vue, CanvasDiagram.vue, canvasUtils.js},utils/{canvasKit.js, markdown.js, sanitizeHooks.js}.MCP:
tools/canvas.ts,client.ts.Docs: requirements §5.18 FR-14..17,
feature-flows/agent-canvas.md,architecture/observability.md,design-system.md+ contract, user docagents/agent-canvas.md,learnings.md(2 entries),security-reports/cso-diff-2026-09-07-ent537-*.Test Plan
tests/unit/test_ent537_canvas_design_kit.py(22) — slot map parity across models.py / canvas.ts / canvasLayouts.js, every layout+slot has a grid-area rule, slot REST round trip, unknown template refused by name, template stored / kept by patch and the voice path, DDL pair on both tracks, prompt classes ⊂ KIT_CLASSES, every kit class styled, canvas components use only canvas-mode entry points,<style>forbidden on every path, router forwardstemplate, kit root overflow.npm run test:unit: 96 files / 2113 tests (newcanvasKit.spec.js,canvasLayouts.spec.js, canvas-mode cases insanitizeHooks.spec.js, reworded bug(workspace): code blocks in agent replies are unreadable — same-tint block, ~11px type, horizontal scroll — and nothing in the thread can be copied #2515 pins).tsc --noEmitclean, 146 tests pass (template + slot schema, omitted-when-undefined).check:tokensOK; raw-colour scanner: kit at zero;check_alembic_heads.pyPASS (1 head, 0054).vite buildinside the frontend container: built.templatestores and returns it, PATCH keeps it, list carries it, 422 on an unknown name; Agent Detail renders the dashboard layout in light and dark; at 430px the layout collapses to one column;<style>, a non-kit class andposition: fixedwere stripped,max-width: 480pxkept.Review notes
/autoplanran with two independent reviewers; the engineering voice found the<style>hole and theslot-dropped-by-renderableBlockstrap; both fixed here. A strategy challenge (dropck-kpi/ck-tableas duplicates of the data kinds) was kept per the issue's AC — same tokens as the report renderers, and the prompt teaches "prefer the kinds"./cso --diff: 0 critical/high; 1 low (kit overflow) fixed; the mermaidthemeCSSclaim was refuted by a fresh-context verifier (rules are namespaced under the diagram id) and hardened anyway.canvasskill push toabilityai/trinity-skills(authored + validated in the trinity-pm clone, roster entrypending) and the abilities wizard references (refactor: delete process engine (Option B) (#430) #482-style). Follow-up: Tailwind utilities remain reachable from chat/report markdown (the class allowlist is canvas-only).Fixes abilityai/trinity-enterprise#537
🤖 Generated with Claude Code
https://claude.ai/code/session_013CgzGLnmPrQk6q5LqZCRru