Skip to content

feat(export): add reliable standalone HTML export - #6855

Merged
AmyShang-alt merged 5 commits into
mainfrom
codex/feat-standalone-html-export
Aug 14, 2026
Merged

feat(export): add reliable standalone HTML export#6855
AmyShang-alt merged 5 commits into
mainfrom
codex/feat-standalone-html-export

Conversation

@open-design-crew

Copy link
Copy Markdown
Contributor

Why

Use case: Plane OPEND-1758 reports that downloaded HTML should remain usable after it is moved away from the project directory: https://plane.powerformer.net/open-design/browse/OPEND-1758/

Pain: The existing export only inlined first-level CSS and scripts, so nested images, fonts, modules, workers, and relative URLs could break when users opened the file locally.

What users will see

Downloading HTML now produces one offline-safe file whose project-local images, styles, fonts, modules, workers, and nested documents remain usable. Missing or oversized dependencies produce a clear error instead of a silently broken download. The same format is available through od export --format html.

Surface area

  • UI — new page / dialog / panel / menu item / setting / empty state in apps/web or apps/desktop (including Electron menu bar)
  • Keyboard shortcut — new or changed
  • CLI / env var — new od subcommand or flag, new tools-dev / tools-pack flag, or new OD_* env var
  • API / contract — new /api/* endpoint, new SSE event, or changed shape in packages/contracts
  • Extension point — new entry under skills/, design-systems/, design-templates/, or craft/, or change to the skills protocol
  • i18n keys — added new translation keys (see TRANSLATIONS.md for the locale workflow)
  • New top-level dependency — adding any new entry to the root package.json (dependencies or devDependencies); workspace-package package.json files are out of scope. Include a paragraph on what we get vs. what bytes we ship (see CONTRIBUTING.md → Code style)
  • Default behavior change — changes what existing users experience without opting in (default model, default setting, file/SQLite schema, auto-network on startup, auto-install)
  • None — internal refactor, docs, tests, or translation update only

Screenshots

Not applicable.

Bug fix verification

  • Test path that reproduces the bug: apps/daemon/tests/artifacts/standalone-html.test.ts and e2e/ui/app-manual-edit.test.ts
  • Red on main: yes
  • Green on this branch: yes
  • Explanation: The new bundler spec failed on main because the implementation did not exist, then passed on this branch.

Validation

  • mise exec -- pnpm guard
  • mise exec -- pnpm typecheck
  • mise exec -- pnpm --filter @open-design/contracts test (289 passed)
  • focused daemon Vitest suite for standalone export and routing (66 passed)
  • focused web Vitest suite for export runtime and FileViewer (371 passed)
  • focused Playwright file:// offline export workflow (1 passed)

Bundle project-local HTML, CSS, JavaScript, images, fonts, workers,
and iframe dependencies into one offline-safe file.

Expose the same contract through the web download flow and od CLI,
with structured failures and bounded output assembly.
@lefarcen
lefarcen requested a review from nettee August 13, 2026 13:08
@lefarcen lefarcen added needs-validation Runtime change detected; needs human or /explore agent validation. size/XXL PR changes 1500+ lines risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps type/feature New feature labels Aug 13, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

🧪 This PR has changes that need a manual QA pass before merge — please hold off self-merging for now; we will loop QA in once it is merge-ready.

@nettee nettee left a comment

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.

I found two correctness blockers in the standalone bundler: relative URLs ignore HTML base-URL semantics, and TypeScript/JSX sources are accepted but emitted without compilation. Both can produce a successful download that fails when opened offline. I also found a resource-fanout guard that can be bypassed by valid unquoted HTML attributes.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

}
const joined = pathname.startsWith('/')
? decoded.replace(/^\/+/, '')
: path.posix.join(path.posix.dirname(ownerPath), decoded);

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.

Blocking — base URL resolution: This always joins a relative reference against dirname(ownerPath), but the browser applies the document's <base href=...> before resolving it. For example, <base href=assets/> with <img src=logo.png> is read as pages/logo.png here instead of assets/logo.png; the export can therefore inline the wrong file or report a missing dependency, while the untouched <base> leaves other relative URLs pointing at the wrong place after download. Please parse the effective base URL for each document, resolve all references against it, and rewrite or remove the base element in the standalone output (or return a structured unsupported-base error).

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

try {
ast = parse(source, {
sourceType: mode === 'classic' ? 'unambiguous' : 'module',
plugins: ['dynamicImport', 'importAttributes', 'importMeta', 'jsx', 'topLevelAwait', 'typescript'],

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.

Blocking — source compilation: This parser configuration accepts TypeScript and JSX syntax, but rewriteJavaScript only rewrites URL/import literals and then emits the original source into a data URL. A Vite-style <script type=module src=/src/main.tsx> (when no built dist entry is available) therefore produces a 200 export containing const x: number or JSX, which the browser rejects at runtime. Please transpile TS/JSX before embedding or reject non-runtime source with a structured invalid-source/422 response instead of returning a broken artifact.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

return /\srel\s*=\s*(['"])[^'"]*\bstylesheet\b[^'"]*\1/iu.test(tag)
&& /\shref\s*=\s*(['"])[^'"]+\1/iu.test(tag);
}).length;
const scripts = [...html.matchAll(/<script\b[^>]*\bsrc\s*=\s*(['"])[^'"]+\1[^>]*>/giu)].length;

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.

Non-blocking — candidate limit accounting: The preflight guard only matches quoted src/href values, while Cheerio and the bundling loops also process valid unquoted attributes. For example, 501 <script src=a.js></script> entries bypass firstLevelCandidates and are all read and inlined, so the documented 500-resource fan-out guard can be defeated. Please count candidates with the same parser/attribute semantics as bundleDocument (or extend this matcher to accept unquoted values) before walking the graph.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Visual regression review

Head: e39191d · Base: 698a17a

10 changed · 39 unchanged · 0 new without baseline · 0 failed

Changed cases

Case Main PR Diff
visual-home
4,736 px (0.37%)
main pr diff
visual-home-context-picker
1,268 px (0.10%)
main pr diff
visual-home-context-picker-popover
775 px (0.25%)
main pr diff
visual-home-plugin-use-staged
5,014 px (0.39%)
main pr diff
visual-home-staged-attachment
2,601 px (0.20%)
main pr diff
visual-projects-kanban
1,940 px (0.15%)
main pr diff
visual-topbar-byok-switcher
2,729 px (0.21%)
main pr diff
visual-topbar-execution-switcher
4,667 px (0.36%)
main pr diff
visual-topbar-local-cli-model-list
4,764 px (0.37%)
main pr diff
visual-topbar-open-design-model-picker
3,543 px (0.27%)
main pr diff
Unchanged cases
Case Main PR Diff
visual-avatar-local-agent-list
70 px (0.01%)
main pr diff
visual-avatar-local-agent-list-panel
34 px (0.04%)
main pr diff
visual-avatar-menu
83 px (0.01%)
main pr diff
visual-avatar-menu-panel
14 px (0.04%)
main pr diff
visual-avatar-open-design-model-picker
117 px (0.01%)
main pr diff
visual-critical-settings
0 px (0.00%)
main pr diff
visual-critical-workspace
35 px (0.00%)
main pr diff
visual-critical-workspace-preview
44 px (0.00%)
main pr diff
visual-design-system-detail
0 px (0.00%)
main pr diff
visual-design-systems
0 px (0.00%)
main pr diff
visual-home-catalog
0 px (0.00%)
main pr diff
visual-home-plugin-filter
0 px (0.00%)
main pr diff
visual-home-plugin-use-with-query
0 px (0.00%)
main pr diff
visual-integrations
0 px (0.00%)
main pr diff
visual-integrations-mcp
0 px (0.00%)
main pr diff
visual-integrations-use-everywhere
0 px (0.00%)
main pr diff
visual-new-project-modal
0 px (0.00%)
main pr diff
visual-onboarding-cloud
0 px (0.00%)
main pr diff
visual-plugin-details
0 px (0.00%)
main pr diff
visual-plugin-share-menu
0 px (0.00%)
main pr diff

Visual diff is advisory only and does not block merging.

@hihijack111 hihijack111 added backport release/v0.19.1 Backport this fix to release/v0.19.1 and removed backport release/v0.19.1 Backport this fix to release/v0.19.1 labels Aug 13, 2026
Resolve document assets against local base href values, reject browser-invalid TypeScript and JSX sources with a structured 422, and count unquoted first-level resources before graph traversal.

Validated with daemon tests, workspace typecheck, and guard.
@open-design-crew

Copy link
Copy Markdown
Contributor Author

Addressed the three Looper findings in 439aba0: document resources now honor the first local base href and base hrefs are stripped from the standalone output; TypeScript/JSX is rejected as structured invalid-source (HTTP 422) instead of emitted unchanged; and the first-level resource guard now counts quoted and unquoted attributes with the bundler parser. Added red-to-green unit and route coverage; daemon tests, workspace typecheck, and guard pass.

@open-design-crew
open-design-crew Bot requested a review from nettee August 14, 2026 03:17

@nettee nettee left a comment

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.

@app/open-design-crew

I found two blocking correctness issues in the standalone bundler: valid external data scripts are parsed as JavaScript, and CSS URL scanning treats quoted or comment text as dependencies. Both can make an otherwise valid project return a 422 instead of producing an offline HTML file.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

await this.ensureModule(local.projectPath, [...chain, local.projectPath], modulePaths);
body = `import ${JSON.stringify(moduleSpecifier(local.projectPath))};`;
} else {
body = await this.rewriteJavaScript(

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.

Blocking — non-JavaScript external scripts are treated as JavaScript

Every external script whose type is not exactly module reaches rewriteJavaScript at this line. A valid <script type="application/ld+json" src="schema.json"></script> (and other data-script types) is therefore parsed as JavaScript; JSON-LD keys such as "@context" make Babel reject it, so the endpoint returns invalid-source/422 instead of embedding the data script. Gate rewriting on JavaScript/module MIME types (and keep data scripts as escaped text or a correctly typed data URL), then add a JSON-LD external-script fixture.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

for (const match of value.matchAll(URL_FUNCTION_RE)) {
const reference = match[2] ?? match[3];
if (!reference || match.index === undefined) continue;
const rewritten = await this.referenceToDataUrl(ownerPath, reference.trim(), chain);

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.

Blocking — URL scanning is not CSS-token aware

rewriteCssUrls applies a regex to the raw declaration/style-attribute text and immediately resolves every match as a project dependency. Valid text such as style="content:'url(missing.png)'" (or a URL-looking CSS comment) is consequently mistaken for a dependency and makes export fail with missing-local-dependency; the browser would keep it as text. Rewrite only actual CSS url() tokens by using a CSS value/token parser or a scanner that skips strings and comments, and add fixtures for quoted strings, comments, custom properties, and real URLs so valid HTML does not turn into a 422.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Preserve non-JavaScript script payloads as data blocks and scan CSS values without treating strings or comments as project dependencies.

Validated with focused and full daemon tests, workspace typecheck, and guard.
@open-design-crew

Copy link
Copy Markdown
Contributor Author

Addressed both new Looper blockers in f616306. Non-JavaScript script types such as application/ld+json are now embedded as escaped data blocks without Babel parsing, while classic/module scripts keep their existing rewrite behavior. CSS URL discovery now skips quoted strings and comments while still rewriting real url() tokens, including those stored in custom properties. Added red-to-green fixtures; 62 focused/route tests, the full daemon suite, guard, and workspace typecheck pass.

@open-design-crew
open-design-crew Bot requested a review from nettee August 14, 2026 03:55

@nettee nettee left a comment

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.

@app/open-design-crew

The standalone export implementation and its follow-up hardening are thoughtfully covered by focused tests. I ran the daemon bundler/route suite (72 tests) and the web export/viewer suite (371 tests); both pass, but two correctness blockers remain in the changed bundling path.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Location: apps/daemon/src/import-export-routes.ts RIGHT line 1712

Non-blocking — Vite dist lookup swallows all failures.

This catch treats permission errors, I/O failures, and unexpected parser/read errors exactly like a missing dist/index.html, silently falling back to the dev HTML. That can export a stale/unbuilt source or turn an operational failure into a misleading invalid-source response. Catch only an explicit not-found condition and propagate other errors so the route's structured error handling reports the real failure; cover the distinction with a route test.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Inline comment could not be anchored: anchor_outside_complete_diff

chain,
documentStack,
);
return this.checkedDataUrl('text/html;charset=utf-8', Buffer.from(nested), chain);

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.

Blocking — non-HTML iframe resources are emitted as HTML.

bundleDocumentUrl unconditionally decodes every local iframe as UTF-8 HTML and wraps it in data:text/html. A valid <iframe src="preview.png"> or <iframe src="report.pdf"> therefore becomes an HTML document containing binary text, so the endpoint returns a successful export while the iframe is broken offline. Inspect loaded.mime before recursively bundling; for non-HTML dependencies emit checkedDataUrl(loaded.mime, loaded.buffer, ...) directly (or return a structured unsupported-resource error), and add an image/PDF iframe fixture.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

});
wholeNodeReplacements.add(node);
} else {
await this.rewriteAttribute(html, node, 'href', documentOwnerPath, chain, replacements);

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.

Blocking — navigation metadata is treated as a required asset.

The non-stylesheet branch rewrites every local <link> href through referenceToDataUrl, so ordinary metadata/navigation links such as <link rel="canonical" href="/products/demo"> or <link rel="alternate" href="next.html"> must exist as project files. If they do not, a valid document fails with missing-local-dependency/422 even though those links are not runtime dependencies. Restrict this rewrite to resource relations such as icon, manifest, preload, and modulepreload, and leave navigation/metadata relations untouched (or classify them as external); add a regression fixture for a missing canonical URL.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Preserve non-HTML iframe MIME types, inline only resource-bearing link relations, and propagate unexpected Vite dist read failures instead of silently falling back.

Validated with focused export tests, the full daemon suite, guard, and workspace typecheck.
@open-design-crew

Copy link
Copy Markdown
Contributor Author

Addressed the latest review findings in 11c71662:

  • Non-HTML iframe dependencies now keep their original MIME type instead of being emitted as text/html.
  • Only resource-bearing <link> relations are embedded; canonical/alternate navigation metadata remains untouched.
  • Vite dist/index.html fallback now applies only to ENOENT; unexpected read/I/O failures propagate to the route error response.

Added red-to-green coverage for image/PDF iframes, resource vs. metadata links, and the Vite dist read-error path. I also audited the adjacent resource classification, MIME, cache/limit, and error-handling paths for the same classes of issue; no further gaps were found.

Validation: focused export tests (65 passed), full daemon suite (8,424 passed / 5 skipped), pnpm guard, daemon typecheck, and workspace typecheck.

Please review the latest head. The existing human QA hold remains in place; no merge action was taken.

@open-design-crew
open-design-crew Bot requested a review from nettee August 14, 2026 05:03

@nettee nettee left a comment

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.

@app/open-design-crew

The standalone bundler is well covered by the focused daemon/web tests, package typechecks, guard, and the offline browser witness. I found two correctness regressions in the changed export paths; the inline comments describe cases that either always fail or change page appearance, so I’m requesting changes before approval.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

res,
409,
'CONFLICT',
'standalone HTML cannot export a historical entry with current project dependencies',

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.

This hard-rejects every non-empty versionId, but the existing FileViewer still exposes “Export as standalone HTML” for historical versions: runProjectVersionExport puts the selected version id into the context and triggerHtmlExport forwards it here. Before this PR, that action fetched the versioned source and fell back to the selected version’s context.content; now every historical click ends in this 409 and no file is downloaded. That is a user-visible regression in an existing export action. Either bundle a versioned dependency snapshot, or hide/disable this menu item for historical versions (and test the disabled/hidden state) until snapshots are available.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

replacements.push({
start: location.startOffset,
end: location.endOffset,
value: `<style data-od-inline-asset="${escapeHtmlAttribute(href)}"${kept}>${escapeStyleBody(css)}</style>`,

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.

The replacement drops stylesheet link semantics for valid disabled/alternate links. For <link rel="stylesheet" disabled>, this emits <style ... disabled>, but disabled is not honored on a <style> element, so CSS becomes active; for rel="alternate stylesheet", removing rel also turns an opt-in alternate theme into active CSS. The exported page can therefore look different from the source. Keep disabled/alternate links as links with an embedded data URL (or otherwise preserve their enabled/alternate state), and add fixtures for both states before replacing local stylesheets.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Hide standalone HTML export for historical versions until dependency snapshots are available, avoiding an action that always fails.

Keep disabled and alternate stylesheets as embedded link elements so standalone export preserves activation semantics. Validated with focused red/green coverage, the full web suite, pnpm guard, and pnpm typecheck.
@open-design-crew

Copy link
Copy Markdown
Contributor Author

Addressed both findings in e39191d:

  • Historical versions no longer show the standalone HTML action because the backend intentionally cannot reconstruct their dependency snapshot. Current-version export remains available.
  • Disabled and alternate stylesheets stay as <link> elements with embedded CSS data URLs, preserving their activation semantics instead of turning them into active inline styles.

Added regression coverage for both cases. Validation passed: pnpm guard, pnpm typecheck, focused daemon/web red-green tests, and the full web suite (6,476 passed; 1 expected failure; 11 skipped). No new button or CSS styling was added.

@open-design-crew
open-design-crew Bot requested a review from nettee August 14, 2026 06:35

@nettee nettee left a comment

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.

@app/open-design-crew The standalone export work is backed by focused daemon/web tests, workspace guard and typecheck, and passing PR checks. I found three blocking cases where the endpoint can return a successful download that is not actually usable offline: reused module closures in nested documents, non-iframe nested HTML, and preserved CSP policies blocking injected resources. The details and fixes are in the inline comments; requesting changes so the new reliability guarantee holds across these inputs.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

private async ensureModule(projectPath: string, chain: string[], modulePaths: Set<string>): Promise<void> {
modulePaths.add(projectPath);
const state = this.moduleState.get(projectPath);
if (state === 'complete' || state === 'processing') return;

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.

Blocking — cached modules drop transitive imports in nested documents. moduleState is shared by the bundler, but each bundleDocument builds a fresh modulePaths set for its own import map. If the entry first completes main.js -> motion.js, a nested document that also loads main.js returns here without adding motion.js; the nested map only contains main.js while its code still imports od-project:/scripts/motion.js. The iframe then fails to load offline. Please retain the complete transitive module closure when a cached module is reused (or reconstruct it from a dependency graph), and add a fixture that reuses a module in the entry and an iframe. 🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

pushAttributeReplacement(html, node, 'style', rewritten, replacements);
}

for (const node of $('iframe[src]').toArray() as any[]) {

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.

Blocking — nested HTML is only recursively bundled for iframe[src]. object data and embed src go through referenceToDataUrl, which base64-encodes raw HTML, while iframe srcdoc is never visited. For an object or srcdoc containing relative image, script, or CSS references, the downloaded data/file URL cannot resolve those project paths, so the export can succeed while the nested page is broken. Please route HTML MIME object/embed through bundleDocumentUrl and bundle srcdoc against the parent document base (or reject these variants with a structured unsupported-resource error), with regression fixtures and an offline browser check. 🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

);
}

let output = applyReplacements(html, replacements.values);

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.

Blocking — preserved CSP can disable injected resources. The bundler leaves source Content-Security-Policy meta tags untouched while replacing external scripts and styles with inline/data URLs and injecting a data-valued import map. A page carrying script-src self or style-src self therefore returns 200 but refuses the generated module, import-map, and CSS in the saved file, so it is not offline-functional. Please remove or rewrite conflicting meta CSP for standalone output, or return a structured unsupported-CSP error, and add a fixture/browser assertion. 🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@AmyShang-alt AmyShang-alt added validated Runtime change validated (via /explore Pass or manual QA). and removed needs-validation Runtime change detected; needs human or /explore agent validation. labels Aug 14, 2026
@AmyShang-alt

Copy link
Copy Markdown
Contributor

@app/open-design-crew

Thanks for the contribution. I completed QA validation for this PR.

QA Acceptance Record

Scope:

  • Standalone HTML export through the web UI, daemon API, and od export --format html at head e39191d652e96f1112a736ff93f7de6f5ae66fea.
  • Offline execution, dependency embedding, structured failures, historical-version behavior, and headless CLI support.

Verified:

  • Focused daemon tests: 76 passed; focused web tests: 371 passed; contracts tests: 289 passed.
  • Targeted Playwright export workflow passed, including reopening the download through file://.
  • Electron exposed the standalone HTML action on the affected current-version file surface.
  • Exported CSS, images, ES modules, and nested iframe content loaded offline with zero HTTP requests, failed requests, or script errors.
  • Missing local dependencies returned structured HTTP 422 errors with the dependency chain; historical export returned HTTP 409.
  • CLI export succeeded with Electron stopped and produced the same SHA-256 artifact as the API; invalid HTML/deck flag combinations were rejected.
  • Guard, daemon/web typechecks, and current-head CI checks passed.

Not verified:

  • Separate macOS and Windows installer builds; this is non-blocking because the changed HTML path is daemon-owned and was validated both in Electron development runtime and headlessly through the CLI.

Risks / notes:

  • Remote HTTP(S) resources intentionally remain external and therefore require network access.

Conclusion:

  • Accepted

@AmyShang-alt
AmyShang-alt added this pull request to the merge queue Aug 14, 2026
Merged via the queue into main with commit 7a37c14 Aug 14, 2026
30 checks passed
@open-design-release-bot

Copy link
Copy Markdown
Contributor

Successfully created backport PR for release/v0.19.1:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/XXL PR changes 1500+ lines type/feature New feature validated Runtime change validated (via /explore Pass or manual QA).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants