fix(export): resolve PPTX export race condition with empty slides#72
fix(export): resolve PPTX export race condition with empty slides#72111wukong wants to merge 2 commits into
Conversation
Users who route Claude Code through a custom model endpoint (e.g. via `cc switch`) need to select `deepseek-v4-pro` or `deepseek-v4-flash` from the model picker. Without these entries, the only way to switch models is to leave html-anything, change the CLI config, and reload. Closes nexu-io#65
The off-screen iframe used to capture each slide for PPTX / PNG-ZIP export could report a 0px scrollHeight when fonts or Tailwind CDN styles had not yet been applied, even after the `load` event fired. This caused `iframeToBlob` to throw "preview has no content yet" for decks that rendered correctly in the live preview. Changes: - Increased the srcdoc load timeout from 4 s → 8 s (Tailwind CDN can be slow) - Added a double-rAF flush before the first capture to let the browser finish layout after the load event - Added a single retry with 1.2 s backoff when the first capture fails Fixes nexu-io#62
nettee
left a comment
There was a problem hiding this comment.
I found two follow-ups in the new export retry path: one around retrying permanent screenshot errors, and one around the longer load timeout still falling through as if loading had succeeded. Details inline.
🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.Location: next/src/lib/export/deck.ts RIGHT lines 47-51
The new 8-second timer still resolves this promise through the same done() path as a real load event, so a slide that never finishes loading is treated as ready and then sent into the capture/retry path anyway. In practice that turns a missing-load failure into a much slower ~9.2s per-slide failure with a less specific downstream error. Please reject or throw on the timeout path instead of resolving it, and only enter the retry/capture logic after an actual load signal.
Inline comment could not be anchored: inline anchor is outside the PR diff anchorable ranges
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | ||
| try { | ||
| return await iframeToBlob(iframe, { scale }); | ||
| } catch (err) { | ||
| lastError = err; | ||
| if (attempt < maxAttempts) { | ||
| // Wait longer before retry — fonts / Tailwind CDN may still be | ||
| // inflight even after the load event fired. | ||
| await new Promise<void>((r) => setTimeout(r, 1200)); | ||
| } |
There was a problem hiding this comment.
This retry loop currently catches every iframeToBlob() failure, but that helper also throws hard errors like "iframe not ready" and "screenshot failed", not just the transient "preview has no content yet" case. Retrying those permanent failures adds 1.2s of delay per slide and makes the final error harder to interpret without improving the outcome. Please narrow the retry to the specific empty-layout race (for example by checking err instanceof Error && err.message === "preview has no content yet") and rethrow other errors immediately.
|
Hey @111wukong! 🎉 Great first contribution — the double-rAF flush + retry approach is the right instinct for the CDN timing problem. @nettee left a review two days ago with a couple of follow-up items that are worth addressing before this lands:
Both are pretty surgical fixes — a follow-up commit should be quick. Let me know if you'd like any guidance on the rejection pattern or error-type narrowing. ❤️ |
|
Heads-up: issue #66 describes the same PPTX export failure on Edge + Windows 11 (the "preview has no content yet" toast), so this fix should also help that reporter once it lands. One note on the root cause: the retry approach here helps, but the underlying race is that Also, issue #66 has a second symptom — the full-slide PDF print producing a garbled layout — that this PR doesn't cover. That one is in |
When exporting a deck to PPTX or PNG-ZIP,
renderSlideToBlobcreates an off-screen iframe withsrcdocequal to the slide HTML. If fonts or Tailwind Play CDN styles have not been applied by the time theloadevent fires (or the 4-second timeout expires), the iframe document measures 0px scrollHeight andiframeToBlobthrows "preview has no content yet" — even though the live preview renders the deck correctly.Changes:
requestAnimationFrameflush after the load guard so the browser has an opportunity to finish layout before the first captureFixes #62