Skip to content

feat(canvas): design kit, starter layouts and the canvas skill — a canvas looks designed without CSS (abilityai/trinity-enterprise#537) - #2566

Merged
vybe merged 1 commit into
devfrom
feature/537-canvas-design-kit
Sep 7, 2026
Merged

vybe merged 1 commit into
devfrom
feature/537-canvas-design-kit

Conversation

@trinity-ability

Copy link
Copy Markdown
Contributor

Summary

  • The kit — components/canvas/CanvasKit.vue: one unscoped, .canvas-kit-prefixed, token-only stylesheet (theme() colours + .dark overrides, zero raw colours, @container collapse so the ~300px Portal rail works) with ck-card, ck-grid-2/3/4, ck-section, ck-kpi, ck-table, ck-callout, ck-chip, ck-figure. Recorded in design-system.md as the v-html twin of the primitives — the one sanctioned exception to primitives-first. Applied on every canvas surface through CanvasPanel.
  • Sanitiser allowlist (canvas mode) — sanitizeCanvasHtml / renderCanvasMarkdown run the one DOMPurify instance with a per-call canvasKit config flag (read by the hook's third argument — no module state). Only exact KIT_CLASSES members survive on class, only bounded width/max-width on style, id is 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 (a roster canvas reaches a customer's Workspace). FORBID_TAGS: ['style'] on every path; sanitizeSvg is the named mermaid exception, and MERMAID_CONFIG.secure deletes themeCSS from directives.
  • Starter layouts — template ∈ dashboard | report | brief | status-board as a nullable column (dual-track: SQLite agent_canvases_template + Alembic 0054), slot on the block. A layout never hides a block. Every writer carries the template (set, patch, voice verbs); MCP set_canvas gains template, blocks gain slot.
  • Taught twice — ### Your Canvas gains one compact worked example, the four layouts and the class list (cap 2,700 → 3,400, stated in the test); the canvas library 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 doc agents/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 forwards template, kit root overflow.
  • Backend set touched by the branch: 363 passed (canvas, voice, prompt, schema parity, cleanup parity, Alembic guards, models centralised); auth guard suites 49 passed.
  • Frontend npm run test:unit: 96 files / 2113 tests (new canvasKit.spec.js, canvasLayouts.spec.js, canvas-mode cases in sanitizeHooks.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).
  • MCP: tsc --noEmit clean, 146 tests pass (template + slot schema, omitted-when-undefined).
  • check:tokens OK; raw-colour scanner: kit at zero; check_alembic_heads.py PASS (1 head, 0054).
  • Production vite build inside the frontend container: built.
  • Live on the local stack: PUT with template stores 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 and position: fixed were stripped, max-width: 480px kept.
  • Reviewer: open a canvas in the Workspace rail (roster audience) at rail width.

Review notes

  • /autoplan ran with two independent reviewers; the engineering voice found the <style> hole and the slot-dropped-by-renderableBlocks trap; both fixed here. A strategy challenge (drop ck-kpi/ck-table as 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 mermaid themeCSS claim was refuted by a fresh-context verifier (rules are namespaced under the diagram id) and hardened anyway.
  • Out of this PR (other repos): the canvas skill push to abilityai/trinity-skills (authored + validated in the trinity-pm clone, roster entry pending) 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

… 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 vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
vybe merged commit a760f22 into dev Sep 7, 2026
30 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants