Commit graph

90 commits

Author SHA1 Message Date
Tom Huang
da2b007a43
feat(daemon): add DeepSeek TUI as a code agent adapter (#439)
* feat(daemon): add DeepSeek TUI as a code agent adapter

Register `deepseek` (with `deepseek-tui` cargo-only fallback) in
AGENT_DEFS via `deepseek exec --auto [--model X] <prompt>` and plain-text
streaming. Ships `deepseek-v4-pro` / `deepseek-v4-flash` as fallback
model hints; users can paste any other id (incl. NIM / Fireworks /
SGLang routes) via the custom-model input.

Web UI gets a DeepSeek-blue gradient icon, label/alias mapping, and
docs/agent-adapters.md §5.9 documents the auth state, prompt-as-argv
Windows size limit, and the upstream gap that prevents stdin delivery
today (clap declares `prompt: String` as a required positional).

Adds .deepseek/ to .gitignore alongside the other per-agent runtime
data dirs so first-launch trust files don't leak into git.

* fix(daemon): drop unsupported deepseek-tui fallback bin

The `deepseek` dispatcher owns `exec` / `--auto`; `deepseek-tui` is the
runtime companion it invokes. Listing `deepseek-tui` in fallbackBins
advertised availability for a host that only had the TUI binary, but
buildArgs still emitted `<resolved> exec --auto <prompt>` — which
deepseek-tui itself doesn't accept, so the first /api/chat run would
fail. Upstream documents both binaries as required (npm and cargo paths
install them together), so the fallback didn't correspond to a supported
install. Pin the absence in the agents test and update docs §5.9 + the
adapter table to match.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): pre-flight DeepSeek TUI prompts against argv byte budget

DeepSeek's exec mode requires the prompt as a positional argv arg (no
`-` stdin sentinel upstream), so a fully composed OD prompt — system
text + history + skills + design-system content + the user message —
can blow Windows' ~32 KB CreateProcess limit (or Linux MAX_ARG_STRLEN
on extreme edges) and surface as a generic spawn failure instead of
a DeepSeek-specific, user-actionable message. The adapter now declares
`maxPromptArgBytes = 30_000` (leaves ~2.7 KB argv headroom for `exec
--auto --model <id>` and Windows quoting), and the /api/chat spawn
path checks the composed prompt against that budget before calling
`spawn`. Oversized prompts fail fast with `AGENT_PROMPT_TOO_LARGE`
and guidance to reduce skills/design context or pick an adapter with
stdin support.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* test(daemon): pin DeepSeek argv-budget guard with regression tests

The previous spawn-path guard inlined the byte-budget check in the
chat handler, so the only safety net for the DeepSeek argv-only
prompt-delivery shape was a static "the field exists" assertion —
nothing actually exercised the AGENT_PROMPT_TOO_LARGE path or the
short-prompt happy path. Extract the check into a pure
`checkPromptArgvBudget(def, composed)` helper in agents.ts, call it
from /api/chat before bin resolution (so the guard is order-
independent and fires regardless of whether the adapter binary is
on PATH in CI), and add a regression test that exercises both the
oversized-prompt branch (over the conservative under-Windows-
CreateProcess budget) and the short-prompt branch, plus a UTF-8
byte-vs-codepoint case and a stdin-adapter no-op case so the guard
can't silently regress or leak onto adapters that ship the prompt
over stdin.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): pre-flight DeepSeek prompts against Windows .cmd-shim quoting

The first-pass argv-byte guard only inspects the raw composed prompt, so
on Windows an npm-installed `deepseek` resolves to a `.cmd` shim and the
spawn path then wraps the call in `cmd.exe /d /s /c "<inner>"` with
every embedded `"` doubled by `quoteWindowsCommandArg`. A quote-heavy
prompt (code blocks, JSON-shaped skill seeds) under the 30,000-byte
budget can therefore still expand past CreateProcess's 32_767-char
`lpCommandLine` cap and surface as a generic spawn ENAMETOOLONG instead
of the DeepSeek-named, actionable `AGENT_PROMPT_TOO_LARGE` the budget
guard was meant to provide. Add a second pure helper
`checkWindowsCmdShimCommandLineBudget(def, resolvedBin, args)` that
mirrors the platform layer's per-arg quoting and recomputes the would-be
command line length whenever the resolved binary is a `.cmd` / `.bat`
shim, and call it from `/api/chat` after `buildArgs` / `resolveAgentBin`
so the same SSE error fires before `spawn`. Pin the new path with a
quote-heavy regression (prompt is under the byte budget but doubles
past the kernel cap) plus no-op tests for non-`.cmd` resolutions, null
bin, and stdin-only adapters so the guard can't drift back.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): extend DeepSeek argv guard to direct .exe Windows installs

The cmd-shim guard added in 9011361 early-returned for non-`.cmd` /
`.bat` resolutions, so a Windows host that resolved `deepseek` directly
to a `.exe` (cargo-installed CLI, hand-built release, anything outside
the npm shim path) bypassed the post-`buildArgs` budget check entirely.
Direct `.exe` spawns skip the `cmd.exe /d /s /c "<inner>"` wrap, but
Node/libuv still composes a CreateProcess `lpCommandLine` by walking
each argv element through `quote_cmd_arg` — every embedded `"` becomes
`\"`, backslashes adjacent to a quote get doubled. A quote-heavy
prompt (code blocks, JSON-shaped skill seeds) under the 30,000-byte
`maxPromptArgBytes` budget can therefore still expand past the kernel's
32_767-char cap on those installs and surface as a generic spawn
ENAMETOOLONG instead of the actionable `AGENT_PROMPT_TOO_LARGE` the
guard was meant to provide.

Add a sibling pure helper `checkWindowsDirectExeCommandLineBudget(def,
resolvedBin, args)` that mirrors libuv's quoting math (empty -> `""`;
no whitespace/quote -> pass-through; quote-only -> simple wrap;
otherwise per-char escape with backslash-doubling around quotes and
trailing backslashes) and recomputes the would-be command line length
whenever the resolved binary is a non-shim Windows install. The two
Windows guards are mutually exclusive: the cmd-shim guard owns
`.bat` / `.cmd`, the direct-exe guard owns everything else, so a
single oversized prompt never double-emits an SSE error. POSIX paths
on POSIX hosts still skip both guards (no CreateProcess in play), and
stdin-delivered adapters still skip the entire post-buildArgs path.

Wire the new helper into `/api/chat` after the cmd-shim check, pin
the path with a quote-heavy regression on a `C:\Program
Files\DeepSeek\deepseek.exe` resolved bin (prompt under the byte
budget but past the kernel cap once libuv-quoted), plus no-op tests
for ordinary prompts, `.cmd`/`.bat` resolutions, POSIX paths,
null/empty bin, and stdin-only adapters, plus a mutual-exclusivity
assertion so the two guards' division of labour can't drift.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): neutralize cmd.exe %var% expansion in DeepSeek argv

