Skip to content

🎞️ fix: Stack Slide Previews and Show File Cards on Every Message - #82

Open
TomasPalsson wants to merge 15 commits into
sync/v0.8.8-rc4from
fix/pptx-slide-scroll-file-cards-rc4
Open

TomasPalsson wants to merge 15 commits into
sync/v0.8.8-rc4from
fix/pptx-slide-scroll-file-cards-rc4

Conversation

@TomasPalsson

@TomasPalsson TomasPalsson commented Sep 28, 2026 •

Copy link
Copy Markdown

Summary

When an agent's code run produces a PowerPoint deck, the side panel squeezes the whole deck into one box that is scaled as if it were a single slide: only the first slide shows, the rest sit trapped inside the box, and scrolling the panel does nothing. The preview bootstrap in buildPptxCdnDocument walked container.children to find slides, but pptx-preview@1.0.7 nests every .pptx-preview-slide-wrapper inside one library-owned .pptx-preview-wrapper, so the only "slide" it ever found was that box; and init received a fixed height, which turns that box into its own scroll region.

Each slide is now wrapped and scaled on its own, the library is initialised with a width only, its wrapper box follows the content width on a transparent background, and the inline margin: 0 auto the library puts on every slide is cleared so a panel wider than 992 px no longer shifts slides right and clips them. Slides stack one under another at the panel's width with 16 px between them, keep their own aspect ratio (16:9 and 4:3 both checked), refit when the panel is resized or expanded, and the panel's own scroll reaches the last slide. The preview document also reserves the scrollbar gutter (html { scrollbar-gutter: stable }), so with space-taking scrollbars a deck whose height sits right at the panel height no longer flips the scrollbar on and off on every refit. The renderer fallbacks (library missing, throws, renders nothing, 8 s timeout) still show the slide list. Previews stored before this change keep their old HTML; remaking the deck picks up the new layout.

Separately, when an agent remakes a file under the same name in a later turn, the chat keeps one file_id for it, and the file's card showed on only one message in the whole conversation: card dedup claimed toolArtifactClaim(fileId) globally and the most recently mounted card won. Dedup is now scoped to the message (${messageId}::${id}, falling back to the bare id where no message is known, which keeps today's behavior there), so every message that holds the file shows one card, and search results pass their message id to the parts they render. Registering the panel's content is decoupled from that claim: a card writes only when its version is newer by lastUpdateTime, and the old global latest-mount claim is kept purely as the tie-break for equal times, so any card opens the newest version and two tied cards settle with at most one write per mount. Diagram (.mmd) cards follow the same rule: each card offers its version to a small per-file "newest seen" Jotai atom and registers the newest one when opened, while its inline diagram keeps showing that message's own source, and Mermaid.registerArtifact never overwrites a strictly newer entry. Unopened diagrams are still not registered, so the panel's navigator is unchanged.

Within one message, a file that several code runs wrote now shows once: mapAttachments keeps the copy with the newest write (updatedAt ?? createdAt, ties to the later entry) under the run that made it, so a background run's older copy appended after a newer foreground write cannot win. Only attachments linked to a tool call compete, and non-file attachments never collapse. The folded "N files" row also lists each file identity once.

Every part of this pipeline (cards, diagram cards, the folded row and the per-message dedup) now uses one key, toolArtifactKey, whose fallback order changes from file_id → filename → filepath to file_id → filepath → filename. Attachments without a file_id are download fallbacks that get a unique per-session path, so keying them by display name merged two different files that happened to share a name; files with a file_id keep exactly the key they had.

How it works

Slide wrapping, packages/api/src/files/documents/html.ts (inline bootstrap script):

-    var previewer = pptxPreview.init(container, { width: SLIDE_W, height: SLIDE_H });
+    var previewer = pptxPreview.init(container, { width: SLIDE_W });
 ...
-      var children = Array.prototype.slice.call(container.children);
+      var slides = Array.prototype.slice.call(container.querySelectorAll('.pptx-preview-slide-wrapper'));
 ...
+        slide.style.margin = '0';
         slide.style.transform = 'scale(' + scale + ')';
-        container.insertBefore(wrap, slide);
+        slide.parentNode.insertBefore(wrap, slide);

Card placement and panel content, client/src/components/Chat/Messages/Content/Parts/:

claim.ts               useToolArtifactClaim(id): display claim keyed `${messageId}::${id}` (bare id with no message)
ToolArtifactCard.tsx   renders when it holds the display claim; writes artifactsState[id] only if newer,
                       or equal-time and holding the global toolArtifactClaim(id) tie-break
ToolMermaidArtifact.tsx  same display claim; offers its version to newestToolArtifactFamily (claim.ts, Jotai)
                         and hands Mermaid the newest one to register when opened
Attachment.tsx         FileAttachmentGroup dedups by fileIdentity (map.ts), keeping the last occurrence
../SearchContent.tsx   wraps each part in MessageContext with the row's messageId

Elsewhere: client/src/components/Messages/Content/Mermaid/Mermaid.tsx keeps the artifact prop's content and never registers over a strictly newer entry; client/src/utils/map.ts mapAttachments keeps one copy per file per message (newest write, linked to a tool call) and exports fileIdentity, which defers to toolArtifactKey in client/src/utils/artifacts.ts (now file_id → filepath → filename).

Type of change

  • Bug fix

Testing

Tested environments/configuration:

  • Browser: Chrome, driven through the DevTools protocol.
  • Deck preview: real .pptx files made with python-pptx (12-slide 16:9, 6-slide 4:3), rendered by this branch's pptxToHtml with the pinned pptx-preview@1.0.7 from jsdelivr, shown in an iframe at 360, 480, 768, 1200 and 1480 px and live-resized. At every width: one block per slide, block width = panel − 32 px, height/width = 0.5625 or 0.75, 0 px sideways overflow, slide flush with its block, no nested scroll box, last slide reachable.
  • Running app: this branch's backend and client build with the e2e fake model and in-memory Mongo (node e2e/setup/start-server.js), signed up through the UI. Because the e2e fake code runner only produces CSV, the conversation was seeded straight into the database in the shape code-run output is stored: turn 1 made report.pptx (6 slides), turn 2 attached data.zip twice plus other.bin, turn 3 remade report.pptx under the same file_id (8 slides) from two code calls. Result: turn 1 and turn 3 each show one card, turn 2's folded row reads "2 files — data.zip, other.bin", turn 1's card opens the 8-slide version, and the panel scrolls from slide 1 to slide 8 at 486 px, 708 px and expanded (1778 px) with no sideways scrollbar. A second seeded conversation: turn 1 wrote flow.mmd (Start → Version one), turn 3 rewrote it under the same file_id (Start → Version two) and, in two separate code runs, wrote data2.zip twice. Result: both turns show the diagram, opening turn 1's diagram in the panel draws "Version two", and data2.zip shows once, under the second run.
  • Scrollbar gutter: with scrollbars forced to 15 px in headless Chromium, holding the content width fixed turns the 2-state refit flip at 840–845 px tall into 1 state with no ResizeObserver errors. On macOS the root gutter itself is not reserved for those forced scrollbars, so the rule still wants a look on Windows/Linux (or macOS with "Always show scroll bars").
  • Not run: a live model plus the real code interpreter generating the deck end to end.

Automated tests:

  • packages/api: new src/files/documents/layout.spec.ts runs the real bootstrap script in JSDOM against a fake pptxPreview that mirrors the library's DOM and inline styles (one block per slide, panel widths 360/768/1480, 30 slides with no inner scroll box, resize refit, 16:9 and 4:3 ratios, each fallback mode, slide spacing, slides pinned on wide panels, scrollbar gutter); html.spec.ts additions. npx jest src/files/documents → 220 passed.
  • client: new Parts/__tests__/ToolArtifactCard.test.tsx (card on every message, one card per message, newest version wins in both mount orders, equal-time ties settle with ≤ 1 write per mount, diagram cards per message, diagram cards open the newest version, search results) and Parts/__tests__/FileAttachmentGroup.test.tsx; utils/__tests__/map.test.ts cases for one copy per file per message (newest write, out-of-order copies, unlinked copies, id-less files by path, non-file attachments); existing ArtifactRouting.test.tsx no-message cases unchanged. npx jest src/components/Chat/Messages/Content src/hooks/Artifacts src/hooks/Files src/utils/__tests__/map.test.ts src/components/Messages/Content/Mermaid → all passed.
  • npm run static-checks -- --against <base> passes (ESLint, Prettier, import sorting, circular dependencies). A repo-wide eslint . reports 97 existing errors in 27 files this PR does not touch; the 13 files it changes report 0 errors and 0 warnings.
  • npx tsc --noEmit clean in packages/api and client.

