* feat(skills): add taste-skill-derived web prototype and HTML PPT examples
Adds five skills distilling Leonxlnx/taste-skill into the project's
SKILL.md + example.html convention so authors can pick a deliverable
shape (web prototype / deck) and a design register (editorial / soft /
brutalist) without the LLM defaulting to Inter-on-white slop.
- web-prototype-taste-editorial: minimalist-skill applied (warm
monochrome, Instrument Serif x Inter Tight, hairline bento)
- web-prototype-taste-soft: soft-skill applied (silver canvas, ambient
mesh, double-bezel cards, button-in-button CTAs, spring motion)
- web-prototype-taste-brutalist: brutalist-skill Swiss-print mode
(newsprint, Archivo Black, viewport-bleeding numerals, hazard-red,
ASCII syntax)
- html-ppt-taste-editorial: 10-slide investor memo deck in the editorial
register, compatible with the existing html-ppt static-preview pattern
- html-ppt-taste-brutalist: 11-slide tactical-telemetry / CRT-terminal
incident debrief deck (scanline overlay, hazard-red alerts, audit log)
Each SKILL.md cites the upstream taste-skill source file and lists hard
rules, banned patterns, required components, and a pre-flight checklist.
* fix(i18n): register new taste skills in German content registry
Adds the five new taste-derived skills to DE_SKILL_IDS_WITH_EN_FALLBACK
so the German display content coverage test sees them.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(skills): close stray </small> in html-ppt-taste-brutalist telemetry cell
The paged-engineers tcell's delta span on slide 04 closed with
`</small></span>`, leaving the example with invalid HTML that downstream
previews/validators can flag.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(packaged): include nvm/fnm/mise agent CLI bins in packaged PATH
`resolvePackagedPathEnv` constructs the daemon PATH for packaged builds
(DMG/AppImage/NSIS), but it only includes hardcoded locations (.local/bin,
homebrew, etc.) and misses npm-global agent CLIs installed under a version
manager.
Add `collectNvmFnmBins()` to @open-design/platform that dynamically
discovers nvm (`~/.nvm/versions/node/*/bin`), fnm, and mise npm-global
bin directories. Wire it into `resolvePackagedPathEnv` alongside the
existing static candidates.
This complements the dev-mode fix in #346 (agents.ts: userToolchainDirs)
by covering the packaged launcher path as well.
* fix(packaged): move nvm/fnm bin discovery into packaged-runtime sidecars
Keep @open-design/platform focused on generic OS/process primitives by
moving the version-manager PATH discovery helpers (existingDirsUnder,
collectNvmFnmBins) into apps/packaged/src/sidecars.ts where they are
actually consumed by resolvePackagedPathEnv.
Closes#279.
Adds an `Export as Markdown` entry to the HTML / deck artifact share menu
in `FileViewer`. The entry downloads the artifact source verbatim with a
`.md` extension and `text/markdown; charset=utf-8` Content-Type — no
HTML→markdown conversion runs. The file body is byte-for-byte identical
to what the Source view shows.
The use case (per the issue thread): users want to pipe the artifact's
raw source into markdown-aware tooling — LLM context windows, vault
apps, etc. — that key off the `.md` extension. A lossy text-extraction
conversion (turndown / similar) was the first thing I tried, but it
loses the embedded styles + scripts that make the deck reproducible
and was rejected by the issue author in favor of pass-through. Keeping
the implementation byte-exact also leaves room for a future
"Convert to Markdown" entry to live alongside this one without
overlap.
Changes:
- `apps/web/src/runtime/exports.ts` — new `exportAsMd(source, title)`
helper; module-level comment updated to include MD in the export
family description.
- `apps/web/src/components/FileViewer.tsx` — new share menu entry,
positioned right after `Export as standalone HTML` and before the
`Save as template` divider so the file-format exports stay grouped
together.
- `apps/web/src/i18n/types.ts` + 14 locale files (`en`, `de`, `ja`,
`ko`, `zh-CN`, `zh-TW`, `es-ES`, `fa`, `pt-BR`, `ru`, `ar`, `pl`,
`tr`, `hu`) — `fileViewer.exportMd` key, mirroring the structure of
the existing `exportHtml` / `exportZip` entries.
- `apps/web/src/runtime/exports.test.ts` — three new cases under
`exportAsMd`: bytes-verbatim with the markdown content type, title
fallback when sanitization yields nothing, and UTF-8 round-trip.
The tests stub `URL.createObjectURL` + a minimal `document` so the
download path works under `environment: 'node'`.
No new dependencies. No daemon-side changes — all client-side.
Test plan:
- [x] `pnpm --filter @open-design/web test` — 23 files / 143 tests green
- [x] `pnpm --filter @open-design/web typecheck` — clean
- [x] `pnpm typecheck` (full repo) — clean
- [x] Manual browser test via `pnpm tools-dev run web`: clicked the
new menu entry on a 10-slide deck artifact; download blob was
`text/markdown; charset=utf-8`, 36,990 bytes, started with
`<!doctype html>...` matching the Source view byte-for-byte.
`inlineRelativeAssets` reduced the replacements with `next.replace(from, to)`
where `to` was the fetched script/stylesheet body. `String.prototype.replace`
with a string replacement interprets `$&`, `$'`, `$\``, `$1`–`$9`, and `$<n>`
as back-reference patterns, so any inlined file containing one of those
sequences (e.g. JSX with `'-$'` currency formatting) had its content
mangled — typically with everything-after-the-match spliced into the middle
of a string literal, producing "Unterminated string constant" parse errors
and preventing the inlined React app from booting.
Switching the second arg to a function replacer makes the body literal and
fixes the Open path for any project with `$`-bearing inlined assets.
Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recent Codex CLI versions (≥ 0.122 per #237 reports) reject a bare `-`
argv element with `error: unexpected argument '-' found`, exiting code 2
before any prompt is read. Open Design currently appends `-` after the
positional flags in `apps/daemon/src/agents.ts`, while also setting
`promptViaStdin: true` to deliver the prompt via stdin pipe. The `-`
became redundant after PR #258 standardized stdin delivery — only its
removal was missed for codex, leaving the agent unusable on `main` /
0.2.0 across every platform that runs a current Codex CLI.
This matches the pattern that was already corrected for `cursor-agent`
upstream of this commit (apps/daemon/src/agents.ts line ~392 comment),
where the same `-` sentinel was found to surface as a literal user
prompt. The fix here is the codex equivalent: remove the trailing
`args.push('-')` and rely on `promptViaStdin: true` for delivery.
The existing `agents.test.ts` cases asserted `args.at(-1) === '-'`,
which encoded the broken contract; those assertions are dropped and a
new regression test pins `args.includes('-') === false` across the
plugin, model, reasoning, and disable-plugins permutations so the
sentinel can't sneak back in via a future buildArgs branch.
Out of scope for this PR (not yet verified, left for separate
investigation):
- `--full-auto` is also flagged as deprecated by current Codex versions
(`use \`--sandbox workspace-write\` instead`). It still parses and
runs — the warning is non-fatal — and the right replacement involves
pinning the auto-approval policy that `--full-auto` implies, not just
`--sandbox`. Cleaning this up safely deserves its own PR after
confirming the modern equivalent.
- OpenCode (`apps/daemon/src/agents.ts:320`) and Qwen Code
(`apps/daemon/src/agents.ts:433`) follow the same `args.push('-')` +
`promptViaStdin: true` shape. Whether their current CLIs also reject
a bare `-` is unverified; if they do, the same minimal fix applies
there. Calling out for maintainer assessment alongside #163 / #218.
Closes#237.
Multi-file React prototypes loaded via Babel-standalone
(`<script type="text/babel" src="…">`) fetch each `.jsx` / `.tsx` source
over HTTP. The daemon's `EXT_MIME` map in `apps/daemon/src/projects.ts`
served `.jsx` as `application/octet-stream` (no entry → fallback) and
`.tsx` as `text/typescript`, neither of which is a recognized JavaScript
MIME. Strict CSPs and browser module loaders reject both, which is one
of the failure modes flagged in #336 for the Open-button render path.
Map both to `text/javascript; charset=utf-8`, matching the existing
treatment of `.js` / `.mjs` / `.cjs`. `.ts` is intentionally left as
`text/typescript` — it has no browser-execution path here; tooling that
reads `.ts` as TypeScript source is the load-bearing consumer.
Tests in `apps/daemon/tests/project-classifiers.test.ts` were updated
to pin the new mappings and document the rationale.
Refs #336.
* fix(web): download project tree as zip from share menu
The "Download as .zip" share action previously produced a single-file
ZIP of the rendered HTML srcdoc. Add a daemon archive endpoint that
bundles the on-disk project tree (scoped to the active top-level
directory when applicable) and have FileViewer call it, falling back to
the in-memory single-file ZIP on failure. UTF-8 filenames are preserved
via RFC 5987 Content-Disposition.
* fix(daemon,web): address PR #341 review — diagnostics, comments, more tests
- Stat the archive root up-front so a missing/non-directory target
surfaces a clear ENOENT/ENOTDIR ("does not exist" vs "is empty"),
instead of being swallowed by the recursive walk and reported as
empty. Distinguishes a deleted project from one with no archivable
files for on-call diagnostics.
- Document the DEFLATE level-6 choice in the archive builder so future
maintainers don't have to guess at the speed/ratio trade-off.
- Add a daemon test that the baseName preserves a multi-byte UTF-8
directory name (café-design), covering the path that feeds RFC 5987
filename* on the server.
- Add a daemon test that a missing archive root surfaces ENOENT with a
"does not exist" message, distinct from the empty-directory case.
- Export archiveRootFromFilePath and archiveFilenameFrom for testing,
and add web unit tests covering the Content-Disposition fallback
chain (UTF-8 filename* → legacy quoted filename= → root slug → title
slug, plus malformed-encoding fall-through).
- Replace the CJK example string in code comments and tests with a
Latin-extended example (café-design) so the codebase stays English
while still exercising multi-byte UTF-8 handling.
PR #258 standardized agent spawning through `createCommandInvocation`,
which on Windows wraps `.cmd` / `.bat` paths in `cmd.exe /d /s /c <line>`
and quotes each argument with cmd-style doubled quotes. PR #232's
follow-up fix for `shell:true` was lost in that refactor, and the new
shape has its own quoting bug on argv-style spawn:
1. cmd.exe `/s /c` strips exactly one leading and one trailing `"` from
the rest of the command line.
2. Node, with `windowsVerbatimArguments` unset, escapes each argv element
using CommandLineToArgvW rules — so the inner `"path with space"`
ends up surfacing to cmd.exe with an extra layer of `\"` escaping
that cmd doesn't understand.
Together these collapse `"C:\Users\Ethical Byte\...\codex.CMD" --help`
into `C:\Users\Ethical Byte\...\codex.CMD --help` with no quoting
preserved, and cmd.exe parses the first space as a token boundary —
"`Ethical` is not recognized as an internal or external command." See
issue #315 for the full repro.
The fix mirrors what Node's own `child_process.spawn({ shell: true })`
does internally: wrap the entire joined command line in an extra `"…"`
and set `windowsVerbatimArguments: true`. The outer wrap absorbs the
`/s /c` strip, leaving inner per-arg quoting intact, and the verbatim
flag tells Node to pass argv through to CreateProcess unchanged.
Changes:
- `packages/platform/src/index.ts`
- Extend `CommandInvocation` with optional `windowsVerbatimArguments`.
- Extract the cmd.exe shim builder into `buildCmdShimInvocation` and
apply the outer wrap + verbatim flag in both `createCommandInvocation`
and `createPackageManagerInvocation`.
- Forward the flag through `spawnBackgroundProcess` and
`spawnLoggedProcess`.
- `apps/daemon/src/server.ts` — agent spawn forwards
`invocation.windowsVerbatimArguments`. This is the call site that
hit #315 in the wild (Codex CLI `.CMD` shim, user dir with space).
- `tools/pack/src/win.ts` — `runPnpm` and `runNpmInstall` forward the
flag through `execFileAsync`. Affects the Windows packaged-build
pipeline when run from a path with spaces.
- `tools/dev/src/index.ts` — `runLoggedCommand` accepts and forwards the
flag; `buildDesktop` propagates it from
`createPackageManagerInvocation`. Affects local dev on Windows.
Tests:
- 9 new unit tests in `packages/platform/src/index.test.ts` stub
`process.platform` so both Windows and POSIX branches run on every
CI runner. Coverage:
- POSIX pass-through.
- Windows non-shim binary pass-through.
- `.CMD` shim with spaces in the binary path (the #315 repro).
- `.bat` shim parity.
- Argv elements with spaces alongside the shim path.
- Argv elements without whitespace stay unquoted.
- `process.env.ComSpec` fallback.
- `npm_execpath` short-circuit (cross-platform).
- POSIX pnpm pass-through.
- Windows pnpm wrapped through cmd.exe.
Closes#315.
The /api/projects/:id/upload endpoint used the projectUpload multer
handler with a 20MB limit. Large design files (e.g., 47MB PPTX) were
rejected with HTTP 413 Payload Too Large.
Bumping to 200MB rather than the originally-proposed 100MB so the cap
comfortably covers the heaviest design assets we realistically see —
high-fidelity PPTX exports, raw camera images, and PDF source files —
without forcing chunked-upload work on the project-side path. The
daemon listens on 127.0.0.1 only and writes via multer's disk storage
backend, so per-request memory pressure stays bounded.
importUpload (separate /api/import/claude-design path) keeps its 100MB
limit; the two endpoints don't have to match and bumping import in
lockstep is a separate decision.
Closes#318
The agent renders PPTX into the project folder, which the UI exposes
via /api/projects/:id/raw/*.pptx. Electron's default behavior drops
those downloads silently into the OS Downloads folder, so users have
no opportunity to pick a destination.
Hook will-download on the BrowserWindow session and call
setSaveDialogOptions for .pptx so the native Save As panel appears
before the download starts. Other formats are unaffected.
* fix(web): keep Design Files view active after deleting a file (#115)
Deleting a file from the Design Files panel was navigating the user
into another file's tab (typically the previously-opened file such as
`index.html`). Two unrelated symptoms contributed:
- `FileWorkspace.handleDelete` always called
`setActiveTab(nextActive ?? DESIGN_FILES_TAB)`, which yanked the user
out of the Design Files panel whenever `tabsState.active` still
pointed at a real file (clicking the Design Files tab only updates
local `activeTab`; it does not touch persisted `tabsState.active`).
- The row popover had `onMouseDown` propagation guard but no
corresponding `onClick` guard on the destructive action path.
Fix in `FileWorkspace.handleDelete`:
- If the deleted file is the one being viewed (`activeTab === name`),
fall back to another open tab (or Design Files if none remain) — the
prior behavior, which is correct in this case.
- Otherwise (deletion came from the Design Files panel, the typical
path), leave `activeTab` alone so the user stays in the list view.
Only null out `tabsState.active` when it referenced the deleted file
to avoid leaving a dangling pointer for the hydration `useEffect` to
resync against.
Defensive cleanup in `DesignFilesPanel`:
- Add `onClick` propagation guard on the popover wrapper (mirrors the
existing `onMouseDown` guard).
- Stop propagation on each popover button (Open / Download / Delete)
and on the download `<a>` so destructive/menu actions can't bubble
into row click handlers under any DOM arrangement.
Closes#115
* fix(web): address review feedback + add multi-tab delete regression E2E
Review feedback (#329):
- Drop the redundant `onClick={(e) => e.stopPropagation()}` on the
download `<a>` wrapper; the inner `<button>` is the actual click
target and already stops propagation.
- Expand the comment in `handleDelete`'s else branch to explicitly
state that `activeTab` is preserved because the user is in a
different context (Design Files panel or another tab) and shouldn't
be navigated away.
Regression coverage:
- Extend `runDesignFilesDeleteFlow` to upload a sibling file
(`keep-me.png`) before `trash-me.png`, so deletion has a fallback
tab that the buggy code would have navigated to. After deletion,
assert the Design Files tab still has `aria-selected="true"` and
the sibling tab is still present (just not auto-activated).
Adds a `host` option to `startServer()` (sourced from `OD_BIND_HOST` env
or `--host` CLI flag, defaulting to `'0.0.0.0'` to preserve existing
behavior) and wires it through `app.listen(port, host, ...)`.
This lets operators restrict the daemon to a single network interface —
e.g. a Tailscale IP — for tailnet-only deployments. Without this knob
the daemon always bound to all interfaces with no override path.
Backward-compatible: the default `'0.0.0.0'` matches current behavior.
The reported URL switches from the hardcoded `127.0.0.1` to the actual
bound address when a specific interface is requested.
Co-authored-by: Paperclip <noreply@paperclip.ing>
PR #239 added `designFiles.showMore` to en + 8 other locales but
missed ar, hu, ko, pl, tr — `pnpm --filter @open-design/web typecheck`
fails with TS2741 on each of those five and the failure has been
blocking every PR's CI on `main` since #239 landed.
Translations follow the verb/counter conventions already used inside
each locale file (ja/zh use "show {n} more"-style; de/es/pt-BR/ru
prefer "show +{n} more"-style; ko uses the standard `개` counter).
uploadProjectFiles built a Map keyed by `f.originalName ?? f.name` from
the server response and matched browser File names against it. The
server's `originalName` is the multer-decoded UTF-8 filename, which can
diverge from `File.name` under encoding edge cases (NFC vs NFD, double-
encoded UTF-8 round-trips, certain CJK combining characters), so the
client marked uploads as failed even when the server stored them
successfully.
The server iterates `req.files` in order and pushes successes to the
response array (apps/daemon/src/server.ts), so position is reliable.
Replace the name-equality matching with positional matching: every
entry in `responseFiles` corresponds to `batch[i]`. The only legitimate
partial-failure path is the daemon's `try/catch` around `fs.stat` for
files that vanished mid-flight, which produces a shorter response - the
unmatched tail of `batch` is then marked failed.
The on-disk timestamp prefix in the multer filename callback is
intentional (collision avoidance) and unchanged. The user-visible name
in the chat composer continues to use `f.originalName ?? f.name`.
Fixes#116
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* feat(deploy): add /api/projects/:id/deploy/preflight for pre-upload inspection
Today the deploy flow is a single POST that builds the file set, ships
it to Vercel, and waits for the public URL to come up. The user has no
visibility into what is actually being uploaded until the operation
either succeeds or fails with a generic 400. There is no way to tell
"my background image is missing" from "my deploy will exceed Vercel
quotas" without rolling the dice and waiting up to ~110 seconds.
Add a preflight endpoint that builds the plan, runs an analyzer, and
returns a typed report without touching Vercel. The endpoint is purely
additive, costs no network round-trips, and gives the UI everything
needed to render a confirm-before-deploy summary.
Implementation:
- packages/contracts/src/api/projects.ts: new public types
DeployPreflightRequest, DeployPreflightResponse, DeployPreflightFile,
DeployPreflightWarning, and the closed enum DeployPreflightWarningCode
with values broken-reference, invalid-reference, large-asset,
large-bundle, large-html, external-script, external-stylesheet,
no-doctype, no-viewport.
- apps/daemon/src/deploy.ts: refactor buildDeployFileSet so the
walk-and-collect logic lives in a non-throwing buildDeployFilePlan
that returns { entryPath, html, files, missing, invalid }.
buildDeployFileSet keeps its existing semantics by delegating to the
plan and throwing when missing or invalid is non-empty, so every
current caller and test is unchanged. Add analyzeDeployPlan that
walks the entry HTML once via parseHtmlTags + parseHtmlAttributes
(no new parser) and emits the warning vocabulary above. Add
prepareDeployPreflight that combines the plan and the analyzer and
returns the public DeployPreflightResponse shape. Soft thresholds
(4 MiB per asset, 75 MiB bundle, 1 MiB entry HTML) are exported as
DEPLOY_PREFLIGHT_LARGE_*_BYTES constants so tests and future tuning
use the same numbers.
- apps/daemon/src/server.ts: new route POST /api/projects/:id/deploy/
preflight. Same body shape as POST /deploy ({ fileName, providerId? }).
Returns DeployPreflightResponse on success. Validation errors map to
BAD_REQUEST, missing entry file maps to FILE_NOT_FOUND, exactly like
the existing deploy POST so the client error-handling stays uniform.
Tests (apps/daemon/tests/deploy.test.ts, 14 new cases under a new
"deploy plan and analyzer" describe block):
- buildDeployFilePlan returns files plus missing and invalid lists
without throwing.
- buildDeployFileSet still throws on missing/invalid, preserving the
pre-refactor contract.
- Analyzer unit tests for broken-reference, invalid-reference,
no-doctype, no-viewport, external-script, external-stylesheet,
protocol-relative external script, large-asset (per-file but not
entry HTML), large-html (entry HTML threshold), and a healthy-input
case that produces zero warnings.
- Preflight integration tests: full payload shape (provider, entry,
files, totals, warnings) on a healthy project, broken-reference path
on a missing asset, and a non-HTML entry rejection.
Net diff: +439 / -8 across 4 files. No new dependencies, no public API
removals. The existing POST /deploy flow is byte-for-byte unchanged
because buildDeployFileSet is a thin wrapper around the new plan.
* fix(deploy): preflight review fixes (P2 doctype anchoring, P2 large-html source path, P3 logging, P3 jsdoc)
Address review feedback on PR #320:
P2 (Codex bot): the no-doctype check used /<!doctype\s+html/i with no
anchor, so a `<!doctype html>` substring inside a <script> template
literal or HTML comment would mask a missing real declaration. Switch
to `new RegExp('^\uFEFF?\s*(?:<!--[\s\S]*?-->\s*)*<!doctype\s+html', 'i')`
which anchors to the document prolog and accepts the HTML5-permitted
optional BOM, comments, and whitespace before the doctype.
P2 (Codex bot): the large-html warning hardcoded `path: 'index.html'`,
but `entry` in the preflight payload is the source project file (e.g.
`pages/landing.html`) and other warnings already use source paths.
Switch the warning path to `entryPath` so file-targeted UI actions
deep-link back to a real project file.
P3 (lefarcen): add a JSDoc block alongside the inline TS annotation
on analyzeDeployPlan so the function follows the JSDoc-first style
of the rest of the file. The TS inline annotation stays because
deploy.ts uses `// @ts-nocheck`, which makes JSDoc `@param`/`@returns`
non-authoritative for callers; both forms together give docs and
correct caller-side types.
P3 (lefarcen): the preflight route catch block now logs non-DeployError
exceptions via console.error before sending the generic 400, so
unexpected failures show up in the daemon log without leaking
internals to the client.
Tests:
- analyzeDeployPlan flags missing doctype even when a fake `<!doctype>`
lives inside a <script> string literal.
- analyzeDeployPlan accepts a doctype that follows a leading HTML
comment and BOM (HTML5 prolog forms).
- analyzeDeployPlan reports large-html against the source entry path
(e.g. `pages/landing.html`), not the deploy-renamed `index.html`.
38 deploy tests pass, 264 across the daemon package, typecheck clean.
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
* fix(deploy): bundle and rewrite assets referenced from inline <style> blocks and style="" attributes
buildDeployFileSet only scanned HTML attributes (src/poster/href/srcset)
and external .css files for asset references. url() and @import refs
inside inline <style> blocks and style="" attributes were silently
skipped, so deployed pages were missing their @font-face sources, hero
backgrounds, and any image referenced from inline CSS, even though the
files existed in the project.
Add two helpers in deploy.ts:
- extractInlineCssReferences(html) collects url()/@import refs from
<style> blocks and style attributes, reusing extractCssReferences and
honoring htmlRawTextRanges so style-like text inside scripts and HTML
comments is ignored.
- rewriteCssReferences(css, baseDir) rewrites url() and bare @import
paths the same way rewriteHtmlReference rewrites src/href, so inline
CSS keeps working when the entry HTML moves up to the deploy root.
Wire both into the existing pipeline:
- buildDeployFileSet pushes inline-style refs onto the pending queue
alongside HTML and supportingFiles refs.
- rewriteEntryHtmlReferences first rewrites <style> block contents,
then re-derives htmlRawTextRanges so the tag-attribute pass still
skips raw-text regions whose offsets shifted.
- rewriteHtmlAttributes treats style="" the same way as srcset, routing
through rewriteCssReferences while preserving the original quoting.
Tests cover extraction, bundling end-to-end (including @font-face,
@import, and subdirectory entries), rewriting of CSS content with
remote/data/absolute refs left intact, and the missing-file error path.
* fix(deploy): harden inline-CSS pipeline per review (P1 script-literal masking, P2 ReDoS, SVG coverage)
Address review feedback on the inline-CSS deploy pipeline:
P1 (Codex bot): rewriteEntryHtmlReferences now computes htmlRawTextRanges
on the input HTML before the <style> block pre-pass, and skips any
<style>...</style> match whose offset falls inside a script string
literal or HTML comment. Previously the rewriter mutated those textual
fragments, which could change runtime JS behavior on subdirectory
deploys. Mirrors the gate already used by extractInlineCssReferences.
P2 (lefarcen): tighten the CSS url() and @import regex character
classes from .*? to [^)]*? and [^'"]*? respectively. Quoted urls
containing literal `)` are now expected to be percent-encoded, which
matches CSS authoring conventions and removes the O(n^2) backtracking
risk on attacker-controlled CSS. The hardened patterns are shared
between extractCssReferences and rewriteCssReferences via
CSS_URL_REGEX / CSS_IMPORT_REGEX so the two paths cannot diverge.
P3 (lefarcen): add an explicit comment on the rewriteEntryHtmlReferences
double-pass so the perf tradeoff is visible inline.
New tests:
- <style> inside <svg>: extraction and subdir rewriting both work.
- <style>-like text inside <script> literals: round-trips byte-for-byte.
- <style>-like text inside HTML comments: untouched.
- Pathological unclosed url(...... runs in <500ms for 100k input.
All 36 deploy tests pass (262 across the daemon package).
---------
Co-authored-by: Nagendhra <nagendhra405@gmail.com>
* feat(settings): split API provider protocols
* fix(settings): preserve API protocol config
* fix(settings): harden API protocol migration
* fix(settings): set apiProviderBaseUrl during v1 migration for known providers
* feat(web): add pet companion with Codex hatch-pet integration
Introduces a customizable floating pet companion (overlay + entry-view rail
+ composer menu + dedicated Settings → Pets section) that supports built-in
pets, user customization (glyph/image/spritesheet), and one-click adoption
of pets packaged by the upstream Codex `hatch-pet` skill via a new
`/api/codex-pets` daemon endpoint. Vendors the unmodified `hatch-pet`
skill under `skills/hatch-pet/` and adds i18n strings across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(scripts): sync community Codex pets from public catalogs
Adds `pnpm sync:community-pets` which fetches all pets from
codex-pet-share.pages.dev (paginated Supabase Functions API) and
j20.nz/hatchery (single-shot JSON), then writes each one as
`<id>/pet.json` + `<id>/spritesheet.webp` under
`\${CODEX_HOME:-\$HOME/.codex}/pets/`. The existing daemon
`codex-pets` registry already scans that folder, so synced pets
appear under Settings → Pets → Recently hatched and adopt with one
click — no manual upload. Supports --source/--out/--force/--limit
flags and validates magic bytes so HTML error pages never end up
masquerading as `.webp` files.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(daemon): tighten codex-pets validation and document vendoring
- sanitizeId now rejects ids that still contain `..` after collapsing,
closing a defensive gap on the path-traversal guard for the
`/api/codex-pets/:id/spritesheet` route.
- listCodexPets emits the sanitised folder name as the public id so the
download route resolves directly against the on-disk folder, even when
`manifest.id` differs (manual drops, sanitiser-touched manifests).
- Drop `@ts-nocheck` from `codex-pets.ts`; module is now strict-typed
with explicit interfaces, an unknown-narrowed JSON.parse path, and a
`pickString` helper guarding manifest fields one by one.
- Restrict the spritesheet response CORS header to sandboxed-iframe
callers (Origin: null) instead of unconditional `*`, matching the
existing raw-file route pattern. Same-origin web traffic does not
need the header (web proxies `/api/*` through the daemon).
- Add `skills/hatch-pet/README.md` explaining the vendoring trade-off,
provenance, and re-sync procedure.
- Add `docs/codex-pets.md` covering where pets live, how to populate the
registry without Codex installed, and the manifest contract.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(i18n): add pet.* keys to Hungarian locale
Hungarian locale was added on main after this branch diverged, so the new
pet.* dictionary keys never landed there and tsc -b reports hu's Dict as
incomplete once main is merged in.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* feat(web): atlas-driven pet animations + bundled community pets
Builds on the existing pet companion (#296) with a richer animation
loop, a curated set of community pets that ship with the repo, and a
one-click sync into ~/.codex/pets/.
- Atlas-mode rendering: PetSpriteFace can now play the full Codex 8x9
sprite atlas and swap rows from a JS-driven frame index. PetOverlay
classifies pointer interactions (idle / hover / drag-direction /
long-idle waiting) and maps them to the matching atlas row, so the
pet waves on hover, runs on drag, and falls into a waiting pose
after 6s of stillness. Single-strip pets keep their existing CSS
steps() animation, with the steps timing fixed to jump-none so frame
cells line up on cell boundaries.
- Atlas adoption: PetSettings exposes both "Use full atlas (animated)"
and "Freeze to this row" — full mode keeps every row for the
interaction state machine, single-row mode crops one strip via the
existing canvas helper. New prepareCodexAtlas downscales the atlas
to a localStorage-friendly PNG while preserving the grid layout.
- Settings tabs: pet sources are now split into Built-in / Custom /
Community tabs so each origin gets its own dedicated surface.
- Bundled pets: scripts/bake-community-pets.ts seeds a curated set
(clippit, dario, nyako-shigure, slavik, trump, tux, yelling-dario,
yorha-sit-2b) into assets/community-pets/. The daemon scans this
alongside the user's ~/.codex/pets/ root, with user pets winning
when ids collide. CodexPetSummary gains a `bundled` flag so the UI
can tag those cards with a "Bundled" pill.
- One-click community sync: daemon-side port of sync-community-pets
exposed via POST /api/codex-pets/sync. Returns the same
wrote/skipped/failed/total summary the CLI prints. Web Pet settings
surface this as a "Download community pets" button under the
Community tab.
- Avatar dropdown + hide rail: EntryView's avatar button is now a
small menu (mirrors the project-view AvatarMenu) with toggles for
hiding/showing the pet rail and opening Settings. PetRail gets a
matching × button for the same hide flow.
- Locales: 7 new pet.* keys for tabs, sync, hide/show, atlas full
mode, and the Bundled pill — translated into all 13 supported
locales.
Typechecks pass across all workspace packages; daemon + web vitest
suites stay green.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): bundled-pets built-in tab, ambient atlas animations, and community sync button
The Built-in tab now sources its catalog from the bundled spritesheets
at `assets/community-pets/` instead of the eight emoji placeholders that
felt boring next to the Codex hatch-pet atlases.
- Daemon: `listCodexPets` flags `bundled: true` by curated-set membership
in `assets/community-pets/`, not by which folder the sprite happened to
be read from. Previously a fully-synced user inbox preempted every
bundled id and left the tab empty.
- Settings → Pets → Built-in renders the same sprite-card grid as
Community, filtered by `bundled: true`, and reuses the existing
`adoptCodexPet` flow. Community tab filters to non-bundled so the
curated set never appears twice.
- Community tab gains the long-promised "Download community pets"
trigger that calls `/api/codex-pets/sync` and shows an inline status
line for the run summary. Strings already existed in every locale; we
just plumbed the button.
- `PetOverlay` gets ambient atlas-row choreography — while idle, the
overlay occasionally swaps `idle` for a random non-idle row (wave /
hop / look) so the pet doesn't feel frozen. User gestures cancel the
beat and take over instantly. `pickAmbientRow` lives next to
`pickAtlasRow` so both row pickers share the fallback discipline.
- One-shot `migrateCustomPetAtlas` heals configs adopted before the
overlay learned row switching by re-downloading the full spritesheet
so hover / drag / ambient variety light up on next launch.
- `BUILT_IN_PETS` is now an empty array (the type stays for backwards
compat); legacy configs whose `petId` still points at an emoji id
(`mochi`, `pixel`, …) fall back to the user's custom slot in
`resolveActivePet` so the overlay never renders blank.
- i18n: refresh `pet.tabBuiltInHint` (drop "emoji companions") and add
`pet.builtInEmpty` across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(skills): add pptx-html-fidelity-audit + wire into export prompt
Adds a python-pptx audit skill that catches the silent drift modes
between an HTML deck and its PPTX export — footer overflow, off-canvas
shapes, missing italic/em, hero slides not centered, oversized
bounding boxes, lost CJK type slots.
The skill ships:
- SKILL.md — five-step workflow (extract dump → walk HTML → audit
table → re-export with footer-rail + cursor-flow → verify)
- references/layout-discipline.md — Cursor primitive, hero budget
centering, per-slide-type code snippets
- references/font-discipline.md — five-layer font audit:
mapping → presence → variable-vs-static → XML latin/ea/cs slots
→ CJK + Latin italic interaction
- references/audit-table-template.md — fixed table format
- scripts/extract_pptx.py — dump every shape (text, geometry,
per-run typography) as JSON
- scripts/verify_layout.py — assert footer-rail + canvas
invariants; nonzero exit on any violation
Wires the skill into the FileViewer "Export as PPTX" flow:
- handleExportAsPptx in ProjectView.tsx now instructs the agent to
apply the footer-rail discipline up front (CONTENT_MAX_Y = 6.70",
Cursor + budget centering, explicit a:latin / a:ea slots), then
run verify_layout.py as a mandatory gate before declaring done.
- If 🟡 typography issues surface, the prompt points the agent at
font-discipline.md.
Verifier exit code is the gate: agent cannot claim "fidelity correct"
without 0 rail violations. If the environment lacks python-pptx, the
agent must say so explicitly rather than eyeball the deck.
* fix(pptx-fidelity): address review feedback from #307
P2 fixes (codex):
- ProjectView prompt now quotes the verifier path so filenames with
spaces (e.g. "Q2 review.pptx") parse as a single argument instead
of two.
- extract_pptx color extraction no longer gates on font.name/size —
a run can inherit name/size from the theme yet set its own color,
and that drift is exactly what the audit needs to catch.
P3 polish (lefarcen):
- SKILL.md "When this skill applies" now shows a 7-line HTML snippet
so readers can self-assess scope at a glance.
- Cursor.take(gap=0.12) gets an inline comment explaining the default
is ~1 line of whitespace at 14pt, adjustable per design system.
- New "Customizing the rails" callout under the constants block, with
pointers to the verifier flags (--content-max-y, --canvas-h/w) and
the export-prompt now documents the same path.
- New "Why geometry-based verification, not visual diff" section
captures the design tradeoff (platform lock-in, imprecision, setup
cost) that previously only lived in the PR body.
- font-discipline.md Layer 2 now includes a PowerShell snippet for
Windows font discovery alongside fc-list / mdfind.
- layout-discipline.md gains a "How to use this file" header so
readers know to skim §1-3 then jump to slide-type snippets.
Also: scripts/.gitignore for __pycache__.
* fix(skills): register pptx-html-fidelity-audit in German display metadata
CONTRIBUTING.md requires every skill in skills/ to have a corresponding
German display metadata entry in apps/web/src/i18n/content.ts; the
content.test.ts assertion catches drift. The CI failure on #307 was
this check tripping on the new skill.
Adds the German example prompt entry. Test suite passes locally.
* feat(web): add brand design systems, card thumbnails, and DESIGN.md side-by-side preview
- Add 7 new brand design systems (arc, canva, discord, duolingo, github, huggingface, openai)
- Show live showcase HTML thumbnails on Design Systems cards
- Add toggleable DESIGN.md side panel in preview modal with syntax-highlighted spec view
- Make preview iframe responsive: render at fixed design viewport and scale to fit so opening the side panel never reflows showcases into broken breakpoints
- Add floating collapse/expand handles on the sidebar boundary for direct hide/show
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): guard ResizeObserver and re-fire sidebar lazy-load on content swap
- Guard `new ResizeObserver(...)` in PreviewModal so the modal mounts in
jsdom (the existing preview-modal-fullscreen test was failing with
`ReferenceError: ResizeObserver is not defined`) and in older embedded
WebViews. Fall back to a window resize listener when the constructor
is unavailable.
- Add a `contentKey` hint to PreviewSidebar so the lazy-load `onToggle`
callback re-fires when the underlying side-panel source swaps while
the sidebar stays open. Wire `system.id` through from
DesignSystemPreviewModal so swapping design systems with the spec
panel open primes a fresh DESIGN.md fetch instead of leaving it stuck.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(web/i18n): add missing ds/preview keys to hu locale
The Validate workspace check failed after main's hu.ts landed
without the four i18n keys introduced by this PR (ds.specToggle,
ds.specLoading, preview.showSidebar, preview.hideSidebar).
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): add per-tool renderer registry for generative UI
Introduce a registry on the web runtime so external surfaces (skill-emitted
tools, MCP tools, future plugins) can render their own tool cards instead
of falling through to the generic command/output fallback. Registered
renderers are consulted before the hardcoded family ladder, mirroring
CopilotKit's `useCopilotAction({ render })` extension point and AG-UI's
four-state lifecycle (`inProgress` / `executing` / `complete` / `error`).
Returning null from a renderer falls back to the built-in card.
Forward `runStreaming` from AssistantMessage so the registry can
distinguish a live tool call from one that was interrupted before the
result arrived.
* fix(web): isolate failing tool renderers from the message stream
Address PR #282 review:
- Wrap the registry-renderer call in ToolCard with try/catch so a
third-party renderer that throws synchronously falls back to the
built-in family card instead of taking down the surrounding
AssistantMessage. Log via console.error for visibility.
- Drop the unused `use` parameter from `deriveToolStatus` (its inputs
are only `result` + `runStreaming`); removes the awkward `void use;`.
- Document the dispose-before-reregister contract on
`registerToolRenderer` for dynamic skill load/unload callers.
- Add a test asserting the throwing-renderer fallback path.
* docs(web): pin the hook-free contract for tool renderers
Address PR #282 review:
- Document on `ToolRenderer` that the callback runs inside ToolCard's
render and must therefore be hook-free. Hookful cards should return
a component element so React mounts the inner component and owns its
hooks across re-registers, fallbacks, and replacements.
- Add a test that exercises the documented pattern: register a renderer
whose body uses useState wrapped in a component element, render,
dispose, then register a different hookful renderer. Verifies that
swapping renderers with different hook shapes doesn't violate the
Rules of Hooks on ToolCard.
Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
* fix(tools-dev): strip ELECTRON_RUN_AS_NODE before spawning desktop
Parent processes such as Electron-based IDEs may set ELECTRON_RUN_AS_NODE=1
in their environment for sidecar/script reuse. When tools-dev inherits this
env via process.env, the spawned electron.exe runs as plain Node and fails
to inject main-process APIs (app, BrowserWindow, protocol all become
undefined). Explicitly drop the variable before spawning so desktop always
boots in real Electron mode regardless of caller environment.
* fix(desktop): ensure BrowserWindow is visible on initial load
Windows focus-stealing prevention can leave detached-spawned GUI windows
minimized or hidden, even when constructed with show:true. Add a small
ensureWindowVisible helper that restores from minimized state and forces
show+focus after the placeholder URL loads. Cross-platform safe: only
acts when window is actually hidden or minimized, preserving any user
window-state adjustments.
PR #295 was squash-merged and the squash commit dropped the branch's
cleanup commit that deleted upstream's stub entries. As a result main's
apps/web/src/i18n/content.ts now carries three TS1117 duplicate-key
errors, breaking the Typecheck workspaces CI job:
- DE_PROMPT_TEMPLATE_CATEGORIES has both 'Game UI' -> 'Spiel-UI' (line 345)
and 'Game UI' -> 'Game UI' (line 358). Keep the German 'Spiel-UI'.
- DE_PROMPT_TEMPLATE_COPY has both an empty {} stub and a filled-in
German entry for 'game-ui-ancient-china-open-world-mmo-hud'. Keep the
filled-in entry.
- DE_PROMPT_TEMPLATE_COPY has both an empty {} stub and a filled-in
German entry for 'social-media-post-sensational-girl-dance-storyboard-8-shots'.
Keep the filled-in entry.
Local verification:
- apps/web tsc -b --noEmit: passes
- apps/web vitest: 19 files / 102 tests pass
Co-authored-by: Joey <joey@open-design.local>
* feat(prompt-templates): add crayon kid-drawing style-transfer template
- illustration-crayon-kid-drawing-rework: an image-to-image prompt that
reworks any reference image into a hand-drawn crayon illustration with
childlike whimsy (castles, candy, stars, clouds, rainbows) on clean white
paper. Tuned for gpt-image-2 edits and works across product shots,
portraits, and screenshots.
- Ships with a cover preview rendered from the Open Design X profile
screenshot via the template's own prompt, downscaled to 1536x1024 JPEG.
- Category reuses existing 'Illustration' enum value.
* fix(prompt-templates): address crayon template review feedback (#295)
- aspect: switch from unsupported 3:2 to 4:3 (per @mrcfps). The app contract
only recognizes 1:1 / 16:9 / 9:16 / 4:3 / 3:4 (MediaAspect in
packages/contracts and MEDIA_ASPECTS in apps/daemon/src/media-models.ts);
gpt-image-* silently falls back to 1024x1024 for unknown aspects, which
would have made this template generate square images instead of landscape.
- Cover preview re-cropped from 1536x1024 to 1365x1024 (4:3) so the
thumbnail matches the declared aspect. Content preserved (center crop).
- prompt: add 'Preserve the general layout and spatial relationships of the
original image — transform the style first, embellish second' so small UI
elements, faces, and logos don't get overwhelmed by castles/candy/stars
decoration (per @lefarcen).
- tags: add 'rework' and 'transform' to improve discoverability for users
searching 'rework my photo' / 'transform screenshot' (per @lefarcen).
- summary: clarify that this is an image-to-image edit requiring a reference
image upload, not a pure text-to-image generation prompt (per @lefarcen).
* fix(i18n): register German content for new templates and their tags
The 'Validate workspace' CI job (apps/web i18n/content.test.ts) enforces
that every template slug, category, and tag that appears in a shipped
JSON template also has a German translation in apps/web/src/i18n/content.ts.
This PR's new template plus two previously merged first-party templates
(#187: game-ui-ancient-china-open-world-mmo-hud and
social-media-post-sensational-girl-dance-storyboard-8-shots) were missing
from the German dictionaries, so the suite was red.
Changes:
- DE_PROMPT_TEMPLATE_CATEGORIES: add 'Game UI' -> 'Spiel-UI'.
- DE_PROMPT_TEMPLATE_TAGS: add 11 tags used by the three templates
(ancient-china, childlike, crayon, dance, fashion, game-ui, hand-drawn,
hud, illustration, image-to-image, mmo, open-world, rework, sequence,
storyboard, style-transfer, transform, wuxia) with German equivalents.
- DE_PROMPT_TEMPLATE_COPY: add German title + summary for
game-ui-ancient-china-open-world-mmo-hud,
illustration-crayon-kid-drawing-rework, and
social-media-post-sensational-girl-dance-storyboard-8-shots.
Local verification: apps/web vitest suite (14 files, 68 tests) passes.
---------
Co-authored-by: Joey <joey@open-design.local>
* fix(daemon): restore startServer Promise contract — return url / { url, server }
PR #258 ("Standardize agent communication via stdin and remove
Windows-specific shims logic") removed the Promise wrap around
`startServer`'s `app.listen()` call. Two regressions shipped to main as
a result:
1. **`returnServer: true` returns just `server`, not `{ url, server }`.**
Both `apps/daemon/sidecar/server.ts:65` and
`apps/daemon/tests/version-route.test.ts:10` cast the return value to
`{ url, server }` and read `started.url`. Post-#258 those reads land
on `undefined`, which becomes `"undefined/api/version"` URLs at
runtime — the sidecar can no longer report its bound port to the
parent process, and `version-route.test.ts` fails with
`ERR_INVALID_URL`.
2. **`returnServer: false` returns nothing.** `apps/daemon/src/cli.ts:77`
calls `startServer({ port }).then(url => console.log(\`[od] listening on \${url}\`))`
— post-#258 that prints `[od] listening on undefined`, and the
subsequent `spawn(opener, [url], …)` to open the browser also gets
`undefined`. The daemon still binds, but the two paths that *consume*
the URL are silently broken.
Both stem from the same change: the new code synchronously kicks off
`app.listen` and returns either the server object or `undefined` before
`listen`'s callback fires, so neither the resolved port nor the URL
string ever reach the caller.
This restores the pre-#258 Promise wrap, which already had the right
shape for all three call sites:
```ts
return await new Promise((resolve) => {
const server = app.listen(port, () => {
const url = `http://127.0.0.1:${server.address().port}`;
resolve(returnServer ? { url, server } : url);
});
});
```
Behavior contract restored:
| `returnServer` | resolves with |
|---|---|
| `false` (default) | `string` — `http://127.0.0.1:<port>` |
| `true` | `{ url: string, server: http.Server }` |
The Promise resolution waits for `listen`'s callback so `port=0`
(ephemeral) callers always see the actual bound port, never the
placeholder.
Test coverage: `tests/version-route.test.ts` already exercises the
`{ url, server }` path via `await fetch(\`\${baseUrl}/api/version\`)`.
After this fix the suite is back to 15 files / 225 tests green
(2 of those — both in `version-route` — failed against current main).
Verification:
- `pnpm --filter @open-design/daemon test` — 15 files / 225 tests pass
- `tsc -p apps/daemon/tsconfig.json --noEmit` — clean
- `tsc -p apps/daemon/tsconfig.tests.json --noEmit` — clean (was failing
on the `as { url, server }` cast before)
* fix(daemon): reject startServer Promise on bind failure / null address
Folds in the two non-blocking improvements @lefarcen flagged on the
review of the parent commit:
1. **Explicit `error` event listener.** `app.listen` throws synchronously
for some failure modes (e.g. invalid port) and emits an `error` event
for others (EADDRINUSE on certain Node versions, EACCES, EADDRNOTAVAIL).
Without an `error` listener the returned Promise would hang forever
on the event-emitting paths instead of surfacing the failure to the
caller.
2. **Null-`address()` guard.** `server.address()` typing allows
`string | AddressInfo | null`. For TCP listeners it's always
AddressInfo, but if a future code path ever returns null (or a Unix
socket path string), the previous fallback `port` would still be
`0` for ephemeral-port callers and quietly produce a
`http://127.0.0.1:0` URL that fetches would fail on. Reject the
Promise instead with an explicit diagnostic.
Both keep the success path identical — `version-route.test.ts` still
exercises the same `{ url, server }` contract on `port: 0` and passes
unchanged. 15/15 daemon test files / 225/225 tests pass.
Refs review feedback on #268 (P2 suggestions, non-blocking).
* fix(daemon): drop a93246d workaround for restored startServer contract
After rebasing onto current `main`, this PR's restored
`{ url, server }` return shape from `startServer` collides with the
workaround applied in `a93246d` (PR #271), which had patched the two
consumers — `apps/daemon/sidecar/server.ts` and
`apps/daemon/tests/version-route.test.ts` — to call `started.address()`
on a bare `Server` handle.
Revert both consumers to the pre-`a93246d` `{ url, server }` shape so
they line up with the contract restored in `5c2f520`. With this:
- `pnpm --filter @open-design/daemon test` — 246/246 pass
- `pnpm --filter @open-design/daemon build` — tsc clean for both
tsconfig.json and tsconfig.sidecar.json
- `apps/daemon/src/cli.ts` (which expects a URL string and was
printing "listening on undefined" on `main`) is now correct without
any further change.
* fix(i18n): add missing chat.comments.* keys to hu.ts
PR #288 added Hungarian without the 12 `chat.comments.*` keys that
PR #284 introduced — the two landed in parallel and only en + the
older locales got both. Without these, `pnpm --filter @open-design/web
typecheck` errors on `src/i18n/locales/hu.ts(3,14): TS2740 ... missing
the following properties from type 'Dict': 'chat.comments.attached',
'chat.comments.emptyAttached', 'chat.comments.saved', and 9 more`,
which blocks every PR's CI.
Drive-by unblock with English fallback strings — proper Hungarian
translations should follow in a separate i18n pass.
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix: language dropdown overflow in settings modal (#281)
Smart-place the Settings language menu: open downward when there is enough space below the trigger, otherwise fall back to opening upward. Expose the available height to CSS via --menu-available-h so the menu is bounded in either direction, scrolls internally, and prevents long labels from overflowing item rows.
* fix(SettingsDialog): address PR review feedback for #281
- Document the 200px threshold used to prefer downward placement.
- Close the language menu on window resize so the placement (computed once at open) cannot become stale.
- Cap the menu max-height at 60vh so it stays comfortable on short viewports (mobile landscape ~400-500px tall).
* feat(web): add dark mode support with system/light/dark toggle
Closes#93.
Adds a three-way theme toggle (System / Light / Dark) to Settings >
Appearance. The chosen preference is persisted in localStorage alongside
the existing config and applied immediately via a data-theme attribute on
<html>.
Implementation
- AppTheme type ('system' | 'light' | 'dark') added to AppConfig
- CSS custom properties for the dark palette defined under
[data-theme="dark"] and @media (prefers-color-scheme: dark) so the
System option follows the OS without JavaScript
- Appearance section added to SettingsDialog with a sun-moon icon
- Several hardcoded light-only colour values replaced with CSS variable
equivalents so they adapt correctly in dark mode
- i18n: appearance/theme strings added to all 11 supported locales
Note: theme variables are currently inlined in index.css. A follow-up
could extract them into per-theme files to support community themes
(e.g. Catppuccin) -- happy to explore that direction if it fits the
project roadmap.
Also fixes (pre-existing)
- Syntax error in locales/tr.ts that was masking missing-key errors
- Missing translation keys in de, es-ES, ja, tr locales
- locales.test.ts EXPECTED_LOCALES not updated after Korean was added
* fix(web): apply theme before first paint to prevent FOUC
Add a synchronous inline script in layout.tsx that reads the saved theme
from localStorage and sets data-theme on <html> before React hydrates,
eliminating the flash of unstyled content that occurred when the theme
was only applied inside a useEffect.
* fix(web): use useLayoutEffect for theme sync to avoid 1-frame flash
useEffect fires after the browser paints, so switching themes in Settings
could show a single-frame flicker of the old theme. useLayoutEffect fires
synchronously before the paint, making theme transitions atomic. Safe
because the component tree is rendered with ssr:false.
* fix(web): fix 3-column theme selector layout and add seg-title overflow guard
.seg-control used a hardcoded 2-column grid; the theme toggle has 3 buttons
so Dark was falling onto a second row. Switch to a CSS custom property
(--seg-cols, default 2) so each usage can declare its own column count.
Also add white-space/overflow/text-overflow to .seg-title so long
translations never overflow their cell.
* fix(i18n): add missing maxTokens keys to Dict and Korean locale
settings.maxTokens and settings.maxTokensHint were used in SettingsDialog
and present in all locale files but absent from the Dict interface, causing
typecheck to fail. Also adds the two keys to the Korean locale to restore
full locale parity.
Fixes the blocking typecheck failure flagged in PR review.
* fix(i18n): remove duplicate maxTokens keys in ko.ts and Dict
Rebase introduced duplicate settings.maxTokens and settings.maxTokensHint
in both ko.ts and types.ts (Dict interface), causing TS1117/TS2300 errors.
Remove the second occurrence of each to restore clean typecheck.
* test(i18n): add pl to EXPECTED_LOCALES in locales.test.ts
Polish locale was added upstream in #273; update the hard-coded list so
the locale parity test passes on CI.
* fix(web): live-preview theme in Settings before Save
AppearanceSection now applies data-theme to <html> immediately on toggle
via useLayoutEffect, so the page re-themes in real time while the dialog
is open. SettingsDialogs useLayoutEffect cleanup reverts to the original
saved theme when the dialog is dismissed without saving.
* feat(media): integrate xAI Grok Imagine (image + video + native audio)
Adds a real provider integration for xAI's Imagine API alongside OpenAI,
Volcengine and HyperFrames. The route surfaces as two model entries:
* grok-imagine-image — POST /v1/images/generations, synchronous, asks
for b64_json so the bytes arrive in one round-trip; sniffs the
returned magic bytes so JPEG payloads land with the right extension
* grok-imagine-video — POST /v1/videos/generations + GET /v1/videos/{id}
polling, with a 4s tick + onProgress heartbeat (mirrors the
Volcengine handler so the agent's bash watchdog doesn't kill long
polls). Native audio (AAC) ships in the same file as the H.264
video — that's the differentiator vs Seedance and Sora.
Picker visibility is gated by NewProjectPanel's hardcoded surface→
provider allowlist; grok is added to image + video. Settings UI picks up
the provider automatically off MEDIA_PROVIDERS.
Credentials: XAI_API_KEY (canonical, matches the official SDK) or
OD_GROK_API_KEY override; both are honoured ahead of any value pasted
into Settings, so users who already export XAI_API_KEY don't have to
re-enter it in the UI.
Verified end-to-end with a real key:
* image: 1024×1024 JPEG, single round-trip
* video: 5s 16:9 H.264 + AAC, ~46s wall clock, pending→done
* fix(i18n): include pl in EXPECTED_LOCALES so locales.test passes
Drive-by unblock for CI on this branch — `pl.ts` and `LOCALES`/`Locale`
in types.ts already include Polish, but the test's hardcoded
EXPECTED_LOCALES didn't, so every PR has been red on
locales.test.ts since the locale was added.
* fix(media): grok review feedback — credential precedence comment + poll timeout diagnostics
Two small fixes from PR #276 review:
* media-config.ts — comment claimed XAI_API_KEY won, but readEnvKey
iterates the array in order so OD_GROK_API_KEY actually wins
(matches every other provider's OD_* override convention). Rewrite
the comment to describe what the code does instead of flipping the
array — the precedence is correct, the comment was wrong.
* media.ts renderGrokVideo — single throw at the bottom couldn't tell
"timed out after N seconds with status still pending" from "submit
returned neither inline video nor a request_id to poll." Split into
two branches so operators know whether to bump
OD_GROK_VIDEO_MAX_POLL_MS or file an upstream contract bug.
Both helpers in `apps/daemon/src/projects.ts` are dispatch points
multiple modules depend on, but had zero test coverage:
- `kindFor` returns the coarse 'image' / 'sketch' / 'code' / 'video' /
'audio' / 'text' / 'html' / 'pdf' / 'document' / 'presentation' /
'spreadsheet' / 'binary' bucket the frontend uses to pick a viewer in
`apps/web/src/components/FileViewer.tsx`. Called from
`projects.ts:66/88/130`, `media.ts:448`, and `document-preview.ts:18`.
- `mimeFor` is the Content-Type the daemon sends when serving the file
directly. Called from the file-serving routes in `server.ts`.
Adding `tests/project-classifiers.test.ts` (15 cases) pins both
contracts so the next bucket addition (e.g. issue #61's recently-merged
`.py` extension, or upcoming `.yaml` / `.toml` / `.sh` requests) can
be made with a clear regression net.
`kindFor` coverage:
- compound `.sketch.json` extension wins over `.json`
- `.html` / `.htm` → `'html'`
- `.svg` → `'sketch'`
- raster image extensions → `'image'`, with `sketch-` prefix → `'sketch'`
- video / audio / markdown / text / code (incl. `.py`) buckets
- office docs → respective `'pdf' / 'document' / 'presentation' /
'spreadsheet'` buckets
- unknown extensions and extensionless names → `'binary'`
- case-insensitive on extension
`mimeFor` coverage:
- every `EXT_MIME` mapping (charset where applicable)
- unmapped extensions → `'application/octet-stream'`
- case-insensitive on extension
No production change — pure test addition.
Verification:
- `pnpm --filter @open-design/daemon test` — 16 files / 240 tests pass
(was 15 files / 225 tests on main; +1 file, +15 tests).
* fix(daemon): emit tool_use from tool_execution_start in pi-rpc
The pi-rpc adapter emitted tool_use from message_end, which fires
before tool execution starts. The web UI pairs tool results to prior
tool_use events, so receiving tool_result without a preceding tool_use
broke tool card rendering and file auto-open behavior.
Move tool_use emission to tool_execution_start, matching the pattern
in copilot-stream.ts. Remove redundant tool call extraction from
message_end (tool_use is now emitted at execution time, usage is
already emitted from turn_end).
Extract mapPiRpcEvent as a pure exported function so tests exercise
the real event mapping logic instead of an inlined copy that can
diverge from production.
Ref: mrcfps review comments on PR #117
* docs(daemon): clarify mapPiRpcEvent mutability contract
The function mutates ctx.sentFirstToken to track streaming state.
Calling it "pure" is misleading; revised the doc comment to say
no I/O or child process interaction instead.
* fix(pi-rpc): remove redundant status(tool) emission from tool_execution_start
Now that tool_use fires inline from tool_execution_start, the
accompanying status(tool) event is redundant: tool_use already
carries the tool name, and the UI renders running state from the
tool card. The extra status pill breaks consecutive tool_use
grouping in AssistantMessage.buildBlocks. Aligns with
copilot-stream, which emits only tool_use from
tool.execution_start with no status event.