Commit Graph
59 Commits
Author SHA1 Message Date
Vitaliy VVSandGitHub 64609e6a2a Refactor RUNTIME_DATA_DIR resolution logic (#391)
* Refactor RUNTIME_DATA_DIR resolution logic

Set OD_DATA_DIR=$HOME/.open-design #390

* Refactor resolveDataDir to accept projectRoot parameter

Add validation if the resolved path is not writable.
2026-05-04 03:47:48 +08:00
JasonandGitHub 339a57c65c fix(security): bind daemon to localhost by default, add origin validation (#365)
* fix(security): bind daemon to localhost by default, add origin validation middleware

The daemon's Express server previously defaulted to binding 0.0.0.0,
exposing all 30+ API endpoints to the local network with zero
authentication. This included endpoints that spawn CLI agents, write
project files, and manage API keys.

Changes:
- Default bind address changed from 0.0.0.0 to 127.0.0.1 in both
  server.ts and cli.ts. Users who need network access can still set
  OD_BIND_HOST=0.0.0.0 explicitly.
- Added origin validation middleware on /api/* routes: requests with
  an Origin header are only allowed from localhost/127.0.0.1/[::1]
  on the daemon's own port. Non-browser clients (no Origin header)
  are unaffected.
- 7 unit tests for the origin validation logic.
- Existing tests: 327/327 passing.

* fix: address PR review — fail-closed, OD_WEB_PORT, HTTPS, Origin: null

Address feedback from @lefarcen, @mrcfps, and Codex review:

P1: Fail-closed when resolvedPort is not yet set (returns 403
instead of passing through).
P2: Include OD_WEB_PORT in allowed origins for split-port proxy
setups (web port ≠ daemon port). Add HTTPS variants.
P3: Exempt Origin: null for sandboxed iframe preview fetches
(/api/projects/:id/raw/*) so artifact previews keep working.
P4: Update help text from 0.0.0.0 to 127.0.0.1.

Test coverage expanded to 13 tests including:
- OD_WEB_PORT split-port proxy scenario
- Origin: null iframe preview
- HTTPS origin variants
- Fail-closed before port resolution
- Cross-origin rejection with OD_WEB_PORT set

All 333 tests passing, TypeScript clean.

* fix: scope Origin: null bypass to raw-file previews only, support non-loopback bind host

Two issues found in second review round:

1. Origin: null was a global bypass for ALL /api routes, allowing
   sandboxed iframes to reach state-changing endpoints (project
   create/delete, agent runs). Now only GET requests to
   /projects/:id/raw/* pass through with Origin: null — the original
   intent for iframe file previews. POST/DELETE with Origin: null
   are rejected.

2. Allowed origins only included loopback addresses. When daemon
   binds to a non-loopback address (--host for Tailscale, LAN, or
   0.0.0.0), browser requests from that address would get 403.
   Now the bound host is included in allowed origins alongside
   loopback, keeping the documented network-access escape hatch
   working for browser clients.

Added negative tests for Origin: null on POST/DELETE/non-raw GET,
and positive tests for non-loopback bind host scenarios.
All 339 tests passing.

* fix: centralize origin policy, add spritesheet to Origin: null allowlist

Addresses @mrcfps's third review round:

1. isLocalSameOrigin() now uses the same policy as the global origin
   middleware: HTTPS + HTTP, OD_WEB_PORT, and the explicit bind host
   (OD_BIND_HOST). Previously it only accepted HTTP on loopback hosts,
   so requests from https://127.0.0.1 or a Tailscale address could pass
   the global guard but 403 on per-route checks.

2. /api/codex-pets/:id/spritesheet is a read-only route that sets
   Access-Control-Allow-Origin: null for canvas drawing by sandboxed
   iframes. Added to the Origin: null allowlist so the middleware
   doesn't block it before the route handler runs.

3. buildAllowedOrigins() extracted as a closure so both the middleware
   and isLocalSameOrigin() share identical logic.

340/340 tests passing.

* test: remove dead fail-closed test with unused 5th argument

The 'fails closed when port is 0' test called request() with an
extra argument the helper ignores, then never asserted on res.
Real coverage lives in the dedicated 'fail-closed before port
resolution' describe block.
2026-05-04 00:08:53 +08:00
4de93fe35c Add OpenAI media OAuth fallback (#377)
Co-authored-by: Fu Yizheng <fyz3120@sina.cn>
2026-05-03 23:16:59 +08:00
lefarcenandGitHub 016c08183f release: Open Design 0.3.0 2026-05-03 23:07:28 +08:00
bezineb5andGitHub 051d9b890d feat(daemon): add Mistral Vibe CLI agent adapter (#354) 2026-05-03 16:19:09 +08:00
louie42andGitHub 61cdc3fe4b fix(daemon): upgrade better-sqlite3 for Node 24 Windows prebuilt support (#357)
Merged per maintainer approval in pr 解决群.
2026-05-03 15:59:41 +08:00
Mohamad YahiaandGitHub 72edd4fc60 fix(clis): Address Codex and Gemini CLIs not being detected by looking for agent CLIs in user toolchain paths (#346) 2026-05-03 14:08:31 +08:00
SidandGitHub 58bbc979e3 fix(daemon): lead copilot spawn with -p - so prompt-via-stdin is consumed (#351) 2026-05-03 13:50:15 +08:00
9d700ec74f feat(daemon): persist code agent startup (#255)
* feat(daemon): persist code agent startup

* fix: complete all suggestions

* fix: types for app config

* chore: revert local origin

* chore: format to single quotes

* fix: duplicate headers

* fix: isLocalSameOrigin rewriting issue

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-03 12:14:04 +08:00
SidandGitHub 0d3de899bd fix(daemon): drop literal - argv from codex spawn so prompts deliver via stdin pipe alone (#342)
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.
2026-05-03 10:45:15 +08:00
SidandGitHub 0851df379e fix(daemon): serve .jsx / .tsx with JS-family MIME so browser loaders accept them (#340)
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.
2026-05-03 10:37:14 +08:00
monshunterandGitHub 30f8036c9a fix(web): make share-menu "Download as .zip" return the actual project tree (#341)
* 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.
2026-05-03 10:34:33 +08:00
SidandGitHub 648374d839 fix(platform): wrap cmd.exe shim invocations to survive /s /c quote stripping (#339)
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.
2026-05-03 10:00:46 +08:00
lefarcenandGitHub e399593528 fix(daemon): increase projectUpload limit from 20MB to 200MB (#319)
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
2026-05-03 09:48:24 +08:00
71370fd9dd feat(daemon): add OD_BIND_HOST env var and --host flag for interface binding (#328)
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>
2026-05-03 09:31:46 +08:00
8897cb85be feat(deploy): add /api/projects/:id/deploy/preflight for pre-upload inspection (#320)
* 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>
2026-05-03 09:00:46 +08:00
0943cb1452 fix(deploy): bundle and rewrite assets referenced from inline <style> blocks and style="" attributes (#314)
* 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>
2026-05-03 08:50:21 +08:00
6fa2077651 feat(web): add pet companion with Codex hatch-pet integration (#296)
* 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>
2026-05-02 23:45:39 +08:00
Nader DabitandGitHub 8c61e43c44 Added Devin for Terminal (#301)
* added Devin for Terminal

* updates based on PR feedback
2026-05-02 22:40:08 +08:00
luo jiyinandGitHub 96db795d7d docs: fix spelling mistakes in CLI comment, spec, and video prompt (#300)
* fix: correct cli typo comment

* docs: fix spec typo

* fix: correct video prompt typo
2026-05-02 22:32:26 +08:00
lefarcenandGitHub 62b01a6dbf release: Open Design 0.2.0 (#297) 2026-05-02 22:28:59 +08:00
5edd852cfa fix(daemon): restore startServer Promise contract — return url / { url, server } (#268)
* 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>
2026-05-02 20:56:06 +08:00
CaprikaandGitHub 0c00f241e7 Add preview comment attachments (#284) 2026-05-02 19:23:46 +08:00
lefarcenandGitHub 9e8177d80a feat(media): integrate xAI Grok Imagine (image + video + native audio) (#276)
* 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.
2026-05-02 17:06:17 +08:00
SidandGitHub 95637b2ae2 test(daemon): cover kindFor and mimeFor file classifiers (#269)
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).
2026-05-02 16:25:47 +08:00
Marc ChanandGitHub a93246d892 chore(ci): add GitHub CI workflow (#271)
* Add GitHub CI workflow

* Address CI workflow review feedback

Generated-By: looper 0.3.0 (runner=fixer, agent=openai/gpt-5.5)
2026-05-02 16:14:33 +08:00
TomandGitHub 5a0f954297 fix(daemon): emit tool_use from tool_execution_start in pi-rpc (#186)
* 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.
2026-05-02 16:06:37 +08:00
SidandGitHub d58b07fdad feat(daemon): auto-detect OpenClaude as a fallback for Claude Code (#263)
Today open-design detects code agents by exact PATH binary name (`claude`,
`codex`, `gemini`, …). OpenClaude (https://github.com/Gitlawb/openclaude)
is an argv-compatible Claude Code fork that ships under the binary name
`openclaude`, so users with only `openclaude` installed see "Claude Code:
not detected" and have to write a wrapper script (issue #235, "Path B"
workaround).

This change adds a generic fallback-bin mechanism and uses it to declare
`openclaude` as a Claude Code fallback:

- `AGENT_DEFS[claude]` gains `fallbackBins: ['openclaude']` — a new
  optional declarative slot. Agents without a fallback are unchanged.
- `resolveAgentExecutable(def)` is a new exported helper that walks
  `[def.bin, ...def.fallbackBins]` and returns the first PATH hit, or
  null. The two existing `resolveOnPath(def.bin)` callsites in
  `probe()` and `resolveAgentBin()` route through this helper instead
  of reading `def.bin` directly.
- `stripFns()` is extended to drop `fallbackBins` from the API response
  payload — same posture as `helpArgs` / `capabilityFlags` / probe-only
  metadata that shouldn't bleed into the public agents list.

Behavior contract:

- If both `claude` and `openclaude` are on PATH, `claude` still wins
  (the order in the candidate list matters).
- If only `openclaude` is on PATH, the agent shows up as "Claude Code"
  and chat / model-listing / spawning all use the resolved openclaude
  path. Users don't need wrappers and the agent picker stays a single
  "Claude Code" entry instead of confusingly splitting into two.
- Agents with no `fallbackBins` field (codex / gemini / opencode / …)
  resolve via `def.bin` exactly as before.

Why fallback (not a separate AGENT_DEFS entry):

The issue author explicitly preferred the fallback shape over a second
top-level "OpenClaude" entry, and OpenClaude is a verbatim Claude Code
fork — the buildArgs / fallbackModels / streamFormat / capabilityFlags
would all be duplicated. A new `fallbackBins` slot keeps the Claude
config single-sourced and lets future drop-in forks be added with one
extra string.

Test coverage (5 new tests in `tests/agents.test.ts`):

1. `claude.fallbackBins` includes `'openclaude'` — declarative regression
   guard so the AGENT_DEFS entry can't silently drift back.
2. `resolveAgentExecutable` prefers `def.bin` when both candidates are
   present (primary always wins).
3. `resolveAgentExecutable` falls back through `fallbackBins` when
   `def.bin` is missing (the OpenClaude-only install case).
4. `resolveAgentExecutable` returns null when no candidate is on PATH.
5. `resolveAgentExecutable` handles defs with no `fallbackBins` field
   (no-regression guard for codex / gemini / opencode / …).

The 4 filesystem-backed cases skip on Windows because resolveOnPath
walks PATHEXT extensions there and the fixture files don't carry them;
the declarative AGENT_DEFS assertion runs on every platform.

All 230 daemon tests pass locally (was 225 before this PR).

Closes #235
2026-05-02 15:29:07 +08:00
SidandGitHub b9d8bd36b9 fix(daemon): treat .py files as previewable code in Design Files (#261)
Python files in Design Files cannot be previewed inline today: the side
panel falls back to BinaryViewer and users can only download or open
externally, even though `.py` is plain text and reasonable to inspect
in-app.

Root cause: `kindFor()` in `apps/daemon/src/projects.ts` (line 278)
classifies files into coarse buckets the frontend uses to pick a viewer.
The `'code'` branch lists `.js / .mjs / .cjs / .ts / .tsx / .json / .css`
but not `.py`, so Python files fall through to `'binary'`. The frontend
already renders the `'code'` bucket via `TextViewer` in
`apps/web/src/components/FileViewer.tsx`, so once a file lands in the
`'code'` bucket it gets the existing read-only code preview for free.

Fix: add `'.py'` to the `'code'` extension list. No frontend change
needed — `TextViewer` handles it on receipt.

Scope: this PR keeps the change minimal to match the issue
(`.py` only). Other commonly-requested text-like extensions
(`.yaml / .yml / .toml / .sh`, plus other code languages) can be added
in a follow-up if maintainers want that surface area.

Closes #61
2026-05-02 15:06:24 +08:00
RinandGitHub 06b1492744 Standardize agent communication via stdin and remove Windows-specific shims logic (#258)
This change moves Claude Code and GitHub Copilot CLI to stdin-based prompt delivery, aligning them with other agents. By doing so, we can bypass OS command-line length limits on Windows and set shell: false universally, improving security and robustness.

Integration of createCommandInvocation ensures that Windows npm shims (.cmd/.bat) are correctly handled while keeping shell: false, addressing the P1 warning from the automated review and maintaining consistent execution across platforms.
2026-05-02 14:59:21 +08:00
SidandGitHub 52677555f7 fix(daemon): include package.json in tarball so packaged app reports correct version (#260)
The packaged app's Settings → About page displays version `0.0.0` instead
of the actual release version because the daemon's `package.json` was
excluded from its `pnpm pack` tarball.

Root cause: `apps/daemon/package.json` declares `"files": ["dist"]`, so
`pnpm pack` only includes the `dist/` directory. At runtime,
`readCurrentAppVersionInfo()` in `apps/daemon/src/app-version.ts` resolves
`new URL('../package.json', import.meta.url)` from the compiled
`dist/app-version.js`, which points at the tarball's root `package.json`.
Because that file isn't packed, the read fails silently, falls through
the catch in `readPackageMetadata()`, and the version falls back to the
`APP_VERSION_FALLBACK = '0.0.0'` constant. `/api/version` and
`/api/health` then report `'0.0.0'` for every packaged install.

Fix: add `"package.json"` to the daemon's `files` array so it ships in
the tarball. The package already declares `"./package.json"` as an
exports entry, so consumers expect this file to be available.

Closes #224
2026-05-02 14:52:23 +08:00
lefarcenandGitHub c2b3d737f2 fix: make max_tokens configurable (closes #29) (#78)
* fix(web,daemon): make max_tokens configurable (closes #29)

BYOK users on custom Anthropic-compatible providers (e.g. Xiaomi MiMo)
hit the hardcoded 8192 cap and saw artifacts truncated mid-stream.

- AppConfig.maxTokens with Settings input (EN/CN + 8 other locales)
- ProxyStreamRequest.maxTokens contract field
- anthropic, anthropic-compatible, and openai-compatible providers all
  forward cfg.maxTokens
- /api/proxy/anthropic/stream and /api/proxy/stream payloads honor it,
  defaulting to 8192 when unset so prior clients are unaffected

Original sketch by @mashu in #78 (50a9d14); rebased to the apps/web
layout and extended to the proxy paths actually used when baseUrl is
set, which is where #29's user actually traffics.

* feat(web): per-model max_tokens defaults

Adds a hand-maintained MODEL_MAX_TOKENS table (Claude 4.5 line → 64k,
mimo-v2.5-pro → 32k) and an effectiveMaxTokens helper layered over the
override field added in 6a3ae5f, so #29's user — and others on supported
models — don't have to discover Settings to avoid mid-stream truncation.

- apps/web/src/state/maxTokens.ts: lookup + helpers
- providers/{anthropic,anthropic-compatible,openai-compatible}.ts:
  forward effectiveMaxTokens(cfg) instead of cfg.maxTokens ?? 8192
- SettingsDialog: input becomes an optional override (blank = default,
  shown as placeholder)
- 10 locale hint strings updated to the new semantics

* feat(web): vendor LiteLLM model metadata for max_tokens defaults

Replaces the 4-entry hand-rolled MODEL_MAX_TOKENS map from 544e67e with
a vendored slice of BerriAI/litellm's model_prices_and_context_window
JSON (1970 chat models, ~97KB raw / ~25KB gzip). Future model launches
land in maxTokens.ts via `pnpm sync-litellm-models` instead of manual
edits.

- scripts/sync-litellm-models.ts: fetches the upstream JSON, filters to
  chat-mode entries, projects each entry to its max_output_tokens (or
  max_tokens fallback), and writes a sorted, license-attributed JSON
- apps/web/src/state/litellm-models.json: generated artifact, committed
- apps/web/src/state/maxTokens.ts: lookup is now
  OVERRIDES → LITELLM_MODELS → FALLBACK_MAX_TOKENS. The OVERRIDES table
  shrinks to just `mimo-v2.5-pro` (LiteLLM only ships MiMo via
  OpenRouter/Novita aliases, not the canonical id Xiaomi's API uses).

LiteLLM is MIT-licensed (BerriAI/litellm/blob/main/LICENSE); attribution
is preserved in both the script header and the generated JSON's
_license field.

* test(web,docs): cover maxTokens lookup + document sync workflow

- apps/web/src/state/maxTokens.test.ts: six vitest cases pinning the
  three-tier lookup (override → LiteLLM → fallback) and the
  effectiveMaxTokens user-override path. Guards against a future sync
  silently dropping the Anthropic 4.5 entries we rely on.
- CONTRIBUTING.md / CONTRIBUTING.zh-CN.md: new "Updating model
  max_tokens metadata" section pointing future maintainers at
  scripts/sync-litellm-models.ts and explaining when OVERRIDES is
  appropriate (it's the rare exception, not the default).

* fix(web): mark Max tokens label as optional in 10 locales

The Settings field is optional (blank means "use the per-model default")
but the label gave no visual cue, breaking the implicit pattern that
every other API-mode field (key/model/baseUrl) is required. Append
"(optional)" — using the locale's natural parenthetical convention
(Chinese full-width brackets, Japanese 任意, Russian опционально, etc.)
— so the field reads as discretionary at a glance.

* fix(web): validate maxTokens override against advertised UI bounds

Addresses Siri-Ray's review on commit 0d98185. The Settings input
declares min={1024}/max={200000}/step={1024}, but until now
effectiveMaxTokens trusted any defined cfg.maxTokens, so a stale or
hand-edited localStorage value (negative, zero, fractional, billions)
would pass straight to the Anthropic SDK on the direct path while the
daemon proxy quietly clamped it back to 8192 on the proxied path —
same config, divergent behavior depending on route.

- maxTokens.ts: add MIN_MAX_TOKENS / MAX_MAX_TOKENS exports and
  isValidOverride helper. effectiveMaxTokens only honors the override
  when it is a finite integer in [1024, 200000]; otherwise falls back
  to modelMaxTokensDefault.
- SettingsDialog.tsx: input bounds now reference the same constants so
  the UI promise can't drift from the runtime check.
- maxTokens.test.ts: six new cases pinning the rejection of negative,
  zero, sub-MIN, super-MAX, non-integer (fractional / NaN / Infinity)
  overrides plus the inclusive MIN/MAX boundaries.

The daemon proxy's existing `> 0` fallback stays as defense-in-depth.
2026-05-02 13:52:54 +08:00
Siri-RayandGitHub 0bafc73d24 Add visible conversation timestamps (#120)
* Add visible conversation timestamps

Generated-By: looper 0.2.7 (runner=worker, agent=codex)

* Fix assistant message timestamps

Generated-By: looper 0.2.7 (runner=fixer, agent=codex)
2026-05-02 11:15:07 +08:00
1edab990bb feat(craft): add brand-agnostic craft references + Refero-derived lint rules (#225)
* feat(craft): add brand-agnostic craft references and refero-derived lint rules

Introduce `craft/` as a third top-level content axis alongside `skills/`
and `design-systems/`, holding universal (brand-agnostic) craft rules
that apply on top of any DESIGN.md. Skills opt in via a new
`od.craft.requires` front-matter array; the daemon resolves the slug
list and injects the matching files between DESIGN.md and the skill
body in the system prompt.

Initial vendor (MIT, adapted from referodesign/refero_skill): typography
craft, color craft, anti-ai-slop. Pilot wired on saas-landing.

Extend the existing lint-artifact pass with two refero-derived rules:
- P0 ai-default-indigo — solid #6366f1 / #4f46e5 / #4338ca / #8b5cf6 as
  accent (not just gradients) is the most-reported AI tell.
- P1 all-caps-no-tracking — `text-transform: uppercase` rules without
  ≥0.06em letter-spacing.

The craft loader silently drops missing files so a skill can
forward-reference future sections (e.g. `motion`) without breaking.

* fix(daemon): skip :root token blocks in ai-default-indigo lint

The ai-default-indigo P0 check scanned the whole HTML for the raw
hex, so brands that intentionally encode indigo as `--accent: #6366f1`
in :root and consume it via var(--accent) downstream were flagged
as AI-default — a false positive that forced the agent to "fix"
valid output. Strip :root token-definition blocks (including
attribute-selector theme variants) before scanning, mirroring the
existing pattern used by the raw-hex P1 check. Hex still flagged
when it appears in component rules or inline styles.

* docs(craft): address PR #225 P3 review feedback

- craft/README.md: explain why missing craft sections are silently
  dropped (forward-compatibility) instead of surfacing a warning.
- craft/typography.md: ground the 0.06em ALL CAPS tracking floor in
  Bringhurst-derived typographic practice rather than presenting
  the threshold as unattributed.
- craft/color.md: cover the edge case where a brand's DESIGN.md
  intentionally encodes indigo as --accent — `var(--accent)` uses
  remain unflagged because the linter only inspects hardcoded hex.
- docs/skills-protocol.md: link the "missing files dropped silently"
  note back to craft/README.md for the canonical slug list and the
  rationale behind the choice.

* fix(craft): address PR #225 P0 review feedback

- tools/pack: copy `craft/` into the packaged resource root alongside
  `skills`, `design-systems`, and `frames`, so the `od.craft.requires`
  integration isn't a silent no-op when the daemon resolves
  `${OD_RESOURCE_ROOT}/craft` in packaged builds.
- packages/contracts: add `craftRequires?: string[]` to `SkillSummary`
  (and therefore `SkillDetail`) so the field that `listSkills()`
  already returns and `/api/skills(/:id)` already serializes via
  `...rest` is part of the documented web/daemon contract instead of
  leaking through as an untyped property.
- apps/daemon/lint-artifact: expand the indigo token-strip pass to
  cover selector lists containing `:root` (e.g. `:root, [data-theme="light"]`)
  and any rule whose body is custom-property-only (e.g. a
  `[data-theme="dark"] { --accent: ... }` theme variant). Real
  component rules with a hardcoded indigo are still preserved so the
  P0 finding still fires; tests cover the new selector-list and
  theme-variant cases.

* fix(craft): address PR #225 follow-up review feedback

- lint-artifact: scope the indigo token-strip to <style> blocks so the
  rule-shaped regex no longer captures leading `<style>` text into the
  selector (which broke `:root` recognition for token blocks that mix
  `color-scheme`/etc. with `--accent`). Run the strip on the extracted
  CSS instead, with a regression covering `:root { color-scheme: light;
  --accent: #6366f1 }`.
- lint-artifact: tighten the custom-property-only exemption to global
  theme-scope selectors (`:root`, `html`, `body`, bare attribute
  selectors like `[data-theme="dark"]`). Component-local rules such as
  `.cta { --cta-bg: #6366f1 }` are no longer exempted, so an agent
  cannot launder default indigo through a local var. Regression test
  added.
- craft/anti-ai-slop.md: stop claiming every rule below is enforced by
  the linter; only several are. The unenforced rules (standard
  Hero→Features→Pricing→FAQ→CTA flow, decorative blob/wave SVG
  backgrounds, perfect symmetry) are now flagged inline as
  "(guidance, not auto-checked)" so the contract with the lint surface
  stays honest.

* fix(daemon): tighten lint-artifact iteration and :root token gating

- all-caps-no-tracking: iterate every <style> block. The previous
  check called `exec` once on a non-global regex, so an artifact
  whose offending uppercase rule sat in a second <style> block
  (e.g. a reset block followed by a components block) slipped
  past. Switch to `matchAll` and break across both loops once a
  violation is found. Regression test covers a second-block
  uppercase rule.
- ai-default-indigo: stop unconditionally exempting any selector
  list containing `:root`. The exemption now requires both
  conditions to hold: every selector in the list is global theme
  scope AND the body is token-shaped (CSS custom properties or
  the `color-scheme` keyword). So `:root { background: #6366f1 }`
  and `:root, .cta { --cta-bg: #6366f1 }` no longer launder a
  hardcoded indigo through the strip pass. Regression tests cover
  both bypass shapes.

* fix(daemon): scope theme-attr exemption and strip CSS comments in token blocks

Address PR #225 review feedback on `ai-default-indigo`:

- The bare-attribute branch of `selectorListIsGlobalThemeScope` accepted
  any `[attr=...]` selector, so a custom-property-only rule on a
  component/state attribute (e.g. `[data-variant="primary"]`,
  `[aria-current="page"]`) was treated as a global theme block and
  stripped before the indigo scan — exactly the component-local indigo
  laundering this lint is meant to catch. Restrict the exemption to a
  small allowlist of known theme switches: `data-theme`,
  `data-color-scheme`, `data-mode`.
- `stripTokenBlocksFromCss` split rule bodies on `;` and matched each
  fragment from the start, so a token block whose body contained a
  normal CSS comment such as `:root { /* brand accent */ --accent:
  #6366f1; }` produced a fragment beginning with the comment, failed
  `isTokenShapedDeclaration`, and the rule was left in scope of the
  indigo scan — a false P0 on a legitimate token definition. Strip CSS
  comments before splitting/classifying declarations.

Add regression coverage: arbitrary component/state attribute selectors
still trip `ai-default-indigo`; `data-color-scheme` theme variants stay
exempted; `:root` token blocks with leading, trailing, and
between-declaration CSS comments are recognized.

* fix(daemon): strip CSS comments and recognize tokens nested in at-rules

The all-caps-no-tracking scan ran against raw `<style>` content, so a
commented-out rule like `/* .eyebrow { text-transform: uppercase; } */`
matched `upperRe` and emitted a P1 for CSS the browser ignores. Strip
CSS comments from the style body before structural matching.

`stripTokenBlocksFromCss` only matched flat `selector { body }` rules,
so a media-query-wrapped token block like
`@media (prefers-color-scheme: dark) { :root { --accent: #6366f1 } }`
had its outer `@media` rule treated as the selector/body pair and the
inner `:root` token block was never stripped, producing a P0 false
positive on legitimate responsive theme CSS. Tighten the body
alternation to `[^{}]*` so the regex matches innermost rules and
recognizes the inner `:root` block directly while preserving the
outer at-rule wrapper.

* fix(daemon): align ai-default-indigo list with documented cardinal sins

The lint's AI_DEFAULT_INDIGO subset omitted #3730a3 and #a855f7, which
craft/anti-ai-slop.md lists as P0-blocked solid accents. An artifact
could hard-code one of those documented colors as a button fill and
slip past the indigo scan unless it happened to be inside a gradient.

Bring the lint set to the exact list documented in the craft doc, and
tighten the doc's wording from "etc." to an explicit enumeration that
points at AI_DEFAULT_INDIGO so the prompt contract and daemon behavior
stay in sync. Add regression tests pinning each newly-included hex.

* fix(daemon): tighten theme-scope selector and scan inline ALL CAPS

The theme-scope exemption used to accept any attribute on `:root`,
`html`, or `body` (e.g. `:root[data-variant="primary"]`), letting an
agent launder default indigo through a component/state attribute and
slip past the `ai-default-indigo` lint. The prefixed branches now
require the attribute name to be one of GLOBAL_THEME_ATTRIBUTES,
matching the bare-attribute branch.

The `all-caps-no-tracking` rule only iterated `<style>` blocks, so
inline declarations like `<span style="text-transform: uppercase">`
produced no finding even though craft/typography.md treats the
≥0.06em tracking floor as having no exceptions. Added a second scan
over `style="..."` attributes that runs the same letter-spacing
check and dedupes against the existing `<style>`-block finding so
the agent gets a single corrective signal per artifact.

* fix(daemon): align uppercase tracking px floor with the 0.06em rule

The previous absolute fallback (>=1.5px) was stricter than the craft
rule it enforces. `font-size: 12px; letter-spacing: 1px` is 0.083em
— above the 0.06em floor — but 1.5px would reject it and trigger an
unnecessary correction loop on compliant small-label CSS.

Extract `hasAdequateUppercaseTracking`: read `font-size` from the same
rule body and compare px tracking against `fontSize * 0.06`; fall back
to a conservative >=1px floor when font-size is inherited (covers the
default 16px body where 1px ≈ 0.0625em). Apply the helper to both the
<style>-block scan and the inline-style scan, and add 12–14px label
tests in both branches.

* fix(daemon): treat rem letter-spacing as absolute, not per-element em

`rem` was previously folded into the same branch as `em` and accepted
at the 0.06 threshold. But `rem` is relative to the root font-size
(16px default), not the element's own font-size, so on a 48px heading
`letter-spacing: 0.06rem` resolves to 0.96px — about 0.02em of the
element, well below the 0.06em rule the lint enforces.

Convert rem to absolute px through the 16px root assumption and reuse
the same px-vs-element-font-size resolution: same-rule `font-size: <n>px`
gives an exact `n * 0.06` floor; otherwise the conservative >=1px
fallback applies. Add regression tests for 48px headings with 0.06rem
tracking (must flag) plus the 16px-element and rem-floor matches that
must keep passing, in both <style>-block and inline-style branches.

* fix(daemon): resolve var() refs in uppercase tracking lint

`hasAdequateUppercaseTracking` only matched literal numeric values,
so a tokenized rule like `letter-spacing: var(--caps-tracking)` —
exactly the pattern the craft prompt steers artifacts toward — was
falsely reported as `all-caps-no-tracking`. Extract `--name: value`
declarations from global theme scopes (`:root`, `html`, theme-attribute
selectors) once per artifact, then expand simple `var(--name)` (and
`var(--name, fallback)`) references in the inspected rule body before
applying the existing 0.06em / px-floor / rem-conversion logic.
References without a matching token and no fallback stay in place,
preserving the conservative "missing tracking" finding.

* fix(daemon): resolve rem and var() font-size in uppercase tracking lint

Previously the px-vs-element-font-size resolution only matched
`font-size: <n>px`. Any rem-based or tokenized display size fell
through to the lenient `>= 1px` body-text fallback, so an artifact
emitting `.display { font-size: 3rem; text-transform: uppercase;
letter-spacing: 1px; }` (a ~48px heading with a 2.88px floor) slipped
past the lint that this helper exists to enforce.

Resolve `rem` font-size via the same root-font assumption already used
for tracking, and treat any explicitly declared but unresolvable unit
(`em`, `%`, `calc(...)`, an unresolved `var(...)`) conservatively —
refuse the lenient fallback so the rule must use either an `em`
letter-spacing or a verifiable px/rem font-size.

`var()` font-size declarations resolve through the existing
`resolveCssVars` pass before the size scan runs, so the same fix
catches the tokenized-display-size pattern (`--display-size: 3rem`).

* fix(daemon): parse declarations to ignore custom-prop names in uppercase tracking lint

The hasAdequateUppercaseTracking and resolveFontSizePx helpers used substring regexes against the rule body, so a token-name declaration such as `--letter-spacing: 0.08em` or `--display-font-size: 48px` could satisfy the `letter-spacing` / `font-size` checks even though it has no rendered effect — letting actual ALL-CAPS-without-tracking rules slip past the P1 lint.

Parse the declaration list, compare exact property names, and skip declarations whose property starts with `--`. Adds regression tests covering token-name letter-spacing (style-block + inline) and a token-name font-size masking the bail-out branch.

* fix(daemon): scope indigo token exemption to --accent only

Previously stripTokenBlocksFromCss removed every custom-property-only
global theme block before the ai-default-indigo scan, which let a
laundered indigo token like `:root { --primary: #6366f1 }` consumed
via `var(--primary)` slip past the lint. The craft contract is that
the only escape hatch is encoding indigo as the design system's
`--accent` token; any other token name is still the LLM-default
color hidden behind an arbitrary name. Narrow the strip pass so a
non-`--accent` token whose value carries an AI-default indigo hex
keeps the rule in scope, and add regression tests for `--primary` /
`--button-bg` global tokens feeding a CTA, including the at-rule
and theme-attribute variants.

* fix(daemon): model CSS cascade in tracking lint and detect blue→cyan trust gradients

Address PR #225 review feedback (3 comments):

- `letter-spacing` / `font-size` selection now picks the LAST matching
  declaration in the rule body, modeling CSS source-order cascade.
  `.eyebrow { letter-spacing: 0.08em; letter-spacing: 0.02em }` renders
  the noncompliant 0.02em the browser actually shows; the previous
  first-match behaviour silently passed it.
- `extractCssTokens` now records every distinct value seen for a token
  across global theme scopes, and `hasAdequateUppercaseTracking`
  enumerates each combination so a default-theme value below the floor
  cannot be rescued by a scoped override that happened to be parsed
  later (`:root { --caps-tracking: 0.02em }` +
  `[data-theme="dark"] { --caps-tracking: 0.08em }` now fires).
- New `trust-gradient` P0 rule pairs blue/sky tokens against cyan
  tokens in `linear-gradient(...)` bodies so `blue→cyan` two-stop
  trust gradients (documented as a cardinal sin in
  `craft/anti-ai-slop.md`) are actually enforced — both the hex form
  (`linear-gradient(90deg, #3b82f6, #06b6d4)`) and the keyword form
  (`linear-gradient(90deg, blue, cyan)`).

Adds 11 regression tests covering each path (cascade override in
<style> and inline form, font-size cascade shifting the floor, both
orderings of the conflicting-token cascade, the don't-over-fire case
when every theme value clears the floor, hex / keyword / sky variants
of the trust gradient, and the don't-double-fire case when
purple-gradient already caught a mixed gradient).

* fix(daemon): apply per-scope cascade in extractCssTokens

When the same CSS custom property is declared more than once inside a
single rule body (e.g. `:root { --caps-tracking: 0.02em;
--caps-tracking: 0.08em }`), CSS source-order cascade collapses to the
last value; the earlier declaration never reaches any element.
`extractCssTokens` was treating intra-scope duplicates as simultaneous
theme alternatives, so `hasAdequateUppercaseTracking` enumerated the
stale 0.02em and emitted a spurious all-caps-no-tracking finding.

Collapse duplicate token declarations within a rule body to the last
value before merging into the cross-scope distinct-value map. Cross-scope
overrides (separate `:root` and `[data-theme]` rules) remain preserved
as distinct values so the conservative theme-cascade check still fires
when ANY applicable theme renders below the floor.

* fix(daemon): scope tracking lint to innermost rules and per-theme tokens

Restrict the upperRe body alternation to [^{}]* so the regex matches
innermost CSS rules and skips at-rule wrappers — an outer @media or
@supports could otherwise capture as a single rule whose selector was
the at-rule and whose body began with the inner selector token, masking
the same-rule font-size and letting noncompliant tracking on large
headings slip through the lenient inherited-size fallback.

Replace the by-name-distinct-values token map with per-scope token
records and a buildResolvedThemes pass that materializes one effective
map per theme. Paired token declarations now stay paired during
evaluation, so theme variants like :root + [data-theme=dark] no longer
generate cross-theme cartesian pairings (e.g. default-size + dark-track)
that emit false positives on legitimate light/dark themes.

---------

Co-authored-by: looper <looper@open-claude.dev>
2026-05-02 11:00:33 +08:00
85714d58c8 fix: daemon OD_DAEMON_URL uses port 0 instead of actual allocated port (#240)
When tools-dev launches the daemon with OD_PORT=0 (letting the OS pick a
free port), app.listen correctly resolves the actual port in its callback,
but the enclosing closure still references the original `port` parameter
(value 0). This causes OD_DAEMON_URL to be set to http://127.0.0.1:0 in
the environment passed to agent subprocesses, breaking media generation
and any daemon callback that relies on isLocalSameOrigin.

Introduce a mutable `resolvedPort` variable, assigned from `actualPort`
inside the listen callback. All downstream consumers (OD_DAEMON_URL,
isLocalSameOrigin calls) now read the resolved value.

Co-authored-by: yyh <yyh@test.cn>
2026-05-02 10:25:57 +08:00
VitaminBFFMandGitHub a263a9c11e fix(daemon): quote agent bin path when spawning with shell:true on Windows (#232)
When `useShell` is true (Windows + .cmd/.bat shim), Node.js spawns the
agent via `cmd.exe /d /s /c "<bin> <escaped-args>"`. Node escapes argv
items but does NOT quote the bin path itself — that is the caller's
responsibility. If `resolvedBin` contains spaces (the common case for
npm shims under user directories with a space in the account name, e.g.
`C:\Users\First Last\AppData\...\claude.CMD`), cmd.exe parses up to the
first space as the command and the rest as arguments, then fails with
`The system cannot find the file 'C:\Users\First'` (or the localized
equivalent). The agent process exits in ~0.1s with code 1 and the chat
surfaces the cmd.exe error verbatim.

The Node DEP0190 deprecation warning that fires alongside the original
spawn is the same root cause documented from the runtime side.

Wrap `resolvedBin` in double quotes when `useShell` is true so the full
path stays a single token. The `/s` flag that Node already passes to
cmd.exe handles the resulting `cmd /d /s /c ""path with spaces" args"`
correctly.
2026-05-02 10:20:51 +08:00
513bd4edea feat(web): pick prompt templates (not design systems) for image/video projects (#192)
* feat(web): pick prompt templates (not design systems) for image/video projects

The New Project panel now shows the curated prompt-template gallery in the
image and video tabs instead of the Design System picker. Design systems
only make sense for prototypes, slide decks, templates, and the freeform
"other" canvas — they don't map onto image/video generation.

The picked template's body is editable in-line ("optimize" affordance) and
the (possibly tuned) prompt is snapshotted into ProjectMetadata as
`promptTemplate`. The system-prompt composer surfaces it on every turn as
a stylistic + structural reference for the agent — same shape we already
use for the saved-template flow.

Also rename the right-side gallery tabs from "Image prompts" / "Video
prompts" to "Image templates" / "Video templates" across all locales.

Made-with: Cursor

* fix(prompt-templates): address review — escape fences, error UI, empty hint, tests

Apply the actionable feedback from the code review:

- Security: escape triple-backticks in `metadata.promptTemplate.prompt`
  before interpolating it into the system prompt's fenced block. A user
  who pasted ``` into the editable template body could otherwise close
  the fence and inject free-form instructions for the agent. Apply the
  same fix in both the contracts composer and the daemon mirror.
- UX: surface fetchPromptTemplate failures via an inline error banner
  outside the popover, with a one-click retry button bound to the last
  failed pick. Previously the error toast lived inside the popover and
  vanished as soon as the popover closed.
- UX: show a subtle hint below the prompt textarea when the body is
  empty, so users who clear it on purpose understand the agent will
  have no template reference.
- Defense in depth: gate the `**referenceTemplate**:` metadata bullet
  on a non-empty prompt body in both composers, so a stale title can
  never appear in the system prompt without the body it claims to
  reference.
- Tests: add tests/system-prompt-template.test.ts covering the happy
  path (image + video), the backtick escape, the truncation cap, the
  empty-prompt skip, the non-media kinds, and the missing-source path.
- i18n: add `promptTemplates.retry` and `newproj.promptTemplateBodyEmpty`
  across all 9 locales; backfill the prompt-template picker keys for
  ja, es-ES, de which landed on main after this branch.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 23:31:31 +08:00
lakatosandGitHub abdd4119b2 fix(daemon): clamp codex reasoning effort to model-supported values (#223)
* fix(daemon): clamp codex reasoning effort to model-supported values

Codex's CLI accepts `none | minimal | low | medium | high | xhigh`, but
real models support narrower subsets — picking Minimal in OD's settings
made every chat turn fail with `agent exited with code 1` because the
OpenAI Responses API rejects `reasoning.effort=minimal` on the gpt-5.2
through 5.5 family with `unsupported_value`.

Add a client-side clamp that maps the user's pick to one the chosen
model accepts (Minimal -> low for gpt-5.5 family, xhigh -> high for
gpt-5.1, gpt-5.1-codex-mini caps at medium). Default / unknown model
is treated as gpt-5.5 since that's codex's current default. The user's
UI pick stays valid; the wire request gets a level the model can use.

* docs(daemon): clarify clampCodexReasoning unknown-model behavior

Per PR review — make explicit that unknown / future model ids pass
through unclamped on purpose. The server error then surfaces as the
signal that a new model family rule should be added here, rather than
silently downgrading every unrecognized id.

* test(daemon): cover codex reasoning-effort clamp matrix

Adds a table-driven test in apps/daemon/tests/agents.test.ts that drives
clampCodexReasoning through the public buildArgs surface. The 18 cases
cover:

- minimal -> low for default / explicit gpt-5.2-5.5
- low / medium / high pass-through on gpt-5.5
- xhigh -> high for gpt-5.1
- gpt-5.1-codex-mini cap (low/minimal -> medium, high/xhigh -> high)
- unknown / future models pass through unchanged
- 'default' reasoning omits the -c flag entirely
- path-style model ids (vendor/gpt-5.5-foo) clamp correctly

Tests stay at the wire level (asserting on the args array codex.buildArgs
produces), keeping the helper non-exported and matching the rest of the
file's style.

* docs(daemon): clarify gpt-5.1-codex-mini accepts medium / high

Per PR review — the original "caps at `medium`" wording was misleading
because the clamp returns `high` for `high` / `xhigh` inputs, which
contradicts the comment. The model actually accepts `{medium, high}`,
so describing it as "accepts only `medium` / `high`" matches the
function's behavior.

Logic unchanged.
2026-05-01 22:58:34 +08:00
Pavel CherkashinandGitHub 8c90ef6f55 fix(daemon): deliver Claude Code prompt via stdin to avoid spawn E2BIG/ENAMETOOLONG (#143)
Claude Code is the last agent that still embeds the composed prompt in
argv (`-p <prompt>`). Composed prompts (system prompt + design system +
skill body + user message) routinely exceed:

  - Linux  : MAX_ARG_STRLEN ~128 KB per argv entry → `spawn E2BIG`
  - Windows: CreateProcess command-line cap ~32 KB direct, ~8 KB via
             `.cmd` shim → `spawn ENAMETOOLONG`

the prompt through `child.stdin` (`promptViaStdin: true`). Claude was
explicitly excluded at the time. #97 mitigated the Windows case for
Claude with a `.od-prompt.md` temp-file fallback, but that path still
leaves a stray file in the project, doesn't trigger on Linux, and is
slower than the stdin route the other agents already use.

This change brings Claude in line with the rest:

  - Drop the prompt from argv; keep the bare `-p` print-mode flag
  - Set `promptViaStdin: true` so server.ts pipes `composed` on stdin
  - Add a regression test that asserts the prompt body never lands in
    argv even at 200 KB

Verified locally that `claude -p --output-format stream-json --verbose`
(no positional prompt) reads the prompt from stdin under the default
`--input-format text`, which has no length cap.

Fixes #82
Refs #100, #52
2026-05-01 22:45:10 +08:00
AresdgiandGitHub 59e4966dda feat(version): add app version awareness (#204)
* feat(version): add app version awareness

* fix(version): detect packaged sidecars across platforms
2026-05-01 17:26:54 +08:00
PerishFireandGitHub f604ff1ec2 Add Windows beta packaging and release assets (#191) 2026-05-01 16:46:15 +08:00
GeorgePSparkandGitHub 534349aa4c feat(daemon): add Kiro CLI agent adapter (#185)
* feat(daemon): add Kiro CLI agent adapter

* docs: add Kiro CLI to README and agent-adapters

* test(daemon): add kiro fetchModels fallback test
2026-05-01 12:25:26 +08:00
74344a370f fix(daemon): add CORS header to raw project file endpoint (#140)
* fix(daemon): add CORS header to raw project file endpoint

srcdoc iframes have a null origin. When preview HTML fetches component
files from /api/projects/:id/raw/*, the browser blocks the response
because the route returned no Access-Control-Allow-Origin header.

Only respond with the header when the request Origin is the string
"null" — the signature of a srcdoc iframe. Real cross-origin requests
from other websites remain blocked, keeping the endpoint safe even if
the daemon is deployed on a remote server.

Fixes #134

* fix(daemon): add CORS header to raw project file endpoint

srcdoc iframes have a null origin. When preview HTML fetches component
files from /api/projects/:id/raw/*, the browser blocks the response
because the route returned no Access-Control-Allow-Origin header.

Only respond with the header when the request Origin is the string
"null" — the signature of a srcdoc iframe. Real cross-origin requests
from other websites remain blocked, keeping the endpoint safe even if
the daemon is deployed on a remote server.

Also add an OPTIONS preflight handler to future-proof the route for
artifacts that may send custom request headers, and add test coverage
for the null-origin allow / real-origin block behaviour.

Fixes #134

---------

Co-authored-by: jiangding001 <jiangding001@ke.com>
2026-05-01 10:39:51 +08:00
ccfuncyandGitHub 6e9f3cda73 fix(providers): proxy Anthropic-compatible streams (#180) 2026-05-01 10:25:33 +08:00
3f266103b0 feat(media): port generation workflow onto main (#12)
Co-authored-by: Elian <elian@EliandeMacBook-Pro.local>
2026-04-30 22:44:00 +08:00
CaprikaandGitHub 454e8373fb [feat] Add Vercel self deploy flow (#167)
* Add Vercel self deploy flow

* Fix Vercel deploy file state and nested assets

* Add deploy hook script injection
2026-04-30 20:39:06 +08:00
PerishFireandGitHub a40d817d28 Add mac packaged runtime and beta release flow (#170)
* feat(pack): add mac packaged runtime control plane

* feat(pack): harden mac packaged runtime lifecycle

Keep packaged state namespace-scoped, make daemon paths explicit through sidecar launch env, and add conservative desktop identity/logging fallbacks for local mac package validation.

* feat(pack): add mac beta release flow

* fix(pack): generate mac update feed fallback

* fix(pack): write portable beta checksums

* fix(pack): make beta artifacts portable

* fix(pack): clean up mac install visuals

* fix(pack): address packaged runtime review feedback
2026-04-30 20:25:49 +08:00
3fb849d047 Fix chat runs surviving web disconnects (#146)
* fix chat runs surviving web disconnects

* fix chat run create abort propagation

Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5)

* fix daemon keepalive reconnect budget

Generated-By: looper 0.0.0-dev (runner=fixer, agent=gpt-5.5)

* fix daemon stream disconnect cancellation

Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5)

* fix daemon stream abort cancellation race

Generated-By: looper 0.0.0-dev (runner=fixer, agent=openai/gpt-5.5)

* fix daemon run cancellation semantics

* fix load

* doc

* 2

* add run refresh recovery

* fix active run refresh status

* fix reattach abort handling

* fix

* fix chat initial scroll

* fix daemon start failures

Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5)

* fix background run recovery

Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5)

* fix stop run status

Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5)

* fix background run recovery

Generated-By: looper 0.2.7 (runner=fixer, agent=openai/gpt-5.5)

* extract daemon run service

* move prompt composition to daemon

* fix prompt module resolution

* fix project id generation

* add project run status

* add designs kanban view with awaiting_input status

- add grid/kanban view toggle on Designs tab; persist choice in localStorage
- introduce awaiting_input project display status (daemon-derived from
  unanswered <question-form>) so projects asking the user aren't shown
  as Completed; ordered between Running and Completed with amber accent
- hide transient queued state from users: coerce queued/starting to
  running in daemon /api/projects projection and drop the queued kanban
  column
- a11y polish on Designs cards: Space activation, aria-labels on delete,
  focus-visible outlines, reveal delete on focus-within and touch,
  prefers-reduced-motion handling
- kanban layout uses flex sizing instead of viewport math; scoped icon-
  only pill button rule fixes view-toggle icon alignment

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-04-30 20:16:46 +08:00
AresdgiandGitHub f430a68766 feat: add markdown and svg artifact renderers (#73)
* feat: add markdown and svg artifact renderers

* fix: harden markdown preview rendering

* fix: address markdown renderer review follow-ups
2026-04-30 20:05:00 +08:00
Xinmin ZengandGitHub 132adac3bb fix(daemon): preserve non-ASCII filenames on multipart upload (#166)
* fix(daemon): preserve non-ASCII filenames on multipart upload

multer@1 hands callers latin1-decoded multipart filenames, and
sanitizeName() then collapses every non-ASCII character to '_'. A
Chinese DOCX uploaded as 测试文档.docx therefore landed on disk as
____.docx, while the response shipped a latin1-mangled originalName
back to the client. The chat composer compared that to the UTF-8
File.name from the picker, missed the match, and reported "some
files could not be stored" even though the bytes were already on
disk.

Add decodeMultipartFilename() that round-trip-checks latin1->utf8
so genuine latin1 names aren't corrupted, and switch sanitizeName()
to keep \p{L}\p{N} (Unicode letters/digits) while still stripping
path separators, control characters, and Windows-reserved
punctuation. Apply the decoder to all three multer storage filename
callbacks so /api/upload, /api/import/claude-design, and
/api/projects/:id/upload all return UTF-8 originalNames.

Closes #144

* fix(daemon): address review feedback on filename decoder

Three changes from review:

1. decodeMultipartFilename now bails out early when any code point in
   the input exceeds 0xFF. multer can hand us an already-decoded UTF-8
   string when the client uses the RFC 5987 `filename*` parameter, and
   the previous round-trip-only check would corrupt those names — for
   example, a properly decoded `测试文档.docx` could be re-mangled into
   `KՇc.docx` because the latin1-encoded bytes happened to form a
   valid UTF-8 sequence. Code points above 0xFF can never appear in a
   genuine latin1-decoding-of-utf8 input, so they're an unambiguous
   signal that no further decoding should run.

2. decodeMultipartFilename now handles null/undefined defensively (it
   was relying on multer always populating originalname; this just
   makes the helper safer to reuse from other call sites).

3. The inline sanitiser regex in the `upload` and `importUpload`
   multer instances is replaced with a direct `sanitizeName()` call,
   matching what `projectUpload` already did. This keeps a single
   source of truth for the allowed character set so future tweaks
   only have to land in one place.

Adds two more tests to the existing sanitize-name suite covering the
above-0xFF early return and the null/undefined inputs (13/13 in that
file, 58/58 across the daemon).
2026-04-30 19:49:43 +08:00