Skip to content

Add canvas authoring DX & pane UX: dark-theme reset, canvasSend helper, JS error banner, doc tabs#84

Merged
0101 merged 13 commits into
mainfrom
canvas-improvements
Jun 23, 2026
Merged

Add canvas authoring DX & pane UX: dark-theme reset, canvasSend helper, JS error banner, doc tabs#84
0101 merged 13 commits into
mainfrom
canvas-improvements

Conversation

@0101

@0101 0101 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Problem

Authoring an effective canvas doc required too much boilerplate, and failures were invisible:

  • Every doc had to hand-roll its own dark-theme CSS to look on-brand.
  • Docs talked to the pane via raw window.parent.postMessage({ action, ... }) — no helper, and oversized messages were dropped silently by the client with no doc-side feedback.
  • Doc-side JS errors vanished inside the iframe with no surface in the UI.
  • A lone AgentDoc rendered as a bare iframe with no labeled tab, and tabs gave no sense of how fresh the underlying file was on disk.

Implements Phase 6 (Canvas Doc Authoring DX) and Phase 8 (Pane UX) from docs/spec/future/canvas-roadmap.md. Full spec: docs/spec/canvas-authoring-dx.md.

Changes

Server — src/Server/CanvasDocServer.fs

  • Base dark-theme CSS reset injected for both doc kinds. Every selector is wrapped in :where(...) (zero specificity, no !important) so any doc rule — even a bare body { } — and the beads SystemView template's own element rules always win the cascade despite being injected at </head>. A plain <body>text</body> now renders dark and readable with zero authored CSS.
  • window.canvasSend(action, payload) helper (AgentDoc only) wrapping the flat postMessage({ action, ...payload }) contract. Validates serialized size against the client cap (MaxPayloadBytes = 64_000) using the same JSON.stringify({ action, ...payload }).length metric the client enforces, logging a clear console error doc-side instead of the silent client-side drop.
  • JS error overlay (AgentDoc only) installing window.onerror + an unhandledrejection listener, forwarding errors to the pane as { action: 'canvas-doc-error', doc, message, source, line, col }. The emitting doc's filename is embedded (escaped) per served doc, giving each error its emitter identity.

Client — src/Client/CanvasPane.fs, Components.fs, CanvasTypes.fs, CanvasUpdate.fs, CanvasState.fs

  • Doc-error banner — a non-blocking, dismissible banner driven by a source distinct from the message-delivery failures already shown via CanvasSendState.Failed. The error is stamped with the emitting doc (DocJsError { ScopedKey; Filename; Message }) only after validating the filename against the focused worktree's docs (isKnownCanvasDoc), and is shown only while that doc is focused — so navigating away by tab, card, or keyboard never shows a stale error. Listener callbacks are grouped into a MessageListenerCallbacks record.
  • Always-visible doc tab — the active doc's tab now always renders, even for a single AgentDoc (previously a bare iframe with no tab).
  • Compact last-modified age — new Components.relativeTimeCompact renders 3m/2h/2d next to each AgentDoc tab from doc.LastModified, refreshing on the pane's existing render cadence.

Docs

  • docs/spec/canvas-authoring-dx.md — new feature spec.
  • docs/spec/future/canvas-roadmap.md — roadmap Phase 8.
  • src/Extension/skill/SKILL.md — teaches canvasSend as the primary API; raw postMessage kept documented as the underlying contract.

