🎞️ fix: Stack Slide Previews and Show File Cards on Every Message - #82
Open
TomasPalsson wants to merge 15 commits into
Open
TomasPalsson wants to merge 15 commits into
TomasPalsson wants to merge 15 commits into
Conversation
…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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…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.
…med files each keep their own card
TomasPalsson
marked this pull request as ready for review
September 29, 2026 12:12
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
buildPptxCdnDocumentwalkedcontainer.childrento find slides, butpptx-preview@1.0.7nests every.pptx-preview-slide-wrapperinside one library-owned.pptx-preview-wrapper, so the only "slide" it ever found was that box; andinitreceived a fixedheight, 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 autothe 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_idfor it, and the file's card showed on only one message in the whole conversation: card dedup claimedtoolArtifactClaim(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 bylastUpdateTime, 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, andMermaid.registerArtifactnever 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:
mapAttachmentskeeps 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 fromfile_id → filename → filepathtofile_id → filepath → filename. Attachments without afile_idare 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 afile_idkeep exactly the key they had.How it works
Slide wrapping,
packages/api/src/files/documents/html.ts(inline bootstrap script):Card placement and panel content,
client/src/components/Chat/Messages/Content/Parts/:Elsewhere:
client/src/components/Messages/Content/Mermaid/Mermaid.tsxkeeps theartifactprop's content and never registers over a strictly newer entry;client/src/utils/map.tsmapAttachmentskeeps one copy per file per message (newest write, linked to a tool call) and exportsfileIdentity, which defers totoolArtifactKeyinclient/src/utils/artifacts.ts(nowfile_id → filepath → filename).Type of change
Testing
Tested environments/configuration:
.pptxfiles made with python-pptx (12-slide 16:9, 6-slide 4:3), rendered by this branch'spptxToHtmlwith the pinnedpptx-preview@1.0.7from 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.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 madereport.pptx(6 slides), turn 2 attacheddata.ziptwice plusother.bin, turn 3 remadereport.pptxunder the samefile_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 wroteflow.mmd(Start → Version one), turn 3 rewrote it under the samefile_id(Start → Version two) and, in two separate code runs, wrotedata2.ziptwice. Result: both turns show the diagram, opening turn 1's diagram in the panel draws "Version two", anddata2.zipshows once, under the second run.Automated tests:
packages/api: newsrc/files/documents/layout.spec.tsruns the real bootstrap script in JSDOM against a fakepptxPreviewthat 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.tsadditions.npx jest src/files/documents→ 220 passed.client: newParts/__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) andParts/__tests__/FileAttachmentGroup.test.tsx;utils/__tests__/map.test.tscases for one copy per file per message (newest write, out-of-order copies, unlinked copies, id-less files by path, non-file attachments); existingArtifactRouting.test.tsxno-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-wideeslint .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 --noEmitclean inpackages/apiandclient.Screenshots / recordings
Deck preview, same deck, same 480 px panel. Before is the base revision; after is this branch.
Running app after the change (dark mode): a remade
report.pptxshows 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.A remade diagram: turn 1's card opens the newest version (Start → Version two).
Risk / compatibility
pptx-preview@1.0.7.scrollbar-gutter: stablerather thanoverflow-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).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