Skip to content

Fix slide import fidelity, editor glitches, and stuck run spinner - #2765

Open
NKoech123 wants to merge 18 commits into
mainfrom
ai_main_6c2a21a7196a41419f7c
Open

Fix slide import fidelity, editor glitches, and stuck run spinner#2765
NKoech123 wants to merge 18 commits into
mainfrom
ai_main_6c2a21a7196a41419f7c

Conversation

@NKoech123

@NKoech123 NKoech123 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Triages and fixes a batch of reported slides bugs: PDF/PPTX import fidelity issues, editor rendering glitches on large decks, a run indicator that could spin indefinitely, and a Google Slides export that silently reported success when it actually fell back to a PPTX download.

Problem

User feedback surfaced several issues: imports of non-16:9 PDFs/PPTX rendered distorted or mispositioned content and lost spaces between words when style changed mid-line, decks with many slides rendered slowly with flickering/glitchy thumbnails, editing an imported deck could fail to save, the runs tray spinner could stick "active" forever after a run died mid-flight, the deck editor could stay dimmed after resizing, and Google Slides export reported success even when it silently fell back to a PPTX download.

Solution

Added a triage plan (plans/slides-feedback-2026-08-07.md) documenting each reported bug, verifying it against the code, and fixing the confirmed issues directly. Fixes span the import pipeline, the slide renderer, the editor sidebar/UI, export flow, and the core runs tray polling logic.

Key Changes

  • Runs tray: keep polling (every 5s) while a run still reads as active even when pollMs={0}, so an abandoned run (budget exhausted, dead worker) can no longer spin forever; added regression tests in RunsTray.spec.tsx.
  • PDF/PPTX import fidelity (html-converter.ts): scale element positions/sizes against the aspect-ratio preset closest to the source slide's own ratio instead of a fixed 16:9/960x540 box, fixing distorted/mispositioned content on portrait or custom-size pages; also scale font sizes by the same EMU-relative factor as positions instead of a fixed pt→px conversion.
  • PDF text parsing (pdf-fidelity-parser.ts): preserve word-gap spacing across a mid-line style change (color/weight) so headings no longer get jammed together (e.g. "7 Airpurifying" → "7 Air purifying").
  • SlideRenderer: defer expensive per-slide autofit measurement for off-screen thumbnails using an IntersectionObserver, avoiding hundreds of reflows at once on large decks.
  • EditorSidebar: remove backdrop-blur on thumbnail overlays (replaced with opaque scrims), use hidden/block instead of opacity toggling, and add content-visibility: auto plus fixed aspect-ratio on thumbnails to stop flickering/dark patches on long slide rails.
  • DeckEditor: recompute sidebarOpen on window resize so the mobile dimming scrim doesn't get stuck covering the editor after narrowing the window.
  • ExportMenu: fallback toast now shows a warning with the server's actual failure reason instead of a misleading success toast when Google Slides export falls back to a PPTX download.
  • Index.tsx: raise the import action timeout to 5 minutes for PDF/PPTX/Google Slides imports so large imports don't silently fail to open in the client while the server keeps processing.
  • patch-deck action: source-preservation guards now only apply to agent callers (tool/mcp/a2a), not the browser editor, fixing human edits to imported decks failing to save.
  • Added changesets and changelog entries for the fixed issues.

Edit in Builder  Preview


To clone this PR locally use the Github CLI with command gh pr checkout 2765

You can tag me at @BuilderIO for anything you want me to fix or change

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@builder-io-integration builder-io-integration Bot changed the title Update from the Builder.io agent Fix slide import fidelity, editor glitches, and stuck run spinner Aug 9, 2026
@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@builder-io-integration builder-io-integration Bot 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.

Builder reviewed your changes and found 2 potential issues 🟡

Review Details

Code Review Summary

PR #2765 addresses several real Slides and shared progress issues: non-16:9 import geometry and text sizing, PDF style-boundary spacing, large-deck thumbnail performance, stale active-run polling, editor resize behavior, source-preserving patch guards, and misleading Google Slides export fallback messaging. The implementation is generally well targeted, uses regression coverage for the core fixes, and the overall risk is Standard because it changes shared polling plus multiple import/editor paths.

Key Findings

🟡 MEDIUM

  • The extended import timeout is not applied to the common uploaded-source generation path, which still uses the default 60-second client timeout for large PDF/PPTX imports.
  • The multiline formatting of import calls breaks the existing source-based generation-flow test; the focused test currently fails three assertions.