Wrap each `%` in `"^%"` inside `quoteWindowsCommandArg` so cmd.exe's
percent-expansion can't substitute env values into a `.cmd`-shim spawn
when the DeepSeek argv carries a prompt mentioning `%DEEPSEEK_API_KEY%`.
Mirror the change in the agents.ts helper used by the budget guard, add
`AGENT_PROMPT_TOO_LARGE` to the contracts so typed SSE consumers
recognize the existing daemon error code, and pin the literal-prompt
contract with regression tests.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)
2026-05-05 00:08:26 +08:00
emilneander
33c3b94b42
feat(daemon): add od mcp - expose Open Design as an MCP server (#399)
* feat(daemon): add `od mcp` subcommand for stdio MCP server

Lets a coding agent in a different repo (Claude Code, Cursor, Zed)
pull files from a locally-running OD project over the Model Context
Protocol — no export/import zip dance.

The MCP server is a thin stdio process that proxies read-only tool
calls to the daemon's existing HTTP API; no daemon-side changes
required. Exposes 8 tools:

  list_projects, get_project,
  list_files, get_file,
  list_skills, get_skill,
  list_design_systems, get_design_system

Wired exactly like `od media`: a hoisted flag set, a SUBCOMMAND_MAP
entry, a thin handler that resolves OD_DAEMON_URL and hands off to
src/mcp.ts. Tool dispatch is a switch over the tool name; each branch
fetches the matching daemon route and surfaces the response as MCP
text content. Binary mimes return a clear error pending phase-2
support.

Lifecycle gotcha worth flagging: Server.connect(transport) only
*starts* the stdio reader; the promise resolves immediately. Without
holding the function awaiting until transport/stdin close, cli.ts's
top-level process.exit(0) kills the server before the first request
arrives. The fix in src/mcp.ts holds until onclose / stdin EOF.

Wire-up example for a consuming repo:

    {
      "mcpServers": {
        "open-design": {
          "command": "od",
          "args": ["mcp"],
          "env": { "OD_DAEMON_URL": "http://127.0.0.1:7456" }
        }
      }
    }

New dep: @modelcontextprotocol/sdk (MIT, official Anthropic SDK).

* feat(daemon): add MCP server instructions for zero-shot LLM context

Hand the consuming LLM a system-prompt-style overview of the OD
workflow so it picks the right tool without prompt-engineering on
the user's side. Mentions get_artifact and project-name resolution
ahead of their actual implementation; both ship in the same batch.

* feat(daemon): resolve MCP project args by UUID, name, or substring

Lets a consuming agent say `project: "recaptr"` instead of pasting a
UUID. Match order: exact id → exact name (case-insensitive) →
slug-normalized name (strips trailing " (N)", normalizes whitespace) →
substring (errors if multiple). UUID inputs short-circuit and never
hit the daemon.

* feat(daemon): surface entryFile and kind on MCP get_project response

Promote metadata.entryFile and metadata.kind to top-level fields so
consumers (including get_artifact in this branch) can find the entry
without digging through nested metadata blobs.

* feat(daemon): add MCP get_artifact tool for bundle retrieval

A design rarely lives in a single file. get_artifact pulls the entry
HTML/JSX plus every sibling it references (tokens CSS, JSX modules,
imported components) in one call, so a consuming agent doesn't need
to parse HTML and round-trip per file.

Three modes:
  auto (default): BFS over relative <script src>, <link href>,
    <img src>, <source/video src>, JSX import/from, CSS url(), with
    depth cap 3 and a visited set. CDN, data:, mailto:, anchors, and
    paths containing .. are skipped.
  all:    every textual file in the project (mirror of /archive
          minus binaries).
  shallow: just the entry file (same as get_file).

Output is a structured JSON blob with name/mime/size/content per
file and the project's manifest metadata at the top.

* feat(daemon): add /api/projects/:id/search route + MCP search_files

Server-side substring search across textual project files. Returns
file, 1-indexed line, and snippet, capped at 1000 matches. Exposed
through the MCP layer as search_files(project, query, pattern?, max?).

Treats the query as a literal substring (regex chars escaped) to
avoid catastrophic-backtracking attacks from LLM-supplied input.
Honors the project dir's existing path-safety guards via listFiles.

* feat(daemon): add since= filter to /files route + MCP list_files arg

Lets a consumer poll for "what's changed since I last looked" without
re-walking every file. Daemon-side: parse since= as ms, filter
listFiles output by mtime. MCP-side: forward as URL query.

* feat(daemon): expose skills and design systems as MCP resources

Catalog reads are stable reference material — they fit MCP's
resources surface (LLM-passive) better than tools (LLM-active).
Skills and design systems each become resources at
od://skills/<id>/SKILL.md and od://design-systems/<id>/DESIGN.md;
existing list_skills / get_skill / list_design_systems /
get_design_system tools remain as fallbacks for clients that don't
handle resources cleanly.

* fix(daemon): tighten MCP correctness in get_artifact and resources

Several silent-failure paths and minor footguns the first pass missed:

  - get_artifact auto: the entry's own fetch now raises a clear
    error instead of returning files: []. Previously a typo in
    `entry:` looked like an empty project.
  - get_artifact: invalid `include` value returns a clear error
    listing the valid modes instead of silently behaving as auto.
  - get_artifact all: includes binary files as metadata stubs to
    match auto's behavior. Both modes are now strict supersets of
    shallow.
  - extractRelativeRefs: gate JS-only patterns (import/from/require/
    dynamic-import) by file mime/extension so prose in markdown or
    HTML doesn't generate spurious 404 round-trips on words like
    "imported from 'X'".
  - extractRelativeRefs: cover <iframe>, <audio>, srcset, and
    CSS @import — common in real OD output.
  - resources/list descriptions are collapsed to a single line
    (newlines + repeated whitespace -> one space) so MCP UIs that
    don't normalize whitespace render cleanly.
  - fetchProjectFile: 0-byte binary files no longer report size: null
    due to falsy short-circuit on Number(content-length).

* perf(daemon): cache MCP project list for 5s in resolveProjectId

A typical agent session calls list_files/get_file/get_artifact several
times in a row, each with a project name argument. Each previously
re-fetched /api/projects. Cache the list in module scope with a 5s
TTL so back-to-back lookups are local; renames in the OD UI still
propagate within a few seconds.

* feat(daemon): MCP UX polish — tool order, annotations, get_artifact maxBytes

Three changes well-behaved MCP clients pick up automatically:

  - Tool ordering. list_projects + get_artifact are now first; LLMs
    that weight earlier entries surface the bundle path before
    per-file fetching. Catalog tools (list_skills, get_skill,
    list_design_systems, get_design_system) sit at the bottom; they
    are also exposed as MCP resources.
  - readOnlyHint / idempotentHint / openWorldHint annotations on
    every tool so clients can skip confirmation prompts on safe
    tools and let the LLM know re-running is fine. Per-tool `title`
    annotations give clients a friendlier display name than the
    snake_case tool id.
  - get_artifact gains a `maxBytes` arg (default 1.5MB). Once the
    accumulated textual content crosses the cap, remaining files
    are dropped and `truncated: true` is set on the bundle so the
    consumer knows to use list_files / get_file for the rest.

* feat(daemon): expose user's active OD project/file via MCP

The "what file are you on?" round-trip the agent had to do every
session is now answered automatically. Three pieces:

  - Daemon: in-memory active-context slot with 5-minute TTL.
    POST /api/active sets {projectId, fileName}; GET /api/active
    returns the current value enriched with projectName, or
    {active:false} when the slot is empty/stale. Cleared on
    daemon restart.
  - Web: a small useEffect in App.tsx posts the active project +
    file to the daemon on every route change. Best-effort fire-
    and-forget; a missing daemon doesn't surface an error.
  - MCP: get_active_context tool (no args) and a matching MCP
    resource at od://focus/active. The tool is listed second,
    right after list_projects, so an LLM picks it up before
    asking for ids. Server instructions tell the model to call
    it FIRST when the user says "this file" / "the design I have
    open" / "what I'm looking at."

End to end: user opens a project in OD, agent in another repo
calls get_active_context() → gets {projectName: "recaptr",
fileName: "recaptr-onboarding-4.html"}, then immediately calls
get_artifact(project: "recaptr") with no further user input.

* feat(daemon): make MCP project arg optional, fall back to active OD context

get_artifact, get_project, get_file, search_files, and list_files now
accept project as optional. When omitted, the MCP resolves project
from /api/active so an agent in another repo can call

  search_files({ query: "Polaroid" })

without first asking the user "which project?". get_file and
get_artifact also default their path/entry to the active file, so
get_file({}) returns whatever the user is currently looking at.

The implicit path stamps `usedActiveContext` on JSON responses (or a
separate `[od:active-context …]` content block on get_file) so the
agent can see exactly which project/file got chosen. Explicit
project args pass through with zero added overhead.

Cuts the common case from two MCP round trips
(get_active_context → search_files) to one. Server instructions and
get_active_context's own description are updated to point at the
new default.

* fix(daemon): require same-origin for /api/active POST and GET

The active-context endpoint was added without isLocalSameOrigin
guard. Since the daemon binds 0.0.0.0 by default, a LAN peer could
GET it to learn what file the user has open, or POST it to redirect
the MCP fallback to a project of their choice. Same-origin only is
the right scope: the web app proxies its requests through Next.js
on the daemon port, and the MCP runs over loopback in-process, so
both legitimate callers pass.

Pattern matches the existing /api/app-config etc. guards.

* feat(daemon): add /api/mcp/install-info for cross-platform install snippets

The Settings -> MCP server panel needs absolute paths to node and
the daemon's built cli.js so it can render snippets that work on a
fresh source clone (where `od` is not on PATH) and dodge the
/usr/bin/od octal-dump tool that ships on macOS/Linux and would
otherwise shadow ours.

Endpoint returns:
  - command: process.execPath (the node binary running the daemon)
  - args: [<absolute path to dist/cli.js>, "mcp"]
  - daemonUrl: http://127.0.0.1:<port>
  - platform: process.platform (so the panel can localize ~/.cursor
    vs %USERPROFILE%\.cursor and Cmd vs Ctrl shortcuts)
  - cliExists / nodeExists: existsSync checks on both binaries
  - buildHint: human-readable build/reinstall instructions when
    either path is missing

isLocalSameOrigin guard same as /api/active. Cached for 5s because
the panel may re-fetch on every open and the paths cannot change
without a daemon restart.

Test file covers the happy path, cross-origin rejection, two
allowed-Origin variants, and the cache by counting fresh resolves
across rapid calls. 5/5 pass.

* refactor(daemon): tighten MCP surface, trim descriptions, polish copy

Three intertwined cleanups that all live in mcp.ts + cli.ts:

1. Drop catalog tools from MCP. list_skills / get_skill /
   list_design_systems / get_design_system are removed. The audience
   is a coding agent in a separate repo consuming Open Design's
   output; it cannot run skills (those are recipes Open Design uses
   to generate) and design-system DESIGN.md is reference material
   that already ships as an MCP resource. Keeping the catalog as
   tools cost ~350 token-overhead per turn for capabilities the
   agent could not act on. Tool count: 11 -> 7.

2. Trim tool descriptions. The active-context fallback explanation
   was repeated in 5 separate tool descriptions; hoisted into
   PROJECT_ARG and explained once in the server `instructions`
   block instead. Saves ~150-200 tokens per tools/list response.

3. User-facing branding pass. Tool titles, tool descriptions,
   resource names, error messages, comments, and `od mcp --help`
   now consistently use "Open Design" rather than "OD". Internal
   abbreviation `OD` is retained only inside the server
   instructions block where it is introduced inline as "Open Design
   (OD)" for compactness across multi-paragraph guidance.

Em dashes replaced with hyphens throughout, per project style.

* feat(web): add MCP server install panel in Settings

New "MCP server" section in the Settings dialog, surfacing
copy-paste install snippets for the major MCP-compatible coding
agents (Claude Code, Cursor, VS Code, Antigravity, Zed, Windsurf).

Highlights:
  - In-brand custom dropdown (reuses the existing .ds-picker
    pattern from the design-system / prompt-template pickers, click
    outside / Escape to close, chevron animates) instead of a
    native <select>.
  - Per-client snippet that uses absolute paths to node + cli.js
    fetched from /api/mcp/install-info on mount, so it works even
    when `od` is not on PATH.
  - Cursor gets a one-click "Install in Cursor" deeplink
    (cursor://anysphere.cursor-deeplink/mcp/install) that pops an
    approval dialog and writes the config for the user. UTF-8-safe
    base64 so paths with accented characters do not throw.
  - Per-OS path hints (~/.cursor on POSIX, %USERPROFILE%\.cursor
    on Windows) and keyboard shortcuts (Cmd vs Ctrl).
  - Build-required warning card when cli.js or the node binary
    does not exist on disk; deeplink button disables in that state.
  - Prominent "restart your client to pick up the new server"
    callout below the snippet, with per-client guidance.
  - Capability list ("what your agent can do") instead of a tool-
    name dump, so non-developer designers can also tell what is
    possible without reading MCP docs.

README adds a short "Use Open Design from your coding agent"
section that points at the panel and summarizes the per-client
flow (one-click for Cursor, JSON merge elsewhere). Read-only by
design; the daemon must be running locally.

* docs(readme): align MCP server section with the Settings panel

The "Use Open Design from your coding agent" section had drifted
from what the panel actually emits and lists.

- Add Antigravity to the supported-client list (previously missing).
- Drop the "(GitHub Copilot)" parenthetical from VS Code so the
  label matches the panel.
- Fix the Claude Code line: we no longer emit a single
  `claude mcp add ...` shell command. The snippet is JSON; the
  panel additionally suggests `claude mcp add-json` as the safer
  way to apply it instead of hand-editing ~/.claude.json.
- Swap the "find the Polaroid section" example for two more
  universal phrases ("build this in my app", "match these
  styles") that match what the panel surfaces.
- Add a one-line "restart or reload your client after install"
  note - this was prominent in the panel and absent from the
  README.
- Trim the /usr/bin/od octal-dump aside; it was technical detail
  that did not earn its space at the README intro level.

* feat(web): add Codex CLI to the MCP server install panel

Codex is a first-class supported coding agent (listed alongside
Claude Code, Cursor, etc. in the README's PATH-detected agent
table) but the install panel was missing it.

Codex stores MCP server config at ~/.codex/config.toml (TOML, not
JSON) under an `[mcp_servers.<name>]` table, and the same file is
shared between the Codex CLI and the Codex IDE extension - so one
install covers both. Added a 7th client entry that emits the right
TOML snippet, expanded the snippet-lang union to include 'toml'
(behaves like 'json' for whitespace handling, just a different
syntax-highlight hint).

For our minimal payload (just command + args), JSON.stringify
happens to produce valid TOML literal values since TOML basic
strings use the same double-quote escape rules as JSON, and TOML
inline arrays match JSON array syntax. No new TOML serializer
needed.

README updated to list Codex among the supported clients.

Schema verified against https://developers.openai.com/codex/mcp.

* fix(daemon): accept any loopback origin in same-origin guard

The previous port-pinned check required the request's Origin to match
either the daemon's own port or OD_WEB_PORT. tools-dev does not pass
OD_WEB_PORT to the daemon process, so any browser POST to /api/active
proxied through the dev web (port 17573 etc.) was rejected with 403,
and get_active_context always returned {active: false}.

Relax to a loopback-prefix match: any http://127.0.0.1:*,
http://localhost:*, or http://[::1]:* origin passes regardless of
port. Cross-origin (https://evil.com) is still rejected. The
trade-off is that another local web app on a different loopback port
could now CSRF the daemon; same-origin checks are inherently a CSRF
defense, not a network ACL.

* fix(web): make Claude Code MCP snippet a real copyable one-liner

claude mcp add-json open-design '<json>' takes only the inner
server-config object, not the full {"mcpServers": ...} wrapper, and
rejected the wrapped shape with "Invalid configuration: : Invalid
input". Pass only the inner config, and inline the JSON into the
command itself so the snippet is a real one-liner the user can copy
and paste, no template substitution.

* test(daemon): drop loopback-prefix assertions superseded by upstream origin policy

The two proxy-flow allow tests were added in ae13094 to cover our
relaxed isLocalSameOrigin. Main's port-pinned implementation (from
#365) now handles the dev-flow via the web sidecar proxy origin
rewrite (#a719f02), making the relaxation -- and these tests --
unnecessary.

Also replace the inline LOOPBACK_*_RE / isLocalSameOrigin replica in
mcp-install-info.test.ts with a direct import from server.ts so both
test files stay in sync with the production guard automatically.

* fix(daemon): bake daemon URL into MCP install-info args

The install panel snippet previously emitted `od mcp` with no daemon
URL, so the MCP server always fell back to the hardcoded default port
7456. When tools-dev starts the daemon on a non-default port the
snippet silently targets the wrong daemon.

Fix: include --daemon-url http://127.0.0.1:<port> as the third arg so
the generated snippet is always tied to the running daemon's actual
port. Update the matching mini-app and assertion in the install-info
test.

* fix(daemon): address MCP reviewer feedback

- extractRelativeRefs: replace blanket `includes('..')` rejection with
  proper POSIX-style path normalization. `../tokens.css` in a nested
  project layout now resolves to `tokens.css` instead of being
  silently dropped.

- getArtifact: add MAX_FILES=200 cap to BFS auto and include=all modes.
  Pass `remainingBytes` to fetchProjectFile so it can bail early when
  the server-advertised content-length would already exceed the budget.

- resolveProjectId: return {id, name, source} instead of a bare id.
  Callers echo `resolvedProject` in the response when the match was by
  slug or substring, letting the agent confirm which project was
  chosen without an extra round-trip.

- getFile: thread `resolved` through so substring matches surface
  the same `[od:resolved-project ...]` annotation.

- @ts-nocheck: add a comment explaining the Zod-vs-JSON-Schema SDK
  mismatch so future contributors don't remove it accidentally.

- get_active_context description: note the ~5-minute cache TTL.

* test(daemon): restore @ts-nocheck on mcp-install-info test

Dropped accidentally when replacing the import header. The directive
suppresses expected test-file noise (baseUrl pre-assignment and
res.json() unknown return type); keeping it avoids littering the test
body with `as any` casts for zero real safety benefit.

* docs(readme): expand MCP section with why-MCP, security model, and recovery note

- Soften "No zip export, no copy-paste" to "Replaces the
  export-then-attach loop" per reviewer feedback.
- Add "Why MCP?" paragraph explaining the structured-API benefit over
  zip exports.
- Add daemon-not-running recovery note (clear error, not a crash;
  start with pnpm tools-dev and retry).
- Add security model callout: read-only, loopback-only, Host/Origin
  guard rejects non-loopback requests.

* docs: complete security model and daemon recovery notes for MCP section

8.3: Expand README security model to include stdio child process context,
trust framing (treat like a VS Code extension), and OD_BIND_HOST opt-in
for LAN exposure.

8.4: Replace terse "daemon not running" note in README with a full
recovery sentence covering the start-agent-before-Open-Design case.
Add the same recovery note as a footer paragraph in IntegrationsSection
so users see it in the Settings panel without needing to read the README.

* fix(daemon): pass resolved through get_artifact so substring matches echo resolvedProject

* feat(daemon): add MCP unit tests and fill description/instructions gaps

- Export extractRelativeRefs, resolveProjectId, resolveProjectArg,
  withActiveEcho, fetchProjectFile, getArtifact for testing
- mcp-extract-refs.test.ts: 10 cases covering flat, nested, deep,
  escape attempts, external/data/anchor/mailto URLs, srcset
- mcp-get-artifact.test.ts: MAX_FILES=200 cap, maxBytes cap,
  per-file content-length pre-check via fetchProjectFile
- mcp-resolve-project.test.ts: uuid/exact/slug/substring source
  values, ambiguity error, withActiveEcho resolvedProject stamping
- get_artifact maxBytes description now mentions the 200-file cap
- Instructions block now mentions resolvedProject field and when it
  appears (slug or substring match)

* docs(daemon): document MCP active-context TTL and surface wake-up hint

Address PR #399 review item P2.5 (active-context TTL undocumented) plus
the related UX gap where the agent had no way to tell the user that
clicking around in Open Design refreshes the cache.

- PROJECT_ARG, get_artifact entry, get_file path: append TTL note to
  argument descriptions so agents see the ~5-minute fallback window.
- get_active_context: when /api/active reports active:false, return
  an explicit hint string explaining the recovery action ("ask the
  user to click into a project") instead of a bare {active:false}
  the agent can't act on.
- get_active_context tool description: mention the new hint payload.
- resolveProjectArg error: extend the missing-active-context message
  with the same TTL + recovery wording for tool calls that omit
  project= and have no fallback.

* feat(daemon): add offset/limit pagination to MCP get_file

Real-world MCP usage hit a wall on large files: get_file returned the
full body, the agent decided the result was too large for its context
budget, and recovered by spawning a sub-agent that ran Python with
manual brace-matching for several minutes. That defeats the value
proposition of skipping zip-export.

Mirror Claude Code's Read tool semantics: get_file now accepts
optional offset (0-indexed line) and limit (default 2000) args, slices
the file in mcp.ts after fetching from the daemon, and stamps an
[od:file-window offset=.. returnedLines=.. totalLines=..] marker on
sliced or truncated responses so the agent can page by re-calling
with the next offset.

- Tool definition: add offset/limit args, expand description.
- getFile helper: line-split, slice, marker, range clamp at EOF.
- Instructions block: mention pagination in the get_file bullet.
- Binary rejection unchanged.
- New tests in mcp-get-file.test.ts cover default behavior, limit
  truncation, mid-file offset, offset past EOF, and binary rejection.

* fix(daemon): set truncated: true when per-file content-length pre-check fires

When fetchProjectFile throws because a file's advertised content-length
exceeds the remaining byte budget, both the include=all loop and the auto
BFS loop silently skipped the file without setting truncated: true. The
bundle could then report truncated: false even though files were dropped.

Introduce BudgetExceededError as a sentinel so callers can distinguish a
budget rejection (truncated: true) from a genuine fetch failure (404,
network) that should just be skipped. Both getArtifact call sites now
check instanceof BudgetExceededError and set truncated accordingly.

Adds a regression test: 5 files of 250 bytes with explicit content-length,
maxBytes=400. Only file 0 fits; files 1-4 each exceed the remaining 150
bytes. totalTextBytes never reaches maxBytes, so only the new path sets
truncated=true. Previously the bundle reported truncated: false.
2026-05-04 22:34:17 +08:00
Daniel Duma, PhD
cfd359e05a
[codex] Fix Gemini CLI trust handling (#352)
* Fix Gemini CLI trust handling

* Preserve agent spawn env filtering
2026-05-04 21:39:59 +08:00
Bryan A
d637297313
feat(preview): live-reload iframes when project files change on disk (#409)
* feat(preview): live-reload iframes when project files change on disk

Add a chokidar-backed file watcher per active project on the daemon, surface
changes via an SSE endpoint at /api/projects/:id/events, and consume them in
the web app to bump the file list. The new mtime then propagates to the
FileViewer iframe through PR #384's ?v=${mtime} cache-bust, reloading the
preview automatically — no manual refresh click.

Daemon:
- New apps/daemon/src/project-watchers.ts: refcounted per-project watcher
  registry. First subscribe lazy-creates a chokidar watcher; last unsubscribe
  closes it. Ignores .git, node_modules, .od, debug, .DS_Store. Returns a
  ready promise so callers can await initial scan.
- New endpoint GET /api/projects/:id/events using the existing
  createSseResponse helper. Sends one ready event after chokidar binds, then
  one file-changed event per add/change/unlink.
- Adds chokidar ^5.0.0 dependency.

Web:
- New apps/web/src/providers/project-events.ts exposing
  createProjectEventsConnection (pure, testable) and useProjectFileEvents
  hook. EventSource with exponential backoff (1s -> 30s cap), reset on a
  successful ready event.
- ProjectView.tsx subscribes when daemonLive && project.id, and on each
  event bumps the existing filesRefresh signal — no FileViewer changes
  needed because PR #384 already URL-loads with mtime cache-bust.

Tests:
- 6 new daemon unit + integration tests (refcounting, real chokidar
  add/change/unlink, ignore patterns).
- 8 new web hook unit tests (URL encoding, payload parsing, malformed
  payload tolerance, exponential backoff, backoff reset on ready,
  close cancels reconnects, no-op when EventSource missing).

Closes #370

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(preview): test ignore patterns relative to watch root

The ignore predicate matched against absolute paths, so segments in the
watch root's ancestors (e.g. the daemon's own .od/ runtime dir, which
contains every project) silenced every event. In production this meant
zero file-changed events ever fired — every file inside a project sat
under .od/projects/<id>/, and .od matched the ignore.

Tests passed because mkdtemp puts test roots in /tmp/od-watchers-XXX/,
which has no .od ancestor.

Fix: compute the path relative to the watch root, then test segments.
Add a regression test that reproduces the production layout
(.od/projects/<id>/...) and asserts events still fire.

Also folds in a small consolidation of the SSE route handler from the
prior commit on this branch (single route, surfaces sub.ready before
emitting `ready`, propagates err.message in the error path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(project-view): only auto-open files that exist in the project

Agent Write/Edit tool results unconditionally called requestOpenFile on the
basename of the edited path, which created permanent placeholder tabs
("Open a file from Design Files.") whenever the agent edited a file outside
the project's working directory (e.g. an upstream repo source file).

Add decideAutoOpenAfterWrite() — a pure helper that gates the auto-open on
the file actually appearing in the refreshed project file list. Same
nextFiles-from-then() pattern already used at ProjectView.tsx:968.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(preview): chokidar resilience + auto-open path-suffix matching

Address PR #409 review feedback in one bundle:

- chokidar error listener (codex P1): FSWatcher is an EventEmitter;
  without an error handler, transient FS faults (ENOSPC, EPERM, EMFILE)
  surfaced as unhandled exceptions and could crash the daemon. Watcher
  now logs in dev mode and continues; refcount cleanup unaffected.
- followSymlinks: false (mrcfps): keep the watcher's resource boundary
  aligned with the project boundary so a symlink inside the project
  cannot traverse externally. Real-chokidar regression test included.
- decideAutoOpenAfterWrite path-suffix matching (mrcfps): pass the
  agent's full file_path through (not just the basename); resolve via
  path-suffix match against project file paths, with single-unambiguous
  basename fallback only when filePath has no slash. Fixes the
  same-basename collision case where an external Write to App.jsx
  could open a project's prototype/App.jsx.
- Dev-mode logs (lefarcen P3): warn when the broadcast subscriber
  loop or the SSE payload parser swallows an error, so subscriber
  bugs and payload-shape regressions don't go silent during testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: DevForgeAI CI/CD Engineer <devforge-ai@development.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 20:47:22 +08:00
Tom Huang
aefba56a3f
feat(skills): open-design-landing rename, kami skills, landing OG (#428)
* feat(skills): open-design-landing rename, kami skills, landing OG

- Rename editorial-collage skills to open-design-landing and -deck; refresh examples and compose script layout
- Add kami-deck and kami-landing skills with HTML examples
- Landing page: og.astro, index wiring, and style tweaks; package.json bump
- Web i18n: German and Russian copy for renamed and new skills
- Daemon test: update skill-asset-rewrite expectations for new paths
- Design systems: README and atelier-zero doc touch-ups
- Cross-skill SKILL.md reference updates

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

* docs(landing-page): document version-slot invariant and deprecation timeline

Address P3 review notes on PR #428:
- Note the `data-github-version` wrapper invariant (version string only)
  near the canonical URL block in `app/page.tsx`.
- Expand the `formatVersion` helper comment in `app/pages/index.astro`
  with concrete `release.name` / `tag_name` example shapes for each
  branch of the regex fallback.
- Tighten the `EditorialCollageDeckInputs` deprecation in
  `skills/open-design-landing-deck/schema.ts` to a specific removal
  version (v0.4.0) and add a "Migrating from editorial-collage-deck"
  section to the skill README.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(landing-page, skills): clarify version slot script and rename migrations

- Describe GitHub version slots as driven by the inline enhancement script,
  not React hydration.
- Add editorial-collage → open-design-landing migration notes; fix README
  link copy (Astro static landing app).
- Extend deck README migration table with shared asset path renames.

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

* fix(daemon): alias deprecated editorial-collage skill ids

The PR renames the editorial-collage / editorial-collage-deck skills
to open-design-landing / open-design-landing-deck, but the daemon
persists exact skill_id strings on projects and resolves them via
listSkills().find((s) => s.id === storedId). After the rename, any
project saved against an old id silently composes without the intended
skill prompt because the listing no longer exposes that id.

Add a SKILL_ID_ALIASES map in skills.ts plus a findSkillById() helper
that rewrites deprecated ids to their current canonical form, then
route every server-side lookup (skill detail, example HTML, asset
proxy, system-prompt composer) through it. Cover the alias map, the
resolver, and end-to-end resolution against a temp skills directory
with a regression test.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): route host od:slide messages through local go()

The host bridge classifies kami-deck as class-driven because go() toggles
.slide.active, but the visible slide is moved by deck.style.transform
which the bridge cannot drive. Listen for od:slide messages and dispatch
them through the local go() so toolbar next/prev and initialSlideIndex
restore actually shift the deck.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(kami-deck): sync deck transform with host-driven .active changes

The previous fix added a local od:slide listener but the host bridge in
apps/web/src/runtime/srcdoc.ts also listens for the same message and
calls setActive() (toggles .slide.active) without driving the deck
transform. Both listeners fired, the bridge re-read the just-toggled
active class, and overshot by one — and the bridge's restoreInitialSlide
path could move .active without a message at all, leaving the deck on
the original transform.

Stop the bridge from double-handling by calling stopImmediatePropagation
in the local listener (registered first because the bridge script is
appended to </body>), and add a MutationObserver that pulls the deck
transform onto whichever slide currently carries .active so the bridge's
direct setActive calls (notably the initial-slide restore) move the deck
too.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(i18n): align French content with renamed/new skills

PR #434 (French localization) merged into main with French copy for the
old editorial-collage / editorial-collage-deck skill ids; this branch
renamed those to open-design-landing / open-design-landing-deck and
added kami-deck and kami-landing. Update content.fr.ts to track the
rename and add French copy for the new kami skills so the
LOCALIZED_CONTENT_IDS coverage test passes once main is merged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(open-design-landing-deck): sync deck transform with host-driven .active changes

Apply the same fix that landed in skills/kami-deck/example.html
(commits 96b255b, 8cbca30) to the open-design-landing-deck composer
runtime: the host bridge classifies this deck as class-driven because
go() toggles .slide.active, but the visible slide is moved by
deck.style.transform which the bridge can't drive. Add an od:slide
message listener that calls stopImmediatePropagation() and routes nav
through the local go(), plus a MutationObserver that pulls the deck
transform onto whichever slide carries .active so the bridge's direct
setActive calls (notably restoreInitialSlide) move the deck too.

Regenerates example.html via scripts/compose.ts; the regeneration also
picks up upstream nav-cta and brand-meta CSS additions in the sister
open-design-landing styles.css that the example had drifted from.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* docs(open-design-landing): align deploy story with Astro landing app

- Update SKILL contract: apps/landing-page is Astro static; clarify
  nextjs-app output_format as a historical enum label and <out>/nextjs
  as a legacy folder name.
- Replace optional-deploy section with fork + pnpm --filter landing-page build.
- Fix styles.css header and regenerate landing + deck example.html so
  inlined comments match.

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

* fix(deck-runtime): bypass interaction lock for host/observer slide sync

The slide deck runtimes for kami-deck and open-design-landing-deck
gate go() behind a 700ms `lock` so wheel/key/touch input bursts can't
overshoot the transform transition. But applying the same gate to the
host bridge's od:slide messages and the MutationObserver watching
`.slide.active` creates a startup race: go(0) at the end of init sets
lock=true, and any host-driven `.active` change inside that window
(notably restoreInitialSlide) fires the observer, which calls go(i),
which exits at the lock guard — leaving the visible deck on slide 1
while the host counter advances to N.

Split the actual state update into an unthrottled `applySlide(n)`
helper that updates transform, `.active`, dot nav, and the progress
bar. Keep `lock` only on the user-input path through `go()`. Route
the message listener, the MutationObserver, and the initial render
through `applySlide` directly so host-driven sync always reaches the
deck transform regardless of the throttle state.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 19:22:46 +08:00
monshunter
bf533c3d72
fix(web): split execution mode tabs and align active chip visuals (#418)
* Refine settings API configuration UX

* Fix PR validation issues

* Address settings review edge cases

* Address BYOK settings review feedback

* Fix BYOK provider labels and model display
2026-05-04 18:07:30 +08:00
Xinmin Zeng
76b41ffef4
fix(daemon): expose skill resources via cwd-relative aliases (#435)
* fix(daemon): expose skill resources via cwd-relative aliases (#430)

Skill bodies referenced repository-absolute paths in their preamble and
relied on every agent CLI honouring `--add-dir` (or its equivalent) to
open them. Most agents (codex, gemini, opencode, cursor-agent, qwen,
pi, hermes, kimi) have no such flag, and Claude Code's directory-access
policy can still block these reads under specific permission/trust
configurations even when `--add-dir` is set, so SKILL side files fail
to load mid-turn.

Stage `.od-skills` and `.od-design-systems` directory links inside the
project cwd before spawning the agent, and rewrite the skill preamble
to address resources via the cwd-relative alias path. The alias
resolves through a directory junction (Windows) or symbolic link
(POSIX), so resources are reachable from inside every agent's working
directory without needing an extra-allowed-directories flag.

`extraAllowedDirs` is kept as a belt-and-suspenders fallback for
Claude/Copilot — if the alias cannot be created (e.g. the user has a
pre-existing real entry under one of the reserved names), `--add-dir`
still covers the absolute paths.

`ensureCwdAliases` is idempotent, repoints when the target moves, and
refuses to overwrite a non-symlink entry under the reserved name to
avoid clobbering user data.

Closes #430.

* fix(daemon): address review feedback — copy active skill, dual preamble paths

Round 2 of PR #435 addressing the unresolved P1/P2/P3 review threads
from @chatgpt-codex-connector, @lefarcen and @mrcfps.

- P1 writable-symlink. Replace the directory link with a per-project
  `fs.cp` of the active skill into `<cwd>/.od-skills/<folder>/`.
  Writes through the staged copy no longer mutate the shipped repo
  resource. A regression test pins this down so a future "optimisation"
  that re-introduces a symlink would fail loud.
- P1 absolute fallback. Rewrite the skill preamble to advertise both
  the cwd-relative alias path (primary) and the absolute path
  (fallback). When `stageActiveSkill` is skipped (no project cwd) or
  errors, Claude/Copilot still reach the resource via the absolute
  path covered by `--add-dir`; other agents that fall back to
  `PROJECT_ROOT` as cwd reach it natively because the absolute path
  is in-cwd there.
- P2 symlinked source root. Use `stat()` instead of `lstat()` on the
  source so a symlinked `SKILLS_DIR` or skill folder is followed
  rather than skipped.
- P2 stale ordering. `ensureCwdAliases` is gone; staging now happens
  independently of prompt composition and never silently diverges
  from what the preamble advertises.
- P3 unsafe spec name. Validate `folderName` against an explicit
  segment policy (no path separators, no dot segments, no absolute,
  no null bytes) so a malformed caller cannot escape the alias root.
- P3 strong types. Drop the `@ts-nocheck` pragma on `cwd-aliases.ts`
  and type the public API (`SkillStagingResult`, `SkillStagingLogger`).

Also drops the `.od-design-systems` alias entirely. Design-system
bodies are read by the daemon and folded into the system prompt
directly, so an agent never has to open them through the filesystem;
exposing that tree as cwd-visible was unnecessary surface area.

Test additions: 17 cases in `cwd-aliases.test.ts` covering the copy
semantics, the write-barrier regression, symlinked-source handling,
legacy-symlink upgrade, alias-root collision refusal, cwd=null guard,
and a parameterised set of unsafe folder names.

* fix(daemon): unify effectiveCwd and use junctions on Windows test fixtures

Round 3 of PR #435 addressing two non-blocking follow-ups from
@mrcfps:

- server.ts: collapse the chat-handler `cwd` resolution into a single
  `effectiveCwd = cwd ?? PROJECT_ROOT` and feed it consistently to
  `buildArgs`, `spawn`, and the ACP/Pi RPC session adapters. Previously
  `spawn(... { cwd: cwd || undefined })` let no-project runs inherit
  whatever working directory the daemon process was started from
  (packaged daemons / service launches do not start in the workspace
  root), which made the "absolute path is in-cwd" guarantee in the new
  skill preamble untrue for that exact case the comment claimed to
  cover. With `effectiveCwd`, agents in no-project mode land in
  `PROJECT_ROOT` and the absolute fallback path emitted by the preamble
  is genuinely a relative-to-cwd path.

- cwd-aliases.test.ts: replace `symlinkSync(..., 'dir')` fixtures with
  a platform-aware `dirLinkType` (`'junction'` on Windows, `'dir'`
  elsewhere). Directory symlinks on Windows require
  SeCreateSymbolicLinkPrivilege / Developer Mode, which is not enabled
  on most CI images, so the previous form would fail the daemon test
  suite on Windows runners even though the production code (which
  uses `fs.cp`) does not need symlinks at all.
2026-05-04 17:48:20 +08:00
Tom Huang
6c2a8ba09f
feat(editorial-collage): introduce Atelier Zero style landing page as… (#366)
* feat(editorial-collage): introduce Atelier Zero style landing page assets and documentation

- Added new design system for Atelier Zero, including a detailed `DESIGN.md` file.
- Created an `editorial-collage` skill with associated assets for a magazine-grade landing page.
- Included example HTML and image assets for various sections (hero, about, capabilities, etc.).
- Updated README files to guide usage and customization of the new skill and design system.
- Introduced a new image generation prompt pack for consistent visual style across the landing page.

* fix(i18n): cover atelier-zero design system and editorial-collage skill in German content

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(editorial-collage): align manifest with shipped assets and address PR review

- Update image-manifest.json widths/heights/ratios to match the actual PNGs
  on disk: hero/about/cap/testimonial/cta = 1024x1024 (1:1), method-1..4 =
  816x816 (1:1), lab-1..5 and work-1..2 = 768x1024 (3:4). Mirror the new
  dimensions in imagegen-prompts.md headings and in README.md.
- Mark testimonial.png as rekey_on_brand_change so the manifest agrees
  with SKILL.md's "regenerate at minimum testimonial.png" guidance, and
  add work-1/work-2 to the rekey list in SKILL.md and README.md.
- Add a Hero (I.) sec-rule and renumber every following section II..VIII
  in example.html so the eight sections walk sequentially I -> VIII and
  the page-of-008 counter starts at 001.
- Delete editorial-artifact-system/ (16 duplicate PNGs + index.html +
  skills.md draft) — the canonical version is skills/editorial-collage/
  and the duplicate had no consumer references.
- DESIGN.md: spell out which dimensions of each magazine reference
  (Monocle/Apartamento/IDEA), document the rationale for single-accent
  vs multi-accent, and extend the anti-pattern list with AI-image-gen
  artifacts the system explicitly rejects.
- SKILL.md: add italic_words validation guidance (trim, cap at 4,
  verb->noun rewrite, punctuation strip) and replace the broken-image
  fallback with an inline SVG placeholder sized to the slot's
  manifest aspect ratio.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* fix(daemon): serve skill example assets via stable API route

Skill example HTML such as `skills/editorial-collage/example.html`
references shipped images via `./assets/*.png`. The web app loads the
example into a sandboxed iframe via `srcdoc`, where relative URLs
resolve against `about:srcdoc` and the PNGs render as broken images in
the Examples preview.

Add a `GET /api/skills/:id/assets/*` route that serves files under the
skill's `assets/` directory with path-traversal guards, and rewrite
`src='./assets/<file>'` / `href='./assets/<file>'` in the example
response to point at that route. The disk preview keeps working
because the on-disk files are unchanged.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* feat(landing-page): add new static Next.js 16 site for Open Design marketing

- Introduced a new landing page application using Next.js 16, featuring a static export setup.
- Added essential files including `package.json`, `next.config.ts`, and TypeScript configuration.
- Implemented global styles in `globals.css` to match the Atelier Zero design system.
- Created a detailed `AGENTS.md` for module-level boundaries and purpose.
- Included various image assets for the landing page, ensuring a visually cohesive experience.
- Established a root layout and main page structure to support the marketing content.

* style(landing-page): enhance topbar layout and improve responsiveness

- Added nowrap styling to topbar elements to prevent text overflow.
- Introduced media query to hide mid text in the topbar for screen widths between 1200px and 1280px.
- Updated layout.tsx to suppress hydration warnings for better rendering consistency.
- Removed redundant "Compiled by Open Design" text from the page component.

* feat(landing-page): implement scroll-reveal animations for enhanced user experience

- Added a new `RevealRoot` component to manage scroll-triggered reveal animations.
- Updated `globals.css` with styles for elements using the `data-reveal` attribute, including opacity, translation, and scaling effects.
- Modified `layout.tsx` to include the `RevealRoot` component for managing animations.
- Enhanced `page.tsx` by adding `data-reveal` attributes to various elements for staggered reveal effects.
- Implemented reduced motion support to ensure accessibility for users with motion sensitivity.

* fix(landing-page): update import paths and enhance link styles

- Changed the import path in `next-env.d.ts` to reference the correct routes type definition.
- Enhanced `globals.css` with new styles for topbar links, work cards, and partner elements, improving hover effects and transitions.
- Updated `page.tsx` to include canonical project URLs and made various links point to these URLs for better navigation and accessibility.

* feat(landing-page): implement headroom-style sticky header with live GitHub star count

- Introduced a new `Header` component to manage sticky navigation behavior on scroll, enhancing user experience.
- Updated `globals.css` to style the sticky header, including transitions and visibility toggling based on scroll direction.
- Modified `page.tsx` to replace the static header with the new `Header` component, which fetches and displays the live GitHub star count.
- Ensured accessibility by providing a fallback for users who prefer reduced motion.

* feat(landing-page): enhance editorial landing page with global ticker and new styles

- Updated `next-env.d.ts` to reference the correct routes type definition for development.
- Enhanced `globals.css` with new styles for the global ticker, including responsive design and improved overflow handling.
- Introduced a new `WIRE_CITIES` and `WIRE_CONTRIBS` data structure in `page.tsx` to display a counter-scrolling marquee of cities and contributors.
- Added a ghost button style for the navigation call-to-action in the header.
- Updated various sections in `page.tsx` to integrate the new ticker and improve overall layout and accessibility.

* refactor(landing-page): update paper texture overlay and remove multica-ai link

- Enhanced comments in `globals.css` to clarify the purpose and behavior of the paper texture overlay.
- Adjusted z-index of the overlay to ensure proper layering with other elements.
- Removed the `multica-ai` partner link from `page.tsx` to streamline the partner section.

* feat(landing-page): implement dynamic contributor marquee with GitHub integration

- Added a new `Wire` component to display a counter-scrolling marquee of cities and contributors.
- The contributor list is fetched live from the GitHub API, ensuring up-to-date information.
- Updated `page.tsx` to integrate the `Wire` component, replacing the static contributor list with dynamic content.
- Enhanced comments for clarity regarding the functionality and purpose of the global wire.

* fix(i18n): add German display copy for editorial-collage-deck skill

The Validate workspace test asserts that GERMAN_CONTENT_IDS.skills covers
every curated skill on disk; the new editorial-collage-deck skill was
missing from DE_SKILL_COPY, causing src/i18n/content.test.ts to fail.

Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code)

* feat(landing-page): migrate marketing site to Astro

* perf(landing-page): remove React client runtime

* perf(landing-page): serve images from Cloudflare resizing

* fix(pr): address landing page review feedback

---------

Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-04 13:39:58 +08:00
Sebastian Westberg
eb9693d56c
fix(daemon): strip ANTHROPIC_API_KEY when spawning Claude Code (#400)
* fix(daemon): strip ANTHROPIC_API_KEY when spawning Claude Code

Claude Code prefers ANTHROPIC_API_KEY over the user's logged-in
subscription auth when both are present, silently billing API usage
even though the daemon's "Local CLI" mode implies the user wants
their `claude login` credentials (Pro/Max plan) to be used. The
daemon inherited the launching shell's env, so anyone who ever
exported ANTHROPIC_API_KEY for SDK work or scripts hit this.

Strip ANTHROPIC_API_KEY from the env passed to spawn for the
claude adapter only, so Claude Code's own auth resolution applies.

Fixes #398

* fix(daemon): strip ANTHROPIC_API_KEY case-insensitively, extract helper, add tests

Address review feedback on #400:

- Extract spawnEnvForAgent helper in agents.ts so the strip is unit-
  testable and lives next to the other agent-spawn primitives.
- Strip ANTHROPIC_API_KEY case-insensitively. Windows env-var names are
  case-insensitive at the kernel level; spreading process.env into a
  plain object loses Node's case-insensitive accessor, so a mixed-case
  Anthropic_Api_Key would survive `delete env.ANTHROPIC_API_KEY` and
  still reach Claude Code via the spawned env block on Windows.
- Add four tests covering: uppercase strip for claude, case-insensitive
  strip for claude, preservation for non-claude adapters, and input
  immutability.

---------

Co-authored-by: Sebastian Westberg <sebastianwestberg@Sebastians-MacBook-Pro.local>
2026-05-04 09:17:37 +08:00
nikshh
3b3275ea3d
fix(daemon): support nested paths in project file serve route (#401)
The /api/projects/:id/files/:name route used a named Express param
that does not match path separators, so requests for files stored in
subdirectories (e.g. assets/logo-mark.svg, fonts/Aeroport.woff2)
returned 404.

Switch to a wildcard route and read req.params[0] so that any depth
of nesting is handled correctly. This fixes missing media and fonts
when opening an HTML project file as a standalone preview.
2026-05-04 08:54:49 +08:00
lefarcen
179b23d7e7
Modernize multi-provider API proxy routing
Adds provider-specific API proxy routes for Anthropic, OpenAI-compatible, Azure OpenAI, and Google Gemini; normalizes provider SSE streams; updates web provider clients/settings/docs/tests; and forwards Gemini token limits.
2026-05-04 04:23:32 +08:00
Vitaliy VVS
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
Jason
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
Ajay Satish
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
monshunter
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
Sid
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
lefarcen
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
NickSeagull's Bot
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
Nagendhra Madishetti
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
Tom Huang
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
Sid
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
Caprika
0c00f241e7
Add preview comment attachments (#284) 2026-05-02 19:23:46 +08:00
Rin
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
lefarcen
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
Tom Huang
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
yyh6666
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
VitaminBFFM
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
Aresdgi
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
PerishFire
f604ff1ec2
Add Windows beta packaging and release assets (#191) 2026-05-01 16:46:15 +08:00
JiangDing
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
ccfuncy
6e9f3cda73
fix(providers): proxy Anthropic-compatible streams (#180) 2026-05-01 10:25:33 +08:00
Tom Huang
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
Caprika
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
PerishFire
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
nettee
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
Xinmin Zeng
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
pan
7294929490
Fix daemon project-root resolution when launched from src via tsx (#162)
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.
2026-04-30 18:31:38 +08:00
Tom
8f34e39b7b
feat(daemon): add pi coding agent adapter (#117)
* 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.
2026-04-30 17:45:11 +08:00
Caprika
bd2e71c708
fix codex plugin disable env (#133) 2026-04-30 15:38:46 +08:00
PerishFire
c6d11018a0
Refresh desktop integration control plane (#123)
* 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.
2026-04-30 14:23:53 +08:00
Renamed from apps/daemon/server.ts (Browse further)