Add canvas authoring DX & pane UX: dark-theme reset, canvasSend helper, JS error banner, doc tabs#84
Conversation
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.
…thoring DX & pane UX
- 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.
There was a problem hiding this comment.
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, pluscanvasSend+ a per-doc JS error overlay for AgentDocs, and aframe-ancestorsCSP on served docs. - Client: new
DocJsErrormodel state + doc-scoped error banner; always-visible doc tab withrelativeTimeCompactage;messageListenercallbacks grouped into a record; hidden-iframe message hardening. - Docs/tests: new authoring spec, roadmap Phase 8, SKILL
canvasSendguidance, 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
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.
Problem
Authoring an effective canvas doc required too much boilerplate, and failures were invisible:
window.parent.postMessage({ action, ... })— no helper, and oversized messages were dropped silently by the client with no doc-side feedback.AgentDocrendered 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:where(...)(zero specificity, no!important) so any doc rule — even a barebody { }— and the beadsSystemViewtemplate'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 flatpostMessage({ action, ...payload })contract. Validates serialized size against the client cap (MaxPayloadBytes = 64_000) using the sameJSON.stringify({ action, ...payload }).lengthmetric the client enforces, logging a clear console error doc-side instead of the silent client-side drop.window.onerror+ anunhandledrejectionlistener, 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.fsCanvasSendState.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 aMessageListenerCallbacksrecord.AgentDoc(previously a bare iframe with no tab).Components.relativeTimeCompactrenders3m/2h/2dnext to each AgentDoc tab fromdoc.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— teachescanvasSendas the primary API; rawpostMessagekept documented as the underlying contract.Tests
buildInjectionper-kind injection (including the escaped embeddeddocfilename),canvasSendsize-cap boundary,relativeTimeCompactthresholds, 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.Category=E2E): the four acceptance criteria plus the two cascade guards for the CSS reset (doc-authoredbodybackground wins; beadsSystemViewbackground staysvar(--bg-deep)) —CanvasAuthoringDxE2ETests.fs.Full suite green: 1147 passed, 3 skipped, 0 failed via
dotnet test src/Tests/Tests.fsproj.