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
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.
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
* 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.
* 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>
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>
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.
* 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>
* 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.
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
* 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>
* 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).
resolveProjectRoot only stripped a 'dist' suffix from the caller's
module directory, so when the daemon sidecar is started by tools-dev
(which runs apps/daemon/src/server.ts directly through tsx) the
computed PROJECT_ROOT pointed at apps/ instead of the repo root.
That made SKILLS_DIR, DESIGN_SYSTEMS_DIR, STATIC_DIR and the default
RUNTIME_DATA_DIR all resolve to non-existent paths, so /api/skills
returned an empty list and the web "examples" page reported
"No skills available. Is the daemon running?".
Treat 'src' the same as 'dist' so resolution works for both the tsx
source entry and the compiled build, and add a regression test for
the src case alongside the existing dist/daemon-root cases.
* feat(daemon): add pi coding agent adapter
Add pi (https://pi.dev) as a supported coding agent, using its
--mode rpc JSON-RPC protocol over stdio for structured event streaming.
Changes:
- apps/daemon/pi-rpc.js: new RPC session handler that drives pi's
--mode rpc protocol, translating typed agent events (text_delta,
thinking_delta, tool_use, tool_result, usage, status) into the
daemon's UI event format. Auto-resolves extension UI requests
(fire-and-forget consumed, dialogs auto-approved) so pi stays
unblocked in the headless web UI. Kills the process after agent_end
since pi's RPC process is designed for multi-prompt sessions.
- apps/daemon/agents.js: add pi agent definition with custom
fetchModels (pi --list-models outputs to stderr, not stdout),
575+ models from 20+ providers, reasoning/thinking level support
via --thinking flag, and streamFormat 'pi-rpc'.
- apps/daemon/server.js: wire pi-rpc stream format to
attachPiRpcSession; skip stdin.end() for pi-rpc since the RPC
session manages stdin bidirectionally.
- apps/daemon/acp.js: export createJsonLineStream for reuse by
pi-rpc.js.
- apps/daemon/pi-rpc.test.mjs: 19 unit tests covering model list
parsing (TSV, dedup, edge cases), RPC event translation (text,
thinking, tools, usage, compaction, retry), sendCommand wire
format, extension UI auto-resolution.
- e2e/tests/structured-streams.test.ts: add pi RPC tool_use/tool_result
event mapping test alongside existing Claude/Copilot fixtures.
Verified end-to-end: daemon /api/chat → pi RPC → SSE stream with
status, text_delta, usage, and tool events. Live E2E test passes
(OD_E2E_RUNTIMES=pi). All 59 project tests green.
* refactor(daemon): migrate pi-rpc to TypeScript
Follow upstream #118 TypeScript migration convention: rename
pi-rpc.js → pi-rpc.ts and pi-rpc.test.mjs → pi-rpc.test.ts
with @ts-nocheck header (same as all other daemon modules).
Import paths remain ./pi-rpc.js per NodeNext module resolution.
* fix(daemon): avoid duplicate usage events in pi-rpc handler
Pi emits both message_end and turn_end per turn, both carrying
usage data. Emitting from both handlers caused double-counting
in the UI and any consumer that aggregates usage.
Remove usage emission from the message_end branch since turn_end
is the canonical per-turn usage source. Keep tool call extraction
in message_end (unique data not available in turn_end).
Add regression test confirming exactly one usage event is emitted
when both message_end and turn_end carry usage for the same turn.
Addresses Copilot P2 review on PR #117.
* fix(daemon): scope pi RPC id counter per session, bump graceful shutdown
Move nextRpcId and sendCommand inside attachPiRpcSession as local
state, matching the pattern in acp.ts where nextId is scoped per
session. Prevents RPC id collisions across concurrent /api/chat
requests.
Bump post-agent_end SIGTERM grace period from 2s to 5s and make it
configurable via PI_GRACEFUL_SHUTDOWN_MS env var for resource-
constrained machines.
Add test confirming concurrent sessions get independent id sequences.
* fix(daemon): wrap parser.feed in try-catch in pi-rpc
Catch errors from parser.feed and route them through the
existing fail() handler instead of letting them propagate
as unhandled exceptions.
* feat(dev): add desktop tools-dev control plane
* refactor(sidecar): split Open Design contracts
Move Open Design-specific sidecar protocol definitions into @open-design/contracts so sidecar and platform can remain descriptor-driven primitives.
* refactor(daemon): organize package sources
Keep daemon app code, tests, and sidecar entrypoints in separate package directories so each layer can be built and verified independently.
* chore(repo): streamline maintenance entrypoints
Centralize agent guidance by directory and reduce root command chains while preserving the existing build scope.
* docs: translate agent guidance to English
* fix(sidecar): tolerate stale IPC sockets
Remove stale Unix socket files only after confirming no listener is active, so tools-dev can restart after unclean shutdowns.