Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
b0e36e7
studio: add the document-viewer wasm and build plumbing
mutewinter Jul 30, 2026
be53baa
studio: add PDF, DOCX, PPTX, XLSX and CSV document viewers
mutewinter Jul 30, 2026
9149ffb
studio: make the expand modal full bleed and follow app zoom
mutewinter Jul 30, 2026
59e207c
studio: depend on @embedpdf/pdfium directly and drop the rotate plugin
mutewinter Jul 30, 2026
1b06d41
studio: fix viewer defects found driving the app
mutewinter Jul 30, 2026
5c7218e
docs: record what the document-viewer build actually found
mutewinter Jul 30, 2026
d9e599e
studio: address review findings on the document viewers
mutewinter Jul 30, 2026
7607a58
studio: inset the expanded file viewer clear of the window controls
mutewinter Jul 30, 2026
ed7e3f7
studio: fix read-only, zoom and fill defects in the document viewers
mutewinter Jul 30, 2026
ec2dcae
studio: build the viewer toolbar out of the app's own zoom and find c…
mutewinter Jul 30, 2026
9246601
docs: record the viewer control and modal changes in the plan
mutewinter Jul 30, 2026
3296fd2
studio: make pdf text selectable and copyable
mutewinter Jul 31, 2026
814bfc3
studio: keep fit-width fitted as the container resizes
mutewinter Jul 31, 2026
ea5ae87
studio: show the file's own actions when right-clicking a document
mutewinter Jul 31, 2026
e31bdb8
studio: stand a skeleton in for a loading document
mutewinter Jul 31, 2026
734e661
docs: record the pdf selection findings and fit-width mode
mutewinter Jul 31, 2026
02254b5
studio: keep document files on preview rows in the file grid
mutewinter Jul 31, 2026
248e0d2
studio: drop the unused preview-capability helper
mutewinter Jul 31, 2026
03c7feb
studio: let the spreadsheet resize its rows and columns
mutewinter Jul 31, 2026
036ffac
studio: offer copy on right-click where the browser cannot see the se…
mutewinter Jul 31, 2026
fc9f819
studio: give every zoom control the same level menu
mutewinter Jul 31, 2026
d248afd
studio: settle fit-width during a resize drag
mutewinter Jul 31, 2026
4f49ae0
docs: record the context-menu split and the xlsx controller-option trap
mutewinter Jul 31, 2026
e5272af
studio: wait for an answer before saying a file is not found
mutewinter Jul 31, 2026
488629b
studio: keep right-click copy enabled over a live selection
mutewinter Jul 31, 2026
5cf134c
studio: scroll to any docx page, not just a rendered one
mutewinter Jul 31, 2026
1b0ae05
docs: record the right-click sampling and the panel loading state
mutewinter Jul 31, 2026
1a14ec6
studio: give every document viewer the native context menu
mutewinter Jul 31, 2026
a6fce96
docs: record why the viewers keep the native context menu
mutewinter Jul 31, 2026
68f1710
studio: copy a spreadsheet cell with the keyboard
mutewinter Jul 31, 2026
01d2bf0
studio: keep a pdf selection through a right-click
mutewinter Jul 31, 2026
08aecb9
studio: discard a page-number edit on escape
mutewinter Jul 31, 2026
a186e4e
studio: keep an icon on the preview-unavailable card
mutewinter Jul 31, 2026
ebfbdb0
studio: add a pdf.js renderer behind a feature flag
mutewinter Jul 31, 2026
18f367f
docs: record the second pdf engine and what a rebuilt selection canno…
mutewinter Jul 31, 2026
07e73ab
studio: serve every vendor path the engines ask for
mutewinter Jul 31, 2026
0e9c93d
studio: fail loudly when the pdf worker cannot be loaded
mutewinter Jul 31, 2026
f02af8f
studio: build the spreadsheet clipboard payload from the worker
mutewinter Jul 31, 2026
204522e
docs: record why the spreadsheet writes its own clipboard payload
mutewinter Jul 31, 2026
89a6ab8
studio: line the pdf.js text layer up with what it painted
mutewinter Jul 31, 2026
f203e9c
docs: record the three pdf.js defaults that had to change
mutewinter Jul 31, 2026
a5ca94e
studio: stop a stray browser selection over pdf pages
mutewinter Jul 31, 2026
ba3d7f0
studio: drop the skeleton from the artifact panel and viewers
mutewinter Jul 31, 2026
a982ec7
docs: record what cover thumbnails would take
mutewinter Jul 31, 2026
46b9921
studio: one delayed spinner for every file that is loading
mutewinter Jul 31, 2026
d710a74
docs: sketch a hidden-window thumbnail service
mutewinter Jul 31, 2026
dfcf4c1
docs: measure what a backend viewer cache would save
mutewinter Jul 31, 2026
4dc0c90
studio: extract the image viewer and hold its zoom controls until load
mutewinter Jul 31, 2026
3cc6462
docs: split document thumbnails into its own plan
mutewinter Jul 31, 2026
535d666
studio: drop the raised node heap limit from the build scripts
mutewinter Jul 31, 2026
22c57de
docs: record pdfium as the pdf engine
mutewinter Jul 31, 2026
0619578
workspace: classify line-delimited JSON as text
mutewinter Jul 31, 2026
6d2b49d
studio: remove the pdf.js viewer
mutewinter Jul 31, 2026
a6f0516
studio: fix fit, find, and page-reveal timing in the document viewers
mutewinter Jul 31, 2026
2d68c87
studio: make the zoom level menu a submenu when it is already inside one
mutewinter Jul 31, 2026
a0971d2
docs: correct the viewer plan against the code
mutewinter Jul 31, 2026
a4a2d22
studio: build the thumbnail rail only once it is opened
mutewinter Jul 31, 2026
38d9db1
Merge remote-tracking branch 'origin/main' into jmack/document-viewers
mutewinter Jul 31, 2026
9bbf4b2
studio: stop the slide viewer fitting twice
mutewinter Jul 31, 2026
5546775
studio: keep the page box from re-navigating on blur
mutewinter Jul 31, 2026
c1fdae1
studio: show fit-width as the active pdf zoom mode
mutewinter Jul 31, 2026
c5f0ed6
docs: record the docx mixed-page-size navigation gap
mutewinter Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/studio/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ dist/
# Vendored uv binary (downloaded by scripts/download-uv.ts)
resources/uv/

