feat(applications): warn before a download too large for the browser - #1617
Merged
Conversation
…1591] Downloading a large application killed the Chrome tab with a generic "Something went wrong while displaying this webpage" and no explanation. The bytes arrive fine — the renderer dies decoding them. `package_component` returns the whole archive as base64 in JSON, and the modal then made four more full-size copies of it: axios buffers the response text, `JSON.parse` copies the base64 string, `atob` copies again, `Uint8Array.from(str, cb)` runs a JS callback per byte on the main thread, and `Blob` copies once more. Roughly 5x the archive inside one renderer, with both strings also subject to V8's 512 MiB cap. Streaming the archive is HarperFast/harper#2150. Until that lands (and for older instances after it does) the honest fix is to say the size up front: - Show the package size and file count in the modal before anything is packaged, measured from the `get_components` tree Studio already has cached. That tree omits `node_modules`, so the total is an exact match for the default `skip_node_modules: true` and a floor once the box is ticked. - Warn past 100 MB — or whenever node modules are included, where the measured total is only a floor — explaining why the tab may die and pointing at scp / the Harper CLI. The button becomes "Download anyway"; nothing is blocked, since a large tree of compressible source can still download fine. - Report the two failures that previously left the "Packaging..." toast spinning forever with no verdict: a failed mutation had no `onError` at all, and a decode throw escaped into react-query. `calculateRootEntries` rebuilds every node from scratch and was dropping the `size` the API sends, which is why this reported "0 B" against a real instance until fixed — carry it through. `size` was likewise undeclared on `APIFileEntry`. Decoding now allocates once and fills in a loop instead of the per-byte callback. Still O(n) on top of `atob`, but strictly cheaper, and it matches `pemToDer` in lib/crypto/envSecret.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces pre-download size estimation and warnings for the Download Application flow to prevent browser crashes on large packages. It adds a utility to measure project package sizes from the cached file tree, displays estimated sizes and warnings in the download modal, improves base64 decoding performance, and adds robust error handling for packaging and decoding failures. Comprehensive unit and integration tests have been added to verify these changes. I have no feedback to provide as there are no review comments.
The coverage report flagged `calculateRootEntries.ts` at 0% — the one file in this branch that had a real bug in it. It rebuilds every node from scratch rather than spreading the API entry, so any field it doesn't name is silently dropped, which is how `size` went missing and made the download modal report "At least 0 B across 159 files" against an instance that was sending a size on every file. Nothing about that is visible from the types, since the field is optional, so it needs a test to stay fixed. Verified non-vacuous: removing `size: node.size` fails the first case. Also cover the two wording branches the modal picks between — "At least" when a file carries no size, and the singular "1 file" — the first being exactly what a caller sees against an instance that doesn't report sizes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kriszyp
approved these changes
Aug 13, 2026
…rge [#1591] Review feedback (kriszyp): the warning keyed off `measured.bytes > LARGE_PACKAGE_BYTES`, which quietly treats an unmeasurable project as a safe one. An instance that reports no file sizes measures to `{ bytes: 0, exact: false }`, so an 800 MB application showed "At least 0 B", no warning, and a plain "Download" button — reproducing the exact tab crash this change exists to prevent, on precisely the older instances least likely to have the streamed download from HarperFast/harper#2150. That state is reachable in practice: it is what the modal rendered against a live cluster before the size carry-through was fixed earlier in this branch. Replace the boolean with `packageCaution()`, which returns 'large', 'unmeasured', or undefined. Absence of a measurement now earns its own caution state — "This download's size is unknown", explaining that the instance didn't report sizes — rather than suppressing the warning. Unmeasured outranks large, since "we don't know" is the honest message even when a partial total happens to clear the threshold. Also stop rendering "0 B": quoting a total of zero dresses up a complete absence of information as a reassuringly small number. The file count is real, so an unmeasured project reads "159 files of unreported size". Verified against a live cluster with sizes stripped from the response: the new state renders as intended and the button reads "Download anyway". All four new tests fail without the guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6 tasks
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.
Addresses the Studio half of #1591. The server half is HarperFast/harper#2152 (closes HarperFast/harper#2150).
Why the tab dies
The bytes arrive fine — the renderer dies decoding them.
package_componentreturns the whole archive as base64 in JSON, and the modal then made four more full-size copies:axios buffers the response text →
JSON.parsecopies the base64 string →atobcopies again →Uint8Array.from(str, callback)runs a JS callback per byte on the main thread →Blobcopies once more. Roughly 5× the archive inside one renderer, and both strings are subject to V8's 512 MiB cap in the browser too. Hence "Something went wrong while displaying this webpage" with no explanation.Streaming the archive end to end is harper#2150. This PR does not consume the stream — that needs a Harper build with #2152 in it to verify against, so it's deliberately left as the follow-up. What ships here works against every Harper version, today.
What this does
Says the size up front. The modal now reads
About 5 MB across 159 files.before you commit to anything. It's measured from theget_componentstree Studio already has cached, so it costs no extra request. That tree omitsnode_modules, which makes the total an exact match for the modal's default (skip_node_modules: true) and a floor once the box is ticked.Warns past 100 MB — or whenever node modules are included, where the measured total is only a floor — with the reason and the alternative, and relabels the button to "Download anyway":
Deliberately one tier, and deliberately not a block. A hard ceiling would have to be guessed from an uncompressed total, and a large tree of compressible source still downloads fine — so the decision stays with the user. This is the warning @jjohnson-hdb asked for in #1591, and "don't do that" really is part of the right answer at 800 MB.
Reports failures instead of hanging. Two paths previously left the "Packaging..." toast spinning forever with no verdict: a failed mutation had no
onErrorat all, and a decode throw escaped into react-query. Both now resolve the toast into a real error.The bug browser verification caught
calculateRootEntriesrebuilds every node from scratch and was silently dropping thesizethe API sends. Against a real instance the modal first rendered "At least 0 B across 159 files" — 159 files found, every size gone.sizewas also undeclared onAPIFileEntryeven though Harper has long sent it. Both fixed;mtimeis dropped the same way but nothing reads it, so I left it alone.Verification
Against the Anvils stage cluster on a real application, serving this worktree on :5173:
About 5 MB across 159 files.— no warning, button reads "Download"…, plus node modules.— warning shown, button reads "Download anyway"Checked in light and dark. Confirmed against the raw operation response that the server really does send per-file
size(which is what identified the mapping bug rather than blaming the instance).New tests:
projectPackageSize.test.ts(9) andDownloadApplicationModal.test.tsx(8), covering the size text, the two warning triggers, the button relabel, that the request is never blocked, and both failure paths. Full suite green — 287 files, 2203 passing. (The 4 vitest "errors" in the summary come fromToolCallGroup.test.tsxand reproduce on a cleanstagecheckout.)Decoding now allocates once and fills in a loop rather than the per-byte callback — still O(n) on top of
atob, but strictly cheaper, and it matchespemToDerinlib/crypto/envSecret.ts.Follow-up
Once #2152 ships, Studio can request
stream: trueand pick the shape off the responsecontent-type, so one code path works against both old and new instances. Two things to handle there: axios buffers (this needsfetch), and Fabric Connect caps bodies at 2 MB and can't stream at all, so proxy-mode instances need an explicit message —resolveInstanceConnectionalready reportsmode: 'direct' | 'proxy'. harper#2150 also carries anestimatemode that would close the include-node_modules blind spot in the sizing above.🤖 Generated with Claude Code