Skip to content

fix: gate canvas event stream on the webview ready handshake - #300

Open
Sanjay Singh (san360) wants to merge 1 commit into
mainfrom
san360-fix-canvas-dataready-replay-race
Open

Sanjay Singh (san360) wants to merge 1 commit into
mainfrom
san360-fix-canvas-dataready-replay-race

Conversation

@san360

Copy link
Copy Markdown
Contributor

Description

Fixes a race in canvas mode where the dashboard could load and never leave its loading screen. It was originally reported as an empty content pane; on current main it shows as a loading screen stuck at "Discovering log directories 0%".

/events replays the latest progress and dataReady as soon as a client connects. The canvas bridge shim re-dispatches SSE messages straight to window. If it subscribes before app.js has attached its message listener, the replay is dropped and onDataReady never runs. Clicking a nav item can't recover the page, because renderPage bails until dataReady arrives.

68ceef6 moved the subscription into DOMContentLoaded. That only works while app.js stays a parser-blocking script. An async or code-split bundle, or a listener attached after an await, brings the race back.

Change (src/canvas/host.ts): the shim now opens /events only when the webview posts { type: 'ready' }. initMessageListener already sends that message, in a microtask, right after attaching its listener, and the VS Code panel already uses it to re-send dataReady. Subscribing is idempotent. Webview code, the RPC protocol and the server replay are unchanged. Reusing the existing handshake also avoids the new window.__coachReady global suggested in the issue.

Tests

  • src/canvas/host.test.ts: the shim test now asserts:
    • no stream opens on page lifecycle events or non-ready messages;
    • ready opens exactly one /events stream, and a repeated ready is a no-op;
    • the replayed dataReady is dispatched.
  • New src/canvas/bridge-shim.test.ts (jsdom): connects the served shim to the real initMessageListener, fires DOMContentLoaded before the dashboard listens, and replays dataReady as soon as the stream opens. It fails against both the DOMContentLoaded shim and the original eager shim, and passes with the fix.

Manual validation (browser, real canvas host, warm cache, app.js throttled to 2.5 s):

Scenario Before (main) After
Warm reload (issue steps) ✅ renders ✅ renders (3/3)
Warm reload, slow app.js ✅ renders ✅ renders
Slow app.js loaded async ❌ stuck: replay dispatched at 24 ms, listener attached at ~2.5 s ✅ renders: /events opens after app.js loads and the listener is attached
Issue-era shim, slow app.js ❌ stuck n/a
Cold start (opened while parsing) — ✅ live progress (855/855 messages delivered), then the dashboard

Also checked: the ?page=curiosity start page, every sidebar page, the workspace Current/All toggle, the harness filter, and no app console errors.

Related Issues

Fixes #269

Checklist

  • npm run check passes (typecheck + lint + spellcheck + knip + tests): every step passes locally except 7 tests (see below)
  • Changes are covered by tests (if applicable)
  • Documentation updated (if applicable): CHANGELOG.md and the canvas extension README

Validation, run on this branch after rebasing onto main (d7a9446):

  • npm run check: typecheck ✅, lint ✅ (0 errors), spellcheck ✅, knip ✅, lockfile-lint ✅. Tests: 1653 passed, 5 skipped, 7 failed. All 7 failures are in src/core/github-app-analytics.test.ts, which shells out to the sqlite3 CLI. That CLI isn't installed on my machine, the same 7 tests fail on unmodified main locally, and they are unrelated to this change.
  • npx vitest run src/canvas/ src/webview/shared-progress.test.ts: ✅ 13 passed
  • npm run build: ✅
  • npm run check-size: ✅ within the 2 MB budget

The canvas host replays the latest progress and dataReady on /events as soon
as a client connects. The bridge shim subscribed on DOMContentLoaded, which
only preceded app.js while the bundle stayed a parser-blocking script; an
async or code-split bundle could still miss the replay, leaving the dashboard
stuck on its loading screen with no way to recover by navigating.

The shim now opens the stream only when the webview posts `ready`, which
initMessageListener sends after its listener is attached - the same handshake
the VS Code panel uses. A jsdom regression test wires the served shim to the
real listener and fails against both earlier subscription strategies.

Resolves #269

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@san360
Sanjay Singh (san360) requested review from Aymen (aymenfurter) and a balanced review from Copilot October 21, 2026 19:49
@github-actions
github-actions Bot requested a review from TamasBoncz October 21, 2026 19:49
@github-actions

Copy link
Copy Markdown

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI 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.

🟢 Approval recommended

The focused change prevents the replay race, has targeted regression coverage, and has no identified blocking issues.

No open issues 🎉

What changed in this PR

Fixes #269 by delaying canvas event subscription until the dashboard signals readiness, preventing startup events from being lost.

Changes:

  • Gates event subscription on the existing ready handshake.
  • Tests delayed listener registration and duplicate readiness messages.
  • Documents the startup fix.
File Description
src/​canvas/​host.ts Opens one event stream after ready.
src/​canvas/​host.test.ts Verifies handshake gating and idempotency.
src/​canvas/​bridge-shim.test.ts Tests replay delivery with the real dashboard listener.
CHANGELOG.md Records the canvas startup fix.
.github/​extensions/​ai-engineer-coach/​README.md Explains handshake-based event subscription.

Review effort: Balanced

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.

Canvas mode can load with an empty content pane: replayed dataReady is dispatched before app.js listens

3 participants