PerishCode follow-up review on PR #3275:
The Path A Nginx block as written would 401 every UI call except the
three open probes (/api/health, /api/version, /api/daemon/status). Same
root cause as the ACK fix in 9d5f6ec — the auth model affects both paths,
not just direct-container deployments.
Verified against source:
- deploy/scripts/install.sh:386 always writes a generated OD_API_TOKEN
into deploy/.env (no opt-out flag).
- deploy/docker-compose.yml:18 requires OD_API_TOKEN (Compose ? syntax)
and binds OD_BIND_HOST=0.0.0.0, so the daemon-side bearer middleware
is always active for the Compose path.
- apps/daemon/src/server.ts:3777 keys the loopback short-circuit on
isLoopbackPeerAddress(req.socket?.remoteAddress) — the TCP peer, not
X-Forwarded-For — so a reverse-proxied request from a Docker bridge
IP never gets the localhost bypass.
Adds proxy_set_header Authorization to the Nginx block, a paragraph
explaining where OD_API_TOKEN comes from, and updates the pitfalls row
that previously only mentioned CORS to also list the missing-bearer
cause.
Address PerishCode review on PR #3275:
- The daemon defaults to OD_BIND_HOST=127.0.0.1 (apps/daemon/src/server.ts),
so the readinessProbe and ClusterIP Service in the previous manifest could
never reach the Pod. Add OD_BIND_HOST=0.0.0.0 + OD_API_TOKEN (required by
the bound-API-token guard for non-loopback binds) and a kubectl secret step.
- The daemon reads OD_ALLOWED_ORIGINS, not OPEN_DESIGN_ALLOWED_ORIGINS.
OPEN_DESIGN_* names are Compose-only aliases mapped in deploy/docker-compose.yml.
Use OD_ALLOWED_ORIGINS for the direct-container ACK path and call out both
names in the network-exposure section and the pitfalls table.
Also adds an Ingress / Bearer-token note for operators fronting the
Service externally.
Adds docs/deployment/cloud/aliyun.md with:
- ECS single-machine deployment using the existing Docker Compose stack
- ACK (Kubernetes) reference manifest and multi-replica caveats
- Image acceleration setup via Alibaba Cloud Container Registry
- ICP filing (备案) overview for mainland China public hosting
- Common pitfalls and references to existing Docker / install-guide docs
Docs-only slice for #1025. Live ROS templates, one-click scripts, and
verification screenshots are out of scope here and tracked as follow-up
work in the issue.
* feat(daemon): add Antigravity agent adapter
Adds Google Antigravity (`agy` CLI) as a coding-agent runtime. Detection
picks up `agy` on PATH, the daemon spawns `agy -p "<prompt>"` for a
single non-interactive turn, and the assistant text reply streams back
on stdout. OAuth is shared with the Antigravity IDE through the system
keyring, so users who have signed into the desktop app are authenticated
on first run with no extra step.
`agy` v1.0.3 has no JSON / stream-json / ACP output mode (upstream issue
#119), no `--model` flag (issue #35), and no MCP forwarding hook yet —
the adapter ships with `streamFormat: 'plain'` and a single `default`
fallback model so the model picker doesn't mislead users into thinking
their choice is wired through. We will upgrade buildArgs + add a
dedicated event parser when upstream ships structured output.
Also gitignores `.antigravitycli/`, the project-local config directory
`agy` auto-creates on every run (upstream issue #175).
* fix(daemon): Antigravity adapter — stdin prompt, brand icon, form loop, empty-output guard
- Switch prompt delivery from argv to stdin (`agy -p -`) to avoid the
30KB maxPromptArgBytes limit that blocked real-world composed prompts
- Add official Antigravity brand SVG icon to agent picker
- Fix repeated question-form loop for plain agents by injecting an
OVERRIDE block when form answers are already present in the transcript
- Add empty-output guard for plain agents so expired auth or silent
failures surface a user-visible error instead of a blank "Done" turn
* feat(daemon): expand Antigravity adapter — model picker, form-loop fix, OAuth launcher, log-file classification
PR #3157 follow-up integrating four iterations from end-to-end manual
testing on Gemini 3.5 Flash + GPT-OSS 120B Medium through `agy` v1.0.3.
Each section is independently verifiable; combined they're what made
the first successful artifact generation work end-to-end.
## Model picker via settings.json (agy has no --model flag)
agy v1.0.3 ships no `--model` CLI flag (upstream issue #35), but the
TUI Switch-Model picker writes the chosen label to
`~/.gemini/antigravity-cli/settings.json`'s `"model"` field, and every
`-p` invocation re-reads that file on startup — verified by capturing
the `--log-file` line `Propagating selected model override to backend:
label="<model>"`. Antigravity's `fallbackModels` now lists the 8
labels its TUI exposes (Gemini 3.1 Pro / 3.5 Flash variants, Claude
Sonnet/Opus 4.6 Thinking, GPT-OSS 120B Medium) and `buildArgs`
persists the user's choice to settings.json right before spawn. The
synthetic `default` id is preserved — picking it leaves settings.json
untouched so a user who switches models from agy's own TUI keeps
their choice.
Introduces `RuntimeAgentDef.supportsCustomModel?: boolean`. AMR's
hardcoded blocklist in `SettingsDialog.tsx` migrates to the
declarative flag (it rejects free-form ids at the ACP layer), and
antigravity opts out because its label set is a server-side enum that
silently fails on unrecognised strings.
## Form-loop fix (transcript sanitizer + stronger OVERRIDE)
The discovery form loop on weak/medium plain-stream models (GPT-OSS
120B Medium, Gemini 3.5 Flash) had two reinforcing causes:
1. `buildDaemonTranscript` packed the prior assistant turn's
literal `<question-form>` markup into the user request on the
next turn, giving the model a template to echo. New
`sanitizePriorAssistantTurnForTranscript` strips
`<question-form>...</question-form>` blocks and ```json fences
that match form-schema shape, replacing them with a brief
placeholder. User content is preserved verbatim (a user who
legitimately mentions `<question-form>` in chat keeps their
message intact).
2. The OVERRIDE block on form-answered turns was 4 lines and only
banned the bare `<question-form>` tag — models still emitted the
fenced JSON, form-asking prose ("Got it — tell me the following"),
and fake system events ("subagents stopped"). The new
`FORM_ANSWERED_SYSTEM_OVERRIDE` enumerates each anti-pattern and
pins them via tests, so silently weakening any line reintroduces
the regression.
Also adds RuntimeAgentDef.resumesSessionViaCli + RuntimeContext.
hasPriorAssistantTurn as forward-looking abstractions (skipTranscript
option on composeChatUserRequestForAgent). Antigravity does NOT opt
in — agy's `-c` resume activates an internal agentic loop with tool
retries and fallback-to-cached-response on tool errors that the OD
system prompt cannot steer; reverted after seeing byte-identical
form re-emissions caused by agy's own retry logic, not OD's transcript.
## One-click OAuth via system terminal
agy print mode can't complete Google Sign-In on its own (the OAuth
callback page asks the user to paste an auth code back into agy, but
`-p` has no input field). Before this commit the auth banner only
told the user to "open a terminal yourself."
Adds `POST /api/agents/antigravity/oauth-launch` and a cross-platform
launcher in `runtimes/terminal-launch.ts`:
- macOS: osascript → Terminal.app `do script "agy"` + activate
- Linux: tries x-terminal-emulator, gnome-terminal, konsole,
xfce4-terminal, xterm in order
- Windows: `cmd /c start "Open Design" cmd /k agy`
The endpoint hardcodes the `agy` command (no user input → no shell
injection surface) and is loopback-gated like the other daemon
endpoints. The chat's `AGENT_AUTH_REQUIRED` banner now renders a
"Sign in via terminal" button next to Retry; clicking it spawns the
terminal so the user can finish OAuth in one click.
## Silent-failure classification (auth vs quota via --log-file)
agy print mode is silent on stdout/stderr for both missing-OAuth AND
quota-exhausted failures — the upstream
`RESOURCE_EXHAUSTED (code 429): Individual quota reached` and the
`not logged into Antigravity` line only surface in agy's
`--log-file`. Without log inspection the daemon misread quota as
"auth required" and showed the wrong banner.
`RuntimeContext.agentLogFilePath` carries a daemon-owned per-run temp
path that antigravity's buildArgs translates to `--log-file <path>`.
The empty-output guard now reads that log on a `code === 0 &&
!childStdoutSeen` exit, feeds the tail to
`classifyAgentServiceFailure`, and routes:
- "not logged into Antigravity" → AGENT_AUTH_REQUIRED with
antigravityAuthGuidance
- "RESOURCE_EXHAUSTED" / "quota" / → RATE_LIMITED with
"Individual quota reached" antigravityQuotaGuidance
- none of the above (rare) → fall back to auth guidance
as the most likely cause
Both surface a terminal launcher in the auth banner: auth gets "Sign
in via terminal", quota gets "Switch model in terminal" — same
endpoint, contextual label. The handler is identical (open agy in a
terminal); the user either signs in or uses agy's Switch Model
picker to pick a model with available quota.
## Validation
- `pnpm guard` pass
- `pnpm --filter @open-design/daemon` runtime + telemetry suites:
192 passed, 1 skipped (the 1 pre-existing `task-type` failure on
origin/main is unrelated to this change)
- `pnpm --filter @open-design/web` typecheck pass; sse / amr-guidance
/ AgentIcon suites pass (51 web tests)
- Manual end-to-end on darwin + Gemini 3.5 Flash and GPT-OSS 120B
Medium: turn-1 question-form rendered correctly, turn-2 produced
`<artifact>` with full HTML (3.3KB Modern Minimal design) instead
of re-emitting the form. agy `--log-file` content correctly
classified as RATE_LIMITED when Gemini Pro quota was exhausted,
and as AGENT_AUTH_REQUIRED when keychain was cleared.
* fix(web/test): align amrAgent fixture with supportsCustomModel contract
The AMR agent definition in the daemon ships `supportsCustomModel: false`
so the Settings model picker hides the free-text "Custom…" option. The
PR changed `allowCustomModel` from `selected.id !== 'amr'` (hardcoded)
to `selected.supportsCustomModel !== false` (declarative), but the test
fixture was not updated to carry the same field — causing the
`__custom__` sentinel to appear in the picker under test.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(daemon): align formAnswerTransition wording with main + scope build directive to discovery
CI surfaced two failures on the merge with main:
- chat-route.test marks submitted discovery form answers ... expected
the main-version wording 'Do not emit another <formId> form.'
- telemetry-message-finalization keeps non-discovery form answers
active ... expected task-type to fall through the else branch
('Treat these form answers as the active user turn'), not the
discovery RULE 2/RULE 3 build branch.
The colleague's earlier fba1e40b form-loop fix tightened both pieces
(stronger wording + grouped discovery|task-type into the build branch)
but didn't update the tests that pin the contract. Revert the
transition wording to main and re-scope the build directive to
'discovery' only. The aggressive form-loop suppression we added in
this PR now lives in the system-prompt FORM_ANSWERED_SYSTEM_OVERRIDE
block, which is far stronger than the user-request transition text
this commit reverts.
* fix(daemon): scope formOverride by form id, detach Linux terminal, move agy log cleanup to finally
- FORM_ANSWERED_GENERIC_OVERRIDE: new exported constant for non-discovery/
non-task-type form ids; contains only the "do not re-ask" suppression
without the RULE 2 / RULE 3 / artifact directive.
- formAnswerTransitionForCurrentPrompt: extend build-transition branch to
include task-type alongside discovery, keeping user-turn and system
override consistent.
- Prompt assembly (server.ts ~10848): derive formOverride from the parsed
form id — FORM_ANSWERED_SYSTEM_OVERRIDE for discovery/task-type,
FORM_ANSWERED_GENERIC_OVERRIDE for all other form ids, empty otherwise.
- launchOnLinux: replace execFileAsync (waited for terminal exit, 3 s cap)
with spawn({ detached: true, stdio: 'ignore' }) + unref(); resolve on
the 'spawn' event so long-lived interactive terminals (xterm, konsole)
are not killed mid-OAuth-flow.
- Antigravity log cleanup: move fs.promises.unlink(agentLogFilePath) into
a try/finally wrapper around the close handler so every exit path
(success, failure, cancel, non-zero exit) cleans up the per-run temp
file, preventing unbounded /tmp accumulation.
- Tests: rename task-type case to assert build-transition behaviour; add
generic-form-id case (preferences) pinning the non-build path; add
FORM_ANSWERED_GENERIC_OVERRIDE content assertions.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(daemon): switch Antigravity buildArgs to chat subcommand invocation
Replace top-level `-p -` with `agy chat [--log-file …] -` so the adapter
uses the documented chat subcommand and stdin sentinel instead of the
unrecognised global -p flag. Update the agent-args test description and
all four deepEqual assertions to assert the ['chat', '-'] shape.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* test(daemon): drop real-platform default-launch assertion from terminal-launch suite
The removed test called launchAgentInSystemTerminal('agy') with no
platform override, which invokes the real system terminal on every
developer machine running the daemon test suite (Terminal.app on macOS,
cmd.exe on Windows, xterm/gnome-terminal on Linux). That is an
unacceptable OS side effect for a unit test.
The behaviour being asserted — that omitting platform selects
process.platform — is a TypeScript default-parameter guarantee, not a
runtime invariant that needs an integration test. The remaining 'aix'
case continues to pin the unsupported-platform failure shape.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(daemon): buffer Antigravity stdout to suppress auth URL before close-time classifier
The plain-stream close handler at code===0 can detect an agy OAuth
prompt in agentStdoutTail and emit AGENT_AUTH_REQUIRED, but by the
time close fires the stdout chunk has already been forwarded to the
client via the plain-stream `send('stdout', { chunk })` path. This
leaves both the raw OAuth URL and the terminal-launch guidance visible
in chat.
Buffer all stdout chunks for the `antigravity` agent instead of
forwarding them immediately. The existing close-time auth-prompt guard
(code===0, !trackingSubstantiveOutput, childStdoutSeen) returns early
when it detects the auth pattern, leaving the buffer unflushed and the
OAuth URL out of the SSE stream. For legitimate assistant output the
buffer is flushed in order just before design.runs.finish so the
chunks still arrive before the run's finished event.
Adds a chat-route integration test using a fake `agy` that exits 0
after printing the canonical auth prompt; asserts that the run emits
AGENT_AUTH_REQUIRED with no event: stdout delta containing the URL.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* test(daemon): isolate antigravity buildArgs argv test from real settings file
Pass a temp antigravitySettingsPath in the RuntimeContext for the
withModel argv assertion so unit tests do not touch
~/.gemini/antigravity-cli/settings.json. Adds the optional
antigravitySettingsPath field to RuntimeContext and threads it
through buildArgs to writeAntigravityModelSelection; production
callers leave it undefined, preserving the existing default path.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(daemon): revert Antigravity buildArgs to `-p -` (the only working agy v1.0.3 invocation)
The looper-reviewer-bot reported `chat` as agy's headless subcommand
based on its environment's agy build, and looper-fixer applied that
shape. The installed CLI (`agy --version` reports `1.0.3`) does NOT
expose a `chat` subcommand — `agy --help`'s `Available subcommands`
section lists only `changelog / help / install / plugin / update`,
and `agy chat - < prompt` exits 0 with empty stdout (the daemon then
forwards it as a 'successful' empty reply, exactly the failure mode
the auth/quota guard at server.ts ~12090 is meant to catch — for the
wrong reason).
`-p` is the documented print-mode flag (`Short alias for --print`)
and `agy -p -` reads the prompt from stdin and prints the model
reply, which the entire end-to-end test sequence in this PR has
verified against (form-loop fix, settings.json model routing,
log-file classification all confirmed working on Gemini 3.5 Flash
+ GPT-OSS 120B Medium with this invocation).
Updates the agent-args test to pin `['-p', '-']` instead of
`['chat', '-']` and adds an inline comment in antigravity.ts noting
that `chat` may exist in a future agy build but is not the contract
on the installed CLI today.
* fix(daemon): serialize Antigravity concrete-model spawns to dodge settings.json race
Reviewer (looper) flagged a concurrency race in the model-routing path:
~/.gemini/antigravity-cli/settings.json is process-global, so two OD
runs starting close together with different concrete models can race
the file — run A writes model A, run B writes model B, then A's agy
finally reads settings.json and executes on model B. The Settings
model picker becomes nondeterministic under parallel conversations.
Adds a per-process promise chain in antigravity.ts:
- acquireAntigravityModelLock(): chain-await + return release fn
- waitForAgyToReadModel(logPath, expected): polls agy's --log-file
for the upstream signal
'Propagating selected model override to backend: label="<X>"'
which model_config_manager.go emits once agy has finished reading
settings.json. Returns true on observed match, false on timeout.
Regex-escapes the expected label so '(' / ')' in 'GPT-OSS 120B
(Medium)' match literally, not as a capture group.
server.ts spawn pipeline now acquires the lock BEFORE buildArgs (which
performs the settings.json write) and schedules a release-once handler
that fires when EITHER (a) the log-file confirms agy read the model
or (b) the child exits — the exit fallback prevents a stuck/crashed
agy from starving the queue for every subsequent antigravity spawn.
Default-model spawns bypass the lock entirely: their buildArgs doesn't
touch settings.json, so there's nothing to serialize.
Tests pin:
- FIFO ordering across 2 / 3 concurrent acquirers
- Wait helper's regex correctly matches parenthesized labels
- Wait helper does NOT match a different model with shared prefix
- Wait helper swallows missing-log-file errors and returns false on
timeout (no spawn-pipeline crash if the log never appears)
194 → 198 passing runtime tests, 0 regressions.
* fix(daemon): close Antigravity lock release race on slow agy startup (looper #263fd2fe7)
Reviewer flagged that the previous serialization scheduled
`releaseOnce` in `.finally()` on waitForAgyToReadModel — meaning the
helper's `false` timeout return ALSO released the lock. If agy took
longer than the 15s polling window to read settings.json (cold start,
swap-thrash, slow network handshake to the upstream backend), run A's
lock dropped at 15s, run B rewrote settings.json with model B, and
run A's still-starting agy then read the wrong model. Same race the
original mutex was meant to close.
Fix the release semantics to be release-on-confirmation-only:
- waitForAgyToReadModel: `false` now strictly means 'I gave up
polling,' not 'agy definitely did not read this.' Document the
contract so a future caller can't conflate the two. Add an
optional AbortSignal so server.ts can stop polling when the child
exits — without it, the leftover watcher could outlive the run
and accidentally match a later concurrent run's log content,
releasing the wrong lock.
- server.ts: schedule `releaseOnce` only when waitForAgyToReadModel
returns true. The exit handler (which fires for crashes, fast
exits, normal completion) is now the canonical fallback that
releases the lock no matter what — the queue can't starve
permanently because agy always exits eventually. The exit
handler also fires the AbortController so the watcher cleans up.
New tests pin:
- timeout returns false WITHOUT any release-implying side effect
- already-aborted signal short-circuits (no readFile calls)
- abort mid-poll wakes the helper from its setTimeout (no
multi-hundred-ms hang waiting out a poll interval that no longer
matters)
198 → 201 passing runtime tests, 0 regressions.
---------
Co-authored-by: qiongyu1999 <2694684348@qq.com>
Reinstates the Studio tool hardening from #3081 on top of current main:
while a task is streaming, the Draw/annotation primary Send action and its
Enter shortcut are disabled, so an annotation can no longer leak into the
active run while the button shows a disabled reason.
This is the synthesis of two stacked-merge-divergent changes rather than a
wholesale revert: Queue stays available, so the value from #1961 (kami) is
preserved — an annotation made during a run is still staged for the next
turn instead of being dropped. Only the button/Enter availability changes;
the downstream queue/streaming-staging handler in ChatComposer is untouched.
- PreviewDrawOverlay: send('send') and canSend now respect sendDisabled.
- Reframed the streaming Draw test to assert Send is disabled while Queue
still emits a queued annotation (preserving the "annotate during a run"
coverage).
- Added unit coverage for the Enter/Send guard and Queue availability while
a task is running.
After #2840 wired plugin and design-template 404s into the same
"no shipped preview" placeholder the skills tab uses, the placeholder
copy still hard-coded "skill" — so users opening a Community/Plugins
card whose manifest declares a preview entry that doesn't ship saw
"No shipped preview for this skill." on a card that is clearly not a
skill.
Adds a noun discriminator to PreviewView.unavailable so the placeholder
reads with the right word per surface — "this skill" on the Skills
tab, "this plugin" on Community/Plugins, "this template" on deck-mode
design-templates. Locales gain three new preview.noun* strings (with
appropriate per-language demonstrative+article) and the existing
unavailable title/body interpolate a {noun} placeholder.
Also fixes a CSS gap in .ds-modal-unavailable surfaced by the same
path: the title and body divs were collapsing onto a single line under
.ds-modal-empty's default flex-row. Mirrors the existing
.ds-modal-error column+gap layout.
Refs #897, #2840.
* fix(daemon): dedupe scheduled routine slots
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): claim scheduled routine runs atomically
Co-authored-by: multica-agent <github@multica.ai>
* Fix routine loser snapshot rollback
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): defer scheduled routine side effects
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): terminate in-memory run on scheduled prepare failure
If `prepare()` throws after `persistPreparedRun()` has mutated the
routine run with real project/conversation/agentRunId values, the catch
in `RoutineService.start_` previously left the in-memory chat run
queued (no `discard()`), so its `completion` promise hung waiting on
`design.runs.wait(run)` forever, and the `routine_runs` row stayed
pinned to `routine-pending-*` placeholders even though the underlying
project/conversation rows for those real IDs had been created.
The catch now calls `handlerStart.discard?.()` so the in-memory run
terminates as `canceled`, releasing `completion`, and passes the real
IDs through `updateRun` so the persisted failed row reflects what was
attempted instead of the placeholder sentinels. A cleanup failure
inside `discard()` is logged via `console.error` rather than swallowed,
following the same surface-don't-swallow rule the loser cleanup path
uses. The original prepare error is still rethrown so the scheduler
advances to the next cadence (the slot claim is already terminal, so
retrying the same slot would just duplicate-claim and lose).
Added regression coverage in `apps/daemon/tests/routines.test.ts` for
both the normal prepare-failure path (real IDs persisted, discard
fired, completion resolved) and the case where the cleanup itself also
throws (failure surfaces via console.error, the row is still finalized
with the real IDs).
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): clear placeholder IDs on scheduled prepare failure
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): finalize routine prepare failures
* fix(daemon): defer manual routine setup cleanup
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): drop loser chat runs and rollback partial snapshot pins
Two follow-ups from the latest scheduler-claim review:
- Duplicate scheduled losers used to call `design.runs.finish(run, 'canceled')`,
exposing a phantom canceled routine run on `/api/runs` even though no
`routine_runs` row, conversation, or messages were ever committed. Split
the handler tear-down into `discardUnstarted` (used for never-inserted
paths — drops the in-memory run via the new `design.runs.drop()`) and the
existing `discard` (used after `prepare()` runs — still finalizes as
canceled and rolls back partial state).
- `resolvePluginSnapshot()` calls `linkSnapshotToProject()` before linking
the conversation/run, so a failure mid-link could leave the reused project
pinned to a snapshot the routine never durably claimed while
`resolvedRoutineSnapshot` stayed null. Capture the intermediate snapshot
id in `partiallyAppliedSnapshotId` when the resolver throws, and let
`discard()` fall back to it for `restoreProjectSnapshotLink` so the
previous project pin is restored either way.
Regression coverage added in `tests/routine-schedule-claims.test.ts`:
- A scheduled loser does not surface a phantom canceled chat run via
`/api/runs` after the slot is lost.
- A resolver that throws after `linkSnapshotToProject()` (forced via a
SQLite trigger on `conversations.applied_plugin_snapshot_id`) still
restores the reused project's previous pin in `discard()`.
* fix(daemon): return prepared routine run ids
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: kami.c <kami.c@chative.com>
The "scene" chip rail under each `/plugins/templates/<kind>/` page
shipped 23 chip labels in English (`UI & product mockups`,
`Brand & logo`, `Storyboards`, `Social & content`,
`Avatar & portrait`, `Illustration & style`, plus the rest of the
24-slug subcategory map covering all seven artifact kinds). Only
the `zh` override carried a translation; every other non-English
locale fell back to English on its scene rail. The result: a
visitor reading the rest of `/ja/plugins/templates/image/` in
Japanese (hero, kind chips, FAQ, card chrome — all localized in
PR #3218) hit a row of English chips at the bottom that read as
machine output rather than first-party copy.
This change fills `subcategory: { ... }` for the remaining 16
landing locales: `zh-tw`, `ja`, `ko`, `de`, `fr`, `ru`, `es`,
`pt-br`, `it`, `vi`, `pl`, `id`, `nl`, `ar`, `tr`, `uk`. The
existing `zh` translation is untouched. Brand-name tokens
(`UI`, `HyperFrames`, etc.) stay in English; localizable terms
(`Apps`, `Brand`, `Logo`, `Avatar`, `Storyboards`, …) are
translated where the language has a clean native equivalent.
Conjunctions follow locale convention — `&` for Latin-script
locales that read it as native chrome, `·` for CJK locales
where it works better than `&` next to ideographs, and
`و / & / และ`-style natural conjunctions for the rest.
Translations were generated with `claude-haiku-4-5` over OpenRouter
using a single batch script with explicit instructions on
chip-width budget (≈120px, target 1–4 native words), sentence
casing, and brand-token preservation. Output was validated for
JSON shape (every locale returns all 23 slugs) before splicing
into the override blocks.
Validation: pnpm --filter @open-design/landing-page typecheck ->
0 errors / 0 warnings; local dev (port 3067) renders the chip
rail in Japanese / Russian / Traditional Chinese / Arabic / German
/ French on `/<locale>/plugins/templates/image/` (and the same
rail on the other six artifact kinds, which share the subcategory
slug map).
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* docs: bump skill count to 137 in TL;DR and header badge
* docs: sync at-a-glance and comparison-table counts, drop broken arithmetic
* docs: sync remaining body references to 137 skills
- Comparison table: design systems 72 -> 129 (match EN README)
- Repository structure tree: add missing kami-deck.html template entry
Both were drift from the English README. The deeper EN-wide count
inconsistency (badge 149/131 vs body 72/31) is tracked in #3250.
* fix(skills): fall back to a stream copy when skill staging hits EPERM
`fs.cp` copies each file with copy_file_range(2), which the kernel rejects
across some filesystem pairs — e.g. a container image layer (`/app`) copied
onto a ZFS/overlay bind mount (`/data`) — surfacing EPERM. Node doesn't fall
back to a userspace copy, so skill staging failed and degraded to absolute
paths, losing the `.od-skills` write barrier.
Retry recoverable copy errors (EPERM/EXDEV/ENOTSUP/EOPNOTSUPP) with a
dereferencing read/write copy that works across any source/dest filesystem;
non-recoverable errors still degrade as before. A test seam injects a
synthetic EPERM since the real errno only reproduces on those mounts.
* fix(skills): preserve source file mode in the EPERM stream-copy fallback
The cross-filesystem fallback copied contents with createWriteStream, which
opens the destination at the default 0644 and drops the source's exec bit.
Skills shell out to staged helper scripts (e.g.
skills/pptx-html-fidelity-audit/scripts/*.py), so on the EPERM/EXDEV path
this fallback repairs they would fail with EACCES.
chmod (masked to 0o777, so the agent-writable staging copy never inherits
setuid/setgid/sticky) + utimes each copied file from the source stat so the
fallback matches fs.cp's mode/timestamp preservation. Adds a regression test
that stages an executable fixture through the synthetic-EPERM seam and
asserts the exec bit survives.
* fix(analytics): bucket feedback agent/model directly on the event
Reason × agent / reason × model splits on
`assistant_feedback_reason_submit` were 25-74% `unknown` because the
event only carried `run_id` — analyses had to join back to
`run_created/run_finished`, which loses rows whenever the feedback is
given to a message whose run sits outside the query window (the common
case for feedback on older messages), and whose `model_id` was `null`
to begin with (the user didn't pick a specific model — went with the
agent's default).
Carry `agent_provider_id` and `model_id` directly on every feedback
event so the analyses no longer need to join. Replace `null/unknown`
with the `default` bucket via `modelIdForTracking` (and let
`agentIdToTracking` fall through to `other`) at every emit site —
`null` was an analyst-hostile mix of "no selection" and "join failed";
`default` is a real, analysable bucket. On `run_finished`, upgrade the
model to the agent-reported value from initializing/model status
events when the user did not pick one — covers ACP, claude-stream,
copilot-stream, json-event-stream, qoder, pi-rpc.
* fix(analytics): use feedbackAgentProviderIdToTracking and assistantFeedbackModelId for feedback events
Wire API-mode agent ids (anthropic-api → anthropic) and agentName-parsed
model ids through the feedback emit path. Previously the feedback props used
agentIdToTracking (no anthropic-api case) and assistantModelDetail (no
agentName fallback), causing model_id='default' and agent_provider_id='other'
for API-mode agents.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(analytics): extend feedback/run schema for full agent/model coverage
Layered on top of the conflict resolution and the v1 emit switchover
in 0c1b30440. Three things the prior commits did not cover:
1) The v2 `assistant_feedback_*` family (page='studio') shares
`AssistantFeedbackBase`. Add `agent_provider_id` + `model_id` once on
the base so all four derived emits (reason_view, click, reason_click,
reason_submit) carry the same context as the v1 family, instead of
leaving the v2 dashboard with the same `unknown` gap the v1 PR was
trying to close.
2) Tighten `FeedbackSubmitResultProps.model_id` and
`feedbackAgentProviderIdToTracking` from `string | null` /
`TrackingFeedbackProviderId | null` to non-null. The web emit paths
already bucket null/empty through `modelIdForTracking` and the
`?? 'other'` fallback; collapsing that at the helper / contract
layer means `null` becomes a TS error at every new emit site, so we
can't regress the unknown bucket again in a future event.
3) Comment on `run_finished.model_id` so reviewers reading
`finishedModelId` see why the agent-reported value upgrades the
request-side one.
* fix(analytics): continue event scan past usage to find agent-reported model
The reverse scan for agentReportedModel was broken: the loop broke on
the first usage event (terminal) before ever reaching the status:initializing
or status:model event (emitted at run start, lower index). This meant
run_finished.model_id always fell through to modelIdForTracking(null) =
'default' for any run that reported usage tokens.
Fix: track haveUsageTokens as a flag and defer the break until both usage
tokens are found and either the model is not needed (user picked one) or
the agent-reported model has been captured. Extract the logic into
scanRunEventsForFinishedProps for unit testability.
Tests: six new cases in run-lifecycle-analytics.test.ts cover the
initializing→usage append order, ACP status:model, detail field fallback,
early exit when reqBodyModel is set, no-status event, and empty events.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(analytics): guard usage block with !haveUsageTokens to prevent early events overwriting terminal tokens
In the reverse-scan loop of scanRunEventsForFinishedProps, the usage block
lacked a !haveUsageTokens guard. When needAgentModel is true and the
agentReportedModel lives at the start of the run (lower index), the loop
walks all the way back past multiple usage events (one per step/turn in
multi-step runs), overwriting inputTokens/outputTokens on each pass. The
surviving values were those of the earliest step, not the terminal total.
Adding !haveUsageTokens to the usage block condition ensures only the first
(terminal) usage event seen in reverse sets the token counts; subsequent
earlier usage events are skipped while the scan continues for agentReportedModel.
Adds a test case for initializing(model) → usage(step1) → usage(terminal)
asserting both terminal token counts and agentReportedModel.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
Increase the backdrop opacity from 44% to 75% and add a blur effect
to better separate the queue screenshot preview from the file-list
preview panel underneath. This prevents visual overlap and makes it
clearer which preview surface is active.
Fixes#3167
* Add preview iframe keep-alive pool
* Fix active preview eviction on prompt context changes
* Evict preview iframes on skill/design-system registry edits
Bridge Settings → Skills / Design Systems to App.tsx so the keep-alive
pool drops any preview iframe whose project depends on the affected id
after every successful mutation. Without this, body-only edits leave
SkillSummary / DesignSystemSummary fields untouched and ProjectView's
signature-driven eviction never fires, so the active preview keeps
serving stale prompt context. The handler also re-fetches the App
shell's skill / design-system lists so summary-field changes propagate
to ProjectView's signature on the next render.
Also extend IframeKeepAlivePool.evictMatching with an includeActive
option so the new handler can drop the currently-visible iframe along
with parked ones; the fallback pool only ever holds active entries so
includeActive is a no-op there.
Regression tests:
- App.previewKeepAlive: clicking a Settings stub that fires
onSkillsChanged / onDesignSystemsChanged drives evictMatching with
includeActive=true and a predicate that matches projects using the
affected id while skipping unrelated projects.
- SkillsSection: onSkillsChanged fires after a body-only edit and
after a delete.
* fix: reattach active keep-alive iframe after eviction
* fix(web): refresh design systems after rename
---------
Co-authored-by: kami.c <kami.c@chative.com>
PR #3185 introduced 9 new copy keys for the templates grid chrome
(`templatesHeroEyebrow`, `templatesHeroLead`, `templatesCounterLabel`,
`cardFeaturedTag`, `cardReadFullPrompt`, `cardUseTemplate`,
`cardShareAria`, `faqHead`, `faqItems`) and used `pcopy.category[slug]`
labels and descriptions on the kind facets. The English base was
filled in but the per-locale `overrides` map was left as a follow-up,
so every non-English visitor saw English chrome on
`/<locale>/plugins/templates/` and English H1 + lead on
`/<locale>/plugins/templates/<kind>/` (except `zh`, which already
shipped a `category` override before PR #3185).
This change fills in all 17 non-English landing locales for those new
chrome keys, FAQ Q&A, and the artifact-category labels:
zh, zh-tw, ja, ko, de, fr, ru, es, pt-br, it, vi, pl, id, nl, ar,
tr, uk. Brand names (`Open Design`, `Claude`, `Claude Design`,
`Anthropic`, `OpenAI`, `HyperFrames`, `Cloudflare`, `Apache-2.0`,
`BYOK`, `PR`, `GitHub`) stay in English in every locale per the SEO
anchor strategy. Artifact category labels are localized with the
native-language word each design / dev community would actually
search for: `プロトタイプ` (ja), `프로토타입` (ko), `Prototyp` (de),
`Prototipo` (es), `Прототип` (ru), and so on. `zh` keeps its
existing `category` translation untouched since it was already
shipped — only the new chrome + FAQ keys land for that locale.
Translations were produced with `claude-haiku-4-5` via OpenRouter
and spot-checked against rendered pages on 5 locales (zh, ja, ko, de,
fr) for natural phrasing, brand-name preservation, and HTML-tag /
entity / variable integrity. The remaining 12 locales follow the
same prompt and are expected to be merge-ready as a v1; native
speakers in the community can refine wording later via small PRs
without coordinating across the whole grid.
Validation: pnpm --filter @open-design/landing-page typecheck ->
0 errors / 0 warnings; local dev (port 3062) renders 231 cards on
each of /zh/, /ja/, /ko/, /de/, /fr/ /plugins/templates/ with hero
eyebrow / H1 / counter / CTA / FAQ head / first FAQ Q all localized,
and /ja/plugins/templates/prototype/ H1 reads "プロトタイプ" with a
localized lead (was English on prod before this PR).
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
The English `CONTRIBUTING.md` got two updates that never made it into
the localized variants:
- `3790c003` (#1170) — replaced the Windows-native disclaimer with a
pointer to the new `docs/windows-troubleshooting.md` guide.
- `6341b267` (#1520) — added a "Use the PR template" bullet to the
Commits & pull requests checklist so reviewers can call out empty
sections instead of asking for them piecemeal.
This PR ports both changes into the German, French, Japanese,
Brazilian Portuguese, and Simplified Chinese variants so non-English
contributors land on the same merge bar.
* fix(web): remove Ingest source panel from Automations tab (#2711)
* fix(web): remove Ingest source panel from Automations tab
The Automations tab carried a free-form "Ingest source" composer that
let users paste arbitrary content (URL, repo path, connector event,
chat snippet) and turn it into a source packet plus evolution proposals.
The form was confusing next to the routine/template flow on the same
page, exposed an internal canonicalization concept users don't need to
think about, and shipped before the surrounding evolution-proposal flow
was wired into a coherent end-to-end story.
Drop the UI surface only:
- Remove the <section className="automations-ingest"> block, the
Template / Source / Compression / Connector selects, the title/source
ref/content fields, the recent-packets list, and the Ingest button.
- Drop the now-dead local state (sourcePackets / sourceForm /
ingestingSource), the patchSourceForm and submitSourceIngestion
helpers, the SOURCE_KIND_OPTIONS / COMPRESSION_OPTIONS constants, the
SourceIngestionForm type and DEFAULT_SOURCE_FORM, the
/api/automation-source-packets refresh leg, and the sourcePackets
side-write inside crystallizeRun.
- Remove the matching .automations-ingest / .automation-ingest-* CSS
block (plus the two responsive overrides) from tasks.css.
- Delete the test case that drove the form in TasksView.templates.test.
Backend stays intact: apps/daemon/src/automation-ingestions.ts, the
POST /api/automation-ingestions route, `od automation ingest` CLI, the
routine-evolution call site, and the AutomationContentPacket /
AutomationSourceKind / AutomationTokenCompressionMode contracts all
remain, since routine scheduling still depends on them.
* fix(web): drop crystallize test assertion on removed packet list
The crystallize test was asserting that the new content packet's title
shows up on the page. That assertion only passed because the daemon
response was being side-written into the deleted sourcePackets state
and rendered in the Ingest source recent-packets strip. With that UI
removed, the packet title has no surface to land on; the proposal title
(`Skill: Artifact polish loop run`) is still asserted and remains the
real signal that crystallize succeeded.
* test(e2e): restore #2305 / #2578 e2e regressions lost in PR #2461 merge
Sync merge c14baf07d (Merge origin/main into release/v0.8.0 inside PR
#2461) took the release-side blob of these three files, silently
reverting #2305 (chore(e2e): improve test framework quality) and #2578
([codex] test(e2e): harden settings and entry regressions):
- e2e/ui/settings-memory-routines.test.ts: 363 -> 2120 lines
- e2e/ui/project-management-flows.test.ts: 758 -> 1080 lines
- e2e/ui/settings-api-protocol.test.ts: 205 -> 390 lines
Restore each file to the version at the main parent of the merge
(866661ac6). No new edits — pure restoration of merged-out content.
* chore(assets): restore #2561 / #2401 brand mark refreshes lost in PR #2461 merge
Sync merge c14baf07d also reverted these three asset blobs to the
release-side (pre-refresh) versions:
- apps/landing-page/public/apple-touch-icon.png: 6122 -> 7983 bytes (#2561)
- apps/landing-page/public/favicon.png: 916 -> 1504 bytes (#2561)
- apps/web/public/app-icon.svg: 672 -> 4964 bytes (#2401/#2439 — optically
centered title-bar inner mark)
The companion landing changes from #2561 (sub-page-layout.astro,
index.astro, favicon.ico, logo.webp) survived the merge; only the
PNG/SVG blobs landed back at the release-side. Restore each to the
version at the main parent of the merge (866661ac6).
* test(web): drop dead automation-ingest-select.test.ts (follow-up to #2711)
#2733 (preserve ingest select chevron) and its #2609 follow-up shipped
on top of the broken main from PR #2461, which kept the Ingest source
panel that #2711 had already deleted on release. Now that the cherry-
pick of #2711 in this PR removes that panel and its .automation-ingest*
CSS, this test loses its subject (".automation-ingest-field select"
class no longer exists) and goes red.
Remove the test instead of keeping a broken assertion against deleted
markup. The shared readExpandedIndexCss helper is still used by other
style tests.
* Fix#3169: Show confirmation toast after export/download
Adds a success toast ("Export started") after any export/download action
completes. The toast uses the existing Toast component with the same
pattern as commentSavedToast and templateSavedToast (2.2s auto-dismiss).
The toast fires from within fireShareExport on both sync and async
success paths, covering all export formats: PDF, PPTX, ZIP, HTML,
Markdown, image, JSX, and React HTML.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
* Gate export toast to file export formats only
The toast was previously wired inside fireShareExport for all callers,
which incorrectly showed "Export started" for template save and deploy
modal opens. Gate to pdf/pptx/zip/html/markdown only. Also fix comma
to semicolon in types.ts.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
---------
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
* feat(mcp): add project creation, capability discovery, and generation tools
Lets an external coding agent (Codex, Cursor, …) drive a full design
loop over `od mcp`, not just read/write files: create a project,
discover what Open Design can make, commission a generation run, poll
it, and open the result in a browser. Complements the existing
write_file / delete_file / delete_project management tools.
New tools:
- create_project — make an empty project to generate into (start_run
needs one). Derives a slug id from the name unless given.
- list_skills / list_plugins — discover what you can ask OD to make.
- start_run / get_run / cancel_run — commission a run (OD spawns its
own agent), poll to completion, cancel. start+poll because MCP is
request/response and generation is minutes-long.
- get_run / get_project now return a browser-openable previewUrl
(entry file served raw; HTML entries render directly).
The external agent never runs a skill itself — it commissions OD to,
so the prior "skills not on MCP" boundary no longer applies.
* feat(mcp): make get_run preview hint directive
Reword the hint MCP clients receive when a run finishes so the agent
is more likely to surface the previewUrl to the user proactively —
mention the user-facing browser explicitly and call out that clients
with a built-in browser pane (e.g. Codex CLI's right-side browser)
should navigate to it directly. Also nudge start_run's hint to flag
that a previewUrl will arrive on success, so the agent knows what to
do with it before it ever sees get_run.
Pure text change; no behavior change in the tool surface or daemon.
* feat(mcp): one-click Install / Remove for Codex from Settings
Adds a toggle button on Settings → Integrations → Codex panel that runs
`codex mcp add open-design …` / `codex mcp remove open-design` via the
daemon, so users no longer need to copy TOML and paste it into
~/.codex/config.toml by hand. The copy-snippet path is unchanged and
remains the fallback when the Codex CLI isn't on PATH.
The daemon shells out to Codex CLI rather than rewriting config.toml
itself — that way we inherit Codex's own merge / dedupe / validation
rules and only track its argv. The runner is dependency-injected for
testability.
New endpoints (under /api/mcp/install/codex/*):
- GET status — probes `codex mcp get open-design`; returns
{ available, installed } so the UI can render the toggle state.
- POST — runs `codex mcp add open-design --env K=V … -- <node> <cli.js> mcp`,
reusing the same payload as /api/mcp/install-info.
- DELETE — runs `codex mcp remove open-design`.
The web UI renders the toggle only inside the Codex client panel
(`client.id === 'codex'`). When Codex CLI is missing it shows a
disabled button with an explanatory hint instead of vanishing, so users
know why one-click isn't available.
* feat(mcp): teach agents to clarify ambiguous format requests
When the user asks for a "PPT" / "deck" / "slides" / "PDF" / "doc",
that's two very different deliverables: Open Design natively produces
browser-viewable HTML/SVG (including HTML-rendered decks), but the
user may actually want a binary .pptx / .docx / .pdf — which OD does
NOT produce and which the agent would have to export from OD's output
itself. Add a paragraph to the MCP server instructions telling the
agent to ASK which one is wanted before kicking off work, rather than
silently picking one or dual-tracking both paths.
Pure prompt-text change in the instructions block; no tool surface or
behavior change. Costs ~10 lines of session-init context (one-time
per MCP session), versus dual-tracked .pptx hedging Codex was
otherwise doing on every ambiguous request.
* feat(mcp): surface agent messages, skip OD discovery, slim list_plugins
Three fixes uncovered while exercising the full MCP-driven generation
loop end-to-end with a real Codex client. Each one is a real
blocker / footgun for the external agent.
1. get_run now includes agentMessage — the inner agent's textual
output reassembled from the SSE event stream. Without this, runs
that ended in a discovery-style clarifying question (e.g. a
<question-form>) looked like "succeeded with empty output" mysteries
to the outer agent. The hint now branches on whether previewUrl
exists: with preview = show preview + relay agentMessage as the
inner agent's note; no preview = relay agentMessage as the actual
deliverable (almost always a clarifying question).
2. create_project sets skipDiscoveryBrief:true by default. The outer
agent IS the user-facing surface for MCP-driven runs, so OD's own
interactive discovery stage just creates a confusing
nested-clarification loop where its question form ends up dropped
(no files = no artifact). Better to let the outer agent gather
requirements and pass a precise prompt or plugin to start_run.
3. list_plugins flattens the daemon's bulky 16-field plugin record
(fsPath, sourceMarketplaceId, installedAt, …) into the few fields
an agent actually picks plugins on: id, title, description, kind,
tags. description / kind come from manifest.description /
manifest.od.{taskKind,kind} which the previous pass-through dropped
on the floor.
* feat(mcp): smart entry fallback + list_agents
Two fixes uncovered by exercising the full Codex-driven loop on a real
machine. Both close the gap between "Open Design has the data" and
"the external agent can find it".
1. get_project / get_run now fall back to scanning the project's file
list when metadata.entryFile is missing. We hit the case where
write_file (and a half-finished inner-agent run) put a perfectly
viewable index.html into the project, but metadata.entryFile stayed
null — so the outer agent got no previewUrl from MCP and resorted
to guessing a file:// path. Priority: declared entryFile, then
index.html anywhere, then a single .html at the project root.
Pure read-side change; no extra fetch when entryFile is already
set.
2. list_agents lets the outer agent stop guessing 'claude' / 'codex' /
'gemini' for start_run.agent. The daemon already exposed
/api/agents with 19 supported CLIs and an `available` flag. The
MCP wrapper defaults to filtering to installed agents only (so the
agent never picks one whose binary won't spawn), with
includeUnavailable:true as an opt-in to see uninstalled ones plus
their installUrl. Models truncated to 10 with modelsCount carrying
the real total — keeps the response token-economical even for
agents (opencode) with 100+ models.
* feat(mcp): tell the outer agent runs take 5–30 min, don't bypass
Direct response to a real Codex client observably cancelling an
in-flight run after 3 polls and substituting its own write_file
output ("文件时间戳没推进 → 我直接覆盖生成") — exactly the failure
mode this MCP surface exists to avoid.
start_run's hint and the session-init instructions block now both
state explicitly:
- Runs typically take 5–30 minutes.
- status:running with unchanged file mtimes is the inner agent
thinking, NOT a hang.
- Do not cancel_run out of impatience.
- Do not substitute write_file as a "faster" workaround — that
discards OD's pipeline-driven design quality.
- Poll every 30–60 seconds; report "still working" to the user
between polls.
- Only call cancel_run if the user explicitly asks.
Pure prompt-text change; no surface or behavior change. Costs ~10
lines of one-time session-init tokens + ~80 more tokens per
start_run response, in exchange for the outer agent actually
trusting the run.
* feat(mcp): persist run events to disk + expose tail-able path
Closes the in-flight visibility gap that made real Codex clients
cancel a 24-min run after 3 polls and substitute their own
write_file output, simply because polling get_run showed no change.
Daemon: every SSE event is now mirrored to a JSON-Lines file at
<RUNTIME_DATA_DIR>/runs/<runId>/events.jsonl. The path is wired
through createChatRunService's new `runsLogDir` option (null
disables, preserving legacy in-memory-only behavior). statusBody
exposes the path as `eventsLogPath`. Failures are best-effort — a
broken stream destroys itself and the run keeps going on the
in-memory event log (SSE clients are unaffected).
MCP: get_run already passed statusBody through, so eventsLogPath
surfaces automatically. The new value is that get_run during a
running status now adds a directive hint telling the outer agent to
`tail -n 50 -f <path>` in its own shell to see live progress —
that's the signal that makes the agent trust the run and stop
cancelling. The succeeded-status hint mentions the path too, for
forensics. No new tool; the field rides existing get_run polls.
Spec-first throughout:
- runs.test.ts adds 4 tests covering write-per-emit, statusBody
field, null-runsLogDir back-compat, and the no-IO guarantee
when persistence is disabled.
- mcp-runs.test.ts adds 1 test for the running-status hint.
* fix(mcp): get_run hint directs callers to pass project explicitly
The success hint in get_run previously said "project defaults to this
run's project", which is misleading: get_artifact has no run context and
falls back to /api/active when project is omitted, not to the run's
project. A client following the old guidance after creating a fresh or
non-active project could fetch the wrong project's files or fail with
"no active project".
The hint now embeds the run's projectId and tells callers to pass it
explicitly: get_artifact({ project: "<id>" }). A focused regression test
in mcp-runs.test.ts verifies the hint contains the projectId and does
not contain the incorrect active-context fallback guidance.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(contracts): add eventsLogPath to ChatRunStatusResponse
The daemon's statusBody() returns eventsLogPath but the shared DTO
lacked this field, leaving web/CLI/MCP callers without a typed
accessor.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* feat(mcp): bind MCP runs to OD conversations + studio deep links
Closes the last gap that made MCP-driven runs feel like a parallel
side door: the user could not see the conversation in OD's studio
page even though the run was real, finished, and had files.
Daemon side: POST /api/runs now falls back to the project's default
conversation when the caller (MCP / SDK) only supplied projectId.
It synthesizes an assistantMessageId, writes a user message with the
prompt as content, and lets the existing
`pinAssistantMessageOnRunCreate` helper create the empty assistant
row. The existing `appendMessageAgentEvent` accumulation path then
streams text_delta events into the assistant row's content — same
as the web /api/chat flow. The response body now echoes the
resolved conversationId + assistantMessageId so MCP callers can
build a deep link.
`buildMcpInstallPayload` now also surfaces `webBaseUrl` (read from
OD_WEB_PORT, the env tools-dev exports for the web listener). MCP
clients use it to build studio deep links.
MCP side: `start_run`, `get_run`, `get_project` now return a
`studioUrl` — a browser-facing OD URL pointing at the studio page
that shows the file preview AND the chat history side by side. The
hint on each tool was updated to tell the outer agent to hand
studioUrl to the user as the primary link (previewUrl falls back to
raw-file when the user only wants the rendered output). The
webBaseUrl is fetched once via /api/mcp/install-info and cached for
5s to keep per-poll cost flat; a tiny `_resetWebBaseUrlCache` export
lets tests start each case with a clean cache.
Contracts: `ChatRunCreateResponse` gains optional conversationId +
assistantMessageId; `ChatRunStatusResponse` gains optional
eventsLogPath. Both additive, no consumer breakage.
Spec-first throughout:
- get_run includes studioUrl on success when webBaseUrl + conversationId are available
- get_run omits studioUrl when webBaseUrl is null
- start_run returns studioUrl and conversationId for the new run
- get_project returns studioUrl using the project default conversation
* fix(mcp): add skill/skillId to start_run so listed skills are actionable
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(test): update mcp-get-project test to handle getWebBaseUrl fetch
The get_project handler now calls getWebBaseUrl (added with the studio
deep-link feature), which fetches /api/mcp/install-info. The test mock
only handled the /api/projects/:id URL and expected a single fetch call,
causing the assertion to fail with "called 2 times" instead of 1.
Fix: handle the /api/mcp/install-info URL in the fetch mock (returning
webBaseUrl: null), update the call count expectation to 2, and call
_resetWebBaseUrlCache in afterEach to prevent cache bleed between tests.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* feat(mcp): tell agents to render studioUrl as a clickable markdown link
Observed in a real Codex client: Codex received studioUrl correctly
but rendered it as inline code (gray code-span), which its built-in
browser pane does NOT make clickable. The user had to copy-paste the
URL into a browser by hand even though Codex / Cursor / Zed all
auto-link markdown `[label](url)` syntax and would navigate it in
their right-side preview pane.
The three studioUrl-mentioning hints now explicitly tell the agent
to render the URL as a markdown link (e.g.
`[Open Open Design studio](URL)`) and never as inline code or bare
text. Pure prompt-text change.
* fix(runs): resolve default agent when MCP caller omits agentId; add McpRunCreateRequest contract type
- POST /api/runs: when no agentId is provided, resolve from app-config
or first available CLI before spawning — mirrors the pattern the
routine handler already uses. Prevents 'unknown agent: undefined'
failures on the create_project -> start_run(prompt) MCP path.
- packages/contracts: add McpRunCreateRequest interface for the
projectId-only / SDK caller shape so typed callers can construct the
request without casts. Exported via index.ts's existing chat re-export.
- packages/contracts/tests: add compile fixture verifying projectId-only,
projectId+message, and projectId+message+agentId shapes all type-check.
- apps/daemon/tests: add mcp-runs test asserting agent arg omitted in
start_run does not include agentId in the POSTed body.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* feat(landing-page): YouMind-style grid + share popover for /plugins/templates/
The list-style catalog rows that landed in PR #3010 read as a long
table of items rather than a discoverable grid. Product feedback (after
benchmarking against youmind.com/zh-CN/seedance-2-0-prompts) wanted:
- A YouMind-shape card with a top accent band, video / poster preview
area, author + attribution row, an excerpt frame, and a primary CTA
paired with a share button.
- Hover-autoplay on the 46 video templates whose manifest carries a
Cloudflare Stream MP4. The data was already there since PR #3010;
the catalog row just rendered the poster as a static `<img>`.
- A counter chip on the right of the hero that surfaces the live total
(`Total · 231`) instead of baking the number into the H1
("231 runnable templates."). The hero now reads as
`OPEN SOURCE CLAUDE DESIGN` eyebrow + `Templates.` static H1, which
also threads the brand keyword into the page's SEO surface.
- A six-question FAQ block below the grid covering license, BYOK keys,
contribution, and the "open source Claude Design alternative"
positioning explicitly.
Implementation:
- `_components/template-card.astro` — new card component. Accent band
hue is derived from `od.mode` so artifacts of the same kind get a
consistent color (video green, prototype blue, deck mustard, image
wisteria, hyperframes coral, audio amber, live-artifact teal),
falling back to a stable per-index hue for unrecognized modes.
Featured tag (yellow, on-brand) is visible when the manifest tag
list contains `featured`; the rest of the card is locale-resolved
via the same `resolveBundledTitle` / `resolveBundledDescription`
helpers PR #3010 added.
- `pages/plugins/templates/index.astro` + `[kind]/index.astro` — grid
layout (`.tpl-grid`, `repeat(auto-fill, minmax(340px, 1fr))`),
hero with counter chip, FAQ section on the parent only. Adjacent
filter strips share a single divider rather than drawing one each,
so the kind + scene chip block reads as one filter unit instead of
three stacked horizontal cuts.
- Hover-autoplay observer + share button click handler bundled into
one `<script>` per page so they share the same boot lifecycle. The
earlier split version dispatched `astro:page-load` from the autoplay
block before the share block's listener attached, which dropped
the share click on the floor; the merged init() runs eagerly when
DOM is ready, re-runs idempotently on `astro:page-load` (Astro view
transition), and uses `data-tpl-init` / `data-tpl-share-bound`
markers to prevent double-binding.
- Card share is a popover, not a system share sheet. The detail page's
`<dialog class="detail-share-dialog">` UI is reused (single instance
per page populated per click), but `<dialog>.show()` runs in
non-modal mode and JS positions it via `getBoundingClientRect()` to
unfold above-right of the trigger button. Outside-click and Escape
close the popover; the existing `data-share-copy` / `data-copy-link`
handlers in `header-enhancer.astro` wire Copy text + Copy link
automatically. Width tuned to 420px so it fits next to a 340px-wide
card without spilling onto the next column.
- `_redirects` already covers retired Skills + Craft routes (PR #3010)
so this grid pivot doesn't need new redirects.
Out of scope for this PR (kept lean):
- Multi-locale hero + FAQ copy. Hero / FAQ render in English on every
locale right now; the `pcopy.tileTemplates` chip rail and per-card
title/description still localize per PR #3010. Locale rollout for
the hero + FAQ is a follow-up.
- Sort + filter buttons in the YouMind reference top-right (we still
show artifact-kind chips only). Sort by featured weight is the
most likely next step.
- `od.featured` weight as a featured proxy. We currently key off
`tags?.includes('featured')` which is 0-match across the catalog
today; promoting the numeric weight into `BundledPluginRecord` is a
separate small commit.
`pnpm --filter @open-design/landing-page typecheck` clean (0 errors).
* feat(landing-page): localize templates chrome + FAQPage JSON-LD + hover-only autoplay
Three follow-ups Looper flagged on the YouMind-style grid (PR #3185):
- **Localizable hero / FAQ / card chrome.** PR #3185 wired the grid
through `pcopy` for record titles + descriptions but hard-coded the
surrounding chrome — hero eyebrow / lead / counter label, FAQ head,
Featured tag, "Read full prompt", "Use this template", and the
share-button `aria-label` — to English. `/ja/plugins/templates/`,
`/zh-CN/plugins/templates/video/`, etc. now ship those strings via
`pcopy.*` keys (`templatesHeroEyebrow`, `templatesHeroLead`,
`templatesCounterLabel`, `cardFeaturedTag`, `cardReadFullPrompt`,
`cardUseTemplate`, `cardShareAria`, `faqHead`, `faqItems`). English
is the base; per-locale overrides for hero copy + 6 FAQ Q&A pairs
remain a follow-up (the PR-#3185 "Out of scope" item), so the 17
non-English locales fall back to English chrome instead of showing
undefined values.
- **`FAQPage` JSON-LD entity.** The visible accordion was a SEO
surface but `jsonLd` was still a single `CollectionPage`. Switched
it to an array and appended a `FAQPage` whose `mainEntity` is each
question + answer from `pcopy.faqItems`, so the structured-data
payload search engines see and the visible <details> share one
source of truth — drift between them is now mechanical, not
editorial.
- **Hover-only autoplay (not viewport autoplay).** The previous
observer played every video the moment its card scrolled into the
viewport, which contradicted the PR's stated hover-autoplay
contract and spawned N simultaneous decoders on a casual scroll.
The IntersectionObserver now hydrates `data-src` -> `src` lazily
(one-shot, then unobserve) at a 300px rootMargin; `play()` and
`pause()` are gated to `pointerenter` / `pointerleave` (plus
`focusin` / `focusout` for keyboard users) on the parent
`.tpl-media` host so hovering anywhere on the preview frame
triggers playback. Same change applied to the `[kind]` route so
faceted pages behave identically.
Validation: pnpm --filter @open-design/landing-page typecheck -> 0
errors / 0 warnings; local dev (port 3061) renders 231 cards / 46
data-tpl-autoplay markers / FAQPage entity present in jsonLd / 6 FAQ
summaries; zh-CN locale falls back to English chrome (expected, the
locale routes themselves remain out of scope per PR #3185).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: The "clear" button for comments is not functioning; the comments no longer have serial numbers.
* fix: The active pin always renders {visibleComments.length + 1}, but showActivePin (= commentCreateMode) is also true while editing an existing comment: onOpenComment at line 6821 calls setCommentCreateMode(true) and setActiveCommentTarget(snapshot) against the saved comment the user just clicked. In that path the overlay now stamps a stale number on top of an existing saved marker (e.g. clicking the pin showing 2 paints an additional 3 at the same position), which contradicts the invariant this PR is restoring — that preview-area numbers match the side-panel numbers.
---------
Co-authored-by: 郑惠 <14549727+felicia-study@user.noreply.gitee.com>
* ci(agent-pr-explore): rewrite prompt — non-lazy disposition + mandatory probe list
Background: on PR #2355 (large AMR runtime add) the agent stopped at smoke level
because the positive path was gated on a missing `vela` binary. The old prompt
explicitly instructed "if setup prerequisites block, return inconclusive
immediately" + "do not spend more than two attempts on test data" + "do not run
arbitrary host shell commands", which made the agent give up rather than:
- use the PR's own `tests/fixtures/fake-vela.mjs`
- set the `VELA_BIN` env the runtime reads
- probe `/api/integrations/vela/*` directly via fetch
This rewrite shifts disposition + adds 4 structured unblock steps:
- **Mindset**: each /explore is a precious, expensive run; be thorough, not lazy.
- **STEP 0** Read PR body for `## Test Plan` section — declared cases = MUST-COVER.
- **STEP 1** Extract diff-driven probe list (new routes / components / env vars /
fixtures / CLI flags). Anything skipped requires explicit written reason.
- **STEP 2** Before giving up, try (a) PR-provided fixtures, (b) build minimal stub
inside container, (c) probe APIs directly via page.evaluate fetch,
(d) search repo / related PRs / docs for unknown terms.
- **STEP 3** 4-7 cases for substantive PRs (was hard cap 3).
- **STEP 4** Login / multi-tab / OAuth — use Playwright multi-page handling;
read creds from env, never echo.
- **SECURITY** strengthened: env vars matching common secret patterns are
confidential; never echo / log / write to file / page.evaluate / report.
- **Report** new required §Mitigations Attempted for Inconclusive verdicts —
must list what was tried + why each didn't unblock.
Kept unchanged: 3-min keepalive constraint, untrusted-data rule, no-host-shell
rule, report markdown structure (✅/⚠️/❌/⚪ + case emoji).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(agent-pr-explore): truncation-aware + soft-request protocol + capability fix
Addresses review feedback from @mrcfps:
(1) STEP 1 — diff probe list was instructed to enumerate "every new
route/component/env/fixture/flag" without acknowledging that the harness
already truncates the diff upstream (file_patch_max_chars + context_max_bytes).
On exactly the large PRs this prompt targets, the agent only sees a slice.
Fix: prompt now explicitly tells the agent the context MAY be truncated and
to (a) note the truncation in §🧭 Scope and (b) emit §📎 Needs to ask the
maintainer to attach the missing source files into the private workspace
for the next run.
(2) STEP 2 — old text told the agent to "create stubs inside the sandbox
container", "rewire env / PATH", and "run gh / grep searches". The harness
does not expose docker exec or arbitrary shell to the agent (capabilities
are fs:write on host + Playwright on host driving the dockerized app via
HTTP). The instructions promised things the agent literally cannot do.
Fix: STEP 2 now spells out the actual capabilities (host fs:write, Playwright
page.evaluate / page.request, host-side $WORKSPACE_DIR if maintainer pre-
attached one). The "unblock by stub" path is rewritten honestly: build a
host-side stub if useful, but acknowledge container env is fixed at
docker run and signal what's needed via §🔑 Needs / §📎 Needs for the next
iteration. The "search repo for unknown term" step (which required gh/grep)
is dropped in favor of using $WORKSPACE_DIR materials.
(3) Soft-request protocol (new):
The agent is READ-ONLY for secrets and workspace — it cannot self-attach.
But it can SIGNAL what was needed via two new optional report sections:
- §🔑 Needs — secret request ("VELA_RUNTIME_KEY: needed to verify ...")
- §📎 Needs — workspace file request ("amr-auth-spec.md: clarifies ...")
The dashboard (synclo platform; see nexu-io/synclo#79 RFC §6.8) will parse
these structurally and surface as one-click attach hints to the maintainer
on the run detail screen. Pure passive signal; no auto-action; zero prompt-
injection risk (no code path takes the values).
Hard rules:
- No pasting of existing secret values (security)
- Each item MUST tie back to a specific blocked case in §🧪 Cases — not
speculative
- Workspace privacy: agent may reference workspace files BY PURPOSE
("verified positive-1 from test-plan.md") but NEVER paste their content
into the report
This commit is non-trivial (~65 lines net) but the changes are tightly scoped:
honesty about capabilities + a new signaling channel that replaces the
impossible direct-action promises.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(agent-pr-explore): remove gh-search + container-exec language from prompt
MINDSET bullet: replace 'gh search issues/prs/code' with in-scope
materials (PR body, diff context, workspace files) plus
page.request/page.evaluate probes, matching actual harness capabilities.
SECURITY bullet: replace the contradictory 'You may run commands INSIDE
the sandbox container' with a clear statement that the agent has no shell
or container exec access, only the Playwright browser.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* ci(agent-pr-explore): fix Needs section misuse and unawaited fetch body
Move fixture env-var wiring request from §🔑 Needs to §📎 Needs and
remove the concrete host path from the example; §🔑 Needs is
secret-name-only and must not carry filesystem paths. Await r.text() in
the page.evaluate fetch example so the body field resolves to a string
instead of an unresolved Promise.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* ci(agent-pr-explore): broaden 📎 Needs to cover env/config wiring alongside file attachments
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* ci(agent-pr-explore): fix step-2b Needs routing and split AMR_USER/AMR_PASS example
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* ci(agent-pr-explore): fix mindset bullet — env var cannot be set mid-run
Replace "set the env var" in the MINDSET mitigations list with
"identify the env var and request the needed startup wiring in §📎 Needs"
to match the actual capability boundary: container env is fixed at
docker run time and cannot be changed by the agent mid-run.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): add DeepSeek Reasonix CLI support via ACP protocol
* fix(auth): tighten Reasonix auth detection + add live model discovery
- Anchor isReasonixAuthFailureText to Reasonix-specific markers
(~/.reasonix/config.json instead of generic api_key matches)
- Add fetchModels with detectAcpModels for live model discovery
in the agent picker (matches kimi/hermes/kilo/kiro/vibe pattern)
* feat(web): add Reasonix short description in agent picker
---------
Co-authored-by: Bernardxu123 <Bernardxu123@users.noreply.github.com>
Fixes#2883
When the video preview panel opens, the Design Files toolbar buttons
would wrap into multiple rows due to reduced available width.
This fix adds:
- flex-wrap: nowrap to keep buttons on a single row
- flex-shrink: 0 to prevent buttons from being squeezed
The action buttons now remain stable and aligned even when the preview
pane reduces the available width.
Co-authored-by: Siri-Ray <2667192167@qq.com>
* feat(contracts): add run media execution policy
* feat(daemon): enforce run media execution policy
* test(daemon): cover media execution policy gates
* feat(plugins): site-wide plugin detail pages, share-to-site links, landing deploy trigger
Why: a merged plugin PR didn't redeploy the landing site (plugins/** was missing from the deploy paths), and the desktop Share menu copied a local/404 link instead of the public marketplace URL. The landing plugin routing left by the detail-page rework also 404'd: the locale listing's cards used a multi-segment href while detail pages were single-segment, and only 388 bundled _official plugins had pages.
What changed:
- Deploy: landing-page deploy/ci trigger on plugins/**, and skip the slow previews step on an exact cache hit (cache key aligned across both workflows so a PR-built cache is reused by main).
- Share URL: packages/contracts/plugin-url.ts owns the single-segment plugin URL scheme; the web Share menu and the landing site both derive links from it. Web links now point at https://open-design.ai/plugins/<slug>/.
- Full detail coverage: detail pages now cover all 403 local plugins (_official incl. atoms + community), each rendered from its local manifest. Fixes the locale-listing 404s and the community manifest-name/catalog-id (- vs /) mismatch.
- Self-host: daemon exposes OD_SITE_ORIGIN via /api/app-config; web falls back to the canonical origin until the daemon answers.
Validation: pnpm guard, pnpm typecheck (all packages), contracts + web tests green, and a full build E2E confirming all 403 catalog ids and locale-listing cards resolve to built detail pages (0 missing).
* chore: retrigger CI
* ci(landing): carry plugins/** trigger + previews cache-hit into #2994 split workflows
Merged origin/main, which split landing deploy into staging + manual production (#2994). git auto-migrated my landing-page-deploy.yml changes into landing-page-staging.yml via rename detection (plugins/** path, fallback-preview-card.ts cache key, cache-hit skip all carried). The new manual landing-page-production.yml didn't have them, so add the previews cache-key alignment + cache-hit skip there too (plugins/** path is N/A — production is workflow_dispatch only).
* fix(ci): wrangler-action uses pnpm so it tolerates landing's workspace dep
This PR added @open-design/contracts (workspace:*) to apps/landing-page/package.json so the landing site can share the plugin-url slug rules. But the landing deploy/preview steps run cloudflare/wrangler-action with packageManager: npm in workingDirectory apps/landing-page, and 'npm i wrangler' chokes on the workspace: protocol (EUNSUPPORTEDPROTOCOL), failing 'Validate landing page'. Switch all three landing wrangler-action steps (staging / ci preview / production) to packageManager: pnpm, which is workspace-aware.
* test(e2e): bundled plugins now offer the README badge
After this branch, buildPluginShareUrl returns a public open-design.ai link for bundled plugins (not just official-marketplace ones), so the home-starter share menu now shows 'Copy README badge'. Update the assertion from toHaveCount(0) to toBeVisible().
* fix(landing): drop @open-design/contracts dep, use a landing-local slug helper
Per review on #2999: the marketing site must not import @open-design/contracts (AGENTS.md boundary — it's the web/daemon product-runtime contract layer). Move the slug/path helpers into landing-local app/_lib/plugin-slug.ts; the web client keeps contracts' plugin-url. The two derive the same scheme and are verified in lockstep by the e2e route check (403 share URLs -> 403 detail pages, 0 missing). landing no longer has a workspace dep, so revert the wrangler-action packageManager back to npm.
* fix(landing): include plugins/_official in previews cache key
Per review on #2999: generate-previews.ts builds bundled-plugin preview jobs from plugins/_official/**/open-design.json and renders fallback cards from manifest fields (title/description/mode/scenario/tags). With plugins/** now triggering the workflow but the cache key not hashing plugin inputs, a plugin-only PR/merge could exact-hit an old cache and skip the preview regen, shipping with a stale or missing /previews/plugins/<manifest-id>.png. Add plugins/_official/** to the cache key in all three landing workflows (ci, staging, production). community is not currently covered by generate-previews so its glob is omitted.
* fix(plugins): include community marketplace installs in share gate
hasPublicPage now covers sourceMarketplaceId === 'community' so the
README badge and public detail link surface for community installs.
Community manifest names carry a community- prefix that diverges from
the landing-page route slug, so URL derivation uses sourceMarketplaceEntryName
(community/<folder>) instead — pluginDetailSlug takes the last segment,
matching the /plugins/<folder>/ route the landing page emits.
Adds component tests for buildPluginShareUrl, badge copy, and the
Open-in-marketplace link for a community/registry-starter record.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
---------
Co-authored-by: mrcfps <mrc@powerformer.com>
* fix(docker): fix container startup crash due to missing OD_API_TOKEN
* fix(docker): forward OD_API_TOKEN to fix docker container boot loop
* fix(docker): enforce non-empty OD_API_TOKEN for docker-compose
* fix(deploy): automate OD_API_TOKEN generation in installer and close compose loop
* docs(readme): guide manual deployment users to configure OD_API_TOKEN
* docs(readme): align working directory paths for manual deployment instructions
* docs(readme): align working directory paths for manual deployment instructions
* docs(readme): restore git clone context for first-time users
* fix(web): Limit the maximum height of the StagedAttachments component.
related issue 3155
* fix(web): limit attachment area overflow to vertical only
* Make share deploys visibly complete
Share deploys were uploading only the referenced entry graph, so sibling screens could fall through to provider fallback pages after deployment. They also completed silently except for the result link block inside the deploy dialog, leaving users unsure whether a redeploy finished.
This includes visible files for Open Design-managed projects in real deploy/preflight payloads while preserving the selected entry as provider-root index.html. Linked-folder projects stay on the referenced-file graph so repo files that are visible in the file panel, like README.md or src/**, do not become public by accident. The web UI also shows a localized success toast at the top of the app after a successful Vercel or Cloudflare Pages upload.
Constraint: Cloudflare Pages Direct Upload serves missing files through its fallback behavior, so deployment payload completeness must be handled before upload.
Constraint: Linked-folder projects can expose arbitrary repository content through the file panel, so whole-project deploy expansion is limited to Open Design-managed project directories.
Rejected: Reintroduce an entry-file dropdown | users wanted full project deployment semantics rather than selecting a root-only artifact.
Rejected: Upload every visible linked-folder file | would make non-runtime repo content publicly reachable after Share deploy.
Confidence: high
Scope-risk: moderate
Directive: Do not remove the selected-entry-to-index.html mapping; it keeps alternate entries like index-v1.html deployable as the root without overwriting them with the launcher.
Directive: Do not expand linked-folder deploys beyond referenced web assets without an explicit user opt-in and review of the privacy model.
Tested: pnpm --filter @open-design/daemon test tests/deploy.test.ts tests/deploy-routes.test.ts
Tested: pnpm --filter @open-design/web test tests/components/FileViewer.test.tsx
Tested: pnpm --filter @open-design/web typecheck
Tested: pnpm guard
* fix(web): gate share-deploy ready hint on actual ready state
The 'Ready · Deployed URL' hint was unconditionally rendered whenever
deployResultCards was non-empty, so a successful deploy that came back
as link-delayed or protected showed contradictory copy next to the
'Public link pending' / 'Deployment protection enabled' badge.
Render the hint only when deployResultState(activeDeployment?.status)
is 'ready' so the success line stays consistent with the badge below.
---------
Co-authored-by: nicejames <nicejames@gmail.com>