# Document-viewer WASM binaries (copied from node_modules by the main build)
resources/vendor/

# Turbo
.turbo

Expand Down
93 changes: 88 additions & 5 deletions apps/studio/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,79 @@ const resolve = {
},
};

let stagingCounter = 0;

// `buildStart` fires on every watch rebuild, so re-copying ~11MB of WASM each
// time is skipped when the destination already holds the current bytes. The
// mtime has to match exactly rather than merely be newer: pnpm hard links from
// its store, which carries the store's timestamps, so a reinstall can drop in a
// same-size asset that is older than what was copied before. Both sides are
// truncated to whole milliseconds because the stamp below goes through a `Date`,
// which drops the sub-millisecond precision a source file can carry.
async function copyVendorAsset({ from, to }: { from: string; to: string }) {
const source = await fs.stat(from);
try {
const target = await fs.stat(to);
if (
target.size === source.size &&
Math.trunc(target.mtimeMs) === Math.trunc(source.mtimeMs)
) {
return;
}
} catch {
// No destination yet.
}
await fs.mkdir(path.dirname(to), { recursive: true });
// Stage then rename so an interrupted or concurrent build cannot leave a torn
// binary in place, and stamp the source mtime so the skip check above holds.
// The staging name is unique per writer: two builds sharing one checkout
// would otherwise interleave into the same temp path, and the rename would
// publish the mixed bytes with a current-looking mtime.
const staging = `${to}.${process.pid}.${stagingCounter++}.tmp`;
await fs.copyFile(from, staging);
await fs.utimes(staging, source.atime, source.mtime);
await fs.rename(staging, to);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}

// Registered on the main build alone. Every `electron-vite dev` and `build`
// invocation builds main, and the destinations are read by the main process, so
// a second registration would only add copies racing each other.
function copyVendorAssets(): Plugin {
const require = createRequire(import.meta.url);
const resourcesDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"resources",
);
const vendorDir = path.join(resourcesDir, "vendor");
const assets = [
{
from: require.resolve("@tailwindcss/browser"),
to: path.join(
path.dirname(fileURLToPath(import.meta.url)),
"resources/tailwind-browser.js",
),
to: path.join(resourcesDir, "tailwind-browser.js"),
},
// The renderer runs from `file://` in production, where `fetch()` of
// bundled assets is blocked, so these are served over the app protocol
// from `resources/` instead of being emitted into the renderer bundle.
{
from: require.resolve("@embedpdf/pdfium/pdfium.wasm"),
to: path.join(vendorDir, "pdfium.wasm"),
},
{
from: require.resolve("@extend-ai/react-docx/docx_wasm_bg.wasm"),
to: path.join(vendorDir, "docx.wasm"),
},
{
from: require.resolve("@extend-ai/react-pptx/pptx_wasm_bg.wasm"),
to: path.join(vendorDir, "pptx.wasm"),
},
{
from: require.resolve("@extend-ai/react-xlsx/duke_sheets_wasm_bg.wasm"),
to: path.join(vendorDir, "xlsx.wasm"),
},
];
return {
async buildStart() {
for (const { from, to } of assets) {
await fs.copyFile(from, to);
await copyVendorAsset({ from, to });
}
},
name: "copy-vendor-assets",
Expand Down Expand Up @@ -216,6 +274,27 @@ export default defineConfig(({ command }) => {
sourcemap: isProduction,
watch: {}, // Enable hot reloading
},
// Each document viewer spawns its parser worker with `new Worker(new
// URL("./x.js", import.meta.url), { type: "module" })`. Pre-bundling
// rewrites `import.meta.url` to the dependency cache, where the sibling
// worker file does not exist, so the dev server answers with the SPA
// fallback HTML and the worker dies on load. Serving these unbundled
// lets Vite's worker transform resolve the real entry.
optimizeDeps: {
exclude: [
"@extend-ai/react-docx",
"@extend-ai/react-pptx",
"@extend-ai/react-xlsx",
],
// Excluding a package stops its CommonJS-only imports from being
// converted to ESM, so those are pre-bundled on their own.
include: [
"@extend-ai/react-docx > utif",
"@extend-ai/react-pptx > regl",
"@extend-ai/react-xlsx > regl",
"react-dom/server",
],
},
plugins: [
...(isAnalyzing ? [analyzer({ analyzerMode: "json" })] : []),
createValidateProductionEnv("renderer"),
Expand All @@ -235,6 +314,10 @@ export default defineConfig(({ command }) => {
],
resolve,
root: path.resolve("src"),
// The document viewers' parser workers are module workers whose entries
// code-split, which the default IIFE worker format cannot express, so the
// build fails without this.
worker: { format: "es" },
},
};
});
19 changes: 19 additions & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@
"zod": "catalog:"
},
"devDependencies": {
"@embedpdf/core": "^2.14.4",
"@embedpdf/engines": "^2.14.4",
"@embedpdf/models": "^2.14.4",
"@embedpdf/pdfium": "^2.14.4",
"@embedpdf/plugin-document-manager": "^2.14.4",
"@embedpdf/plugin-interaction-manager": "^2.14.4",
"@embedpdf/plugin-render": "^2.14.4",
"@embedpdf/plugin-scroll": "^2.14.4",
"@embedpdf/plugin-search": "^2.14.4",
"@embedpdf/plugin-selection": "^2.14.4",
"@embedpdf/plugin-thumbnail": "^2.14.4",
"@embedpdf/plugin-tiling": "^2.14.4",
"@embedpdf/plugin-viewport": "^2.14.4",
"@embedpdf/plugin-zoom": "^2.14.4",
"@extend-ai/react-docx": "0.8.1",
"@extend-ai/react-pptx": "0.1.2",
"@extend-ai/react-xlsx": "0.15.0",
"@instrument-org/eslint-config": "workspace:*",
"@instrument-org/typescript-config": "workspace:*",
"@julr/vite-plugin-validate-env": "^2.2.0",
Expand Down Expand Up @@ -144,6 +161,7 @@
"@testing-library/react": "^16.3.2",
"@types/ms": "^2.1.0",
"@types/node": "catalog:",
"@types/papaparse": "^5.5.2",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/semver": "^7.7.0",
Expand All @@ -166,6 +184,7 @@
"jsdom": "^29.1.1",
"katex": "^0.16.27",
"motion": "^12.35.2",
"papaparse": "^5.5.4",
"playwright": "^1.57.0",
"posthog-js": "^1.374.2",
"prosemirror-commands": "^1.7.1",
Expand Down
Loading