Skip to content

fix(canvas): open canvas links, repair truncated tool calls, unfreeze scrolling - #2419

Open
wgqqqqq wants to merge 1 commit into
GCWing:mainfrom
wgqqqqq:fix/canvas-links-and-scroll-freeze
Open

fix(canvas): open canvas links, repair truncated tool calls, unfreeze scrolling#2419
wgqqqqq wants to merge 1 commit into
GCWing:mainfrom
wgqqqqq:fix/canvas-links-and-scroll-freeze

Conversation

@wgqqqqq

@wgqqqqq wgqqqqq commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Three problems that all end with the user unable to reach a canvas. They are separate causes but they share the canvas path, so they land together.

1. Canvas links did not open

The most common report: the model emits a canvas reference and clicking it does nothing.

bitfun-canvas:// survived neither react-markdown's default urlTransform (which drops unknown schemes) nor rehype-sanitize's protocol allowlist, so the reference rendered as inert text or as a dead anchor.

  • markdownUrlTransform.ts — an explicit transform that preserves the scheme, wired into both the plain and math renderers.
  • The sanitize schema now allows the scheme on href.
  • A remark autolink pass handles bare references the model emits as plain text, and <...> angle autolinks.
  • canvasArtifactRef.ts — one shared parser/validator, so the markdown link, the tool card, and the panel agree on what a valid reference is. canvasArtifactTab.ts centralizes the tab-open event so those three call sites no longer each build their own detail payload.
  • A malformed reference now warns instead of silently doing nothing, and a panel that cannot load its artifact renders a real error state instead of an empty frame.

2. Truncated tool calls produced unusable canvases

When the model's output hits its token ceiling mid-argument, the accumulated JSON was forwarded as-is and CreateCanvas/UpdateCanvas failed to deserialize — the user saw a generic parse failure rather than anything actionable. tool_call_accumulator.rs now repairs the truncation, so the tool reports a real error against a parsed argument set.

3. Canvas scrolling froze intermittently

Reported as: scroll the canvas a little, it stops responding; closing and reopening the tab fixes it.

The frame document was synthesized with document.write into about:blank, so it never went through a normal document load. WebKit rebuilds the compositor when the host window is hidden and shown again, and such a frame can come back without its scroll node — the canvas stays painted but stops responding to the wheel, and since key={frameDocumentKey} is stable, only replacing the tab recovered it.

Log evidence: the frame loaded cleanly at 16:56:12 with one Canvas iframe HTML written; at 17:01:25 the window became visible and at 17:01:31 focus refreshed; the next canvas event was a panel remount at 17:01:43 with reason=initial (the user reopening it). No remount, no rewrite, no error in between — the DOM was intact the whole time.

The fix:

  • The frame now uses srcdoc, which keeps the same-origin semantics the postMessage boundary relies on while going through the document loading path. A blob URL would load normally too, but it needs its own origin and lifetime handling, which is why it was removed in 4be854e — it stays rejected.
  • An overflow toggle on focus and visibilitychange rebuilds the scroll node without discarding the document or the scroll offset, for the case where the frame comes back degraded anyway.
  • The startup watchdog was re-armed on every effect re-run, so a window focus refresh logged a spurious Canvas iframe did not report runtime startup warning against a healthy canvas. It is now armed only for a new document.

GenerativeWidgetFrame moves to srcdoc for the same transport reason. Its height is parent-driven and it does not scroll internally, so that one is consistency rather than a user-visible fix.

Verification

  • src/web-ui: 492 test files / 3651 tests pass; tsc --noEmit clean; eslint clean on the touched directories.
  • New tests cover the srcdoc transport (asserting it is neither document.write nor a blob URL), the watchdog not re-arming on an unchanged document, and focus-driven scroll revival leaving overflow-y unchanged.
  • bitfun-agent-stream 71, bitfun-agent-tools 104 + 15, bitfun-core tool_pipeline 39 — all pass. cargo clippy clean on the changed crates. cargo fmt --check reports no diff in any file this PR touches.

The WebKit compositor behaviour behind #3 is inferred from the logs and from what recovers the frame; it is not something I could instrument on this machine. Worth a real-device pass: open a canvas, scroll it, switch away and back a few times.

… scrolling

Three problems that all end with the user unable to reach a canvas.

Canvas links did not open. `bitfun-canvas://` survived neither
react-markdown's default url transform nor rehype-sanitize, so the model's
reference rendered as inert text or a dead anchor. The scheme now has an
explicit url transform and sanitize allowance, a remark autolink pass for
bare references the model emits as plain text, and one shared
`canvasArtifactRef` parser so the markdown link, the tool card, and the panel
agree on what a valid reference is. A malformed reference warns instead of
silently doing nothing, and a panel that cannot load its artifact says so
rather than showing an empty frame.

Truncated tool calls produced unusable canvases. When the model's output hit
its token ceiling mid-argument, the accumulated JSON was forwarded as-is and
`CreateCanvas`/`UpdateCanvas` failed to deserialize. The accumulator now
repairs the truncation so the tool reports a real error against a parsed
argument set instead of a parse failure.

Canvas scrolling froze intermittently. The frame document was synthesized
with `document.write` into `about:blank`, so it never went through a normal
document load; WebKit rebuilds the compositor when the host window is hidden
and shown again, and such a frame can come back without its scroll node. The
canvas stayed painted but stopped responding to the wheel, and only closing
and reopening the tab recovered it. The frame now uses `srcdoc`, which keeps
the same-origin semantics the message boundary relies on while going through
the document loading path, plus an overflow toggle on focus and visibility
change to rebuild the scroll node without discarding the document or the
scroll offset. A blob URL would also load normally but needs its own origin
and lifetime handling, so it stays rejected as it was in 4be854e.

The startup watchdog was re-armed on every effect re-run, so a window focus
refresh logged a spurious "did not report runtime startup" warning against a
healthy canvas. It is now armed only for a new document.

GenerativeWidgetFrame moves to `srcdoc` for the same transport reason. Its
height is parent-driven and it does not scroll internally, so this is
consistency rather than a user-visible fix there.
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.

1 participant