Skip to content

feat(web): preview archives, sqlite databases, and decoded images - #21467

Open
maria-rcks wants to merge 5 commits into
t3/large-file-previewsfrom
t3/binary-file-previews
Open

maria-rcks wants to merge 5 commits into
t3/large-file-previewsfrom
t3/binary-file-previews

Conversation

@maria-rcks

@maria-rcks maria-rcks commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

heic chips before the fix
heic chips after the fix
heic and heif chips at 390px in dark mode
archive fallback before
archive entries after
sqlite fallback before
sqlite rows after
tiff fallback before
tiff decoded after
heic fallback before
heic decoded after
sqlite at 390px in dark mode
browsing a zip folder and opening its text entry
paging the sqlite table forward and backward

archives and sqlite files now open in the files viewer from attachments and agent links. ZIP/JAR, TAR, TAR.GZ/TGZ and GZ show folders, sizes and dates, with text/image entry previews. sqlite opens bytes in memory with read-only table counts and 50-row pages. TIFF and HEIC/HEIF fall back to worker decoding after native decoding fails, preserving image zoom. inputs are capped at 50 MB, archive expansion at 200×/50 MB/2,048 entries, and image dimensions at 32 megapixels. mobile is out of scope. PSD remains unsupported because layered compositing needs a separate decoder and validation pass.

sql.js 1.15.0 supplies maintained browser SQLite WASM; tiff 7.1.3 supplies maintained TIFF decoding. both and tiff's iobuffer dependency use MIT licenses and are registered in the license configuration; OSV returned no known advisories for these versions. existing JSZip and heic-to are reused. parsers load lazily in workers; the new archive/database renderer and all parsers are absent from the main source map. source-map comparison identified imageCompression/imageDimensions and PierreEntryIcon moving into main. the image viewer now loads through a dynamic import, the existing HEIF classifier preserves the compression boundary, and shared icons retain their own chunk. production builds through testbox measured base 6932387 → original head d34e168 → 26597b3: bootstrap 7.68 → 7.70 → 6.66 kB; main 1,061.01 → 1,072.10 → 1,050.76 kB, before gzip. the new viewer and decoder modules remain absent from main. HEIC/HEIF chips share TIFF's image icon across file trees, attachments, composer references and transcript links.

verified real ZIP/JAR/TAR/TGZ/TAR.GZ/GZ and sqlite/.db attachments, a real Codex turn's file links, text/image entries, all 125 sqlite rows across three pages, table switching, TIFF/HEIC decoding and zoom, side panel/full screen, light/dark, and 390px paging. corrupt and excessive-ratio ZIPs reject; a DOCX still renders with the shared ZIP guards. scoped lint, web/shared typechecks, 108 shared preview tests and the production web build passed through testbox. the fix pass also verified a fresh Codex turn returning HEIC/HEIF/TIFF links, light and 390px dark icon rendering, TIFF attachment decoding and zoom, and HEIC decoding from an agent link. the final production bundle also opened and zoomed the TIFF attachment and paged the sqlite table. 27 icon tests, 10 browser image/dialog tests, 108 shared preview tests, web typecheck and scoped lint passed through testbox. the new recording export failed when the preview host disconnected; the existing recordings remain above. native Electron and relay/tunnel clients were not exercised. the ci fix at 1b88a01 moves the dynamic decoder import outside the hook so React Compiler can optimize AttachmentImage without loading the decoder eagerly. the existing asset test now covers all eight supported binary extensions with server-assigned MIME types despite a text/html claim, while unsupported 7z files stay downloads; the desktop picker expectation includes HEIF. reproduced all three failures on 26597b3, then passed 44 asset tests, 9 desktop window tests, 108 shared preview tests, all 1,044 React Compiler coverage cases, 25 file-panel tests, 27 icon tests and 13 browser image/dialog/HTML-recovery tests through testbox. the follow-up at 98422ab also updates the mobile file-chip test to accept the eight binary formats, retains 7z rejection, and verifies those formats through exact host-file URLs; all 19 mobile file-chip tests and 44 asset tests pass. the unrelated workflow-highlighting test passed all three cases in isolation without changes. scoped lint passed with one pre-existing warning, and web/server/desktop/mobile typechecks and the production web build passed. same-worker production builds measured 26597b3 → 1b88a01: bootstrap 6.66 → 6.66 kB and main 1,050.76 → 1,050.76 kB before gzip; the binary viewer and decoder sources remain absent from main. this ci pass changes no UI behavior and retains the existing evidence; real-provider e2e was not repeated. final github ci and the one final pr-check snapshot passed on 98422ab, including Test, Test Web, all eight server shards, lint, build/typecheck, release smoke, Rust, and native fingerprint checks.