The RunsTray polling, fidelity math, PDF spacing guard, and browser-editor source-preservation gating otherwise appear sound. I also verified the generation-flow test failure directly against the current checkout.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +848 to +856
const imported = (await callAction(
"import-file",
{
filePath: file.path,
format: "pdf",
deckId: deck.id,
importIntoDeck: true,
},
{ timeoutMs: IMPORT_ACTION_TIMEOUT_MS },

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.

🟡 Apply the extended timeout to source-deck imports used before generation

The five-minute timeout is only passed by the direct import/reference-picker calls in this component. The main create-a-deck-from-upload flow calls importUploadedDeckIntoDeck() before generation, and that helper invokes the same import-pptx / import-file actions without timeout options, so large/image-heavy source decks still hit the 60-second default and can be reported as failed while the server continues importing. Pass the shared timeout through that helper (covering both PDF and PPTX) so every source-deck import path gets the intended budget.

Additional Info
Found independently by 2 of 4 code-review agents; confirmed by surrounding call-path inspection.

Fix in Builder

const imported = (await callAction("import-google-slides-reference", {
presentationUrl: selection.url,
})) as { id?: unknown };
const imported = (await callAction(

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.

🟡 Keep generation-flow assertions compatible with the reformatted calls

The existing Index.generation-flow.test.ts extracts this function from the source and asserts it contains the literal callAction("import-pptx" and callAction("import-file". Moving the action names onto the next line makes those assertions fail; the focused test currently reports three failures. Update the source-based assertions to tolerate whitespace/newlines or update them to assert the new timeout behavior.

Additional Info
Confirmed by running `pnpm --dir templates/slides exec vitest run app/pages/Index.generation-flow.test.ts` on HEAD.

Fix in Builder

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Visual recap — screenshot failed

A recap was published, but the PR-comment screenshot could not be captured or uploaded. Open the interactive recap directly:

Open the full interactive recap

Diagnostic:

light: screenshot captured but image upload failed

@builder-io-integration builder-io-integration Bot 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.

Builder reviewed your changes and found 4 potential issues 🟡

Review Details

Incremental Code Review Summary

The latest commit adds a “Revise with AI” action to the Slides inline formatting menu, including selection preview, instruction capture, a bounded revision prompt, and a new prompt-builder test. The earlier import-timeout and generation-flow-test comments remain unresolved and were intentionally not reposted. This incremental review is Standard risk because the new feature changes editor state, agent-chat delivery, and keyboard-visible controls.

New Findings

🟡 MEDIUM

  • The AI revision can be overwritten by the still-dirty inline editor when the user later exits editing.
  • The revision prompt does not retain the owning deck ID, so navigation before execution can pair the selected slide with the wrong deck.
  • The UI reports successful delivery and clears the instruction without awaiting the confirmation-capable local chat API.
  • Desktop keyboard users lose access to Duplicate/Delete thumbnail actions because display: none is only reversed by pointer hover.

The prompt construction tests are useful, and the selection snapshot is taken before the AI input receives focus. 🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +229 to +239
sendToAgentChat({
message: buildReviseSelectionPrompt({
selectedText: aiTargetText,
instruction,
slideId,
}),
submit: true,
chatTarget: "local",
});
toast.success(t("raw.sentToAgent"), { description: instruction });
setShowAiInput(false);

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.

🟡 Commit or exit inline editing before sending a revision

Submitting the AI request only closes the prompt; the selected block remains a dirty contentEditable session. When the user later clicks away, the inline editor can serialize its stale DOM through onUpdateSlide and overwrite the agent's server-side revision. Persist the current inline edit and release the edit state before allowing the remote update to reconcile, and cover the send-then-exit interaction.

Additional Info
Found by 1 of 3 incremental code-review agents; follows the SlideEditor dirty/edit and exit paths.

Fix in Builder

Comment on lines +229 to +233
sendToAgentChat({
message: buildReviseSelectionPrompt({
selectedText: aiTargetText,
instruction,
slideId,

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.

🟡 Retain the owning deck when targeting the revision

The prompt includes slideId but not the owning deck ID, and asks the agent to resolve the target through view-screen. If the user navigates to another deck before the local agent executes, it can combine the captured slide ID with the newly active deck and fail the update. Capture and include the exact deck/slide pair from the editor context.

Additional Info
Found by 1 of 3 incremental code-review agents; confirmed against the current target-capture flow.

Fix in Builder

Comment on lines +229 to +238
sendToAgentChat({
message: buildReviseSelectionPrompt({
selectedText: aiTargetText,
instruction,
slideId,
}),
submit: true,
chatTarget: "local",
});
toast.success(t("raw.sentToAgent"), { description: instruction });

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.

🟡 Confirm local chat delivery before reporting success

sendToAgentChat is fire-and-forget, but this path immediately shows a success toast and clears the instruction. The local chat API exposes sendToAgentChatAndConfirm specifically for rejected, missing-engine, or timed-out delivery; use it and retain the instruction with an error toast when delivery is not confirmed.

Additional Info
Found by 1 of 3 incremental code-review agents; the confirmation API and failure cases exist in packages/core/src/client/agent-chat.ts.

Fix in Builder

{/* Actions - always visible on touch devices */}
{!readOnly && (
<div className="absolute top-2 right-2 flex gap-0.5 sm:opacity-0 sm:group-hover:opacity-100">
<div className="absolute top-2 right-2 flex gap-0.5 sm:hidden sm:group-hover:flex">

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.

🟡 Keep thumbnail actions keyboard accessible

At desktop widths, sm:hidden sm:group-hover:flex removes Duplicate and Delete from the tab order until a pointer hover occurs. Keyboard focus does not satisfy group-hover, so desktop keyboard users cannot reach these controls. Add a focus-within visibility rule (or otherwise preserve focusability) while retaining the compositor optimization.

Additional Info
Found by 1 of 3 incremental code-review agents; direct regression from opacity-only hiding to display:none.

Fix in Builder

@builder-io-integration builder-io-integration Bot 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.

Builder reviewed your changes and found 1 potential issue 🟡

Review Details

Incremental Code Review Summary

The latest commit adds Delete-key support for images embedded in flow-layout content, with focused interaction tests and a changelog entry. The new helper correctly avoids deleting arbitrary flow containers, and the targeted editor tests pass. The previously reported six issues remain unresolved and were intentionally not reposted. This remains Standard risk because the change mutates persisted slide HTML and selection state.

New Finding

🟡 MEDIUM

  • Delete on an imported fidelity image can remove only the nested <img> while leaving its absolute persisted fmd-pptx-image wrapper and durable object ID behind. The new flow-image branch needs to resolve and remove the owning object for imported images.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +2412 to +2414
} else if (isDeletableFlowImage(element)) {
e.preventDefault();
element.remove();

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.

🟡 Delete the containing image object rather than only its nested image

isDeletableFlowImage accepts every <img>, but imported fidelity images are nested inside an absolute fmd-pptx-image wrapper carrying the persisted object ID. Selecting that nested image and pressing Delete removes only the <img>, leaving an invisible wrapper and durable object metadata in the saved slide. Resolve the selected image to its owning persisted image object before removal, and add a regression asserting both wrapper and image are removed.

Additional Info
Found independently by 1 of 3 incremental agents; focused interaction tests pass but do not cover nested imported fidelity images.

Fix in Builder

@builder-io-integration builder-io-integration Bot 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.

Builder reviewed your changes and found 4 potential issues 🔴

Review Details

Incremental Code Review Summary

The latest commit adds an authenticated image proxy for PDF export, URL/DNS validation, redirect checks, response caps, and temporary DOM image restoration around rasterization. The focused tests pass, and the earlier seven review comments remain unresolved and were intentionally not reposted. Because this increment introduces a network proxy and SSRF-sensitive URL handling, the risk level is now High.

New Findings

🔴 HIGH

  • DNS validation is separate from the subsequent fetch, leaving a DNS-rebinding SSRF path.
  • Hexadecimal IPv4-mapped IPv6 literals can bypass the private-address filter.
  • The response-size limit is checked only after buffering the complete upstream body.

🟡 MEDIUM

  • PDF rasterization explicitly omits credentials while re-fetching authenticated same-origin proxy URLs, so proxied images can still render blank.

The URL unit tests cover common private ranges and redirects, and the export code restores temporary DOM mutations correctly, but the new proxy needs hardened outbound fetching and bounded streaming before merge. 🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +44 to +45
for (let hop = 0; hop <= MAX_PROXY_REDIRECTS; hop++) {
if (!(await resolvesToPublicAddress(target.hostname))) {

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.

🔴 Bind the fetch to the address validated against SSRF

resolvesToPublicAddress() performs a DNS lookup, but fetch(target) performs a separate hostname resolution. A DNS-rebinding host can answer with a public address for validation and then return a loopback, RFC1918, or metadata address for the fetch, bypassing the redirect checks. Resolve once and pin the outbound connection to the validated address (including independently at every redirect hop).

Additional Info
Found independently by 2 of 3 incremental agents.

Fix in Builder

if (/^f[cd]/.test(addr) || addr.startsWith("fe8") || addr.startsWith("fe9"))
return true;
if (addr.startsWith("fea") || addr.startsWith("feb")) return true;
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(addr);

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.

🔴 Reject hexadecimal IPv4-mapped IPv6 private addresses

The IPv4-mapped IPv6 check only accepts dotted IPv4 tails. Valid forms such as ::ffff:7f00:1 and ::ffff:a9fe:a9fe fall through as public even though they map to loopback and 169.254.169.254. Normalize mapped addresses regardless of textual representation and add regression cases for these forms.

Additional Info
Found by 1 of 3 incremental agents; the current tests cover dotted mapped IPv4 only.

Fix in Builder

return { error: "Image too large" };
}

const body = Buffer.from(await response.arrayBuffer());

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.

🔴 Enforce the image size cap while streaming the response

For chunked responses or an upstream that omits/lies about Content-Length, await response.arrayBuffer() buffers the entire body before the 15 MiB check runs. Read the response stream with a byte counter and abort/cancel as soon as the cap is exceeded, then construct the buffer only after the bounded read completes.

Additional Info
Found by 1 of 3 incremental agents; focused tests do not cover chunked oversized bodies.

Fix in Builder

Comment on lines +194 to +195
fetch: {
requestInit: { cache: "no-cache", mode: "cors", credentials: "omit" },

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.

🟡 Preserve session credentials when rasterizing proxied images

The new proxy requires a session cookie, but the modern-screenshot fetch options explicitly set credentials: "omit". After preload replaces a non-CORS image with the authenticated same-origin proxy URL, the screenshot library re-fetches it without the cookie and receives 401, so those images can still be blank in exported PDFs. Use same-origin credentials for proxy requests while continuing to omit credentials for direct third-party images.

Additional Info
Found by 1 of 3 incremental agents; proxy authentication and screenshot fetch options are visible in the changed code.

Fix in Builder

@NKoech123

Copy link
Copy Markdown
Contributor Author

@builderio-bot verify the above issues exist and fix them. Feel free to be skeptical and let me know the changes you've made

@builder-io-integration

Copy link
Copy Markdown
Contributor

I verified all 11 findings against the code. All 11 were real — I found no false positives. Two were serious enough to be worth calling out.

The two that mattered most

The proxy I added last turn didn't actually work. modern-screenshot re-fetches images with credentials: "omit". My proxy route requires a session cookie, so every proxied image would have come back 401 and still rendered blank. The reviewer caught a bug that defeated the entire feature. Now credentials: "same-origin" — cookies go to our origin, cross-origin requests stay anonymous as CORS requires.

My SSRF guard had a real bypass. I checked IPv4-mapped IPv6 with /^::ffff:(\d+\.\d+\.\d+\.\d+)$/ — dotted notation only. ::ffff:7f00:1 is the same loopback address written in hex and sailed straight through as "public". So did ::ffff:a9fe:a9fe (the metadata endpoint). I replaced the text matching with real hextet expansion, which also now catches NAT64, IPv4-compatible, zone-indexed, and fully-expanded forms.

The rest

Finding Fix
DNS rebinding (HIGH) Validating then calling fetch resolved DNS twice. Now node:http's lookup hook validates at connect time, so the address checked is the address dialled. No new dependency.
Size cap after buffering (HIGH) arrayBuffer() buffered everything before the 15 MB check. Now streams with a byte counter and destroys the socket at the cap.
Import timeout Real gap — I'd only fixed the direct-import buttons. importUploadedDeckIntoDeck(), the main create-from-upload path, still used the 60s default. Constant is now shared and applied.
Generation-flow test Already fixed earlier this session (regex instead of literal source match).
Dirty contentEditable Real race — the block stays editable, so clicking away would serialize stale text over the agent's revision. Now commits the edit before sending.
Missing deck id Prompt said "the current slide"; navigating decks before execution could mispair. Deck id now included.
Fire-and-forget toast Switched to sendToAgentChatAndConfirm, with a spinner, an error toast, and the instruction preserved on failure.
Keyboard access Real regression I introduced — sm:hidden removes buttons from the tab order and group-hover never fires for keyboard. Added group-focus-within; focusing the slide button reveals them.
Imported image delete Real — .fmd-pptx-image wrappers hold the persisted object id, so deleting the inner <img> left a ghost object. New findPersistedImageObject removes the owner, scoped to image wrappers so it won't swallow a card.

Two things to know

I hit two stale package builds unrelated to this PR (@agent-native/core brand-kit exports and @agent-native/recap-cli) that were failing tests and guards after the main merge. Rebuilt both — no source changes.

Also, oxfmt silently reverted two edits I'd applied via sed. I caught it because typecheck failed afterward, re-applied, and re-verified post-format. Worth knowing if you see it again.

Final state: 761 tests pass, monorepo typecheck 0 errors, format and all guards clean. 23 new tests across the IPv6 classifier, connection pinning, image-owner lookup, and import timeouts.

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.

2 participants