fix(harness): show one message in the canvas Render-failed state [SAP-3199] - #840
fix(harness): show one message in the canvas Render-failed state [SAP-3199]#840gwitwer wants to merge 5 commits into
Conversation
…-3199] The canvas drew two error strings on top of each other when an agent could not render. The app's Render-failed card is a transparent layer over the iframe (`.canvas-render-error` is inset:0 with no background), and the document inside that iframe painted its own copy of the reason underneath it. The stylesheet already claimed the document's error text was hidden by the board snippet. That was the intent and the mechanism was never there, so the two strings overlapped and neither was readable. Hide the prose the same way the document already hides its title, badge and legend: those are app chrome the SPA draws around the iframe, and the failure reason is the same kind of duplicate. The paragraph gets a stable class, a rule beside the existing header rule hides it under `[data-canvas-embedded]`, and a small head script sets that flag when `window.parent !== window`. It is its own script so the flag is set before first paint and a parse error in the much larger run-state script cannot leave it unset. Opened standalone there is no card coming, so the prose stays and is the only message. Both directions are pinned by specs, mutation-tested three ways. No user-visible string changed, and the card itself is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH
Review — PR #840 (round 1)No confidentiality issues: no non-Sapiom company names, no internal hostnames, no private 1. Missing changeset for a user-visible fix in a published package
What breaks without it: the fix publishes with no release note, so a consumer upgrading 2. The hide is unconditional on framing, but the card that replaces it is not
The concrete break is already written down in the code the PR did not touch: // canvas-run-state.ts:411
} catch {
/* malformed payload — keep the in-document error panel visible */
}That fallback is now dead. If Fix in Nits
VerdictRequest changes: add the changeset, and close the blank-board hole in #2 (or say why the |
Review round 1 on #840. The hide was unconditional on framing while the card that replaces it was not. `bootCanvasError` posts the reason from the run-state script, which can fail to post (a hand-authored document with a malformed `#sapiom-render-error` payload, or none at all) or abort as a whole before it is called. The prose stood down either way, so a document that could not post left an empty board and no message anywhere, which is worse than the overlap this PR fixes. It also made the `catch` in `bootCanvasError` promise something it could no longer deliver. The head script's flag is now optimistic and withdrawn if it turns out to be wrong: a successful post marks the root, and at load an unmarked document takes its flag back and shows its own prose. One mechanism covers every path that fails to post, including the whole-script abort the split was meant to survive. Also from the review: a changeset for both published packages, and the `painted` walk now reports a box that lies outside the frame separately rather than counting it clear, since `elementFromPoint` answers null out there and a second message below the fold would have passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH
Round 1 addressedBoth findings were real. Pushed in 9f77036 and 23bad72. 1. ChangesetAdded 2. The fail-safe pointed the wrong wayCorrect, and it is the sharper of the two. My own rationale for splitting the flag I did not take the suggested shape exactly, because clearing the attribute on the
New spec, as you asked, in
Nit: a centre point outside the frameFair, Local: build, typecheck and lint clean. Also removed three throwaway screenshot probes that went into 9f77036 by mistake. |
Review — PR #840 (round 2, delta since
|
…e hidden Review round 2 on #840 asked for this, and it is worth having: both embedded specs rendered the error panel, so nothing covered the common case, an embedded board that posts nothing and therefore has its optimistic flag withdrawn. The spec renders a real graph, embeds it, and asserts the withdrawal happened (`data-canvas-embedded` is gone) while `.canvas-header` and `.canvas-legend` stay clipped to their 1px box and the title paints nothing. Mutation-tested by gating the header rule on the flag, which is the shape round 2 assumed the code had: the spec fails, so it is not passing vacuously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH
Round 2 addressed1. The withdrawal does not un-hide the header and legend. Disagreeing, with a spec.
.canvas-header,
.canvas-legend {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip-path: inset(50%); ...
}No attribute, no So withdrawing the flag on a successful board takes nothing away from it: the chrome Round 1's suggested shape and round 2's objection both read the flag as the thing 2. No spec covered the embedded successful render. Fair, and added.That was a real gap regardless of #1, and it is the assertion that settles the
It passes on the current code. Mutation-tested against the world round 2 assumed, by So the spec does detect that regression, and the unmutated code does not have it. 3. Round 1's shapeAgreed, and it is why I did not follow it literally. Checks
|
Before and after, real server, real agent treeTwo isolated checkouts, each with its own build and its own agent tree, so neither
Each ran Measured in the page, not just looked at.
Lightbefore, two strings through each other: after, one message: Darkbefore: after: The card is unchanged in all four: same glyph, same short claim, same one-line reason, |
They came back in 44df5d6 because that commit staged the package with -A while they were sitting untracked on disk. Ignored now so a wide add cannot pick them up again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH




Fixes SAP-3199.
The bug
When an agent cannot render, the canvas pane drew two error strings on top of
each other: the app's Render-failed card and the rendered document's own prose,
in the same area, unreadable.
Two things paint the same failure:
CanvasPane.tsxdraws the Render-failed card (glyph, short claim, one-linereason, Ask/Retry, full reason behind Details).
.canvas-render-errorisposition: absolute; inset: 0with no background, so it is a transparentlayer over the iframe.
buildErrorPanelHtml) paints its own paragraph:"Could not extract this agent's step graph: ... Use the workbench actions to
ask your coding agent to fix it or retry the deterministic render."
Nothing hid the second one, so both showed through each other.
The stylesheet already claimed otherwise. The comment above
.canvas-render-errorread "the document's own error text is hidden by the boardsnippet". That was the intent; the mechanism was never there. This PR implements
it and corrects the comment.
The fix
The document stands its prose down while it is embedded, exactly the way the
title, badge and legend already do.
canvas-template.tshas a rule that visuallyhides
.canvas-headerand.canvas-legendbecause they are app chrome the SPAdraws around the iframe, with the reason spelled out: "floating a second copy
over the board would just crowd the diagram". The failure reason is the same kind
of duplicate, so it gets the same treatment, one line lower in the same block.
buildErrorPanelHtmltags the paragraphcanvas-render-error-note.:root[data-canvas-embedded] .canvas-render-error-note { display: none }sits beside the existing header rule.
data-canvas-embeddedwhenwindow.parent !== window. It is its own script, before the body, so the flagis set before first paint and a parse error in the much larger run-state script
cannot leave it unset.
Opened standalone there is no card coming, so the prose stays and is the only
message. That direction is pinned by its own spec.
Nothing about the card itself changed, and no user-visible string changed.
Verification
Real harness server (
node dist/cli/bin.js,--state-rootof its own) against areal agent tree, selecting a project folder with no
index.ts, which is the reproin the ticket. Light and dark, before and after, below.
Specs, in
e2e/canvas-render.spec.ts:no element in the document paints a second copy. "Paints" is hit-tested with
elementFromPointrather than inferred from the box, because the title andbadge sit inside a clipped 1px
.canvas-headerwhile keeping full-size rectsof their own. Every text-bearing element is walked, not just the two known
ones, so a third string added later cannot quietly reintroduce the overlap.
Mutation-tested, three ways, each confirmed red before restoring:
window.parentcheck)Local:
pnpm build,pnpm typecheck,pnpm lintall clean.canvas-render.spec.tsand
canvas-template.spec.ts13 passed;canvas-render.test.tsandcanvas-template.test.ts52 passed. Per repo policy the full suite is left to CI.Before and after screenshots against a real server, light and dark, are in a
comment below, with the measured
documentProseVisibletable. Review rounds 1and 2 are answered in their own comments.
Linear: https://linear.app/sapiom/issue/SAP-3199
🤖 Generated with Claude Code
https://claude.ai/code/session_01WoapZZPdPVtz11TQCK5eQH