microsoft / microsoft/AI-Engineering-Coach
Canvas mode can load with an empty content pane: replayed dataReady is dispatched before app.js listens
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4.2k
- Forks
- 585
- Avg merge
- 22h 5m
- Merged PRs (30d)
- 16
Description
Description
In canvas mode the dashboard can load with an empty content pane: the sidebar renders and populates, the main area stays blank, and there is no console error. Clicking any nav item fills it immediately, which makes it look like a data or filter problem. It is neither — the RPCs have data the whole time.
The cause is a race between the SSE replay and the app bundle.
1. /events replays state to every client the moment it connects (src/canvas/host.ts:181-182):
if (lastProgress) res.write(`data: ${JSON.stringify({ type: 'progress', ...lastProgress })}\n\n`);
if (ready) res.write(`data: ${JSON.stringify({ type: 'dataReady', currentWorkspace })}\n\n`);
2. BRIDGE_SHIM dispatches every event straight to window with no buffer (src/canvas/host.ts:302):
function dispatch(data){window.dispatchEvent(new MessageEvent('message',{data:data}));}
3. The shim is inlined in <head>, ahead of <script src="/app.js"> — a ~1MB bundle.
So when the parse has already finished (a warm cache, or simply a reload), the replayed dataReady is dispatched before app.js has registered its message listener. The event is dropped, onDataReady never runs, and with it neither the initial navigateTo(currentPage) nor the getWorkspaces/getHarnesses calls. A later click calls navigateTo directly, which is why interaction appears to fix it.
Steps to Reproduce
- Run the canvas host and let it finish parsing once, so the cache is warm.
- Reload the page.
- The sidebar renders; the content pane stays empty.
It is a race, so it does not reproduce every time — a slow bundle parse lets the listener win. It reproduces reliably here on a corpus of ~1,000 sessions where the cached parse completes in about five seconds while the page is still loading.
Confirming it is this and not missing data:
// Content is empty, yet the filter's own RPC has data:
document.querySelector('#content').children.length // 0
await rpc('getStats', { workspace: 'supermodular-os' })
// → { totalSessions: 519, totalRequests: 1193 }
// And the nav link is already marked active, so navigateTo() ran —
// it is renderPage that produced nothing, because onDataReady never fired.
document.querySelector('.nav-links a.active').dataset.page // "dashboard"
Expected Behavior
The dashboard renders on load, regardless of whether the parse finished before or after the bundle.
Notes toward a fix
Buffering in the shim until the app signals it is listening is the smallest change, and keeps the fix on the canvas side where the replay lives:
var queue = [], listening = false;
function dispatch(data){
if (!listening) { queue.push(data); return; }
window.dispatchEvent(new MessageEvent('message', { data: data }));
}
window.__coachReady = function(){
if (listening) return;
listening = true;
var pending = queue; queue = [];
for (var i = 0; i < pending.length; i++) {
window.dispatchEvent(new MessageEvent('message', { data: pending[i] }));
}
};
with initMessageListener calling it once its listener is attached:
(window as unknown as { __coachReady?: () => void }).__coachReady?.();
Under VS Code the global is absent and the call is a no-op, since that bridge does not replay.
Two alternatives, in case either fits the codebase better: have the client request current state over RPC after registering its listener rather than relying on the replay, or move BRIDGE_SHIM after app.js — though the shim must define acquireVsCodeApi before the bundle runs, so that one likely needs more care than it sounds.
I have this running locally and it fixes the blank pane; happy to open a PR if the approach looks right.
Extension Version
0.1.0 (reproduced against main as of 2026-09-17; host.ts line numbers are from that checkout)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the replay logic in src/canvas/host.ts around lines 181-182 and 302, then inspect initMessageListener where the app message listener is attached. Run the canvas host, warm the parse cache, and reload repeatedly to observe the blank content pane. Done means the dashboard renders on reload whether replayed dataReady arrives before or after the app bundle listener.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- full-stack
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100