feat(files): preview .pptx in the docked pane, including uploads - #1122
Merged
Merged
Conversation
…ching Audited 17 sheets across four real agent-generated workbooks pulled from the dev files store. Three gaps, all traceable to the docstring: freeze_panes: set on 0 of 17 sheets. Every generated workbook scrolls its header row off the top. The docstring never mentions it. number_format: 16 cells across all 17 sheets carry anything but 'General', and two of the four workbooks have none at all. Currency renders as 1234567.891 and rates as 0.1834. Also never mentioned. Formulas are written with no cached result -- openpyxl does not evaluate them. Excel fills them in on open, so a human never sees the problem, but until then the cell is empty to every other reader: all four formulas in the corpus read back as None under `data_only=True`. That blinds read_excel_spreadsheet to totals the agent itself just wrote, and would show a blank cell in the in-app preview pane. Deliberately NOT added: column widths, fills, alignment and borders. The audit shows the model already emits all four heavily (461 fill / 1150 alignment / 1057 border cells) with no example to copy, so documenting them would be prefix cost for no behavior change. The three above are the ones it never does unprompted. Guidance is folded into the example the model copies rather than added as separate notes, following the word-tool fix in 6498ad7. This docstring is part of the cacheable `toolConfig` prefix, so it costs a one-time cache re-write per session on the next turn after deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
read_powerpoint_presentation extracts speaker notes, but create_powerpoint_presentation never says how to write them -- the toolset could read a capability it could not produce. Across four real decks from the dev files store (27 slides), not one slide has notes. The design guidance already pushes for sparse slides; without notes that detail is simply lost rather than moved to where a presenter would say it. Same cacheable-prefix cost note as the Excel and Word docstring fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pptx-preview reads OOXML and reproduces each slide's own geometry, including the theme inherited from slide masters and layouts — which is what preserves branding on decks built from an uploaded template. Tested against every .pptx in the dev files store: three real agent-generated decks render correctly, including a 4.3 MB branded template deck with 15 images in 122 ms. It reaches for ECharts in exactly one place, rendering a *native* OOXML chart part, through a static namespace import that cannot be tree-shaken. Our decks never contain one — create_powerpoint_presentation embeds charts as matplotlib PNGs via add_picture — and the survey found zero chart parts across all four decks. Shipping ECharts anyway would be 337 kB gzipped, five sixths of the viewer's weight, and every release below 6.1.0 carries GHSA-fgmj-fm8m-jvvx. So `echarts` resolves to a local stub instead: the lazy chunk is 48 kB gzipped rather than 357 kB, and `npm audit` returns to its exact pre-existing baseline of 11 findings — pptx-preview contributes none. The cost is that an uploaded deck containing a native chart throws, and the viewer reports it as unreadable. shims/echarts-stub/README.md records the trade and why a top-level dependency is the mechanism that works (tsconfig `paths` does not reach a dependency's own imports, and a `file:` spec nested under `overrides` resolves relative to wherever npm places the package). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pane was already format-agnostic apart from the renderer, so this is a viewer component plus a widened gate: previewKindFor() replaces the docx-only predicate and drives both the viewer switch and the header's subtitle, which was hardcoded to "Word document". The MIME check now cross-references the extension rather than comparing against a single constant. The extension picks the viewer before any request is made, so a file named .pptx that the server records as a .docx has to fail in the service rather than reach a renderer that cannot read it. Two things differ from the docx viewer, both forced by the library: pptx-preview takes a fixed pixel size at init() and has no responsive mode, so the deck is rendered once at 960px and CSS-zoomed to fit. A rail drag re-fits without re-parsing. Same zoom-not-transform reasoning as the docx viewer. It also writes background, height and overflow as *inline* styles on its wrapper — a black backdrop and a nested scroller — so those three overrides carry !important. Without them the deck sits in a black letterbox inside a second scrollbar, which is what it did until this was caught in the browser. The zero-slide guard is the one piece of behaviour with no counterpart in the docx viewer. pptx-preview resolves *successfully* when it cannot make sense of a presentation's theme or layout parts, returning no slides — reproduced against PR873-Verification-Deck.pptx in the dev files store, whose theme is stripped to 1.8 KB. Unchecked that paints an empty pane with no error and no retry, which reads as the app being broken rather than the file being unreadable. .xlsx is deliberately still excluded, and file-preview.model.ts records why: the npm build of SheetJS is frozen at a 2022 release with unfixed advisories, and the only maintained grid renderer is built on ExcelJS, which throws outright on a workbook containing a native chart — which is what create_excel_spreadsheet's own documented example produces. Verified in the browser against all four real decks: fidelity, fit at two rail widths with no horizontal overflow, resize re-fit, the failure path, and dark mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… one The docked pane's only entry point was the Preview button on the generated-file download card, so a deck the platform created previewed and an identical deck the user uploaded did not. The uploaded one fell through to the presigned-URL branch, which hands the browser an OOXML file it cannot render — "open" silently became "download", and the agent's own fallback was to read the file and describe it in prose. Attachment cards now route the formats the pane can render to the pane, keyed off `isPreviewableFilename` — the same gate the download card uses, so the two surfaces cannot disagree about what is previewable. Markdown keeps its modal and everything else still opens in a tab. The button's accessible name follows the action rather than stating "Open" for something that previews. Verified against a real uploaded deck in dev: 10 slides, branding intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ing $ into files Two clauses, both prompted by watching a real session. Asked to preview an uploaded deck, the agent read it, and when pushed for a *visual* preview it called create_powerpoint_presentation and rebuilt the deck from scratch — a lossy copy of a file already sitting in the session, purely to obtain a Preview button, which only generated files had. It was not being obtuse: the docked pane has exactly two callers, both user clicks, and no SSE event can open it, so the model had no way to show anything. Its only verbs were read and create. The pane now also opens on uploaded files, so the prompt just has to say the viewer exists and belongs to the user. It draws the line at "show me" (point at the button) versus "tell me about" (read the file), because reading is right for summarize/check/edit and wrong for looking — a text dump is not what was asked for, and it puts the whole document in the cacheable history where it is paid for on every later turn. That session had spent 9,053 characters of tool results on it. The second clause fixes a systematic defect this work surfaced. The KaTeX guidance said "other uses of $ should be use the HTML entity $" with no scope, so the model applied it inside generated files: a deck came out holding the literal string "$100K", which is wrong in PowerPoint too, not just in our preview. I first wrote this off as noise because none of the eight real files in dev contained the entity — but none of them contain a dollar sign at all, so the corpus never had the chance to show it. Every file that did carry currency reproduced it. The rule is now scoped to chat markdown and explicitly excluded from files, code and tool arguments. Verified against the running stack with inference-api pointed at this branch: "Show me the preview-pane-test deck" now answers "Click the Preview button" with zero tool calls, and a deck asked for with dollar amounts comes out holding $100K, not $100K. Caveat worth knowing: this steers new conversations. In the session that had already read-then-created four times, Haiku followed its own precedent and rebuilt the deck anyway. Base prompt grows 6,244 -> 7,695 chars (~362 tokens), so it costs a one-time cache re-write per session on the next turn after deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The gap that mattered — previewing an UPLOADED deck — is fixed in the UI (9ab491e); it needs no prompt at all. What the prompt still has to prevent is the model reading a file, or re-creating it, just to show it to someone. Cut the explanation of what the viewer is, the uploaded-vs-generated aside and the .xlsx exception, and kept the rule. 362 -> 169 tokens of cacheable prefix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card is the whole click target — there is no separate button — and the only hint that clicking did anything was an "open in new tab" glyph that faded in on hover. So at rest it advertised nothing, on a touch screen it advertised nothing ever, and the glyph it did show described the wrong action once .docx/.pptx started opening a side pane instead of a tab. Previewable types now carry a persistent eye + "PREVIEW" in the header strip, the same pairing the generated file's download card uses so the two read as one feature. Other types keep the quieter hover hint. Two contrast fixes found by measuring rather than eyeballing: the label started at gray-500, which is 4.55:1 on the pptx header tint and scrapes past AA by a hair, now gray-600 at 7.12:1; and the hover state used primary-accessible, which is BSU navy and lands at 1.41:1 in dark mode -- effectively invisible. Hover is now a neutral emphasis that measures 14.92:1 there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chip icon rendered black in both themes while the label beside it, carrying the identical class, came out in the file type's accent. Cause: a text-<colour> utility placed directly on an <ng-icon> never paints it — the component's own host rule outranks a plain class. It does inherit, so the accent now sits on the wrapper and reaches icon and label together.⚠️ This is not confined to this card. Sampling the live DOM, none of the ng-icons carrying a colour class were getting it: "text-gray-400" rendered black, "text-gray-500" rendered white. They look right only where the inherited colour happens to be right, and there are ~449 such call sites. Out of scope here, but it is a real platform-wide bug. PREVIEW now takes the same accent so the header strip reads as one unit, at the same weight and opacity as the type chip rather than a dimmed version — measured on the live DOM, the accent is already 3.37:1 on its own light-mode tint, so anything held further back would be worse than a label that is itself under AA.⚠️ Also pre-existing and worth its own fix: that 3.37:1 is the type chip's own contrast in light mode, below the 4.5:1 AA floor for text this size, across every file type. Dark mode measures 8.60:1 and is fine. The likely fix is a -700 step for the light-mode accent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mode The chip accents were set one step too light for the tints they sit on. Measured off the rendered DOM rather than the palette, on the chips' own light-mode grounds: family -600 -700 pdf 4.12 5.49 doc 4.82 6.28 sheet 3.08 4.72 markdown 5.16 6.58 image 5.78 7.24 code 3.37 4.92 presentation 3.37 4.92 Four of seven were under the 4.5:1 floor for text this size, including both orange families used by the PPTX and HTML chips. Every family clears it at -700, and the colour still reads as the file type. Dark mode was never affected (8.60:1 on the pptx chip) and is untouched. Applied to the five surfaces where the token colours chip *text*: the attachment card, the generated-file download badge, the file card, the artifact library and the shared artifact card. file-browser.page.ts keeps -600 — there the token tints an icon, which answers to the 3:1 non-text bar, and per the previous commit a colour class on an <ng-icon> does not apply at all. One caveat: the artifact-library and file-card chips sit on a -100 ground rather than -50, where sheet lands at 4.4957:1 — short of 4.5 by 0.004, inside sRGB rounding, and reported as 4.5 by tools that round. Every other family there clears 5.0. Worth a look if that chip ever gets restyled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Artifacts pop their panel from onArtifact; a generated .docx or .pptx left the user to find the Preview button themselves. Now the pane opens on it, which is the whole point of having a viewer. The hook hangs off tool_result rather than an SSE event of its own, because the office tools do not have one — the download card is a file_download inline visual inside the tool result. That placement also gives the live-vs-hydrated distinction for free: tool_result only ever arrives mid-stream, so reopening an old conversation replays the card without seizing the rail, matching seedFromHydration on the artifact side. Verified both ways in the browser. It runs before the tool_use block lookup, not after, so an unmatched result still surfaces its file — the block bookkeeping failing does not mean the file is absent. Viewed-session only, the same guard as onArtifact: a conversation streaming in the background must never take the rail from the thread being read. Formats the pane cannot render (.xlsx) are skipped rather than opening a pane that could only show an error, and a legacy card carrying no upload_id is ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from
claude/docx-artifact-viewer-439f94
to
develop
September 16, 2026 00:56
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.
Stacked on #1121 — based on
claude/docx-artifact-viewer-439f94, so this diff is only the commits beyond it. GitHub will retarget todevelopwhen #1121 merges.What this does
Extends the docked preview pane to
.pptx, wires it to uploaded files as well as generated ones, and fixes three defects found while verifying..xlsxis deliberately not included — see below.xlsx — declined
Not a bundle-size call; three independent blockers:
xlsx) on npm is frozen at 0.18.5 (2022-03-24) with two unfixed advisories, including high-severity prototype pollution.npm auditreportsfixAvailable: false— the fixes ship only from SheetJS's own CDN. Not something to parse user-uploaded files with.Cannot read properties of undefined (reading 'anchors')) on an openpyxl workbook containing a native chart — which is exactly whatcreate_excel_spreadsheet's own documented example produces. Isolated: the identical workbook without the chart parses fine.xlsx-preview, the only maintained ExcelJS-based grid renderer, returns<object type="text/html" data="blob:…">per sheet plus an inline<script>withwindow.onloadhandlers. Angular's sanitiser strips the script; bypassing sanitisation on user-file HTML isn't a trade worth making.Download-and-open remains the path for spreadsheets. Recorded in
file-preview.model.tsso the next person doesn't re-litigate it.pptx — shipped
pptx-preview1.0.7 (ISC) renders 3/3 real agent-generated decks from the dev files store, including a 4.3 MB branded-template deck (7 slides, 15 images, 122 ms). Fidelity is genuine OOXML layout — decks built on an uploaded corporate template keep their branding from the slide master.ECharts is stubbed out.
pptx-previewimports it as a static namespace purely to draw native OOXML chart parts, which our decks never contain —create_powerpoint_presentationembeds charts as matplotlib PNGs viaadd_picture, and a survey of every.pptxin the dev store found zero chart parts. Shipping it anyway costs 337 kB gzipped, five sixths of the viewer's weight, and every release below 6.1.0 carries GHSA-fgmj-fm8m-jvvx.npm auditreturns to its exact pre-existing baseline of 11 findings — this dependency contributes none. The cost is that an uploaded deck containing a native chart throws and reports as unreadable.shims/echarts-stub/README.mdrecords the trade, and the two mechanisms that failed first (tsconfigpathsdoesn't reach a dependency's own imports; afile:spec underoverridesresolves relative to wherever npm places the package).Uploaded files, not just generated ones
The pane's only entry point was the Preview button on the generated-file download card. An uploaded deck fell through to the presigned-URL branch, which hands the browser an OOXML file it can't render — so "open" silently became "download", and the agent's fallback was to read the file and describe it in prose.
Attachment cards now route the formats the pane can render to the pane, keyed off the same
isPreviewableFilenamegate, so the two surfaces can't disagree.Producer-tool fixes
Audited 17 sheets across four real workbooks rather than reading the docstrings. That corrected two hypotheses:
get_column_letter,PatternFill,AlignmentandBorderare imported but demonstrated nowhere, yet the model already emits all four heavily (461/1150/1057 cells) — documenting them would be prompt-cache cost for no behaviour change.The real gaps:
freeze_panes: 0 of 17 sheets. Every generated workbook scrolls its header off.number_format: 16 cells across all 17 sheets. Currency renders as1234567.891.Noneunderdata_only=True. This blindsread_excel_spreadsheetto totals the agent itself just wrote, and Excel repairs it silently on open, so it would never surface as a bug report.read_powerpoint_presentationextracts notes the create tool never documented how to write.System prompt
Two clauses, both from watching a real session.
Asked to preview an uploaded deck, the agent read it (9,053 characters of tool results into the conversation), then — pushed for a visual preview — called
create_powerpoint_presentationand rebuilt the deck from scratch to obtain a Preview button. Not obtuse:FilePreviewStateService.open()has two callers, both user clicks, and no SSE event opens the pane, so its only verbs were read and create. Trimmed to four lines covering just that (+169 tokens of cacheable prefix).Second: the KaTeX guidance said "other uses of
$should be use the HTML entity$" with no scope, so the model applied it inside generated files — a deck came out holding the literal string$100K, which is wrong in PowerPoint too. Now scoped to chat markdown.Two defects found while verifying
<ng-icon>never applies — the component's host rule outranks it. The PPTX chip icon rendered black while the label beside it, with the identical class, came out orange. Fixed here by colouring the wrapper (ng-icon does inherit).text-gray-400rendered black,text-gray-500white — across ~449 call sites. Out of scope here, but real.-700; dark mode was never affected.Verification
Against a full local stack pointed at dev data, with inference-api running this branch:
create_powerpoint_presentation, clicked Preview, and read all four slides in the pane.docx: correct viewer mounts, subtitle reads "Word document"🤖 Generated with Claude Code