Tests

  • Unit: buildInjection per-kind injection (including the escaped embedded doc filename), canvasSend size-cap boundary, relativeTimeCompact thresholds, and doc-error attribution (a background/hidden doc's error stamped with the emitter, unknown emitter dropped) — CanvasDocServerTests.fs, CanvasAwarenessTests.fs, CanvasPaneTests.fs, ComponentsTests.fs.
  • E2E (Playwright, Category=E2E): the four acceptance criteria plus the two cascade guards for the CSS reset (doc-authored body background wins; beads SystemView background stays var(--bg-deep)) — CanvasAuthoringDxE2ETests.fs.

Full suite green: 1147 passed, 3 skipped, 0 failed via dotnet test src/Tests/Tests.fsproj.

0101 added 11 commits June 22, 2026 11:33
Feature spec for Canvas Authoring DX & Pane UX (parent tm-canvas-improvements-kqo) and the roadmap Phase 8 entry these tasks implement against.
Extend baseStyle with a :where()-wrapped, zero-specificity dark-theme reset (body bg/fg #1e1e2e/#cdd6f4 + system font, headings, code/pre, table, links; no !important), injected for both doc kinds via buildInjection. Adds BuildInjectionTests covering both-kind injection and the zero-specificity guarantee.
…r + update SKILL.md

Add window.canvasSend(action,payload), injected in the AgentDoc arm of buildInjection. It wraps the flat postMessage contract and enforces the same size cap (64000 UTF-16 code units via JSON.stringify length) the client applies in CanvasPane.fs, dropping doc-side with a console.error instead of a silent client-side drop. SKILL.md now teaches canvasSend as the primary API. Adds BuildInjectionTests covering the AgentDoc-includes/SystemView-omits split, the JSON.stringify metric, the cap drift-guard, and the strict-> boundary.
Server: errorOverlayScript (AgentDoc-only) chains window.onerror + unhandledrejection and forwards doc-side JS failures to the pane as the flat {action:'canvas-doc-error',message,source,line,col} message, with a try/catch loop guard and no suppression. Client: DocJsError stamp (ScopedKey+Filename+Message), Canvas.DocError state, canvasDocError/dismissCanvasDocError reducers, SelectCanvasDoc clears, a focus-gated canvas-doc-error-banner (flex child, never overlays; distinct amber CSS), and a pane-internal messageListener arm that intercepts (does NOT forward) the error. Tests: 3 server (overlay include/omit + try/catch guard) + 7 model (stamp/drop/distinct-source/newest-wins/dismiss/dismiss-leaves-sendstate/SelectDoc-clears). Spec updated to document the stamp-and-gate doc-scoping. E2E owned by verify task tm-canvas-improvements-129.
…d age

Tab strip always renders the active doc's tab (drops lone-AgentDoc suppression), preserving SystemView-first ordering. AgentDoc tabs show a compact on-disk age via new Components.relativeTimeCompact (now/Nm/Nh/Nd). Adds .canvas-tab-age CSS, 15 relativeTimeCompact unit tests, and updates 2 E2E canvas assertions.
…action parameter

Merge payload first then apply {action} last (Object.assign({},payload,{action:action})) so the explicit action argument always wins over a payload action key. Adds regression test to BuildInjectionTests.
…essageListener callbacks

A-02: thread the emitting doc's filename through the canvas-doc-error overlay (embedded server-side via JsonSerializer) so canvasDocError stamps the error with the doc that actually threw - validated against the focused worktree's docs (isKnownCanvasDoc) - instead of the active tab. Msg becomes CanvasDocError of filename*message.

A-04: group messageListener's 4 same-typed callbacks into a MessageListenerCallbacks record passed by name, mirroring CanvasPaneCallbacks.
…sDocServerTests

Replaced the imperative 'for kind in [ SystemView; AgentDoc ] do' loop in test
'both doc kinds inject the dark-theme base reset' with a List.iter pipeline,
satisfying the no-loops style rule. Build clean; BuildInjectionTests 20/20 pass.
- Components: extract a shared formatTimeDelta helper behind relativeTime/relativeTimeCompact, removing the duplicated time-bucketing ladder.
- Move DocJsError + CanvasSendState out of the focus-scoped Navigation module into a new CanvasTypes module.
- CanvasDocServer: restrict canvas-doc framing with a CSP frame-ancestors header (loopback origins only) so untrusted pages can't frame docs and harvest postMessages.
- Doc-error attribution: the overlay now derives its own worktree from location.pathname and posts a wt field; CanvasDocError carries the scopedKey; the reducer stamps from the emitter, not FocusedElement (fixes cross-worktree misattribution).
- Pane message listener: drop generic-forward and navigate-canvas-doc messages that originate from a hidden (non-active) canvas iframe.
- Update canvas-pane spec (tab-bar behavior) and tests, including a cross-worktree doc-error attribution regression test.
Brings in App.fs refactor into focused view/state modules (#82) and global config writer data-loss fix (#81). All 1147 tests pass.
Copilot AI review requested due to automatic review settings June 23, 2026 13:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements Phase 6 (Canvas Doc Authoring DX) and Phase 8 (Pane UX) for the canvas pane. Server-side, every served canvas doc now gets a zero-specificity dark-theme CSS reset, and AgentDocs additionally get an injected window.canvasSend(action, payload) helper (with a client-matching size cap) plus a window.onerror/unhandledrejection overlay that forwards doc-side JS failures to the pane. Client-side, those failures surface in a new doc-scoped, dismissible error banner; the active doc always renders a labeled tab (even when it's the only doc) with a compact last-modified age. Supporting docs and the authoring SKILL are updated, and canvas shared types are relocated into a new CanvasTypes.fs module.

Changes:

  • Server: inject a :where()-wrapped dark reset for both doc kinds, plus canvasSend + a per-doc JS error overlay for AgentDocs, and a frame-ancestors CSP on served docs.
  • Client: new DocJsError model state + doc-scoped error banner; always-visible doc tab with relativeTimeCompact age; messageListener callbacks grouped into a record; hidden-iframe message hardening.
  • Docs/tests: new authoring spec, roadmap Phase 8, SKILL canvasSend guidance, and broad unit + E2E coverage.
Show a summary per file
File Description
src/Server/CanvasDocServer.fs Dark-theme reset, canvasSend, JS error overlay, per-doc filename threading, CSP header
src/Client/CanvasTypes.fs New module holding CanvasSendState (moved) + DocJsError
src/Client/Navigation.fs Removed CanvasSendState (relocated)
src/Client/CanvasPane.fs Doc-error banner, always-render tab + age, MessageListenerCallbacks record, hidden-iframe guard
src/Client/CanvasUpdate.fs canvasDocError/dismissCanvasDocError reducers, listener wiring, clear-on-select
src/Client/CanvasState.fs / AppTypes.fs / App.fs / CanvasView.fs Thread new DocError state + CanvasDocError message through model/view
src/Client/Components.fs Extract shared formatTimeDelta; add relativeTimeCompact
src/Client/index.html CSS for doc-error banner and tab age
src/Client/Client.fsproj Compile CanvasTypes.fs after Navigation.fs
src/Tests/* (Components/CanvasPane/CanvasDocServer/CanvasAwareness/AuthoringDxE2E) Unit + E2E coverage for the new behaviors
docs/spec/canvas-authoring-dx.md, canvas-roadmap.md, canvas-pane.md New/updated specs
src/Extension/skill/SKILL.md Teach canvasSend as the primary API
.gitignore Ignore src/Tests/TestResults/

Two issues are worth addressing before merge: the injected :where(body) reset's padding:1rem overrides equal-specificity *{padding:0} resets (changing the beads SystemView body padding from 0 to 1rem, untested by the background-only E2E guard), and the new canvas-authoring-dx.md Technical Approach/Expected Behavior describe a superseded focus-derived-worktree design that no longer matches the shipped emitter-worktree implementation.

Copilot's findings

  • Files reviewed: 23/24 changed files
  • Comments generated: 2

Comment thread src/Server/CanvasDocServer.fs
Comment thread docs/spec/canvas-authoring-dx.md Outdated
0101 added 2 commits June 23, 2026 15:59
Address PR #84 review findings:

- The injected :where(body){padding:1rem} reset (zero specificity) won the
  source-order tiebreak over BeadspaceTemplate.html's *{padding:0} universal
  reset, regressing the beads dashboard body padding from 0 to 1rem. Reset
  margin/padding on the template's body selector directly (specificity 0,0,1)
  so it wins regardless of source order. Strengthened E2E Item 5b to assert
  body padding stays 0px.
- canvas-authoring-dx.md described a superseded focus-derived-worktree design.
  Updated message shape (wt+doc), Expected Behavior, and Technical Approach to
  the shipped emitter-worktree stamping (3-tuple CanvasDocError) that closes
  the cross-worktree misattribution.
- Refined the baseStyle cascade comment to be accurate about the per-property,
  body-selector-only override caveat.
@0101 0101 enabled auto-merge (squash) June 23, 2026 14:50
@0101 0101 merged commit 2632412 into main Jun 23, 2026
1 check passed
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