Screenshots / recordings

Deck preview, same deck, same 480 px panel. Before is the base revision; after is this branch.

Before After
Before: one boxed slide, the rest unreachable After: slides stacked at panel width

Running app after the change (dark mode): a remade report.pptx shows a card on both turns, and the turn-1 card opens the newest (8-slide) version. Before this change the turn-1 card was not rendered at all, so there is no before image for it.

Cards on both turns

Turn-1 card opens the newest version

Expanded panel, last slide, nothing clipped

A remade diagram: turn 1's card opens the newest version (Start → Version two).

Turn-1 diagram card opens the newest version

Risk / compatibility

  • Stored previews are not rewritten; decks generated before the change keep their old HTML until remade.
  • No new script sources and no CSP change; the preview still loads the same pinned pptx-preview@1.0.7.
  • When a file is open in the panel, every card for that file shows as selected.
  • Where no message context exists, cards keep today's one-card-per-file behavior.
  • The scrollbar fix uses scrollbar-gutter: stable rather than overflow-y: scroll, so short decks on platforms with space-taking scrollbars do not draw an empty track; overlay-scrollbar platforms are unaffected (measured: no width change on macOS).
  • Id-less attachments now get a path-based key instead of a name-based one; an open panel entry for such a file is re-registered under the new key on the next render.
  • When parallel or handed-off agents in one message write the same file_id, the file now shows once, under whichever copy was written last, rather than under each lane; storage only holds that last write anyway.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: N/A

…version

Two cards for the same file across different messages now both render
instead of one message winning the only visible chip. Registration into
the artifact panel now compares update timestamps (falling back to a
mount-order tie-break) so an older card mounting after a newer one, or
remounting after the newer card unmounts, can never clobber the newer
content. Search and shared conversation views now scope every rendered
part to its own message so the same fix applies there.
Extends the JSDOM pptx bootstrap harness with an options object (native
aspect ratio, renderer install/behavior, a capturable 8s safety-net
timer, and a mutable render-slot width) so it can drive every panel
width, a 30-slide deck with no inner scroll box, a resize-triggered
refit, both 16:9 and 4:3 decks, and each fallback trigger (renderer
missing, renderer throws, empty slide list, empty slide wrappers,
render timeout).

Clears the pptx-preview library's own inline width/background once
slides are wrapped, since JSDOM's getComputedStyle doesn't apply the
existing stylesheet's !important override the way a real browser does,
and restores the 16px spacing between stacked slide blocks now that
they live inside the library's own wrapper box instead of directly
under #lc-render.
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ed55259c-9021-4d1e-9a1a-75d41cfd4858

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

…sage

A message that ran a tool twice on the same output file (e.g. rewriting
data.zip) showed a card for every run. mapAttachments now collapses
attachments that share a file identity to their last occurrence before
grouping by tool call, so a repeated file surfaces once, under its
newest run. Non-file attachments (no file_id or filepath) are untouched.
A code-execution diagram rewritten in a later turn now shows a card on
every message holding that file, but opening an older message's card
still displayed the stale content. Each ToolMermaidArtifact now offers
its version to a shared per-file "newest seen" record on mount, and
hands the newest entry to Mermaid for registration while keeping its
own inline render unchanged. Corrects two comments claiming shared
conversation views mount with no message context, which Share/Message
already provides.
… copy over a later unlinked duplicate, and key id-less files by filepath instead of filename
… of the message pipeline

Two id-less attachments that share a display name but live at different
filepaths no longer collapse into one chip in the folded attachment group.
@TomasPalsson
TomasPalsson marked this pull request as ready for review September 29, 2026 12:12

This branch has not been deployed

No deployments
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