Commit Graph
106 Commits
Author SHA1 Message Date
nettee d5ee6b3587 Merge origin/main into fix-image-timeout 2026-05-07 17:53:18 +08:00
xunchaandGitHub a8418ac730 Fix Windows link code folder dialog (#698)
* Fix Windows link code folder dialog

* Add Windows folder dialog coverage

* Complete Indonesian locale copy
2026-05-07 17:27:01 +08:00
nettee e8d6c94b8c fix(daemon): extend OpenAI image request timeouts 2026-05-07 17:15:13 +08:00
PerishFireandGitHub 6efac8887e Improve Windows beta packaging and installer flow (#768)
* Optimize Windows packaged web output

* Fix packaged contracts runtime build

* Optimize Windows packaged size pruning

* Prune Windows root Next payload

* Remove Windows bundled Node runtime

* Prune Windows standalone duplicate Next

* Add tools-pack cache foundation

* Cache Windows packaged build layers

* Cache Windows workspace builds

* Cache Electron-ready Windows app

* Split Windows tools-pack module

* Cache Windows dir build outputs

* Split Windows pack build modules

* Document Windows NSIS smoke namespace limits

* Move Windows NSIS smoke note to agents guide

* Optimize Windows beta packaging

* Bump packaged beta base version

* Improve Windows installer namespace UX

* Improve Windows tools-pack cache keys

* Stabilize Windows beta cache version keys

* Cache Windows workspace build outputs

* Optimize windows release beta cache layers

* Cache windows release dependencies

* Trim windows release cache before save

* Refresh windows tools-pack cache key

* Improve windows installer preflight prompts

* Fallback NSIS installer strings to English

* Fix Windows installer cleanup and preflight

* Improve Windows NSIS state logging

* Fix system NSIS Persian language alias

* Use long-path removal for Windows uninstall

* Fix mac tools-pack tests on Windows

* Address Windows packaging review feedback

* Fix Windows installer cache namespace isolation

* Include web output mode in Windows tarball cache key

* Use unique Windows release cache save keys
2026-05-07 16:44:15 +08:00
Nagendhra MadishettiandGitHub bb2015766a feat: Critique Theater Phase 5 (panel prompt template + system composer wiring) 2026-05-07 16:35:04 +08:00
JHRandGitHub c00f89dbe4 fix(daemon): allow portless Origin in CORS whitelist for Chrome compatibility 2026-05-07 16:29:06 +08:00
Nagendhra MadishettiandGitHub 25a3ffd298 fix(daemon): add legacy data dir migrator
Add a one-shot OD_LEGACY_DATA_DIR migrator so packaged Desktop users can recover 0.3.x repo .od data into the 0.4.x data root. The migrator stages payloads before promotion, refuses unsafe merges and symlinks, rolls back failed promotion or marker writes, and extends packaged daemon startup handling for long migrations while failing fast on daemon exits.

Closes #710
2026-05-07 15:19:04 +08:00
shangxinyu1andGitHub 9b501f12a5 Support overriding the Codex executable path (#755)
* Support overriding the Codex executable path

* Replace save-as-template prompts with an in-app dialog

* Seed local packaged app config from workspace

* Fix packaged config and connection test overrides

* Keep tools-pack mac config seeding self-contained

* Require absolute CODEX_BIN overrides
2026-05-07 15:00:52 +08:00
SidandGitHub 0b2b456694 fix(daemon): deliver Copilot prompt via stdin
Avoid Windows ENAMETOOLONG by keeping Copilot prompts out of argv and sending them through stdin instead.\n\nFixes #705
2026-05-07 11:52:30 +08:00
monshunterandGitHub e6e5928be1 feat(web): add connection tests for execution settings (#507)
* feat(settings): add connection test for providers and CLI agents

Adds a "Test" action in the Settings dialog that verifies the configured
provider (Anthropic/OpenAI/Azure/Google) or CLI agent without sending a
real chat. Backed by a new daemon endpoint and shared contracts, with
categorized inline statuses and i18n strings across all supported locales.

* fix(settings): address connection test review feedback

* fix(daemon): pass empty MCP servers for connection probes

* fix(connection-test): address review blockers

* fix(daemon): fail json stream runs on structured errors

* fix(contracts): build connection test subpath export

* Use draft CLI env in agent connection tests

* fix(i18n): add fallback ids for new curated content
2026-05-07 11:25:37 +08:00
832ea7d864 fix: batch of small bug fixes (#283, #275, #390) (#530)
* fix(web): add hover tooltips to Design Files action buttons (#283)

The batch-download, select-all, and clear-selection buttons in
DesignFilesPanel had no title attribute, so users hovering them saw no
tooltip. The other action buttons (refresh, new sketch, paste, upload)
already had titles. Added titles to the three missing ones using the
existing translation keys, so hover behavior is consistent across the
panel.

Closes #283.

* docs: point pi-ai links to pi-mono packages (#275)

The pi project moved from a standalone repo to the pi-mono monorepo.
The old URL https://github.com/mariozechner/pi-ai now 404s. Replaced
both shapes of reference:

- The reference-style [piai]: definition now points at
  https://github.com/badlogic/pi-mono/tree/main/packages/ai
  (the multi-provider LLM API package).
- Inline links whose visible text is the CLI tool 'pi' or 'Pi' now
  point at
  https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent
  (the interactive coding-agent CLI), so a reader clicking 'pi' in
  the daemon-discovery section lands on the actual binary's docs.

Affected: README.md and 10 translated READMEs, plus docs/spec.md,
docs/architecture.md, docs/references.md, docs/roadmap.md.

Closes #275.

* fix(daemon): expand $HOME / ${HOME} in OD_DATA_DIR (#390)

Some launchers (systemd unit files, NixOS modules, certain Docker
entrypoints) pass OD_DATA_DIR with a literal '$HOME' or '${HOME}'
because no shell ever expands them. resolveDataDir previously only
handled '~/' shorthand, so '$HOME/.open-design' fell through to
path.resolve(PROJECT_ROOT, '$HOME/.open-design') and produced paths
like /opt/open-design/$HOME/.open-design.

resolveDataDir now expands '~', '~/...', '$HOME', '$HOME/...',
'${HOME}', and '${HOME}/...' to os.homedir() before the absolute /
relative branch runs. Rebuilds via path.join so the platform separator
is correct on Windows even when the input used forward slashes.

Tests: 7 unit tests cover empty/undefined, '~', '~/...', '$HOME',
'$HOME/...', '${HOME}/...', absolute paths, and relative paths.

Closes #390.

* fix(daemon): accept backslash separators + hermetic resolve-data-dir tests

Round 1 review feedback on PR #530.

The previous regex only matched forward-slash separators, so a Windows
launcher passing OD_DATA_DIR=$HOME\.open-design or ${HOME}\.open-design
fell through to path.resolve(projectRoot, ...) and produced a directory
named $HOME or ~ under projectRoot. The regex now accepts both forward
and back slashes for the home-prefix separator.

The previous tests called the real resolveDataDir against literal
~/od-test, $HOME/od-test, etc., which created and write-checked
directories under the developer's or CI runner's actual home. The tests
now stub os.homedir() with vi.spyOn to a per-test mkdtemp directory and
remove it in afterEach, so no test ever writes outside its own sandbox.
Added explicit fixtures for the Windows backslash forms ($HOME\od-test,
${HOME}\od-test, ~\od-test) so launcher coverage stays cross-platform.

12/12 resolve-data-dir tests pass, daemon typecheck clean.

* fix(docs,daemon): apply pi-mono links to README.es and await test cleanup

Round 2 review feedback on PR #530.

README.es.md was added in upstream #552 after my pi-mono link sweep
landed, so the daemon-discovery paragraph (line 222), the [piai]
reference (line 684), and the Pi table row (line 709) still pointed
at the broken https://github.com/mariozechner/pi-ai URL. Applied the
same replacements: the [piai] ref now points at packages/ai, and the
inline Pi link now points at packages/coding-agent. Spanish readers
get the same coverage as the other 11 locales.

The absolute-path test in tests/resolve-data-dir.test.ts dropped its
fixture via void rm(abs, ...), so a failed async removal could leak
rdd-abs-* directories from the suite. The test is now async and
awaits the rm in the finally block, matching the awaited cleanup in
afterEach. 12/12 resolve-data-dir tests still pass, daemon typecheck
clean.

* fix(daemon): share $HOME expander between OD_DATA_DIR and OD_MEDIA_CONFIG_DIR

Round 3 review feedback on PR #530.

resolveDataDir (server.ts) now expands $HOME / ${HOME} / ~, but
media-config.ts had its own resolveOverrideDir that only handled ~/.
Because configFile() falls back to OD_DATA_DIR when OD_MEDIA_CONFIG_DIR
is unset, setting OD_DATA_DIR=$HOME/.open-design split state: SQLite,
projects, and artifacts went to the expanded path while
media-config.json stayed under <projectRoot>/$HOME/.open-design.
Stored provider keys then appeared missing on the next read.

Extracted the home-prefix expansion into apps/daemon/src/home-expansion.ts
so resolveDataDir and resolveOverrideDir share one resolver. Both now
recognize ~ / $HOME / ${HOME} (bare tokens) and ~/, ~\, $HOME/, $HOME\,
${HOME}/, ${HOME}\ (prefix forms with either separator).

Three new media-config routing tests cover the OD_DATA_DIR fallback for
$HOME/..., ${HOME}/..., and the OD_MEDIA_CONFIG_DIR explicit-override
$HOME/... case so the co-location guarantee is locked down by tests.

Daemon typecheck clean. Tests pass on Linux CI; the existing pattern in
the file uses process.env.HOME which os.homedir() reads on POSIX.
Resolve-data-dir tests stay hermetic via vi.spyOn.

* docs(daemon): media-config comments reflect full $HOME / ${HOME} expansion

Round 3 review feedback on PR #530 (lefarcen, P3 non-blocking).

The file-header and resolveOverrideDir() function comment said only
~/ expands. Updated both to mention the shared expandHomePrefix()
helper and the full set of forms it handles (~, $HOME, ${HOME} with
either separator), so a future reader does not need to chase the
implementation to understand what env values are accepted.

* test(daemon): stub os.homedir() in media-config routing tests

Round 4 review feedback on PR #530.

The new $HOME / ${HOME} routing tests relied on process.env.HOME being
read by os.homedir(), which works on POSIX but is unreliable on Windows
(Node prefers USERPROFILE / profile APIs there). The tests would expand
to the real user home while fixtures were written under the per-test
homeDir, causing platform-specific failures in the same area this PR
is making cross-platform.

The inner describe block now stubs os.homedir() via vi.spyOn to return
the per-test homeDir, matching the pattern in resolve-data-dir.test.ts.
Restored in afterEach. The four $HOME-form routing tests now pass on
both POSIX and Windows.

Daemon typecheck clean. The two OAuth fallback test failures unrelated
to this change (real ChatGPT/Codex tokens in the local env) remain
out-of-scope.

* fix(i18n): drop duplicate uk.ts promptTemplates keys after rebase

Upstream #674 added the same Ukrainian translations my earlier
commit added. The rebase landed both copies; tsc rejects duplicate
property names. Drop my copies so #674 (which is now upstream) is
the single source for these keys.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-07 11:17:02 +08:00
Gabriel VazandGitHub 6abd7676c8 fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling (#591)
* fix(daemon): unbreak Claude Design ZIP import on Node 24 and raise file ceiling

- Skip inflateRawSync when an entry's central-directory uncompressedSize is 0;
  Node 24 rejects { maxOutputLength: 0 } with ERR_OUT_OF_RANGE, which silently
  killed the entire import for any zip containing an empty file or a streaming
  entry whose size is only present in the data descriptor.
- Raise MAX_FILES from 500 to 5000. Real-world design-system exports commonly
  exceed 500 files; MAX_TOTAL_BYTES (100 MB) and MAX_FILE_BYTES (25 MB) already
  cap pathological inputs.
- Add regression tests for both: zero-byte deflate entry, central directory
  advertising uncompressedSize=0, and a 600-file zip.

Refs #590

* fix(daemon): preserve real payload when central uncompressedSize is 0

Reviewers correctly flagged that the previous Buffer.alloc(0) fast-path
trusted the central directory's uncompressedSize, which is unreliable for
streaming/data-descriptor zips: an entry whose central record reports 0
can still carry real deflated bytes. The earlier fix wrote empty files to
disk in that case, and the post-condition body.length !== uncompressedSize
check still passed because both sides were 0.

- Inflate streaming entries with maxOutputLength = MAX_FILE_BYTES when the
  central directory advertises 0, so legitimate non-empty payloads decode
  fully instead of being silently truncated.
- Move size enforcement post-decode: per-file and total-byte budgets are now
  computed from the actual decoded length, and the strict equality check is
  skipped when central was 0 (i.e., genuinely unknown).
- Keep the empty-deflate degenerate case (compressed.length === 0) safe by
  short-circuiting before zlib instead of relying on uncompressedSize.

Tests:
- New: streaming-zip case with central uncompressedSize=0 + non-empty body
  asserts the on-disk file matches the original bytes (would have been
  silently truncated under the previous fix).
- New: oversized streaming entry (> MAX_FILE_BYTES) is still rejected even
  though the central directory under-reports.
- The original 0-byte and >500-file regressions remain covered.
2026-05-07 10:31:30 +08:00
SidandGitHub 1bd1f3a661 fix(daemon): surface OpenCode error frames + treat empty-output runs as failed (#700)
* fix(daemon): surface OpenCode error frames + treat empty-output runs as failed

Closes #691. OpenCode runs would silently complete in ~3 seconds without
producing any visible chat output and still be rendered as a successful
turn — three independent bugs along the structured-stream path conspired
to produce this silent-failure shape.

## Bug 1 — `apps/daemon/src/json-event-stream.ts:85-91`

OpenCode emits structured error frames on stdout (e.g. provider auth
failures, network errors, schema mismatches) and still exits 0. The
parser was downgrading these to `{type: 'raw', line: ...}`, which the
chat UI does not render as an assistant message. The error string was
discarded as "no-op output."

Fix: emit a proper `{type: 'error', message, raw}` event matching the
qoder-stream contract that the daemon's existing error-handling path
already recognises.

## Bug 2 — `apps/daemon/src/server.ts:4199-4205`

Even after Bug 1 was fixed, the json-event-stream branch wired the
parser to a bare `(ev) => send('agent', ev)` lambda — bypassing the
`sendAgentEvent` wrapper that interprets `type:'error'` events and
sets the `agentStreamError` flag the close handler reads to flip the
run to `failed`. So an emitted `error` event would just be forwarded
as a no-op `agent` SSE event with no lifecycle effect.

Fix: route json-event-stream through `sendAgentEvent`, mirroring the
qoder-stream-json wiring at line 4175.

## Bug 3 — `apps/daemon/src/server.ts:4220-4234`

Even after Bugs 1 and 2 are fixed, there's still a class of runs where
OpenCode never emits any error frame, never emits any substantive
event, and exits 0. Pre-fix this was marked `succeeded` and the user
saw a blank chat with no diagnostic.

Fix: track `agentProducedOutput` inside `sendAgentEvent` (set on
`text_delta`, `thinking_delta`, `tool_use`, `tool_result`, `artifact`
— deliberately NOT on `status` / `usage`, since a model can emit
token-usage numbers for an empty completion). When the close handler
sees `code === 0 && trackingSubstantiveOutput && !agentProducedOutput`
the run is marked `failed` with an explicit AGENT_EXECUTION_FAILED
SSE error so the chat shows a clear reason instead of a silent
empty turn.

The check is gated by `trackingSubstantiveOutput` so it only fires
on streams that actually contribute to the output flag (currently
qoder-stream-json and json-event-stream). ACP sessions and plain
stdout streams keep their existing success/failure determination.

## Tests

- 3 new unit tests in `apps/daemon/tests/json-event-stream.test.ts`
  pin the OpenCode error event shape: full repro
  (`error.data.message`), `error.name` fallback, and the
  generic-fallback shape when `error` is empty.
- All 60 daemon test files (851 tests) pass on `pnpm --filter
  @open-design/daemon test`. All 42 web test files (309 tests) pass
  on `pnpm --filter @open-design/web test`.
- Full repo `pnpm typecheck` clean.

## Live verification

Verified end-to-end via a stub `opencode` binary that mimics each of
the failure shapes against `pnpm tools-dev run web`:

1. Stub emits `{"type":"error",...}` then `exit 0` — run now ends as
   `failed` with the OpenCode error message surfaced as an SSE
   `error` event. Pre-fix this was `succeeded` with an empty chat.
2. Stub emits nothing then `exit 0` — run now ends as `failed` with
   "Agent completed without producing any output…" diagnostic.
   Pre-fix this was `succeeded` with an empty chat.
3. Stub emits a normal `step_start` / `text` / `step_finish` sequence
   then `exit 0` — run still succeeds. (Regression check.)

## Out of scope (mentioned for the next person)

- `claude-stream-json` and `copilot-stream-json` still wire to a bare
  `(ev) => send('agent', ev)` and don't currently parse `type:'error'`
  frames. If their CLIs ever start emitting structured error events
  the same pattern (route through `sendAgentEvent` + emit proper
  `type:'error'`) applies. Not in scope here because we have no
  evidence those CLIs do this today, and changing the wiring without
  a confirmed failure mode risks regressing currently-working flows.
- ACP sessions (`pi-rpc`, `acp-json-rpc`) own their own success /
  failure determination via `acpSession?.hasFatalError()` and the
  empty-output guard explicitly skips them via
  `trackingSubstantiveOutput`.
- Plain stdout streams have no event-level tracking, so the empty-
  output guard skips them too. Diagnosing a no-output plain-stream
  agent is a separate problem that needs different signals.

* chore: retrigger CI on top of green main (post #697 i18n backfill)
2026-05-07 02:00:19 +08:00
42e4d080bd feat(skills): add social-media-dashboard skill + Totality Festival design system (#678)
* feat(skills): add social-media-dashboard skill + Totality Festival design system

- New skill 'social-media-dashboard': single-screen creator analytics
  dashboard with platform switcher (X / GitHub / LinkedIn / YouTube /
  Instagram), KPI row, growth chart with annotations, top-post / top-PR
  preview, trending topics, and top comments. Includes a self-contained
  example.html (Totality Festival styled, X + GitHub tabs, live KPI
  ticker, GitHub contributors grid, world-map audience geography).
- New design system 'totality-festival': cosmic-premium dark glassmorphic
  system with amber corona highlights and cyan atmospheric accents.
  Mirrors Google Labs' design.md spec example so skills can be
  validated against an upstream reference.
- Fix swatches parser in apps/daemon/src/design-systems.ts so it
  recognises the '- **Name:**' bold-with-inner-colon form used by
  several existing systems (ant, totality-festival, ...). Previously
  only the '**Name** (`#hex`)' form was matched, which left their
  picker thumbnails empty.

* feat(skills): polish social-media-dashboard example + add PR preview

- Top Post media block: replace empty gold frame with an inline SVG
  thumbnail (radial glow + ascending data curve + amber/cyan pulse dots
  + play icon + 'LIVE · 0:22' caption). Visually echoes the live-artifact
  story the post copy is selling.
- Hoist the brand-mark linearGradient into a global SVG defs block at
  the top of <body> so all three avatars (header, user, top-post) can
  reference url(#brandRing) and render the teal arrow consistently.
  Previously only the header SVG carried the gradient definition, so
  the user and post avatars rendered as empty rings under headless
  capture.
- Add hero.png preview to .preview/ for the PR description.

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-05-06 21:50:23 +08:00
98e40c1cfc feat(daemon): export project transcript to disk for downstream consumption (prereq for #450) (#493)
* feat(daemon): export project transcript to disk for downstream consumption

Adds exportProjectTranscript(db, projectsRoot, projectId, options?) — a
pure function that walks SQLite-backed conversation history and writes a
structured, lossless JSONL transcript to <projectDir>/.transcript.jsonl.

This is the input primitive that #450's "Finalize design package"
synthesis step needs. Landed ahead of the synthesis endpoint as a small,
reviewable, well-tested unit — no HTTP route, no LLM call, no web UI in
this diff. PR 2 will wire POST /api/projects/:id/finalize on top of it.

Format: JSONL with header line + per-conversation marker lines +
per-message lines. Compact encoding saves ~20–30% on synth-call tokens
vs pretty-printed JSON. schemaVersion field reserved on the header for
incompatible changes later.

Coalescing: events_json carries streaming text_delta / thinking_delta
chunks plus tool_use / tool_result / thinking_start markers and
telemetry. The export collapses runs of same-type deltas into terminal
text / thinking blocks via arrival-order with type-change flush,
preserving any interleaving with tool blocks. Telemetry (status, usage,
raw) is dropped. thinking_start is treated as an explicit flush trigger
so multiple thinking blocks in one message survive intact.

Content fallback: user-typed messages persist as plain text in
messages.content with events_json = NULL because user input does not
flow through the streaming pipeline. When event-derived blocks come
back empty, fall back to a single text block from content so user
prompts are not silently dropped.

Atomic write: tmp filename includes pid + crypto-random suffix so
concurrent exports for the same project cannot collide. fsync before
rename so a crash between rename and power loss cannot lose bytes.

Hidden file: leading dot keeps .transcript.jsonl out of listFiles
(projects.ts:54), the archive zip, and the gallery.

Tests: 14 unit cases — empty project, text/streaming coalescing, tool
ordering, telemetry filtering, type-change flush, text↔thinking↔text
interleaving, thinking_start flush, multi-conversation chronological
order, atomic-write hygiene, content fallback (both directions),
malformed events_json, and unsafe project id rejection.

Refs: nexu-io/open-design#450

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

* fix(daemon): address PR #493 review feedback for transcript export

Addresses every blocker, P2, and P3 raised on
https://github.com/nexu-io/open-design/pull/493:

- Blocker (event-shape mismatch): coalescer now switches on the
  PersistedAgentEvent kind discriminator (text/thinking/tool_use/
  tool_result), reading the shared `text` field for content kinds,
  matching what apps/web/src/providers/daemon.ts:347-394 actually
  persists into messages.events_json. Empirical confirmation: live
  SQLite contains zero `type` keys.
- Removed @ts-nocheck from the source file; added inline types for
  Db (Database.Database), ConversationRow, MessageRow, AttachmentRef,
  CommentAttachmentRef, and Block. Tests retain @ts-nocheck per
  codebase convention. Note: db.ts still uses @ts-nocheck, so the
  new types catch drift inside transcript-export.ts itself, not at
  the SQLite-helper boundary.
- parseEvents now distinguishes null / malformed / not_array / ok
  cases; non-null-but-unparseable rows emit a console.warn with
  project+message id before falling back to content.
- Switched temp-write from writeSync (which can return short) to
  writeFileSync({flag:'wx'}); explicit fsync via reopen before
  rename, per reviewer concern about partial-write durability.
- Added per-project lockfile (.transcript.lock) acquired with
  openSync(..., 'wx') and released in finally; concurrent exports
  throw the new TranscriptExportLockedError. Stale-lock recovery
  is documented as a known limitation in the file header.
- Header gains attachmentCount, commentAttachmentCount, and explicit
  attachmentsInlined: false. Per-message lines gain attachments /
  commentAttachments references (paths only, not bytes; synthesis
  reads files from disk by path). schemaVersion bumped 1 -> 2 so
  the change is explicit; v1 was never consumed.
- mkdirSync(dir, { recursive: true }) at entry covers projects
  with DB rows but no on-disk directory yet (codex bot finding).
- Refactored node:fs imports from named to default
  (import fs from 'node:fs') so vitest spies in tests #15-#17 can
  redefine properties on the underlying CJS exports object. ESM
  namespace imports of node:fs produce a frozen Module Namespace
  Object that vi.spyOn cannot mutate; default-import returns the
  CJS module.exports which is mutable.
- Inline PersistedAgentEvent union: the daemon tsconfig does not
  resolve the `@open-design/contracts/api/chat` subpath export, so
  the union is restated in the source. Schema-mismatch tests cover
  the case where the contract would diverge.
- Test count 14 -> 24: failure injection for writeFileSync /
  fsyncSync / renameSync, existing-file replacement, lockfile
  contention (lockfile-pre-create design — synchronous API can't
  race via Promise.allSettled), parse-warning cases (malformed +
  not-array), attachments header + per-message coverage, missing-
  project-dir case.

Refs nexu-io/open-design#450 (does not close).

* fix(daemon): preserve thinking-segment boundaries on status thinking-start

Codex flagged this as a P2 on PR #493 a39d430:
https://github.com/nexu-io/open-design/pull/493#discussion_r3188524878

The web translator emits `{ kind: 'status', label: 'thinking' }` at every
thinking_start (apps/web/src/providers/daemon.ts:367-369). The previous
default branch dropped all status events without flushing the active
accumulator, so two thinking segments separated only by that marker
merged into one block — losing the original boundary and making the
transcript non-lossless for downstream synthesis.

coalesceBlocks now matches `status` explicitly: when `label === 'thinking'`
the prior accumulator flushes; other status labels and usage / raw drop
without flushing as before. Behavior fix within schemaVersion 2; no
shape change.

Test #25 verifies the boundary preservation:
  [thinking 'a', thinking 'b', status 'thinking', thinking 'c', thinking 'd']
  → blocks: [thinking 'ab', thinking 'cd'] (two blocks, not one)

Existing test #5 still passes because it uses status with label 'streaming'
which remains pure telemetry and does not flush.

Suite: 506 -> 507 daemon tests, all green.

Refs nexu-io/open-design#493

---------

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-06 21:48:39 +08:00
Xinmin ZengandGitHub 2455c70d51 fix(daemon, packaged): unbreak GUI-launched agent detection on minimal PATHs (#442) (#614)
* fix(daemon, packaged): unbreak GUI-launched agent detection on minimal PATHs (#442)

GUI-launched daemons (Finder/Dock on macOS, .desktop on Linux) inherit a
stripped PATH from launchd / the desktop session and don't read the
user's interactive shell rc files, so any CLI installed via `npm i -g`
under a sudo-free prefix like ~/.npm-global was silently undetected.

Two layers maintained their own copies of the user-toolchain bin list
(`apps/daemon/src/agents.ts:userToolchainDirs` for the resolver,
`apps/packaged/src/sidecars.ts:resolvePackagedPathEnv` for the packaged
sidecar PATH builder) and had already drifted on `~/.asdf/shims` and
`~/Library/pnpm`. Adding ~/.npm-global to one side would have
preserved the same anti-pattern.

Extracts `wellKnownUserToolchainBins` into @open-design/platform as the
single source of truth, has both layers consume it, and extends the
list to cover ~/.npm-global/bin, ~/.npm-packages/bin, plus
$NPM_CONFIG_PREFIX/bin / $npm_config_prefix/bin for users with a
non-standard prefix. New vitest coverage in the platform package and
a regression test in apps/daemon/tests/agents.test.ts modelled on the
existing mise case.

Verified end-to-end: under PATH=/usr/bin:/bin:/usr/sbin:/sbin (the
launchd default a `.app` actually inherits), `resolveAgentExecutable`
now returns ~/.npm-global/bin/gemini instead of null.

* fix(daemon): isolate OD_AGENT_HOME resolution from $NPM_CONFIG_PREFIX leakage

Address review feedback on PR #614:

- mrcfps spotted that the daemon wrapper called wellKnownUserToolchainBins
  without passing `env`, so the helper read its default process.env. A
  developer or CI runner with NPM_CONFIG_PREFIX / npm_config_prefix
  exported would inject that real <prefix>/bin into resolveOnPath() even
  while the OD_AGENT_HOME hook pointed home at a temp fixture, making
  agent-detection tests environment-dependent. Reproduced locally: with
  OD_AGENT_HOME=<tmp> + NPM_CONFIG_PREFIX=/Users/me/.npm-global,
  resolveAgentExecutable({ bin: 'codex' }) returned the real machine's
  binary instead of null. Wrapper now passes `env: {}` whenever
  homeOverride is set, alongside the existing includeSystemBins gate.

- lefarcen suggested also handling whitespace-only NPM_CONFIG_PREFIX
  values (e.g. NPM_CONFIG_PREFIX=" ") so the helper does not emit a
  bogus "<whitespace>/bin" entry. Added a .trim() check before
  appending.

- lefarcen also suggested a comment pointer from the daemon wrapper to
  the platform helper so readers don't have to grep. Added the
  reference inline.

Coverage:
- packages/platform/tests/index.test.ts: new whitespace-prefix case.
- apps/daemon/tests/agents.test.ts: new env-isolation regression
  asserting that OD_AGENT_HOME + NPM_CONFIG_PREFIX cannot leak the
  real prefix bin into the sandbox.

* test(daemon): preserve $NPM_CONFIG_PREFIX across the env-isolation case (#614)

Address mrcfps's second-round review on PR #614: the env-isolation
regression sets `process.env.NPM_CONFIG_PREFIX = realPrefix` in its
body and then unconditionally `delete`s it in `finally`. On a developer
machine or CI runner that already exported `NPM_CONFIG_PREFIX`, that
mutates the worker-wide env for every later test, making downstream
env-sensitive assertions order-dependent.

Move the save/restore into the file's existing afterEach hook (mirroring
the OD_AGENT_HOME / OD_DAEMON_URL / OD_TOOL_TOKEN pattern) and drop the
in-test `delete`. Same coverage, no worker-state mutation.

* fix(platform): prioritise $NPM_CONFIG_PREFIX over the conventional npm guesses (#614)

Address mrcfps's third-round review on PR #614: when the user has
explicitly configured a prefix via $NPM_CONFIG_PREFIX (or
$npm_config_prefix), that's where `npm i -g` puts the *current*
binaries. The conventional guesses ~/.npm-global / ~/.npm-packages
often hold *stale* installs from an older prefix the user has since
rewritten — searching the env-driven prefix first matches npm's own
resolution order (env > .npmrc > default) and gives "explicit beats
convention" semantics.

Move the env-driven push above the conventional `dirs.push(.npm-global,
.npm-packages)`. Add a vitest case in the platform package that asserts
$NPM_CONFIG_PREFIX/bin's index in the result is strictly less than
~/.npm-global/bin's and ~/.npm-packages/bin's.

`resolveOnPath()` and the packaged PATH builder both preserve insertion
order, so first hit wins and the new ordering propagates to both
layers.

* fix(platform): lift $NPM_CONFIG_PREFIX above every conventional bin (#614)

Address mrcfps's fourth-round review on PR #614: the previous fix only
moved $NPM_CONFIG_PREFIX/bin ahead of ~/.npm-global / ~/.npm-packages,
but ~/.local/bin still appeared earlier in the array. Under a minimal
GUI-launch PATH a stale agent in ~/.local/bin (also a shared dumping
ground for pip --user / cargo install / hand-built binaries) could
outrank the user's *current* explicit npm prefix.

Move the env-driven push to the head of `dirs` so the explicit prefix
wins over every conventional location below — ~/.local/bin included.
Matches npm's own resolution order (env > .npmrc > default) across the
whole list, not just the npm-prefix block.

Tightened the existing order test to assert `explicitIdx === 0` and
that ~/.local/bin's index is strictly greater than the explicit
prefix's index, so a future drift would fail loudly.
2026-05-06 20:35:19 +08:00
zztdanandGitHub f3024fdc22 feat(media): add Nano Banana image provider (#631)
* feat(media): add Nano Banana image provider

* fix(media): support Gemini API key headers for Nano Banana

* refactor(media): move Nano Banana model override flag into provider metadata
2026-05-06 20:26:31 +08:00
mambaandGitHub 570d06419c feat[qoder cli] add Qoder CLI agent support (#626)
* chore(agent): 增加对 Qoder CLI 的支持和识别

- 在 QUICKSTART 文档中添加 Qoder CLI 为可选本地 agent CLI
- 更新代码中 agents.ts 注释包含 Qoder CLI 扫描支持
- 修改首次加载时检测的可用 CLI 列表,加入 Qoder CLI
- 在多个语言版本的 README 中增加 Qoder CLI 支持及相关徽章统计
- 更新 agent 适配器与事件解析相关的代码注释和文档,包含 qoder-stream-json 解析器
- 调整 Windows 下 spawn 行为以支持 Qoder CLI 的 stdin 提供 prompt
- 修复多语言文档对支持的 CLI 数量描述错误,确保数据保持同步

Change-Id: I388f2f61c60ce8faa7cef5d84eb407950f8bdbfb
Co-developed-by: Qoder <noreply@qoder.com>

* chore(agent): 增加对 Qoder CLI 的支持和识别

- 在 QUICKSTART 文档中添加 Qoder CLI 为可选本地 agent CLI
- 更新代码中 agents.ts 注释包含 Qoder CLI 扫描支持
- 修改首次加载时检测的可用 CLI 列表,加入 Qoder CLI
- 在多个语言版本的 README 中增加 Qoder CLI 支持及相关徽章统计
- 更新 agent 适配器与事件解析相关的代码注释和文档,包含 qoder-stream-json 解析器
- 调整 Windows 下 spawn 行为以支持 Qoder CLI 的 stdin 提供 prompt
- 修复多语言文档对支持的 CLI 数量描述错误,确保数据保持同步

Change-Id: Id33f125b7c0b1a1c0b0274073da74d1578c324f7
Co-developed-by: Qoder <noreply@qoder.com>

* feat(agent-icon): 添加新的Qoder徽标SVG图形组件

- 新增qoderGlyph函数,返回指定大小的SVG格式图形
- 图形包含多路径定义,颜色使用深灰和绿色填充
- 该组件可用于替代或补充现有AgentIcon图标功能
- 提升应用程序的品牌标识和视觉表现力

Change-Id: I4eca18166b5e33bc6229b40b2531d5a54607a560
Co-developed-by: Qoder <noreply@qoder.com>

* Translate to English:

---

**docs(readme): update to expand CLI agents to 16**

- Increased the number of coding agent CLIs from 11 to 16
- New agents included: Devin for Terminal, Kiro CLI, Kilo, Mistral Vibe CLI, DeepSeek TUI

**docs(readme): update to expand supported coding agents to 16**

- Increased the number of supported code agent CLIs from 11 to 16
- Added support for new CLI tools: Devin for Terminal, Kiro CLI, Kilo, Mistral Vibe CLI, DeepSeek CLI
- Added automatic CLI detection and switching while maintaining support for more agents
- Added BYOK proxy TUI
- Expanded compatibility and support coverage in the README’s multiple language versions
- Reflected changes across all README translations (Arabic, German, French, Japanese, Korean)
- Updated badges and descriptions to reflect CLI count and feature changes
- Added event parsers and protocols for the new CLIs in the agent transport implementation
- Updated the BYOK proxy and tool exploration features to be compatible with the expanded CLIs

Change-Id: I89786b4a0b09bd279fb23265c2177076206fc5af
Co-developed-by: Qoder <noreply@qoder.com>

* feat(daemon): 支持 imagePaths 参数作为附件路径传递给 Qoder

- 修改 buildArgs 函数,添加 --attachment 参数处理 imagePaths 中的绝对路径
- 过滤并忽略空字符串、非字符串及相对路径的 imagePaths 项
- 在单元测试中覆盖 imagePaths 参数支持及无效项过滤逻辑
- 在文档中补充 Qoder 运行时适配器对 --attachment 参数的说明

Change-Id: Ibfc3583ba86c6d258d524912559e97b77bf1dc87
Co-developed-by: Qoder <noreply@qoder.com>

* docs(runtime): 说明Qoder适配器继承用户令牌的环境变量

- 添加文档说明检测代理仅为可用性探针,不进行身份验证
- 说明Qoder CLI账号状态独立,认证通过运行时错误路径反馈
- 详细描述子进程环境继承机制及静态环境变量与用户私密令牌区分
- 明确QODER_PERSONAL_ACCESS_TOKEN通过守护进程环境传递,不写入静态环境
- 解释Qoder验证由Qoder CLI负责,支持持久登录和自动化环境变量注入

test(agent): 添加QODER_PERSONAL_ACCESS_TOKEN环境变量继承测试

- 验证qoder适配器环境继承守护进程中的QODER_PERSONAL_ACCESS_TOKEN
- 确认qoder适配器未在静态环境变量中定义用户令牌
- 保证用户私密令牌不会被写入静态适配器环境配置

Change-Id: Ie61869afbe497df1b16879b4e47b35123f758ed8
Co-developed-by: Qoder <noreply@qoder.com>

* fix(daemon): 改进Qoder模式支持及错误处理机制

- 更新Qoder CLI参数,使用`--yolo`替代`--permission-mode bypass_permissions`
- 将工作目录参数从`--cwd`改为`-w`以符合Qoder文档
- 在agent流事件处理中新增错误捕获并通过SSE错误事件发送
- 运行结束时若检测到agent流错误,则标记运行失败
- 测试中fix(daemon): 优化Qoder代理参数与错误处理

- 调整Qoder启动参数,改用`--yolo`和`-w`替代旧参数,避开argv长度限制
- 增强代理流事件处理,捕获并通过SSE错误通同步更新Qoder参数使用及相应断言
- 新增端到端测试,覆盖Qoder助手错误通过SSE错误通道反馈及运行状态失败处理
- 补充工具函数辅助测试事件流读取与运行状态轮询

Change-Id: I5d933745c3659e093b0d2d807f22726e7f83eb48
Co-developed-by: Qoder <noreply@qoder.com>

* feat(qoder-stream): 识别并报告Qoder运行错误事件

- 新增messageFromResult函数以从结果对象提取错误信息
- 在处理result事件时根据is_error字段触发error事件
- error事件携带具体错误消息和原始数据
- 添加测试验证Qoder运行返回is_error且退出码为0时正确触发错误事件
- 更新qoder流解析测试以校验错误事件映射
- 在聊天路由测试中增加针对Qoder错误运行的端到端场景验证

Change-Id: Ie98ac518135dbec3181c52de5a49afdea993e279
Co-developed-by: Qoder <noreply@qoder.com>
2026-05-06 19:54:03 +08:00
Deheng HuangandGitHub 09b78c2f9b feat(daemon): let Codex image projects use built-in imagegen (#622)
* feat(daemon): let Codex image projects avoid API-key setup

Codex has a built-in image generation path available inside the agent runtime, while the generic media dispatcher still routes gpt-image models through the daemon OpenAI provider. Pass the active agent id into prompt composition so Codex-only gpt-image projects can use built-in imagegen first without changing non-Codex media behavior.

Constraint: Existing media contract remains the default path for non-Codex agents and explicit provider fallback
Rejected: Add a nested daemon Codex media provider | heavier auth, streaming, timeout, cancellation, and output parsing surface for this parity fix
Confidence: high
Scope-risk: narrow
Directive: Keep this override after the media contract so it can intentionally supersede dispatcher-only wording for Codex gpt-image projects
Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/system-prompt-template.test.ts
Tested: pnpm --filter @open-design/daemon typecheck
Tested: pnpm guard
Tested: pnpm typecheck
Not-tested: Live Codex image generation inside the Open Design UI

* fix(daemon): harden Codex imagegen prompt routing

PR review found the Codex override could be superseded by the web-supplied media contract, trusted unvalidated image model metadata, and assumed generated image paths outside the workspace were readable.

This keeps the override daemon-owned, appends it last in the live prompt, validates against registered gpt-image model IDs, allowlists only Codex's generated_images folder, and tightens copy-failure instructions.

Constraint: The web contracts composer still emits the generic media contract without agent identity.

Rejected: Mirror Codex-specific prompt logic into contracts/web | duplicates daemon model registry and still leaves final ordering fragile.

Confidence: high

Scope-risk: narrow

Directive: Keep Codex imagegen override appended after client systemPrompt so it remains the final media instruction for Codex gpt-image projects.

Tested: pnpm --dir apps/daemon exec vitest run -c vitest.config.ts tests/system-prompt-template.test.ts tests/agents.test.ts tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon typecheck

Tested: pnpm guard

Tested: pnpm typecheck

Not-tested: Live Codex image generation inside the Open Design UI

* fix(daemon): keep Codex add-dir writable scope narrow

PR review found Codex --add-dir grants writable workspace access, so passing skill, design-system, and linked reference directories through the same chat allowlist broke their documented read-only boundary.

This routes chat extra directories by active agent: Codex receives only the validated generated_images output directory needed for built-in imagegen, while non-Codex adapters keep the existing resource and linked-directory read access behavior.

Constraint: Codex CLI treats --add-dir as writable sandbox expansion.

Constraint: The daemon still stages active skill files into the project cwd as Codex's read-safe path.

Rejected: Keep one shared extraAllowedDirs list for all agents | grants Codex write access to read-only resources.

Confidence: high

Scope-risk: narrow

Directive: Do not add read-only resource/reference directories to Codex --add-dir unless Codex gains a read-only allowlist flag.

Tested: git diff --check -- apps/daemon/src/server.ts apps/daemon/tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon exec vitest run tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon typecheck

Tested: pnpm guard

Tested: pnpm typecheck

Not-tested: Live Codex image generation inside the Open Design UI

* fix(daemon): validate Codex imagegen add-dir grants

PR review found the generated_images grant still trusted symlinked paths and rendered the Codex override before proving the sandbox grant would be present.

This validates the generated_images directory before prompt assembly, rejects final-component symlinks and protected-root canonical escapes, passes Codex the canonical grant path, and only appends the Codex imagegen override when that same path is in extraAllowedDirs.

Constraint: Codex --add-dir grants writable workspace access, so path aliases into read-only resource roots must be rejected.

Rejected: Keep returning the nominal CODEX_HOME path after validation | leaves Codex operating through a symlink alias instead of the audited grant target.

Confidence: high

Scope-risk: narrow

Directive: Keep Codex imagegen prompt rendering downstream of generated_images validation and grant resolution.

Tested: git diff --check -- apps/daemon/src/server.ts apps/daemon/tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon exec vitest run -c vitest.config.ts tests/agents.test.ts tests/chat-route.test.ts

Tested: pnpm --filter @open-design/daemon typecheck

Tested: pnpm guard

Tested: pnpm typecheck

Not-tested: Live Codex image generation inside the Open Design UI
2026-05-06 18:28:16 +08:00
99d443c512 fix(daemon): ignore .venv and other large dirs in project file watcher (#531)
* fix(daemon): ignore .venv and other large dirs in project file watcher

A project containing a Python virtual environment (.venv) could exhaust
the daemon's file descriptor table — chokidar recursively watched every
file in the tree, opening ~18 000 fds. With the fd table full, macOS
posix_spawn returned EBADF when the daemon tried to create stdio pipes
for a child process (codex exec, or any other agent), surfacing as
"spawn failed: spawn EBADF" on every chat request.

Adds .venv, venv, __pycache__, .mypy_cache, .pytest_cache, .tox,
.ruff_cache, target, vendor, and .cargo to the per-segment IGNORE_NAMES
set so the watcher skips these trees in any project.

* fix(daemon): narrow project-watcher ignores to safe Python dirs only

Remove target, vendor, and .cargo from IGNORE_NAMES — they match any
path segment, so src/vendor/… or .cargo/config.toml (a valid Rust
project file) would be silently dropped from file-change events.

Keep only the Python-specific names (.venv, venv, __pycache__, and the
mypy/pytest/tox/ruff caches) which are never legitimate authored source
at any depth and were the root cause of the fd-exhaustion bug.

Add a real-chokidar test covering all seven newly added ignore dirs.

---------

Co-authored-by: hbrown <hbrown@mitre.org>
2026-05-06 18:07:08 +08:00
PerishFireandGitHub f1cdb2844a test(e2e): gate beta packaged runtime (#637)
* test(e2e): gate beta mac packaged runtime

* test(e2e): separate ui automation layout

* test(e2e): move localized content coverage

* chore(release): prepare packaged 0.4.1 beta validation

* test(e2e): keep ui lane playwright-only

* fix(web): keep chat recoverable after conversation load failure

* fix(desktop): honor native mac quit
2026-05-06 17:44:29 +08:00
GoemonandGitHub 95bd7e5373 fix(daemon): add required env field to McpServerStdio + recover from -32602 on set_model (#627)
* fix(daemon): add required env field to McpServerStdio in live-artifacts MCP descriptor

The ACP schema's McpServerStdio marks env as a required field
(List[EnvVariable] with no default). Omitting it causes Pydantic V2
Union validation to fail across all three variants (HttpMcpServer,
SseMcpServer, McpServerStdio), returning -32602 Invalid params on
session/new for agents with mcpDiscovery: 'mature-acp' (Hermes,
Devin, Kimi).

This bug is invisible when mcpServers resolves to an empty array
(no live-artifacts token), so it only manifests when the MCP
live-artifacts integration is enabled.

* fix(daemon): recover from -32602 Invalid params on session/set_model

Extend the existing -32603 Internal error recovery logic to also
handle -32602 (Invalid params) when the set_model request fails.
This allows the prompt to proceed with the default model instead of
hanging or timing out.

Some ACP agents may not support session/set_model or may reject the
model ID — treating this as a non-fatal condition and falling back to
the default model is more resilient than failing the entire run.

* fix(daemon): narrow -32602 handling and update test fixtures for env field

Address PR #627 review feedback:

1. Narrow -32602 Invalid params suppression to setModelRequestId only.
   Unexpected-id -32602 errors are now treated as real protocol failures
   and propagated via fail(), matching the reviewer's suggestion. Only
   -32603 Internal errors from unexpected IDs are still suppressed as
   cleanup noise.

2. Update all buildLiveArtifactsMcpServersForAgent test fixtures to
   include the new required env: [] field.
2026-05-06 16:28:43 +08:00
SidandGitHub 33255a8fdf Fix agent CLI config and workspace focus mode (#604)
* fix agent CLI config and workspace focus mode

* address CLI env review follow-ups
2026-05-06 16:06:56 +08:00
zzandGitHub dd702c7254 fix(acp): normalize mcpServers to stdio shape for Kimi/Hermes ACP (#612)
Kimi CLI 1.35.0 expects MCP stdio servers to include 'type', 'name',
'command', 'args', and 'env' fields. Open Design was passing only
'name', 'command', and 'args', which caused session/new to return
JSON-RPC -32602 Invalid params when MCP discovery was enabled.

This change normalizes every MCP server descriptor to the full ACP
stdio shape before sending it over the wire.
2026-05-06 15:03:51 +08:00
TomandGitHub 5df04c29a3 feat(daemon): add model name to pi initial status and RPC abort on cancel (#618)
* feat(daemon): add model name to pi initial status and RPC abort on cancel

- Emit status:initializing with model name before pi responds so the UI
  shows 'pi · claude-sonnet-4-5' — matching Claude Code, Copilot, Gemini,
  and Cursor Agent model-name parity
- Replace raw SIGTERM with RPC abort command on cancel, giving pi a
  chance to clean up gracefully before SIGTERM fallback
- Wire run.acpSession onto the run object so cancel() can dispatch to
  session.abort() for pi and ACP adapters
- Add stdinOpen guard so sendCommand is a no-op after stdin closes
- Add 4 tests covering initializing status, abort wire format, and
  stdin-closed guard

* fix(daemon): gate stdout parser after abort to prevent post-cancel events

Once abort() sets finished=true, the stdout listener kept feeding
chunks into mapPiRpcEvent, so text_delta/tool/status events could
still be emitted during the PI_ABORT_GRACE_MS window. Add a finished
guard at the top of the parser callback so no agent events are
forwarded after abort, while still draining stdout cleanly.

Adds a test that aborts mid-session, then feeds message_update and
tool events, proving zero post-abort agent events are emitted.

* refactor(daemon): own SIGTERM fallback in cancel, rewrite abort tests as integration

- Move SIGTERM fallback from pi-rpc abort() to runs cancel() so the
  termination guarantee is centralized — a misbehaving session can't
  leave the child alive indefinitely (address lefarcen P3 on L130)
- Remove the setTimeout/SIGTERM from abort(); it now only sends the
  RPC abort command, termination is the caller's responsibility
- Rewrite initial-status and abort tests as integration tests that
  exercise attachPiRpcSession against mock child processes instead
  of duplicating private sendCommand/send helpers inline (address
  lefarcen P3 on L453 and L491)
- All 28 tests pass
2026-05-06 12:20:40 +08:00
TomandGitHub 6b2792b03a fix(daemon): remove --no-session from pi adapter to persist session files (#557)
* fix(daemon): remove --no-session from pi adapter to persist session files

The pi agent was the only adapter explicitly passing `--no-session`
in its `buildArgs`, preventing pi from writing session files.
All other adapters either run in single-shot mode by design or use
the ACP JSON-RPC session lifecycle without suppressing persistence.

Removing `--no-session` lets `--mode rpc` retain its default behavior
of writing session state, which is needed for multi-prompt continuity
and matches the rest of the harness ecosystem.

* test(daemon): add pi buildArgs regression tests; fix docs for --no-session removal

- Adds test for pi.buildArgs base shape: returns ["--mode", "rpc"]
  and does not include --no-session (prevents regression).
- Adds test for --model and --thinking option passthrough.
- Updates pi-rpc.ts lifecycle comment to remove [--no-session].
- Updates README.md and all localized READMEs to reflect the
  corrected pi CLI invocation.
2026-05-06 00:26:09 +08:00
ChildhoodAndyandGitHub 009d7a5478 refactor(daemon): eliminate duplicate dist tree from two-tsconfig build (#553)
Move sidecar source under src/ so a single tsconfig produces all daemon
output. Removes the parallel dist/src/ tree that was emitted by
tsconfig.sidecar.json (it included src/**/*.ts to type-check the
`../src/server.js` cross-tree import).

Build now emits:
- dist/<flat>            (cli.js, server.js, app-version.js, ...)
- dist/sidecar/{index,server}.js

`dist/sidecar/server.js` reaches the main daemon via `../server.js`
instead of `../src/server.js`, so there is no second copy of the source
tree in the published tarball.

Background — issue #534 (already fixed by #537):
The packaged Settings → About panel showed 0.0.0 because the sidecar
chain loaded the duplicated `dist/src/app-version.js`, where the fixed
`new URL('../package.json', import.meta.url)` resolved to a non-existent
`dist/package.json`. #537 patched the symptom by walking parents until a
real `package.json` is found and by writing `appVersion` into the Linux
packaged config. Both stay in place — they're sound defenses — but the
underlying duplicate-emit was never addressed; any future relative
resource lookup (templates, schemas, prompts) anchored on
`import.meta.url` would have hit the same trap.

This change removes the trap.
2026-05-05 23:31:14 +08:00
Justin GaoandGitHub cbe2baf596 feat(web): add skills & design systems management page in settings (#535)
* feat(web): add skills & design systems management page in settings

Add a new "Library" section in Settings that lets users browse, search,
preview, and enable/disable skills and design systems. Disabled items are
excluded from the create-project picker. Phase 1 — browse/toggle only.

Closes #497

* fix(web): persist empty disabled lists and deduplicate DS preview

Use empty array instead of undefined when all items are re-enabled so
the daemon merge clears the key. Move DS preview panel outside the
category group loop so it renders once, not per group.

* fix(web): address review feedback on library settings

Clear disabled lists on invalid daemon writes, memoize enabled item
filters in App.tsx, and guard preview fetch against rapid-click race
conditions.

* fix(web): hydrate disabled lists from daemon and keep full lists in ProjectView

Merge daemonConfig.disabledSkills/disabledDesignSystems during bootstrap
so the values survive localStorage resets. Pass unfiltered skills and
design systems to ProjectView so existing project metadata resolves
correctly.
2026-05-05 22:50:25 +08:00
cc6da191e8 fix(version): resolve daemon package.json from any compiled layout (#537)
Settings -> About used to display 0.0.0 in packaged builds because
`readCurrentAppVersionInfo` resolved `'../package.json'` relative to
`import.meta.url`, which only points at the daemon package root from the
flat CLI build (`dist/app-version.js`). The sidecar build emits
`dist/src/app-version.js`, where the same relative path lands on the
non-existent `dist/package.json`, so `readPackageMetadata` returned null
and the version fell back to APP_VERSION_FALLBACK.

Walk up from `import.meta.url` to find the nearest real `package.json`
instead, so the daemon reports its actual version regardless of whether
it runs from TypeScript source (tools-dev), the flat CLI dist, or the
nested sidecar dist used by the packaged desktop app. The OD_APP_VERSION
env still wins inside `resolveAppVersionInfo`, so callers that already
inject it (mac/win packagers) keep working.

Also write `appVersion` into the Linux packaged config so Linux follows
the same env-injection path as mac/win and stays consistent with the new
fallback resolution.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 22:02:05 +08:00
79fcaef129 Add Tweaks mode for HTML previews with picker, pod selection, and batched chat attachments (#513)
* Add tweaks mode for HTML preview comments

* Fix tweaks geometry and restore critique migration

* Harden tweaks mode reload sync

* Guard tweaks batch sends during active runs

---------

Co-authored-by: puma <puma@pumas-MacBook-Air.local>
2026-05-05 21:09:20 +08:00
VioletandGitHub 34e8db175d fix(daemon): preserve ANTHROPIC_API_KEY when ANTHROPIC_BASE_URL is set (#514)
* fix(daemon): preserve ANTHROPIC_API_KEY when ANTHROPIC_BASE_URL is set

The claude adapter currently strips ANTHROPIC_API_KEY unconditionally
so that Claude Code's own auth resolution (claude login) wins instead
of silently falling back to API-key billing.

However, when ANTHROPIC_BASE_URL is set the user is intentionally
routing Claude Code to a custom endpoint (e.g. a Kimi/Moonshot proxy).
In that case claude login is meaningless, so preserve the API key so
the child can authenticate against the custom base URL.

* fix(daemon): guard against empty ANTHROPIC_BASE_URL values

Address review feedback: check that ANTHROPIC_BASE_URL contains a
non-empty, non-whitespace string before preserving ANTHROPIC_API_KEY.
This prevents the #398 billing guard from being bypassed when the
variable is set to '' or whitespace.
2026-05-05 21:02:49 +08:00
Marc ChanandGitHub c3d9136a0c Add live artifacts and Composio connector catalog (#381)
* docs: add live artifacts implementation spec

* docs: align live artifacts implementation plan

* Ralph iteration 1: work in progress

* Ralph iteration 2: work in progress

* Ralph iteration 3: work in progress

* Ralph iteration 4: work in progress

* Ralph iteration 5: work in progress

* Ralph iteration 6: work in progress

* Ralph iteration 7: work in progress

* Ralph iteration 8: work in progress

* Ralph iteration 9: work in progress

* Ralph iteration 10: work in progress

* Ralph iteration 11: work in progress

* Ralph iteration 12: work in progress

* Ralph iteration 13: work in progress

* Ralph iteration 14: work in progress

* Ralph iteration 15: work in progress

* Ralph iteration 16: work in progress

* Ralph iteration 17: work in progress

* Ralph iteration 18: work in progress

* Ralph iteration 19: work in progress

* Ralph iteration 20: work in progress

* Ralph iteration 21: work in progress

* Ralph iteration 22: work in progress

* Ralph iteration 23: work in progress

* Ralph iteration 24: work in progress

* Ralph iteration 25: work in progress

* Ralph iteration 26: work in progress

* Ralph iteration 27: work in progress

* Ralph iteration 28: work in progress

* Ralph iteration 29: work in progress

* Ralph iteration 30: work in progress

* Ralph iteration 31: work in progress

* Ralph iteration 32: work in progress

* Ralph iteration 33: work in progress

* Ralph iteration 34: work in progress

* Ralph iteration 35: work in progress

* Ralph iteration 36: work in progress

* Ralph iteration 37: work in progress

* Ralph iteration 38: work in progress

* Ralph iteration 39: work in progress

* Ralph iteration 40: work in progress

* Ralph iteration 41: work in progress

* Ralph iteration 42: work in progress

* Ralph iteration 43: work in progress

* Ralph iteration 44: work in progress

* Ralph iteration 45: work in progress

* Ralph iteration 46: work in progress

* Ralph iteration 47: work in progress

* Ralph iteration 48: work in progress

* Ralph iteration 49: work in progress

* Ralph iteration 50: work in progress

* Ralph iteration 51: work in progress

* Ralph iteration 52: work in progress

* Ralph iteration 53: work in progress

* Ralph iteration 54: work in progress

* Ralph iteration 55: work in progress

* Ralph iteration 56: work in progress

* Ralph iteration 57: work in progress

* Ralph iteration 58: work in progress

* Ralph iteration 59: work in progress

* Ralph iteration 60: work in progress

* Ralph iteration 61: work in progress

* Ralph iteration 62: work in progress

* Ralph iteration 63: work in progress

* Ralph iteration 64: work in progress

* Ralph iteration 65: work in progress

* Ralph iteration 1: work in progress

* Ralph iteration 2: work in progress

* Ralph iteration 3: work in progress

* Ralph iteration 4: work in progress

* Ralph iteration 5: work in progress

* Ralph iteration 6: work in progress

* Ralph iteration 8: work in progress

* Ralph iteration 9: work in progress

* Ralph iteration 17: work in progress

* Add Composio-backed connectors

* Add Composio-backed connector catalog

* Fix connector callback flow

* Update live artifact connector refresh

* Fix live artifact refresh updates

* Improve live artifact viewer toolbar

* Refine live artifact source tabs

* Expand Composio connector catalog

* Improve Composio connector browsing

* Fix artifact refresh source safety checks

Generated-By: looper 0.4.1 (runner=fixer, agent=opencode)

* Fix live artifacts PR feedback

Generated-By: looper 0.5.0 (runner=fixer, agent=opencode)

* Fix live artifact preview CORS validation

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

* Fix connector OAuth IPv6 loopback hosts

Allow bracketed IPv6 loopback Host headers when deriving connector OAuth callback URLs so IPv6-bound daemons can complete connection flow.

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

* Preserve live artifact refresh permissions

Respect explicit refresh permission choices during live artifact create and update flows so revoked connector sources remain gated.

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

* Fix live artifact preview cache freshness

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

* Fix live artifact refresh validation

Guard manual refreshes with local daemon checks and reject daemon_tool sources without a toolName before refresh execution.

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

* Fix Composio credential invalidation

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

* Fix live artifact CORS methods

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

* Fix workspace validation

Restore media config test isolation under Vitest setup data-dir overrides and add the missing French live artifact display copy so the workspace test suite stays aligned.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector safety filtering

Keep agent-preview connector listings aligned with execution safety policy and prune stale Composio OAuth state records before they accumulate.

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix agent runtime cleanup

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix live artifact daemon access

Validate local-only live artifact routes against the peer socket address and pass daemon-resolved CLI paths to ACP MCP descriptors.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector run limit pruning

Evict stale connector rate-limit buckets so long-lived daemon processes do not retain per-run entries indefinitely.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector compact schemas

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Improve connector connection feedback

* Adjust connector gate positioning

* Fix live artifact refresh commits

Avoid marking refresh candidates failed after snapshot or state persistence errors by deferring live artifact mutations until the durable refresh metadata is written. Also align connector OAuth callback host validation with daemon loopback handling.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Improve connector search relevance

* fix(daemon): harden connector connection state

Require loopback daemon validation before connector connect side effects and only clear provider-owned connector statuses during credential reset.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): guard connector disconnect route

Require local daemon request validation before connector disconnect side effects.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): guard composio config updates

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): dispatch live artifacts mcp first

Route the live-artifacts MCP server before the generic MCP CLI so od mcp live-artifacts starts the dedicated server instead of failing generic argument parsing.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): handle integer connector schemas

Allow JSON Schema integer connector inputs while preserving fractional-value validation so generated connector tool schemas accept valid page sizes and limits.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix: align live artifact refresh error codes

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Fix live artifact connector refresh flow

* Update live artifact design cards

* Add beta badge to live artifact form

* Remove live artifact tile model

* Fix live artifact refresh sync

* Fix live artifact MCP refresh durability

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Fix live artifact refresh safety

Enforce persisted refresh opt-out and connector auto-read gating before refresh sources execute.

Generated-By: looper 0.5.5 (runner=fixer, agent=opencode)
2026-05-05 16:42:11 +08:00
76e6c7a9f6 feat: Critique Theater Phase 4 (persistence + transcript + orchestrator) (#481)
* docs(specs): add Critique Theater design spec for panel-tempered artifacts

* docs(specs): add Critique Theater implementation plan

* docs(specs): rename UI to Design Jury, add lane-density modes, ship-rule explainer, label sizing

* feat(contracts): add CritiqueConfig schema and defaults

* fix(contracts): apply Task 1.1 review (CRITIQUE_PROTOCOL_VERSION rename, descriptions, RoleWeights export)

* feat(contracts): add PanelEvent discriminated union and isPanelEvent guard

* fix(contracts): apply Task 1.2 review (exhaustive event-type list, runId guard, import order)

* feat(contracts): add CritiqueSseEvent variants and panelEventToSse mapper

* test(daemon): add v1 wire-protocol golden fixtures for Critique Theater parser

* feat(daemon): add v1 streaming parser for Critique Theater wire protocol

* chore(contracts): add .js extensions to relative imports for NodeNext consumers

* fix(daemon): satisfy noUncheckedIndexedAccess in v1 parser regex match access

* test(daemon): cover parser failure modes; fix unclosed-PANELIST swallow bug

* fix(daemon,contracts): address PR #387 review

- parser now clamps panelist + DIM scores against the run-declared scale
  captured from <CRITIQUE_RUN scale=...>, not a hardcoded 100
- PANELIST appearing before any <ROUND n=...> opens now throws
  MalformedBlockError rather than emitting events with NaN round
- DIM_RE and MUST_FIX_RE hoisted to module scope and lastIndex reset per
  call so the parser hot path stops recompiling regex per artifact
- overflow check after drain simplified to a plain buf.length > cap test
  (the prior compound condition was always true on the right side and
  obscured intent)
- scoreThreshold <= scoreScale refine gains a 1e-9 epsilon so floating
  slack does not reject semantically valid configs
- round-1 designer ARTIFACT guard gains a comment naming the spec
  invariant and the v2 relaxation path
- 3 new regression tests cover the panelist-without-round, scale=10
  clamp, and scale=20 plumbing cases

* docs(specs): rationale for non-goals, failure-mode rate targets, Phase 10 matrix, Phase 14 doc layout

* Merge branch 'main' into feat/critique-theater

Resolves the contracts/index.ts conflict by keeping the .js extensions added
by chore(contracts) 2d6e8d6 and slotting in the new export for ./api/app-config
introduced upstream by #255 (9d700ec). Critique Theater additions
(./sse/critique, ./critique) preserved in their original positions.

Verified after merge:
  pnpm --filter @open-design/contracts test    -> 10/10 pass
  pnpm --filter @open-design/contracts typecheck -> exit 0
  pnpm --filter @open-design/daemon typecheck  -> exit 0
  pnpm --filter @open-design/web typecheck     -> exit 0

Two daemon tests in tests/media-config.test.ts fail both before and after the
merge because they read real OAuth credentials from the developer machine
instead of using mock fixtures. That's an upstream isolation issue on
origin/main, not something this branch introduces.

* fix: unblock web build and address mrcfps PANELIST oversize bypass

The chore commit that added .js extensions to satisfy daemon's nodenext
typecheck broke apps/web's Next.js build, because webpack tried to resolve
the literal ./common.js when only common.ts exists on disk. Replaced with
a subpath approach: contracts/exports gains a './critique' entry pointing
straight at src/critique.ts (which has no relative imports), and daemon
imports route through @open-design/contracts/critique instead of the
barrel. Web keeps the bundler-friendly barrel; daemon's nodenext walks
only the leaf module. All 13 contracts source files reverted to no-.js.

Separately, mrcfps flagged that parserMaxBlockBytes was only enforced on
the leftover buffer after drain returned, so a complete oversized block
arriving in one chunk slipped past the cap. Added an explicit per-block
size check inside drain for every buffered block type (PANELIST,
ROUND_END, SHIP). Three regression tests yield the whole stream as a
single chunk and assert OversizeBlockError fires before any events emit.

* fix(daemon): close three v1 parser invariant gaps from mrcfps review

Three independent gaps that all let malformed or oversized protocol
output pass the v1 envelope contract:

(1) Envelope guard. ROUND, PANELIST, ROUND_END, and SHIP now throw
MalformedBlockError when state.inRun is false. Without this, a stream
that omits <CRITIQUE_RUN> could still emit panelist_* events without
the run_started handshake, leaving downstream reducers with no run-level
config.

(2) UTF-8 byte length. Both the per-block size check and the post-drain
buf-size check now compare Buffer.byteLength(text, 'utf8') against
parserMaxBlockBytes. The previous string-length comparison let multibyte
content (CJK, emoji) inside <NOTES>/<SUMMARY> exceed the configured
byte cap while staying under the JS string length cap, bypassing the
daemon's resource guard.

(3) Header-end ordering. PANELIST, ROUND_END, and SHIP now require the
opener's > to appear before the matched closing tag. A malformed opener
like <PANELIST role="x" score="8"</PANELIST> previously fell through
to the closing tag's > and emitted events for an invalid block.

Four regression tests cover each gap (ROUND-without-run,
SHIP-without-run, multibyte-byte-cap, malformed-opener).

* feat(daemon): add critique_runs persistence (Task 4.1)

Introduces a new SQLite table critique_runs to back the orchestrator's
run lifecycle. Plan called for ALTER TABLE artifacts ADD COLUMN ..., but
artifacts is not a DB concept in this repo; runs get their own table.

- migrateCritique(db) creates the table + two indexes idempotently and
  is wired into the existing migrate(db) flow on daemon boot.
- CRUD helpers (insertCritiqueRun, getCritiqueRun, updateCritiqueRun,
  listCritiqueRunsByProject, deleteCritiqueRun) round-trip rounds_json
  through helpers so callers see typed CritiqueRunRow.
- reconcileStaleRuns flips stale 'running' rows to 'interrupted' with
  a recoveryReason='daemon_restart' marker, supporting the spec's
  daemon-restart-mid-run failure mode.
- Public CritiqueRunStatus union excludes the in-flight 'running' value
  but the runtime CHECK accepts it, matching the spec's lifecycle.
- 11 vitest cases cover migration idempotence, round-trip, default
  rounds, status validation, update + list ordering, deletion, and
  reconciliation, plus FK CASCADE on project deletion.

* feat(daemon): add Critique Theater transcript writer (Task 4.2)

Streams PanelEvent sequences to .ndjson on disk under the artifact dir,
gzipping to .ndjson.gz when the cumulative UTF-8 byte size crosses
gzipThresholdBytes (default 256 KiB). Uses Node fs streams plus
zlib.createGzip so the writer never holds the full transcript in memory.
readTranscript inverts the path and streams events back, picking the
right pipeline by file extension. Covers happy path, large multibyte,
empty input, mid-stream failure cleanup, and unknown-extension reject.

* feat(daemon): add Critique Theater orchestrator (Task 4.3)

Drives one run end-to-end: parses stdout via parseCritiqueStream, scores
each round through scoreboard helpers, persists lifecycle to critique_runs,
and emits CritiqueSseEvent variants on the existing project event bus.
Honors per-round and total timeouts, applies fallbackPolicy when no
<SHIP> arrives, and tees events into writeTranscript so transcripts
stream to disk without buffering the whole run in memory. Defensive entry
validation throws RangeError on invalid CritiqueConfig before any side
effect.

Also adds scoreboard.ts (computeComposite, decideRound, selectFallbackRound)
and re-exports panelEventToSse/CritiqueSseEvent from the critique subpath
so daemon imports never touch the barrel. Fixes missing .js extensions in
sse/critique.ts that caused NodeNext module resolution errors.

* feat(daemon): wire Critique Theater orchestrator into spawn path (Task 4.4)

Adds loadCritiqueConfigFromEnv to read OD_CRITIQUE_* keys with strict
validation at boot. Branches the existing CLI spawn flow on cfg.enabled:
when false (the M0 default) the legacy single-pass generation runs
unchanged; when true the orchestrator owns the run end-to-end. Same SSE
bus, same artifact dir, no behavior change for users until they flip the
flag.

* fix(lockfile): regenerate to include contracts zod + vitest entries

The earlier conflict resolution took main's lockfile and ran pnpm
install, but the install pass on Windows didn't write the contracts
package's zod and vitest entries back into the lockfile. CI's
--frozen-lockfile install rejected the resulting state. Re-running
pnpm install with --no-frozen-lockfile rewrites the lockfile so it
now matches every package.json across the workspace, including
contracts/zod ^3.23.8 and contracts/vitest ^2.1.8. Verified locally:
pnpm install --frozen-lockfile passes.

* fix(daemon): parser ship envelope, SHIP-before-round guard, real artifactRef (Defects 3 + 5)

- ParserOptions gains projectId + artifactId; the parser threads them into
  every emitted ship event's artifactRef so downstream consumers see the
  real run identity instead of empty placeholders.
- <SHIP> now requires at least one closed <ROUND_END> in the same run;
  malformed streams that emit SHIP before any round complete now throw
  MalformedBlockError instead of bypassing the round-1 artifact invariant.
- The SHIP handler validates the inner <ARTIFACT> block is present and
  non-empty; missing artifact raises MissingArtifactError.
- Three new regressions: SHIP-before-round, SHIP-without-artifact,
  artifactRef populated from parser options.
- Orchestrator threads projectId + artifactId into parserOpts.
- Test fixtures updated to include <ARTIFACT> inside <SHIP> blocks.

* fix(daemon): orchestrator owns lifecycle, gzip atomicity, fallback on timeout (Defects 2,4,7,8)

- Orchestrator now accepts child + childExitPromise, races parser /
  child-exit / abort / timeout in one awaited flow, and SIGTERMs the
  child on every non-clean termination. Server awaits the result so
  the run lifecycle has a single owner.
- ChildExitError surfaces when child exits non-zero mid-stream; the
  run is classified as failed with cause cli_exit_nonzero.
- Timeout / abort with at least one completed round elects a fallback
  via selectFallbackRound and emits a synthetic ship event with
  status=timed_out or interrupted; the score persists to
  critique_runs instead of staying null.
- applyTimeouts includes childExitRace in every Promise.race so early
  child exits are classified without waiting for the total timeout.
  iter.return() cleanup is capped at 200ms to prevent hang on
  stalling generators.
- writeTranscript writes gzip output to transcript.ndjson.gz.tmp,
  fsyncs, then atomic-renames. Crashes mid-write leave no partial
  .gz or .gz.tmp on disk.

* fix(daemon): plain-stream gating, per-run artifact dir, boot reconcile (Defects 1, 2, 6)

- Spawn-path branch now inspects def.streamFormat and only routes through
  runOrchestrator when format === 'plain'. Adapters emitting wrapper
  formats (claude-stream-json, copilot-stream-json, json-event-stream,
  acp-json-rpc, pi-rpc) fall through to legacy single-pass with a
  one-time stderr warning per format. Per-format decoding into the
  orchestrator is reserved for v2.
- critiqueArtifactDir is now path.join(ARTIFACTS_DIR, projectId, runId)
  so concurrent or sequential runs in the same project never overwrite
  each other's transcript or final HTML. Persistence stores the relative
  per-run path.
- reconcileStaleRuns is now invoked after openDatabase on every daemon
  boot with staleAfterMs = critiqueCfg.totalTimeoutMs. Stale running
  rows from a prior crash flip to interrupted with rounds_json.
  recoveryReason='daemon_restart'. Logs a one-line warning naming the
  flipped count when greater than zero.
- Spawn now passes child + childExitPromise to runOrchestrator so the
  orchestrator can race child exit against the parser, abort signal,
  and timeouts in one awaited flow. Server awaits the orchestrator's
  result and surfaces failures through the existing run lifecycle.

* fix(daemon): daemon-authoritative scoring, lifecycle status, stderr ordering, insert type

Round 2 review feedback on PR #481.

1. CritiqueRunInsert.status now accepts 'running' so the boot-reconcile
   tests (and any caller seeding an in-flight row) typecheck without
   casting. The runtime check in insertCritiqueRun already accepted
   'running' against the DB constraint set, only the public type was
   stricter than the DB.
2. round_end keeps the daemon-computed composite authoritative. The
   agent's <ROUND_END composite=...> attribute is advisory: a divergence
   beyond COMPOSITE_TOLERANCE emits a composite_mismatch parser_warning
   so the discrepancy is observable, but the daemon value is what scores
   and persists. Same policy for must_fix.
3. SHIP-handling derives the final status from decideRound(...) using the
   daemon's scored round rather than trusting <SHIP composite=... status=...>.
   A run that the agent claims as shipped but whose daemon composite is
   below threshold now finalizes as below_threshold, so a malformed or
   adversarial stream cannot force a ship.
4. server.ts captures the orchestrator's result and maps the critique
   terminal status to the chat run lifecycle. shipped/below_threshold
   finalize as 'succeeded'; timed_out/interrupted/degraded/failed
   finalize as 'failed'. cancelRequested is honored.
5. stderr forwarding and child.on('error') registrations moved BEFORE
   the orchestrator await so a CLI that floods stderr cannot fill the
   OS pipe and deadlock until the total timeout, and so an early
   child error fired during the run is observed by the same listener
   used after.

Tests:
- tests/critique-authority.test.ts: 3 new regressions (lying ship
  downgraded to below_threshold, mismatch warning emitted, aligned
  composites stay quiet).
- All four affected suites green: 14 orchestrator + 10 spawn-wiring +
  3 boot-reconcile + 3 authority = 30/30.

Workspace typechecks: contracts, daemon, web all exit 0.

* fix(daemon,contracts): inline critique SSE, signal-terminated child, null shipped artifactPath

Round 3 review feedback on PR #481.

1. packages/contracts/src/critique.ts inlines CritiqueSseEvent +
   panelEventToSse + CRITIQUE_SSE_EVENT_NAMES + a local mirror of
   SseTransportEvent. The previous re-export from './sse/critique.js'
   broke the workspace web build (Turbopack cannot rewrite .js to .ts
   on a relative source import) while removing the .js extension broke
   daemon's NodeNext typecheck (it walks this leaf via the './critique'
   subpath export which requires explicit .js extensions). Inlining
   removes the cross-file relative import entirely so both consumers
   walk one self-contained file. packages/contracts/src/sse/critique.ts
   is removed and its co-located test moves up to
   packages/contracts/src/critique.test.ts. The barrel
   packages/contracts/src/index.ts drops the redundant
   './sse/critique' re-export since './critique' already exports the
   same symbols.

2. apps/daemon/src/critique/orchestrator.ts treats a signal-terminated
   child as a terminal race rejection. Previously the race only caught
   non-zero numeric exit codes and treated code === null as
   indefinitely pending, so a SIGTERM from /api/runs/:id/cancel
   resolved childExitPromise as { code: null, signal: 'SIGTERM' } and
   the orchestrator fell through to the no-SHIP fallback path,
   persisting below_threshold instead of interrupted. The race now
   rejects with a new ChildSignaledError when signal !== null, and a
   new catch branch classifies the run as 'interrupted' and (if at
   least one round closed) emits a synthetic ship event with
   status='interrupted' so the persisted row and the SSE transcript
   reflect the actual cause.

3. Same file, ship-handling: artifactPath is now persisted as null on
   shipped runs until a future phase actually extracts the
   <SHIP><ARTIFACT> body to disk. Previously the orchestrator wrote
   ${artifactDir}/${artifactId} even though no file existed at that
   path, so any later replay/export/UI code that trusted
   critique_runs.artifact_path would dereference a missing file. The
   transcript still records the ship event with the artifact reference
   so consumers can find the run.

Tests:
- apps/daemon/tests/critique-lifecycle.test.ts: 2 new regressions
  (SIGTERM-terminated child after one closed round persists
  'interrupted' with a synthetic ship event of the same status; shipped
  run leaves artifactPath null in result and DB row).
- 43 critique-suite tests pass: 14 orchestrator + 11 transcript +
  10 spawn-wiring + 3 boot-reconcile + 3 authority + 2 lifecycle.

Workspace typechecks: contracts, daemon, web all exit 0.

* fix(daemon): buffer raw SHIP, emit only normalized; reject SHIP for unclosed round

Round 4 review feedback on PR #481.

The parser-event loop used to unconditionally collectedEvents.push(event)
and bus.emit(panelEventToSse(event)) for every event, including raw
<SHIP>. SSE clients and the transcript could see the agent's forged
status="shipped" / composite="9.5" before decideRound(...) ran, even
when the daemon later corrected the persisted DB row to below_threshold.
The loop now skips ship events entirely; the orchestrator buffers the
raw shipEvent, runs daemon-authoritative scoring, and emits a single
normalized ship payload built from the daemon's computed composite,
selectFallbackRound's mustFix, and decideRound's status. The transcript
and SSE bus now only ever see the daemon-scored ship.

The unknown-round fallback used to make agent-claimed status/composite
authoritative when SHIP referenced a round that was never closed: a
malformed stream could close low round 1, then send <SHIP round="2"
status="shipped" composite="10">, completedRounds.find(r => r.n === 2)
was undefined, and the orchestrator persisted the agent's value. That
re-opened the scoring-integrity hole the previous round was meant to
close. The orchestrator now drops a SHIP whose round isn't in
completedRounds, emits a parser_warning, and falls through to the
no-SHIP fallback policy. The synthetic ship from selectFallbackRound
gets emitted instead, with daemon-authoritative round/composite/status.

Tests:
- tests/critique-authority.test.ts: extended the lying-ship regression
  to also assert the emitted critique.ship payload is downgraded
  (status='below_threshold', composite < threshold), so the SSE bus
  cannot see the agent's claim. Added a new regression where SHIP
  references an unclosed round 2: the agent ship is dropped, a
  parser_warning fires, the fallback selects round 1, and the only
  emitted critique.ship has round=1 and status=below_threshold.
- 44 critique-suite tests pass: 14 orchestrator + 11 transcript + 10
  spawn-wiring + 3 boot-reconcile + 4 authority + 2 lifecycle.

Workspace daemon typecheck exits 0.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-05 15:50:35 +08:00
PerishFireandGitHub bbdd4e84b5 chore: enforce test directory conventions (#496)
* chore: enforce test directory conventions

Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only.

* ci: use guard and package-scoped tests

Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases.

* ci: align stable release guard check

Use the new repository guard in stable release verification after replacing the residual-JS-only script.

* chore: tighten test layout enforcement

Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests.

* chore: clarify no-emit test tsconfigs

Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs.
2026-05-05 15:34:22 +08:00
Chris TamandGitHub a3a0ae6ced fix(daemon): respect baseUrl path verbatim in OpenAI-compat proxy (#410)
* fix(daemon): respect baseUrl path verbatim in OpenAI-compat proxy

`appendVersionedApiPath` previously force-injected `/v1` unless the
supplied baseUrl ended with `/vN`. That broke any provider whose
OpenAI-compatible surface lives under a sub-path:

  https://api.deepinfra.com/v1/openai → ".../v1/openai/v1/chat/completions"
  https://openrouter.ai/api/v1        → ".../api/v1/chat/completions" (worked, by luck of /vN suffix)

Now the auto-`/v1` only fires when the user supplied no path at all, so
DeepInfra, OpenRouter, and any other sub-path-mounted compat surface
route to the right endpoint while the canonical
`https://api.openai.com` / `https://api.anthropic.com` shortcuts still
work. Adds a regression test table covering the matrix.

* Account for Anthropic style URLs
2026-05-05 13:22:54 +08:00
GoemonandGitHub b5598572d8 fix(daemon): scope -32603 recovery to session/set_model request id (#492)
Address review feedback from lefarcen and mrcfps on PR #492:

P1: Track setModelRequestId to scope the recovery block to the exact
session/set_model request. This prevents duplicate prompt sends if
session/prompt returns -32603 (which would otherwise match on
expectedId + non-default model conditions).

P1: Add promptRequestId === null guard so the recovery path only
triggers before a prompt has been sent.

P2: In detectAcpModels, only suppress -32603 on unexpected ids.
Expected-id -32603 errors (initialize, session/new) are real probe
failures and should reject immediately rather than causing a silent
15s timeout.

P2: In attachAcpSession, expected-id -32603 errors that don't match
setModelRequestId now call fail() instead of falling through. This
prevents initialize/session/new/session/prompt failures from being
silently swallowed.
2026-05-05 13:03:13 +08:00
0ab98fed07 fix(daemon): correct Claude Code --add-dir capability detection (fix #430) (#440)
Probe 'claude -p --help' (subcommand help) instead of 'claude --help'
(global help), because --add-dir and --include-partial-messages are
subcommand-specific flags that do not appear in the global help output.

Previously, successful capability probing would set caps.addDir = false
because the flag was not found in global help, causing the daemon to
never pass --add-dir to spawned Claude Code processes. This broke
absolute-path reads for skill seeds and design-system specs, as
Claude Code's sandbox policy blocks access to paths outside the
project cwd unless explicitly allowed via --add-dir.

The probing-failure / timeout path already worked correctly (empty
caps object defaults to passing --add-dir). The failure manifested
only when help probing succeeded and explicitly failed to locate
the subcommand flags.

Co-authored-by: bojiehuang <bojiehuang@bojiehuangdeMacBook-Pro.local>
2026-05-05 12:52:31 +08:00
Justin GaoandGitHub cc8add4f09 feat(daemon): add link code folder support for agent context (#455)
* feat(daemon): add link code folder support for agent context

Users can now link local code directories to a project so the AI agent
reads their source code via --add-dir when generating designs. The
import menu's "Link code folder" item opens a native OS folder picker,
and linked folders appear as removable chips below the chat input.

- Add linkedDirs field to ProjectMetadata contract
- Add POST /api/dialog/open-folder endpoint (osascript/zenity/PowerShell)
- Add validateLinkedDirs with path safety checks (absolute, exists, blocklist)
- Append linked dirs to extraAllowedDirs in startChatRun
- Add system prompt hint listing linked code folders
- Render linked folder chips in ChatComposer with add/remove
- Add i18n strings for all 16 locales
- Add 8 unit tests for validateLinkedDirs

* fix: address PR review feedback

- Add JSDoc type annotations to validateLinkedDirs for strict mode
- Check path.isAbsolute before resolve to catch relative inputs
- Allow linking when projectMetadata is undefined (default to prototype)
- Remove redundant PATCH in ProjectView callback

* fix: use inline TS annotations instead of JSDoc in linked-dirs.ts

* fix(daemon): harden linked-dirs validation against security bypasses

- Resolve symlinks with realpathSync.native before checking blocklist
- Reject filesystem root (/) and drive roots as linked dirs
- Canonicalize blocklist entries to handle macOS /etc -> /private/etc
- Validate linkedDirs on project creation, not just PATCH
- Re-validate persisted linkedDirs in startChatRun before use
- Add tests for root, symlink-to-blocked-dir, and realpath resolution

* fix: narrow union type before accessing .error in tests
2026-05-05 12:46:39 +08:00
PerishFireandGitHub 3935aeb421 Optimize packaged mac artifact size (#424)
* optimize mac package payload reporting

* optimize(pack): package standalone web runtime

* optimize(pack): default to standalone web runtime

* chore(release): bump beta base version

* fix(pack): compress mac artifacts and report packaged version

* fix(pack): preserve Next server fallback

* fix(pack): clarify standalone startup failures

* fix(release): gate beta platform builds

* fix(web): bind standalone backend to parent

* fix(pack): harden standalone and beta publishing
2026-05-05 10:37:19 +08:00
Ajay SatishandGitHub ec7dafc007 feat(daemon): add Kilo CLI (ACP) (#480)
* docs(readme): refresh contributors wall

* docs(readme): refresh contributors wall

* feat: kilo cli

* fix: use default model option for kilo

* chore: add  agent_diff id unique test

* chore: add deepseek to docs
2026-05-05 09:17:56 +08:00
IrixandGitHub 6380c48a48 Update Codex sandbox invocation (#477) 2026-05-05 09:15:32 +08:00
alburtandGitHub b4e69ac61b fix(copilot): copilot prompt processing with correct command format (#466) 2026-05-05 00:28:42 +08:00
ChildhoodAndyandGitHub bc04d61903 feat(design-files): add batch ZIP download with multi-select (#405)
* feat(design-files): add batch ZIP download with multi-select

Add checkbox-based multi-select to the Design Files panel and a new
POST /api/projects/:id/archive/batch endpoint that zips selected files
using the project name as the archive filename.

* fix(i18n): add missing batch-download keys to uk locale

The upstream main branch has a uk.ts locale that didn't exist in the
fork. Without these keys the web typecheck fails against the full
locale set.

* fix(design-files): harden batch archive and prune stale selections

- Use lstat to reject symlinks, dotfiles, and .artifact.json sidecars
  in buildBatchArchive (mirror listFiles/collectFiles allowlist)
- Reject invalid names explicitly instead of silently skipping
- Prune stale filenames from selected set on files/projectId change

* fix(daemon): tighten batch archive allowlist with segment-level checks

- Check every path segment for hidden directories, not just basename
- Walk intermediate directories with lstat to reject symlinks at any level
- Fail-fast on any ineligible file instead of silently skipping
2026-05-05 00:10:26 +08:00
Tom HuangandGitHub 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
emilneanderandGitHub 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, PhDandGitHub 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
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
47eeaf445d feat: Critique Theater foundation (contracts + parser, Phases 0-2) (#387)
* docs(specs): add Critique Theater design spec for panel-tempered artifacts

* docs(specs): add Critique Theater implementation plan

* docs(specs): rename UI to Design Jury, add lane-density modes, ship-rule explainer, label sizing

* feat(contracts): add CritiqueConfig schema and defaults

* fix(contracts): apply Task 1.1 review (CRITIQUE_PROTOCOL_VERSION rename, descriptions, RoleWeights export)

* feat(contracts): add PanelEvent discriminated union and isPanelEvent guard

* fix(contracts): apply Task 1.2 review (exhaustive event-type list, runId guard, import order)

* feat(contracts): add CritiqueSseEvent variants and panelEventToSse mapper

* test(daemon): add v1 wire-protocol golden fixtures for Critique Theater parser

* feat(daemon): add v1 streaming parser for Critique Theater wire protocol

* chore(contracts): add .js extensions to relative imports for NodeNext consumers

* fix(daemon): satisfy noUncheckedIndexedAccess in v1 parser regex match access

* test(daemon): cover parser failure modes; fix unclosed-PANELIST swallow bug

* fix(daemon,contracts): address PR #387 review

- parser now clamps panelist + DIM scores against the run-declared scale
  captured from <CRITIQUE_RUN scale=...>, not a hardcoded 100
- PANELIST appearing before any <ROUND n=...> opens now throws
  MalformedBlockError rather than emitting events with NaN round
- DIM_RE and MUST_FIX_RE hoisted to module scope and lastIndex reset per
  call so the parser hot path stops recompiling regex per artifact
- overflow check after drain simplified to a plain buf.length > cap test
  (the prior compound condition was always true on the right side and
  obscured intent)
- scoreThreshold <= scoreScale refine gains a 1e-9 epsilon so floating
  slack does not reject semantically valid configs
- round-1 designer ARTIFACT guard gains a comment naming the spec
  invariant and the v2 relaxation path
- 3 new regression tests cover the panelist-without-round, scale=10
  clamp, and scale=20 plumbing cases

* docs(specs): rationale for non-goals, failure-mode rate targets, Phase 10 matrix, Phase 14 doc layout

* Merge branch 'main' into feat/critique-theater

Resolves the contracts/index.ts conflict by keeping the .js extensions added
by chore(contracts) 2d6e8d6 and slotting in the new export for ./api/app-config
introduced upstream by #255 (9d700ec). Critique Theater additions
(./sse/critique, ./critique) preserved in their original positions.

Verified after merge:
  pnpm --filter @open-design/contracts test    -> 10/10 pass
  pnpm --filter @open-design/contracts typecheck -> exit 0
  pnpm --filter @open-design/daemon typecheck  -> exit 0
  pnpm --filter @open-design/web typecheck     -> exit 0

Two daemon tests in tests/media-config.test.ts fail both before and after the
merge because they read real OAuth credentials from the developer machine
instead of using mock fixtures. That's an upstream isolation issue on
origin/main, not something this branch introduces.

* fix: unblock web build and address mrcfps PANELIST oversize bypass

The chore commit that added .js extensions to satisfy daemon's nodenext
typecheck broke apps/web's Next.js build, because webpack tried to resolve
the literal ./common.js when only common.ts exists on disk. Replaced with
a subpath approach: contracts/exports gains a './critique' entry pointing
straight at src/critique.ts (which has no relative imports), and daemon
imports route through @open-design/contracts/critique instead of the
barrel. Web keeps the bundler-friendly barrel; daemon's nodenext walks
only the leaf module. All 13 contracts source files reverted to no-.js.

Separately, mrcfps flagged that parserMaxBlockBytes was only enforced on
the leftover buffer after drain returned, so a complete oversized block
arriving in one chunk slipped past the cap. Added an explicit per-block
size check inside drain for every buffered block type (PANELIST,
ROUND_END, SHIP). Three regression tests yield the whole stream as a
single chunk and assert OversizeBlockError fires before any events emit.

* fix(daemon): close three v1 parser invariant gaps from mrcfps review

Three independent gaps that all let malformed or oversized protocol
output pass the v1 envelope contract:

(1) Envelope guard. ROUND, PANELIST, ROUND_END, and SHIP now throw
MalformedBlockError when state.inRun is false. Without this, a stream
that omits <CRITIQUE_RUN> could still emit panelist_* events without
the run_started handshake, leaving downstream reducers with no run-level
config.

(2) UTF-8 byte length. Both the per-block size check and the post-drain
buf-size check now compare Buffer.byteLength(text, 'utf8') against
parserMaxBlockBytes. The previous string-length comparison let multibyte
content (CJK, emoji) inside <NOTES>/<SUMMARY> exceed the configured
byte cap while staying under the JS string length cap, bypassing the
daemon's resource guard.

(3) Header-end ordering. PANELIST, ROUND_END, and SHIP now require the
opener's > to appear before the matched closing tag. A malformed opener
like <PANELIST role="x" score="8"</PANELIST> previously fell through
to the closing tag's > and emitted events for an invalid block.

Four regression tests cover each gap (ROUND-without-run,
SHIP-without-run, multibyte-byte-cap, malformed-opener).

* fix(lockfile): regenerate to include contracts zod + vitest entries

The earlier conflict resolution took main's lockfile and ran pnpm
install, but the install pass on Windows didn't write the contracts
package's zod and vitest entries back into the lockfile. CI's
--frozen-lockfile install rejected the resulting state. Re-running
pnpm install with --no-frozen-lockfile rewrites the lockfile so it
now matches every package.json across the workspace, including
contracts/zod ^3.23.8 and contracts/vitest ^2.1.8. Verified locally:
pnpm install --frozen-lockfile passes.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-04 20:28:28 +08:00
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
monshunterandGitHub 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