stack: main ← #21355 ← #21363 ← #21362 ← #21466 ← #21467 ← #21469.

Built by gpt-6.1-sol in Codex, orchestrated from T3 Code.

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Oct 19, 2026
@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 19, 2026
@github-actions github-actions Bot added the size:XXL 1,000+ changed lines (additions + deletions). label Oct 19, 2026
Comment on lines +123 to +127
if (
![1, 2, 3, 4].includes(metadata.components) ||
![8, 16].includes(metadata.bitsPerSample) ||
![0, 1, 2, 3].includes(metadata.type)
)

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.

🟠 High files/imageDecode.worker.ts:123

Tiled RGB/RGBA previews are emitted with missing color channels, so the resulting PNG is incorrect. The tiled path in tiff's readTileData writes only one sample per pixel, but this worker accepts multi-component tiled images and converts their data as RGB; reject tiled images with more than one component or decode all samples.

Suggested change
if (
![1, 2, 3, 4].includes(metadata.components) ||
![8, 16].includes(metadata.bitsPerSample) ||
![0, 1, 2, 3].includes(metadata.type)
)
if (
(metadata.tiled && metadata.components > 1) ||
![1, 2, 3, 4].includes(metadata.components) ||
![8, 16].includes(metadata.bitsPerSample) ||
![0, 1, 2, 3].includes(metadata.type)
)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/imageDecode.worker.ts around lines 123-127:

Tiled RGB/RGBA previews are emitted with missing color channels, so the resulting PNG is incorrect. The tiled path in `tiff`'s `readTileData` writes only one sample per pixel, but this worker accepts multi-component tiled images and converts their data as RGB; reject tiled images with more than one component or decode all samples.

} else {
files = new Map();
let entries: ArchiveEntry[];
if (

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.

🟡 Medium files/binaryPreview.worker.ts:165

A valid empty ZIP uploaded as download with MIME application/zip falls through to tarEntries and is rejected as having no readable entries. This branch recognizes ZIPs only by filename or the local-file signature; also recognize the ZIP end-of-central-directory signature (or pass the known ZIP format through).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/binaryPreview.worker.ts around line 165:

A valid empty ZIP uploaded as `download` with MIME `application/zip` falls through to `tarEntries` and is rejected as having no readable entries. This branch recognizes ZIPs only by filename or the local-file signature; also recognize the ZIP end-of-central-directory signature (or pass the known ZIP format through).

Comment on lines +22 to +23
if (view.getUint16(offset + 10, true) > ARCHIVE_MAX_ENTRIES)
throw new Error("This archive contains more than 2,048 entries.");

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.

🟠 High files/boundedZip.ts:22

Valid ZIP64 archives with at most 2,048 entries are rejected as oversized, so ZIP64 ZIP/JAR/DOCX files cannot be previewed. ZIP64 uses 0xffff in this legacy EOCD field as a sentinel rather than the actual count; validate the ZIP64 count or defer this limit to the parsed entries.length check below.

-    if (view.getUint16(offset + 10, true) > ARCHIVE_MAX_ENTRIES)
-      throw new Error("This archive contains more than 2,048 entries.");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/boundedZip.ts around lines 22-23:

Valid ZIP64 archives with at most 2,048 entries are rejected as oversized, so ZIP64 ZIP/JAR/DOCX files cannot be previewed. ZIP64 uses `0xffff` in this legacy EOCD field as a sentinel rather than the actual count; validate the ZIP64 count or defer this limit to the parsed `entries.length` check below.

/\.(zip|jar)$/i.test(input.name) ||
new DataView(input.bytes).getUint32(0, true) === 0x04034b50
) {
const archive = await boundedZip(input.bytes, ARCHIVE_ENTRY_MAX_BYTES);

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.

🟠 High files/binaryPreview.worker.ts:169

A ZIP/JAR with any member larger than 10 MB fails here before its entries are listed, so users cannot browse the archive even though the entry handler is intended to reject only opening oversized members. Pass the 50 MB BINARY_PREVIEW_MAX_BYTES limit to enumeration and keep the 10 MB check in the entry handler.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/binaryPreview.worker.ts around line 169:

A ZIP/JAR with any member larger than 10 MB fails here before its entries are listed, so users cannot browse the archive even though the `entry` handler is intended to reject only opening oversized members. Pass the 50 MB `BINARY_PREVIEW_MAX_BYTES` limit to enumeration and keep the 10 MB check in the `entry` handler.

onError={() => setError("Unable to load image.")}
/>
</div>
<PreviewImage key={url} src={url} name={props.name} mimeType={props.mimeType} />

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.

🟡 Medium files/AttachmentFilePreview.tsx:281

When an image fails to load, PreviewImage shows its local failure surface instead of setting this component’s error, so the parent FileSurfaceFailure and its retry action never appear. Report image load failures through setError so retry can refresh the asset URL and reload the image.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/AttachmentFilePreview.tsx around line 281:

When an image fails to load, `PreviewImage` shows its local failure surface instead of setting this component’s `error`, so the parent `FileSurfaceFailure` and its retry action never appear. Report image load failures through `setError` so retry can refresh the asset URL and reload the image.

)
throw new Error("This TIFF color format is not supported.");
await validateTiffStrips(bytes, metadata);
const image = decodeTiff(bytes, { pages: [0] })[0]!;

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.

🟠 High files/imageDecode.worker.ts:130

TIFFs with a non-default Orientation tag fail to produce a preview, so valid camera or scanner images (for example, Orientation=6) show “Unable to load image.” The decodeTiff call at line 130 reaches tiff@7.1.3's decodeImageData, which throws Unsupported orientation; handle the orientation before decoding or reject it explicitly with a clear error.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/imageDecode.worker.ts around line 130:

TIFFs with a non-default `Orientation` tag fail to produce a preview, so valid camera or scanner images (for example, `Orientation=6`) show “Unable to load image.” The `decodeTiff` call at line 130 reaches `tiff@7.1.3`'s `decodeImageData`, which throws `Unsupported orientation`; handle the orientation before decoding or reject it explicitly with a clear error.

} finally {
await reader.cancel();
}
} else if (image.compression !== 1 && image.compression !== 32773)

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.

🟠 High files/imageDecode.worker.ts:104

PackBits-compressed TIFFs pass this validation but fail during decodeTiff, so valid images using compression 32773 cannot be previewed. The tiff decoder throws Unsupported Compression: PackBits; reject 32773 here unless a PackBits decoder is added.

Suggested change
} else if (image.compression !== 1 && image.compression !== 32773)
} else if (image.compression !== 1)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/imageDecode.worker.ts around line 104:

PackBits-compressed TIFFs pass this validation but fail during `decodeTiff`, so valid images using compression `32773` cannot be previewed. The `tiff` decoder throws `Unsupported Compression: PackBits`; reject `32773` here unless a PackBits decoder is added.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 5.0 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.9 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 8 ✅
Claude Total thread wire — 5.0 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 21.2 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: 4e08914 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

macroscopeapp Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This PR introduces substantial new archive, SQLite, HEIF, and TIFF preview workflows, plus changes existing image and Office preview paths and production bundling. The added lint suppressions and unresolved archive/image correctness findings make the runtime and maintenance impact unsuitable for automatic approval.

Needs human approval because:

  • 8 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@maria-rcks
maria-rcks changed the base branch from t3/office-file-previews to t3/large-file-previews October 19, 2026 05:31
) {
if (view.getUint32(offset, true) !== 0x06054b50) continue;
if (offset + 22 + view.getUint16(offset + 20, true) !== bytes.byteLength) continue;
if (view.getUint16(offset + 10, true) > ARCHIVE_MAX_ENTRIES)

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.

🟠 High files/boundedZip.ts:26

A ZIP can declare at most 2,048 entries here while containing hundreds of thousands of central-directory records, causing JSZip.loadAsync(bytes) to allocate an object for every record before the later entries.length check; opening it can exhaust the preview worker's memory or hang it. The check reads only the EOCD's declared count, but JSZip 3.10.1 parses records until it reaches the end of the directory, so validate the actual record count with a byte-size bound before calling loadAsync, or use a parser that enforces the limit during parsing.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/files/boundedZip.ts around line 26:

A ZIP can declare at most 2,048 entries here while containing hundreds of thousands of central-directory records, causing `JSZip.loadAsync(bytes)` to allocate an object for every record before the later `entries.length` check; opening it can exhaust the preview worker's memory or hang it. The check reads only the EOCD's declared count, but JSZip 3.10.1 parses records until it reaches the end of the directory, so validate the actual record count with a byte-size bound before calling `loadAsync`, or use a parser that enforces the limit during parsing.

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

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants