Commit graph

419 commits

Author SHA1 Message Date
lefarcen
df8a0faff6
feat(runtimes): register AMR (vela) as an ACP stdio agent (#2355)
* feat(runtimes): register AMR (vela) as an ACP stdio agent

AMR is the vela CLI's ACP runtime mode. `vela agent run --runtime opencode`
speaks ACP JSON-RPC over stdio (see vela's
`specs/current/runtime/manual-agent-run-openrouter.md`); per
`docs/new-agent-runtime-acp.md` we expose it through the same `streamFormat:
'acp-json-rpc'` transport that already powers Hermes, Devin, Kimi, etc.

The new `defs/amr.ts` is the entire wiring — `buildArgs` returns
`['agent', 'run', '--runtime', 'opencode']`, `fetchModels` reuses
`detectAcpModels`, and the fallback list seeds the OpenRouter ids vela's
e2e baseline uses. `executables.ts`/`app-config.ts`/`metadata.ts` get the
matching `VELA_BIN`/`VELA_LINK_URL`/`VELA_RUNTIME_KEY`/`VELA_OPENCODE_BIN`
allowlist + install/docs URLs, so users can configure the per-agent env in
Settings without leaking into other adapters.

Coverage: `tests/fixtures/fake-vela.mjs` is a minimal ACP stub that returns
the documented `initialize` / `session/new` / `session/set_model` /
`session/prompt` shapes; `tests/amr-acp-integration.test.ts` spawns it via
`child_process.spawn` and drives a full turn through `attachAcpSession` and
`detectAcpModels`, so the ACP transport contract for AMR is end-to-end
verified locally even before a real `vela` binary is installed.

Validated:
- pnpm guard
- pnpm typecheck (all workspace projects)
- pnpm --filter @open-design/daemon test (2881/2881)

Deferred: real OpenRouter-backed turn through a built `vela` binary —
the runtime def needs no changes for that path, only `VELA_RUNTIME_KEY`
and `VELA_LINK_URL` in env (or Settings).

* fix(runtimes/amr): pin a concrete default model and bare openai ids

End-to-end validation against a freshly-built `vela` (nexu-io/vela@main)
+ OpenRouter surfaced two contract details the first AMR runtime def
got wrong:

1. vela rejects `session/prompt` with `session/set_model must be called
   before session/prompt`. attachAcpSession in apps/daemon/src/acp.ts
   skips set_model whenever the picked model is the synthetic 'default'
   id, so AMR's fallback list must NOT include DEFAULT_MODEL_OPTION. The
   def now ships a concrete `gpt-5.4-mini` as both `fetchModels`'
   default option and `fallbackModels[0]`, which makes attachAcpSession
   always send a real `session/set_model` for AMR turns.

2. `vela --runtime opencode` auto-prepends `openai/` to whatever modelId
   it forwards to opencode's openai provider. With OpenRouter-style ids
   like `openai/gpt-5.4-mini`, opencode receives the double-prefixed
   `openai/openai/gpt-5.4-mini` and replies `ProviderModelNotFoundError`.
   The new fallback list ships the bare ids opencode's openai registry
   actually knows about (gpt-5.4, gpt-5.4-mini, gpt-5.4-fast, etc.).

Stub + tests:
- tests/fixtures/fake-vela.mjs now enforces the set_model gate the same
  way real vela does, so a regression that silently goes back to
  model: 'default' would surface as a fatal error in tests instead of a
  hidden production failure.
- tests/amr-acp-integration.test.ts pins both contracts: no 'default' /
  no 'openai/' prefix in fallbackModels, and a negative case that
  asserts session/prompt fails when no model is set.

Adds `apps/daemon/scripts/verify-amr-real-vela.mjs` — a small dev-time
runner that drives `attachAcpSession` against a real `vela` binary and
prints the daemon's chat events, so future protocol drift can be checked
against an actual OpenRouter call.

Verified locally: `vela agent run --runtime opencode` + OpenRouter
returns the prompted string ("AMR-E2E-PASS") through the full daemon
pipeline; daemon test suite stays 2883/2883.

* fix(runtimes/amr): substitute concrete model when chat run sends 'default'

A plugin-driven AMR run from the UI surfaced a real-world hole in the
prior commit:

  json-rpc id 3: session/set_model must be called before session/prompt

The Default-design-router plugin (and any caller that doesn't pin a
real model) sends `model: 'default'` straight through, which the AMR
runtime def cannot accept — vela rejects `session/prompt` without
`session/set_model` and attachAcpSession skips set_model whenever
model === 'default'. Just leaving DEFAULT_MODEL_OPTION out of the
adapter's `fallbackModels` is not enough: the chat-run handler in
server.ts still forwarded 'default' verbatim.

This adds `resolveModelForAgent(def, resolved, env?)` as the
single source of truth for the substitution:

  1. If the caller picked a real id, pass it through.
  2. Else, if `def.defaultModelEnvVar` is set and the daemon process
     env has a non-empty value for it, return that (operator escape
     hatch — see below).
  3. Else, if the def's `fallbackModels` does NOT contain a 'default'
     id, return `fallbackModels[0].id`.
  4. Else, return the original value (the historic shape — defs that
     list 'default' themselves are untouched).

AMR sets `defaultModelEnvVar: 'VELA_DEFAULT_MODEL'`, so when
opencode's openai-provider registry deprecates `gpt-5.4-mini`
upstream, an operator can swap the fallback id without a code change
by exporting `VELA_DEFAULT_MODEL=gpt-5.5` before launching tools-dev
/ od. Worth noting the env var must live in the daemon's `process.env`
(Settings-UI per-agent env values only reach the spawned child, not
the daemon's resolver) — the new field's docblock spells this out.

Coverage:
- `tests/runtimes/resolve-model.test.ts` — 8 unit tests covering all
  four resolver branches plus the env-override happy path / fallback /
  ignore-when-user-picked-a-real-id case.
- `pnpm --filter @open-design/daemon typecheck` clean.

* chore(runtimes/amr): move AMR to the top of the base agent list

So `AMR (vela)` shows up first in the agent picker / status views,
ahead of claude / codex. Pure ordering change; no behavior delta.

* feat(amr): Sign-in / Sign-out button on the AMR Settings card

The first half of the AMR work assumed the operator would set
VELA_RUNTIME_KEY / VELA_LINK_URL on the daemon process and never
surfaced login state to users. This adds the missing UX so a fresh
install can drive the full path from Settings:

  - GET  /api/integrations/vela/status   reads ~/.vela/config.json
    for the active profile and returns { loggedIn, profile, user }
    (without leaking the runtime/control keys themselves).
  - POST /api/integrations/vela/login    spawns `vela login` once
    (409 if one is already in flight). The vela CLI opens the user's
    browser to the device-authorization page itself — Open Design
    only needs to kick the subprocess off.
  - POST /api/integrations/vela/logout   removes ~/.vela/config.json
    so the next status read returns logged-out.

`AmrAgentCard` is a dedicated agent-card component for AMR because
the existing `<button>` row can't host an interactive sub-control
(nested interactive elements). It polls /status after a login click
until the daemon reports loggedIn=true (or 5 minutes elapse), and
exposes a Sign-out action on hover. Other adapters (claude, codex,
hermes, …) keep their existing `<button>` card.

i18n: 8 new keys (settings.amrLogin / Logout / LoggingIn / etc.)
added to en + zh-CN. Other locales spread `en` and inherit the
English copy until translations land.

Coverage:
- `tests/integrations/vela.test.ts` pins the config.json reader
  against a tmp HOME — including the negative case where a profile
  has user info but no runtimeKey (still logged-out), and the
  secret-leak guard ("rt-secret-*" must not appear in the projection
  payload).
- `tests/components/AmrAgentCard.test.tsx` covers all four UI
  states (logged-out, logging-in, logged-in, logging-out) plus the
  click-propagation invariant the divergent card was built to keep.

`pnpm --filter @open-design/daemon test` 2901 / 2901 passing.
`pnpm --filter @open-design/web test` 1719 / 1719 passing.
`pnpm typecheck` + `pnpm guard` clean.

Dev script side-effects: `apps/daemon/scripts/verify-amr-real-vela.mjs`
no longer requires both VELA_RUNTIME_KEY and VELA_LINK_URL — if
VELA_PROFILE is set, the vela CLI is allowed to resolve credentials
from `~/.vela/config.json`. Added the two AMR `.mjs` fixtures to
`scripts/guard.ts` allowlist with the executable-fixture / dev-runner
rationale.

* fix(connection-test): substitute model for AMR before attachAcpSession

The chat-run path in server.ts already routes the requested model through
`resolveModelForAgent` so AMR / vela (whose CLI demands an explicit
`session/set_model` before `session/prompt`) gets the def's first
concrete fallback id when the chat run ships `model: 'default'`.
`connectionTest.ts` was wiring `attachAcpSession({ ..., model: model ?? null })`
directly, which made the Test Connection button on the AMR Settings
card deadlock with the same `session/set_model must be called before
session/prompt` error the chat-run path already handles — surfaced as a
permanent "Testing connection…" spinner in the UI.

Reuse the same helper here so Test Connection mirrors chat-run behavior.

* test(amr): three-layer end-to-end coverage for the AMR login + turn flow

The PR up to this point shipped runtime + UI code with unit-level Vitest
coverage. This commit adds the cross-layer regression net the live demo
relied on:

1. apps/daemon/tests/integrations/vela.routes.test.ts (HTTP, Vitest)
   Spins up the real daemon Express app via `startServer({port:0,...})`,
   persists `agentCliEnv.amr.VELA_BIN = <fake>` into app-config.json,
   and exercises every /api/integrations/vela/* endpoint against the
   extended fake-vela stub:
     - status reads ~/.vela/config.json under various states
     - login spawns the fake, waits for config.json to appear, returns
       pid + startedAt + profile
     - 409 already-running guard with the stub's delay knob
     - logout removes the file (idempotent)
     - secrets (runtimeKey / controlKey) never leak in the projection
     - login → status round-trip flips loggedIn=false → true

2. e2e/tests/amr/turn.test.ts (tools-dev orchestrated, Vitest)
   Boots a namespaced daemon + web pair through `createSmokeSuite`,
   inlines a self-contained fake `vela` binary that handles BOTH
   `vela login` (writes ~/.vela/config.json) and
   `vela agent run --runtime opencode` (ACP stdio with the
   `session/set_model must precede session/prompt` gate the real binary
   enforces), then drives a complete /api/runs lifecycle for
   `agentId: 'amr', model: 'default'` and asserts the assistant message
   captures the fake's streamed text. This is the test that would have
   surfaced today's plugin-default-model regression (the `set_model
   before prompt` error) at PR time instead of demo time.

3. e2e/ui/amr-login-pill.test.ts (Playwright)
   Mocks /api/agents + /api/integrations/vela/{status,login,logout}
   to drive the Settings AMR card through the full Sign in → Signed in
   → Sign out cycle. Pins the AmrLoginPill polling contract and the
   aria-label semantics (the pill's accessible name is "Sign out" once
   logged in, regardless of which label the hover-state text shows).

fake-vela.mjs extensions:
   - Handles `vela login` argv by writing
     ~/.vela/config.json for the active VELA_PROFILE and exiting 0 —
     mirrors real vela's on-disk side-effect without the device-auth
     loop.
   - FAKE_VELA_LOGIN_DELAY_MS knob so route tests can observe the
     in-flight state of the spawn lifecycle.
   - FAKE_VELA_LOGIN_USER_EMAIL / _USER_PLAN to assert the surfaced
     user fields end-to-end.

Validated:
   - `pnpm guard` + `pnpm typecheck` (all workspace projects)
   - `pnpm --filter @open-design/daemon test`: 2998 / 2998 passing,
     including the new 8-test integration suite.
   - `cd e2e && pnpm test tests/amr`: 1 / 1 passing.
   - `cd e2e && pnpm exec playwright test ui/amr-login-pill.test.ts`:
     1 / 1 passing (6.7s).

* feat(amr): package native cli and refine login ui

* feat(amr): wire vela cli beta packaging

* docs(amr): document vela ci packaging review

* docs(amr): refine vela ci integration review

* fix(ci): refresh nix pnpm dependency hashes

* fix(pack): clean up Vela CLI packaging

* fix(pack): bundle Vela CLI support files

* fix(amr): recover login attempts from stale auth state

* test: expand AMR and automations coverage

* fix(amr): address review follow-ups

* test(web): align tasks fixtures with contracts

* fix(daemon): type wildcard route params

* fix(ci): refresh PR merge validation

* fix(amr): clear env credentials on logout

* feat(settings): inline local CLI model configuration

* fix(amr): recognize daemon env credentials

* [codex] Fix Vela companion packaging (#2979)

* Fix Vela companion packaging

* Update Nix pnpm dependency hashes

* [codex] Surface AMR account failures (#2980)

* fix: surface AMR account failures

* fix: cover AMR recovery error guidance

* chore: bump beta base version to 0.8.1 (#2990)

* Fix AMR profile and packaged runtime review issues

* Detect packaged AMR OpenCode companion tree

* feat(web): polish AMR frontend flows

* Polish AMR onboarding card

* fix: read AMR login state from dot-amr config (#3048)

* test: tighten AMR credential and packaging coverage

* test: restore AMR executable test env helper

* [codex] Fix packaged mac Dock identity and AMR label (#3076)

* Fix packaged mac sidecar Dock identity

* Rename AMR assistant label

* Fix AMR live models and dot-amr login state (#3073)

* fix: read AMR login state from dot-amr config

* fix: load live AMR models before runs

* fix: point AMR onboarding link to production wallet

* fix: address AMR model review feedback

* fix: persist live AMR model fallback

* [codex] Fix AMR link catalog model ids (#3088)

* Fix packaged mac sidecar Dock identity

* Rename AMR assistant label

* Fix AMR link catalog model ids

* Fix AMR model normalization typecheck

* Use live AMR model for default runs

* fix: polish AMR runtime settings UI

* Accelerate AMR startup defaults (#3092)

* Surface AMR insufficient balance wallet URL (#3099)

* fix(web): polish onboarding controls (#3112)

* fix(web): show CLI scan loading state

* Avoid duplicate AMR wallet recharge links (#3117)

* Avoid duplicate AMR wallet recharge links

* Use Vela CLI 0.0.3 test package

* chore(nix): refresh pnpm deps hash

* Fix AMR wallet guidance display

---------

Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>

* chore(pack): pin Vela CLI 0.0.3-test.1 (#3127)

* chore(nix): refresh pnpm deps hash

* chore(pack): pin Vela CLI 0.0.3

* chore(nix): refresh pnpm deps hash

* fix(web): suppress AMR exit 130 fallback (#3136)

* feat(web): nudge users to hosted AMR on model/auth/quota failures (#3083)

* feat(web): nudge users to hosted AMR on model/auth/quota failures

When a non-AMR agent run fails with an auth / quota / upstream model
error, surface an inline nudge under the error pill linking to Open
Design's hosted AMR gateway (https://open-design.ai/amr). The nudge
fires `surface_view` (element=run_failed_toast) on impression and
`ui_click` (element=go_amr) on the link.

Also teach the daemon to classify CLI-agent auth/quota/upstream failures
(Claude Code, codex, ...) into specific API error codes
(AGENT_AUTH_REQUIRED / RATE_LIMITED / UPSTREAM_UNAVAILABLE) instead of
the generic AGENT_EXECUTION_FAILED, so both the error message and the
nudge key off accurate codes. AMR's own runs are excluded from the
nudge — they keep the dedicated sign-in / recharge affordances.

* feat(web): rework failed-run AMR guidance into per-case error UI

Replace the single inline nudge with a per-case failed-run experience
driven by the run's error code + agent:

- The error card is now neutral gray (was red) and always carries a
  retry button; it is driven by the persisted per-message error event so
  it survives a reload.
- Non-AMR agent hitting a model/auth/quota wall: a theme-color promotion
  card under the error card offers "switch to AMR & retry" — switches the
  run to AMR, opens Settings on the AMR card, and auto-retries once the
  account signs in (ProjectView polls vela login status, independent of
  the Settings pill lifecycle, with success / 5-min-timeout / unmount
  exits).
- AMR agent unauthorized: clearer copy + an "authorize & retry" button.
- AMR agent out of balance: clearer copy + a "top up" button to the AMR
  wallet, with manual retry.
- Settings AMR card: when opened from the nudge, it scrolls into view and
  pulses, and an authorize-button coachmark (a fake hand cursor that
  rises in and dismisses on hover) points at the sign-in control when not
  yet authorized.

analytics: surface_view (run_failed_toast) on the promotion card and
ui_click (go_amr) on its action are retained. i18n adds chat.amrCard.*
and chat.amrError.* (en / zh-CN / zh-TW translated; other locales fall
back to en) and drops the old chat.amrErrorGuidance keys.

* fix(daemon): require status context for numeric service-failure codes

Per review on #3083: the model-service classifier matched bare HTTP
status numbers (`500`, `502`, `429`, `401`), so ordinary CLI output like
`line 500`, `read 502 bytes`, or `exit code 401` could be misclassified
as a provider outage / auth wall and wrongly surface the AMR nudge. Now
a status number only counts when it carries explicit context (`HTTP 500`,
`status 503`, `code: 401`, `502 Bad Gateway`); textual provider phrases
(overloaded, bad gateway, service unavailable, rate limit, …) are
unchanged. Adds fixtures proving unrelated numeric output stays null.

* fix(web): keep error pill for failed runs ChatPane's card doesn't cover

Per review on #3083: the per-message gray error pill was suppressed for
every persisted error status event, but ChatPane only renders the
replacement top-level error card for `retryableAssistantMessage` (the
last failed assistant). So a failed turn that is no longer last (after a
follow-up) or an older failed run in history showed neither the pill nor
the card — its error detail vanished, undercutting reload/history
survival. ChatPane now passes `errorCardOwnerId` (the assistant id whose
error the card represents); AssistantMessage suppresses only that one
pill and keeps rendering StatusPill for all other error events.

* fix(daemon): don't treat a process exit code as an HTTP status

Follow-up to review on #3083: the status-context helper accepted a bare
`code` prefix, so `exit code 401` / `process exited with code 429` still
matched and got classified as AGENT_AUTH_REQUIRED / RATE_LIMITED (the
very `exit code 401` case the comment calls out as noise). `code` now
only counts when qualified (`status code` / `error code` / `response
code`) or punctuation-bound (`code: 401`); bare `exit code N` no longer
matches. Adds fixtures for exit-code lines returning null.

* chore(web): translate AMR card / error keys for 16 remaining locales

PR #3083 added 10 new `chat.amrCard.*` / `chat.amrError.*` keys but only
provided en/zh-CN/zh-TW translations; the other 16 locales fell back to
English. Translate the card title/body, three chips, primary CTA, and
the AMR self-error (auth / balance) messages and buttons for ar, de,
es-ES, fa, fr, hu, id, it, ja, ko, pl, pt-BR, ru, th, tr, uk.

* fix(amr): address review feedback on #2355

Targeted fixes for the unresolved review threads on #2355. Each fix
includes / updates a focused test.

- runtimes/executables.ts: `packagedVelaOpenCodeCompanionTree` now
  verifies the inner `opencode` executable exists + is runnable, not
  just the directory. This closes the false-positive availability path
  that let `detectAgents()` surface AMR as available even when the
  packaged companion was empty / partially copied (mrcfps, 4 threads).

- runtimes/executables.ts: `resolveAmrOpenCodeExecutable` now prefers
  the bundled `<OD_RESOURCE_ROOT>/bin/libexec/opencode/opencode` over a
  stale `opencode` on the user's PATH, so packaged AMR builds can't be
  hijacked by a global installation.

- web/EntryShell.tsx: when the Local CLI scan returns an available
  agent and the previously-selected agent is AMR, switch the selection
  to the first available local agent so the runtime and persisted
  agent agree before Continue.

- server.ts (model-probe branch): for AMR, check `readVelaLoginStatus`
  BEFORE rejecting on an empty live-model catalog — a signed-out user
  was getting `AMR_MODEL_UNAVAILABLE` ("choose a model") instead of
  the correct `AMR_AUTH_REQUIRED` (sign-in affordance).

- server.ts (default model fallback): if the user asked for the AMR
  agent default and the cached id is no longer in the FRESH catalog,
  fall back to `liveModels[0]` from the probe instead of rejecting the
  run as `AMR_MODEL_UNAVAILABLE`.

- integrations/vela.ts: route `vela login` through
  `createCommandInvocation` so an npm/Node-style `vela.cmd` / `.bat`
  shim on Windows gets the correct `cmd.exe /d /s /c …` wrapping with
  verbatim args (matches `execAgentFile` / chat-run spawning).

- tools/pack/src/linux.ts: in containerized Linux builds, bind-mount
  the host directory of `OPEN_DESIGN_VELA_CLI_BIN` and rewrite the env
  to the container-side path. The host path was being passed in as-is
  even though the default container only mounts /project, /tools-pack
  and cache/home — `copyOptionalVelaCliBinary` saw a missing path.

Deferred (out of scope for this PR):
- `od amr status/login/logout/cancel` CLI subcommands (AGENTS.md
  UI/CLI dual-track rule, server.ts:5763) — sizable surface; tracked
  for a separate focused PR.
- Strict `--require-vela-cli` for Windows + mac-x64 beta builds:
  prematurely blocked — `@powerformer/vela-cli` only publishes the
  `darwin-arm64` platform binary today; adding the flag elsewhere
  would fail the builds. Revisit once win/x64/linux binaries ship.

* fix(amr): hoist sendAmrAccountFailure above the AMR catalog preflight (TDZ)

The new signed-out AMR branch in the catalog preflight at server.ts:10875
calls `sendAmrAccountFailure(...)` to emit AMR_AUTH_REQUIRED, but the
const declaration sat ~100 lines below at the outer function scope. Because
`const` is TDZ-aware, that branch would have thrown `ReferenceError:
Cannot access 'sendAmrAccountFailure' before initialization` for the
exact users it tries to help — defeating the original intent.

Hoist the helper to just above the AMR preflight block so it's available
to every AMR code path in this function. Behavior elsewhere is unchanged.

Also rerun the daemon test suite: `launch.test.ts > resolveAgentLaunch
uses packaged built-in Vela for AMR` was creating the
`<resourceRoot>/bin/libexec/opencode/` companion *directory* only, but
this PR's earlier tightening of `packagedVelaOpenCodeCompanionTree`
also requires the inner `opencode` executable. Add it to that fixture
to match the new contract; the test was a sibling of the executables /
env-and-detection fixtures already updated in 13fc4f4.

Addresses #2355 review (mrcfps, 2026-05-28).

* feat(web): add hover cancel for AMR login (#3158)

* feat(web): add hover cancel for AMR login

* fix(web): don't bounce AmrLoginPill back to 'Signing in…' after local cancel

Both codex-connector (P2) and looper (CHANGES_REQUESTED) on this PR
flagged the same race in the new local-cancel path: `handleCancelLogin`
dispatches `notifyAmrLoginStatusChanged('login-canceled')` immediately
after `/login/cancel` returns, but the `AMR_LOGIN_STATUS_EVENT` listener
unconditionally re-enters `refresh()` and then restarts polling
whenever `/api/integrations/vela/status` still reports
`loginInFlight: true`.

That is a real race because the daemon's `cancelVelaLogin()` only sends
SIGTERM (escalating to SIGKILL after `LOGIN_CANCEL_KILL_GRACE_MS` =
2000 ms) and keeps the child in `activeLoginProcs` until it actually
exits — so the first `/status` read after a successful cancel can
legally still come back as in-flight. Under that window the pill flips
back to 'Signing in…' and can later surface the timeout/error path even
though the user already canceled, defeating the behavior promised in
the PR description.

Fix the listener instead of every dispatch site: in the
`login-canceled` branch, after the local reset (stopPolling +
setPending(null) + clear refs), optimistically mark every subscribed
pill instance as not-in-flight (`setStatus((c) => c ? { ...c,
loginInFlight: false } : c)`) and `return` — skip the
refresh-and-reconcile branch below entirely. The next explicit refresh
(component mount, user interaction, or a `status-changed` event) will
pick up the daemon's confirmed state once the child has actually
exited.

Add a focused regression test that holds `/api/integrations/vela/status`
at `loginInFlight: true` even after a successful `/login/cancel`,
asserting that the pill stays at the Canceled → Authorize sequence and
never bounces back to 'Signing in…'. This test fails on the pre-fix
listener and passes on the new behavior; existing
'cancels an in-flight AMR sign-in…' and 'reconciles late AMR browser
completion to Signed in after local cancel' tests continue to pass.

Addresses review feedback on #3158 (chatgpt-codex-connector, nettee).

---------

Co-authored-by: lefarcen <935902669@qq.com>

---------

Co-authored-by: a1chzt <chizblank@gmail.com>
Co-authored-by: Amy <1184569493@qq.com>
Co-authored-by: Mason <jinmeihong0201@gmail.com>
Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com>
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
2026-05-28 05:09:55 +00:00
jaehanbyun
62972f14a3
fix(web): portal plugin details modal (#3065)
Signed-off-by: jaehanbyun <awbrg789@naver.com>
2026-05-28 04:03:41 +00:00
icc
74fa8a754a
fix: dismiss tab search on outside mouse down (#3102)
Co-authored-by: icc <iccccccccccccc@users.noreply.github.com>
2026-05-27 14:45:35 +00:00
吴杨帆
c554f14973
fix(web): refresh chat skills after Settings skill mutations (#3020)
SkillsSection kept its own skills list in sync after create/delete, but
App-level skills (used by the chat composer) were only loaded at boot.
Propagate a refresh callback so new skills appear in chat immediately.

Fixes #3017
2026-05-27 14:44:28 +00:00
初晨
3abcb3a4d2
fix(connectors): expire stale auth credentials (#2385)
* fix(connectors): expire stale auth credentials

Mark connector credentials as expired when provider reads report auth-shaped failures so Memory stops presenting stale connected apps as healthy.

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

* fix(connectors): avoid expiring grants on platform 401

Only delete connector credentials for provider tool errors attributable to the current connector so Composio platform auth failures do not wipe valid grants.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 14:44:10 +00:00
Mason
1083df8769
Hide social sharing without explicit preview URL (#3108) 2026-05-27 10:55:31 +00:00
Mason
e40947ac0d
Add social sharing for template previews (#2924)
* Add template social sharing menu

* Update plugin share e2e expectations

* Add additional template social share targets

* Remove Bilibili template share target

* Open social share destinations in new tabs

* Address template share review feedback

* Use canonical public plugin share URLs

* Gate public plugin share links by marketplace provenance

* Update plugin share e2e for local-only badges

* Limit public share URLs to official marketplace
2026-05-27 10:21:35 +00:00
YOMXXX
72be9f4e63
test(web): pin the Web Storage shim's sandbox-iframe end-to-end behavior (#1403 verify) (#3072)
PR #1306 routed artifacts whose source matches htmlNeedsSandboxShim() through buildSrcdoc(), which injects a localStorage / sessionStorage polyfill before any user script runs. Issue #1403 stayed open as a verification placeholder against the original repro shape — a React tree whose useState initializer reads localStorage in a sandboxed iframe.

file-viewer-render-mode.test.ts already covers the routing decision. This commit closes the loop on the runtime payload: a real-shape React artifact is fed through buildSrcdoc, the produced doc is run inside a Node vm context whose window forbids Web Storage the same way an allow-scripts iframe does, and we assert (a) the bare sandbox raises SecurityError on access, (b) the shim takes over and exposes a working in-memory store, (c) the original boot script that read localStorage from initializers runs cleanly, and (d) the shim does NOT clobber a working native storage when one is present (the allow-same-origin path stays untouched). Also pins shim ordering — the shim script must appear before the first user storage read for the polyfill to be effective.
2026-05-27 06:35:17 +00:00
YOMXXX
9e76bf0556
fix(web): swap in typographic fallback when a Home media poster fails to load (#2955) (#3070)
MediaSurface rendered preview.poster straight into an <img> with no error handler, so an official Community card whose poster URL 404'd / failed to decode / hit a dead host left the browser's default broken-image glyph on the discovery surface. Reported on the Home page where several official image-template cards looked unreliable side-by-side with healthy ones.

Track a per-URL load-failure flag and swap in the existing plugins-home__media-fallback element (the typographic glyph + media icon) when the <img> fires onError. The flag resets whenever preview.poster changes, so filter rotations or a daemon repopulating the preview after an offline flip get a fresh attempt instead of staying stuck on the fallback.

Regression tests cover the four shapes: default <img> render, error -> fallback swap, poster URL change resets the failed state, and the original no-poster branch still goes straight to the fallback.
2026-05-27 06:34:58 +00:00
吴杨帆
9641c9e11c
fix(web): parse ask-question blocks as question-form alias (#1194) (#3053)
Accept <ask-question> as an alias for <question-form> and locate close
tags with a Unicode-safe scan so Turkish dotted-I prose before the tag
does not desync parser indices.
2026-05-27 06:34:36 +00:00
吴杨帆
6155ad8cbe
fix(web): surface Claude Design zip import failures (#1862) (#3047)
Show a toast when the daemon rejects a ZIP import instead of silently
closing the file picker with no feedback.
2026-05-27 06:24:38 +00:00
吴杨帆
26ef90ffd9
fix(web): clear prompt when removing Home example chip (#2989) (#3045)
Removing the selected example prompt chip now also clears the composer
input so chip state stays in sync with the inserted example text.
2026-05-27 06:21:35 +00:00
吴杨帆
4808cdab3c
fix(web): render srcdoc artifacts directly after leaving URL-load (#3042)
Lazy srcdoc transport was still active after URL-load preview switched off,
leaving the visible iframe on an empty activation shell until Edit forced a
full srcdoc reload. Mount real artifact HTML whenever srcdoc is the active
transport and remount when leaving URL-load.

Fixes #2791
2026-05-27 06:21:01 +00:00
吴杨帆
582a03195f
fix(web): clarify finalize BYOK requirements for Local CLI users (#3041)
Local CLI chat does not supply BYOK credentials to finalize synthesis.
Resolve per-protocol saved settings before calling the daemon and show an
actionable toast instead of a generic BAD_REQUEST when credentials are
missing.

Fixes #2959
2026-05-27 06:20:33 +00:00
吴杨帆
17c78f64a3
fix(web): focus newly created automations after save (#3035)
Expand and briefly highlight the saved routine row so users can
review it immediately. Extract newest-first sort helper and add
regression tests for list ordering and post-create focus.
2026-05-27 04:44:38 +00:00
吴杨帆
7ed3b9b0de
fix(web): align manual edit canvas on device viewports (#2960) (#3033)
Use the same relative positioning as comment preview clips so edit
mode clicks hit the scaled iframe instead of a full-bleed absolute layer.
2026-05-27 04:41:54 +00:00
吴杨帆
19142b0d11
fix(web): resolve skill vs type-chip routing conflicts (#2972) (#3031)
Clear the opposing selection when the user picks a skill or scenario
chip, and omit skillId when a scenario plugin is active on submit.
2026-05-27 04:37:21 +00:00
吴杨帆
0e9289e77e
test(web): cover duplicate example card deduplication (#3026)
Add a regression test for ExamplesTab skill-id deduplication so duplicate
catalog entries render a single task-selection card.

Closes #2889
2026-05-27 04:36:12 +00:00
chaoxiaoche
fce444bcab
Consolidate chat comments preview on main (#2906)
* feat(web): queue chat sends

* feat(web): render code comment directives

* feat(web): add preview comments and manual edits

* fix(web): polish shared chrome controls

* fix(web): align queued send loading state

* feat(web): open primary project artifacts

* fix(web): keep queued sends and tests aligned

* fix(web): restore docked comment tools layout

* fix(web): align preview comment toolbar

* fix(web): place local cli beside handoff

* fix(web): move agent menu beside handoff

* fix(web): make project instructions a direct header action

* fix(web): compact handoff and toolbar labels

* fix(web): clarify handoff menu and annotation label

* fix(web): restore compact cursor handoff trigger

* fix(web): align agent menu trigger with handoff

* fix(web): add draw toolbar close action

* fix(web): move inspect editing into edit mode

* fix(web): avoid reserving comment sidebar in annotation mode

* fix(web): float preview comments panel

* fix(web): keep edit canvas full width

* fix(web): polish preview annotation tools

* fix(web): highlight active preview comments

* fix(web): open comments panel after annotation save

* fix(web): polish comment handoff controls

* fix(web): remove palette preview tool

* fix(web): simplify draw annotation toolbar

* fix(web): restore queued tasks into composer

* fix(web): restore queued send strip styling

* fix(web): hide internal comment target ids

* fix(web): align manual edit panel header

* test(web): cover visual interaction contracts

* fix(web): address PR feedback regressions

* fix(web): preserve artifact chrome state

* fix(daemon): restore project raw file routes

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-26 10:31:19 +00:00
Yuhao Chen
3655230571
fix(web): align draw note enter action (#2971) 2026-05-26 07:36:34 +00:00
蓝宙
b5b975769a
fix Windows folder import from project modal (#2863)
Co-authored-by: Lanzhou3 <217479610+Lanzhou3@users.noreply.github.com>
2026-05-26 06:52:07 +00:00
Yuhao Chen
1770789fa0
fix(web): hide recent project pagination chrome (#2966) 2026-05-26 06:47:48 +00:00
nettee
6c6ee44e07
docs(media): clarify custom providers and ComfyUI status (#479) (#2942)
Closes #479

Generated-By: looper 0.9.0 (runner=worker, agent=opencode)
2026-05-26 06:22:02 +00:00
PerishFire
f4d53486ec
fix(web): warn BYOK users that API mode has no file-edit tools (#2943)
The BYOK chat path streams model text directly through the provider API and
does not run the agent-runtime scaffolding that wires up Read/Write/Edit
tools — so a model that "decides" to edit a file just emits HTML or code
back into the chat. When users switch from Local CLI (Claude Code) to
BYOK with an Anthropic key and then ask the agent to keep adjusting a
design, nothing on disk changes and the failure mode is silent.

Until the BYOK tool loop is implemented (#313 / #699 / #719), surface a
clear notice in the BYOK panel of Settings that explains the limitation
and points users at Local CLI mode for file edits.

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

Co-authored-by: libertecode <libertecode@proton.me>
2026-05-26 06:16:14 +00:00
MrBeanDev
fc53feccd9
feat(web): add Aider and Trae CLI brand icons, refresh Pi mark (#2956)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
actionlint / Lint GitHub Actions workflows (push) Failing after 1s
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 2s
ci / Browser tests (push) Failing after 2s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
AgentIcon fell back to an initial-letter pill for Aider and Trae CLI
because neither id was registered in ICON_EXT and no asset shipped. Add
the bundled brand marks so both agents render their real logo:

- aider.png — Aider's published avatar, downscaled to 96px (~2.6KB).
- trae-cli.png — Trae's app icon, downscaled to 96px (~2.3KB). Keyed on
  the `trae-cli` runtime id so the file and ICON_EXT entry match exactly.

Both vendors only publish rasterised marks, so they follow the existing
PNG-fallback path used for Devin.

Pi shipped a stale single-glyph silhouette in MONO_ICONS. Replace it
with the current dark-tile mark (white glyph on #09090b) and drop it
from MONO_ICONS — the tile has baked colors, so CSS-mask rendering with
currentColor would flatten it to a solid square. It now renders through
<img> like the other color-baked brands.

Adds AgentIcon test coverage for all three.
2026-05-26 06:14:15 +00:00
Shivam
c8af02e3a5
Clear manual edit selection after delete (#2864)
Co-authored-by: Shivam <shivam2931120@users.noreply.github.com>
2026-05-26 06:11:22 +00:00
leessju
2573318523
fix(web): live-update preview during Comment mode (#2844)
The raw HTML fetch for the preview source used no cache-bust hint, so
an agent edit while Comment mode was on returned stale bytes from the
browser HTTP cache. With identical source, srcDoc was byte-equal to the
last activated HTML, canActivateSrcDocTransport bailed via its dedupe
check, and the iframe stayed on the pre-edit frame until Comment was
toggled off (at which point url-load took over with its own ?v=mtime
cache-bust). Cache-bust on file.mtime + reloadKey + filesRefreshKey
so fresh HTML reaches the shell on every change.

A null mid-burst (chokidar emits agent rewrites as unlink+add+change)
would also blank source and snap srcDoc empty; ignore null responses
so the previous frame stays until valid HTML arrives.

Subsequent activations in the same shell would document.open + write
over the iframe. The window message listener survives, but
iframe.onLoad does not refire for document.write, so host-side re-init
(slide nav sync, scroll restore, bridge replay) is silently skipped —
the visible page can drift out of sync with the host's tracked state
(e.g. the bottom indicator reads 3 while the iframe rendered page 4 of
the freshly edited deck). Under Comment, force a fresh shell mount on
the second activation so onLoad fires and the full re-init pipeline
runs against the new HTML. Manual Edit keeps the postMessage path
(its patched HTML must not lose host-side scroll/slide state).

Co-authored-by: nicejames <nicejames@gmail.com>
2026-05-26 06:11:03 +00:00
leessju
7f8d750d8a
Preserve composer drafts across refreshes (#2839)
Users can type a prompt in a conversation, reload the app, and expect that unsent text to remain tied to the same conversation. Store only the active conversation's composer draft under a project+conversation localStorage key and clear it once the draft is submitted or queued.

Constraint: The composer already remounts by activeConversationId, so persistence can stay local to ChatPane/ChatComposer without changing daemon contracts.

Rejected: Persist draft text in SQLite messages | unsent drafts are local UI state and should not appear in conversation history.

Confidence: high

Scope-risk: narrow

Directive: Keep initialDraft higher priority than stored drafts so seeded workflows are not overwritten by stale local text.

Tested: pnpm --filter @open-design/web test tests/components/ChatComposer.send-key.test.tsx tests/components/ChatComposer.queue-button.test.tsx

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

Co-authored-by: nicejames <nicejames@gmail.com>
2026-05-26 04:03:09 +00:00
Marc Chan
3d358fc877
fix(web): keep Vercel static builds writing to out (#2946)
* fix(web): keep Vercel static builds writing to out (#1628)

Generated-By: looper 0.9.1 (runner=worker, agent=opencode)

* fix(web): preserve explicit dist dir overrides

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

* fix(web): preserve explicit dist dir overrides

Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
2026-05-26 03:37:27 +00:00
dev-kp-eloper
01f80d4b06
fix(tabs): treat Home tab as a singleton and prevent duplication (#2580)
* fix(tabs): treat Home tab as a singleton and prevent duplication

* fix(tabs): address review comments on Home tab singleton navigation and diagnostics
2026-05-26 03:14:52 +00:00
Sid
5ff673f884
fix(web): keep Add to My plugins success affordance after panel remount (#2897)
The Add to My plugins button looked like nothing happened: the action
succeeded (the plugin showed up on the Plugins page) but the originating
panel only ever showed a transient "Sending..." state. Two root causes:

1. ProjectView toggles `hiddenPluginActionPaths` during the install action,
   which unmounts `PluginActionPanel` while the API call is in flight.
   The panel's own `noticeByFolder` state went with the unmount, so the
   `setNoticeByFolder(...)` call after `await onRequestPluginFolderAgentAction`
   landed on a dead fiber and the success notice never rendered.
2. The `PluginInstallOutcome` contract leaves `message` optional. When
   it is absent the surface had no fallback affordance — the button
   silently reverted to "Add to My plugins" with nothing else changed.

Lift `busyKey` / `noticeByFolder` / `runPluginAction` from `PluginActionPanel`
up into `AssistantMessage` so the state survives the unmount cycle. Render
notices inside the panel for visible folders and add an orphan-notice slot
that covers folders the parent has hidden in the meantime. Default to an
"Added to My plugins." notice when the install resolves without a message.
Publish / contribute paths still rely on outcome messages they always
emit, so they're unaffected.

Fixes #2876
2026-05-26 02:25:32 +00:00
elihahah666
80e685c656
fix(web): polish design system source flows (#2933)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 2s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 1s
ci / Runtime trace (push) Has been skipped
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-25 12:38:05 +00:00
Renfei
e3e990fce3
test(i18n): lock zh-CN to tier-1 explicit translation parity (#2920)
Refs #1894.

The existing locale-shape test (`Object.keys(dict).sort() === englishKeys`)
passes for every locale today, but most modules satisfy it via `...en`
spread — so an English key added without a matching translation falls
back to English at runtime, the test still passes, and locale drift
accumulates silently.

`zh-CN.ts` is the one locale today that declares all 2302 keys
explicitly, with no `...en` spread. This change pins that property as
a regression test:

- New: `keeps zh-CN explicitly translated for every English key (tier-1
  parity lock)` — asserts the `'key':` literals in the source file
  match the full English key set, using the existing
  `explicitLocaleKeys` helper.
- New: `keeps the zh-CN locale source free of the `...en` spread
  fallback` — paired source-grep guard so a future refactor can't sneak
  the spread back in.

Both cases pass today without any locale-content edits (verified
locally against `main` at the time of writing: 2302 explicit keys,
no `...en` match). Net effect: a future PR that adds an English key
must update `zh-CN` synchronously or CI fails loudly for this one
locale, instead of letting the gap widen in silence.

Scope kept deliberately narrow per the discussion on #1894 — this
does not touch `id.ts` (which currently uses `...en`), does not change
the wider policy decision (enforce all locales / tier-1 subset /
report-only), and does not duplicate the `pnpm i18n:coverage` report
that shipped in #1896. It just locks `zh-CN`'s current tier-1 state
so the rest of the policy discussion can proceed without losing that
ground.

Co-authored-by: zhongrenfei1-hub <231221504+zhongrenfei1-hub@users.noreply.github.com>
2026-05-25 20:08:41 +08:00
chaoxiaoche
dc2cb6b371
feat(web): render code comment directives (#2871)
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-25 11:09:59 +00:00
chaoxiaoche
7a70a02d83
feat(web): queue chat sends (#2870)
* feat(web): queue chat sends

* fix(web): allow queued sends from streaming composer

Keep the send button functional while a run is streaming so follow-up prompts still flow into the queue path, and cover it with a regression test.

* fix(web): polish queued send follow-ups

Keep pinned chats auto-following when the queued strip changes height, remove unused queueing scaffold, and localize the queued-send strip copy.

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: mrcfps <mrc@powerformer.com>
2026-05-25 10:43:01 +00:00
chaoxiaoche
2b7b6590ae
feat(comments): add comment attachment API (#2869)
* feat(comments): add comment attachment API

* ci: add fork PR workflow approval script

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
2026-05-25 07:24:21 +00:00
lefarcen
77b865e4d1
fix(web): handle dropped file read failures (#2858) 2026-05-25 06:05:17 +00:00
Marc Chan
619087a6b4
refactor(web): split global CSS by ownership (#2609)
* refactor(web): split global CSS by ownership

* test(web): expand CSS imports in style checks

* fix(web): keep privacy consent banner above modals
2026-05-25 05:48:28 +00:00
Hashem Aldhaheri
5d032aedec
fix(web): treat plugin preview 404 as unavailable, matching skill helper (#2840)
Bundled plugins whose manifest declares `preview.entry` but ship no
example HTML (e.g. example-live-artifact pointing at ./index.html that
isn't in the package) made the daemon return 404 on
/api/plugins/:id/preview. The web turned that into the generic
"Couldn't load this example. The example HTML failed to fetch." error
modal, which suggested a transient failure even though Open Design was
running fine and the asset was simply absent.

Mirror the symmetric treatment fetchSkillExample already has from
#897: map 404 -> { unavailable: true, kind: 'html' } in both
fetchPluginPreviewHtml and fetchPluginExampleHtml, and forward the
typed unavailable view through PluginExampleDetail so PreviewModal
renders its calm "no shipped preview" placeholder. Other HTTP failures
keep their existing { error: 'HTTP N' } shape so genuine errors still
surface a Retry affordance.

Red test was added first (registry.test.ts) and confirmed to fail on
main with the old "HTTP 404" payload before this commit was applied.
2026-05-25 05:19:01 +00:00
Chris Seifert
472e10a991
Polish the design system review panel (#2848)
* Polish the design system review panel

- Float the "Needs work" feedback form below the review buttons as a popover
  instead of cramming it into the section header row.
- Collapse a section once it is marked "Looks good" so reviewed sections tidy
  away. Clicking Looks good always collapses; the chevron still re-expands.
- Make the inline preview iframe scrollable and drop the click-to-open wrapper.
- Make the whole section header the expand/collapse trigger via a stretched
  button, with the title rendered as display-only text (no nested buttons).
- Rework the publish header: name the system in the h1 ("Review <name> design
  system"), move publishing into a Publish button inline with the h1, and make
  the disabled-state tooltip say what is actually needed.
- Animate the generation mark's small block (hop, spin, bounce) while waiting,
  with a reduced-motion guard.
- Tighten the title/subtitle gap to 2px.

* fix(web): reopen a regenerated design-system section instead of leaving it collapsed

renderReviewCard collapsed any section whose stored review decision was "looks-good", reading only the decision and not the section's current status. When a section is regenerated after approval its status moves back to "updated", but the stale "looks-good" decision kept it collapsed, so the "review it again before publishing" notice and the review buttons stayed hidden. A regenerated change was easy to miss in the re-review queue.

reviewedGood now also requires !needsAttention, so a section that moved back to a needs-attention status reopens by default. Manual collapse with the chevron and the force-open during an active run are unchanged.

Adds a regression test that marks a section looks-good, regenerates its preview file, and asserts the section stays expanded. It is red on the old decision-only check and green with the status guard.

* fix(web): keep the disabled-publish guidance reachable instead of on the disabled button

The publish button carried its disabled-state guidance in a title attribute, but the button is disabled in exactly the case that sets the title (!published && !githubEvidence.ready). A disabled control does not fire hover or focus, so the tooltip never appeared, and the explanation for why publishing was blocked went missing right when it was needed.

The guidance now lives on a wrapper span that is never disabled. The disabled button is set to pointer-events: none so a hover falls through to the wrapper and surfaces its title, and the wrapper carries cursor: not-allowed so the blocked cursor stays.

Adds a regression test that asserts the guidance sits on a non-disabled wrapper holding the button, not on the button itself. It is red when the title is on the disabled button and green with the wrapper.
2026-05-25 05:17:17 +00:00
Chris Seifert
085963a1e2
fix(web): show published design systems as "Published" on project cards (#2849)
A published design-system project still showed its last generation run status on its card. When that run had failed, the published system read as "Failed" on the home Recent projects strip and in the Projects grid. The system is published and live, so showing a stale run failure there is wrong.

Cards now read "Published" when the backing design system's status is published, keyed off the design system summary instead of the project run status. Every other project keeps its run status.

A shared isPublishedDesignSystemProject helper lets the home strip and the Designs grid apply one rule. The label uses a new designs.status.published key in the locale files, with a green style that matches the existing succeeded color.
2026-05-25 03:14:26 +00:00
Chris Seifert
af997b7cf5
feat: rename editable design systems from Settings + od CLI (#2812)
* feat: rename editable design systems from Settings + od CLI

Editable (user-created) design systems can already be renamed via
PATCH /api/design-systems/:id, but the capability was not surfaced
in the UI or CLI.

- Settings -> Design Systems: editable cards show a hover-reveal pencil
  next to the name that opens a rename modal; built-in cards stay
  read-only. Reuses common.rename/save/cancel (no new i18n keys).
- CLI: 'od design-systems rename <id> --title <new> [--json]', backed by
  a unit-tested pure arg parser (design-system-rename-args.ts).

Both surfaces call the existing PATCH endpoint.

* Route od design-systems --help and -h to the rename-aware usage

The dispatcher only special-cased the `help` subcommand, so
`od design-systems --help` and `-h` fell through to the generic library
list, which advertises only `list` and `show`. That left `rename` off the
main discovery path even though this PR ships it.

Pulled the usage text and the help-arg check into a small pure module so
`help`, `--help`, and `-h` all render the same rename-aware usage, and added
a test that asserts the flag forms route to help and that the text lists
rename. The pure module keeps the assertion off process.exit / console.log.

* Reject --title flag-as-value and keep the rename modal open on failure

Two rename edge cases from review.

CLI: parseDesignSystemRenameArgs took the next token after --title
unconditionally, so `rename user:acme --title --json` parsed the title as
"--json" and could rename the system to a flag name instead of failing usage
validation. A separate --title value must now be a real token; a leading dash
means the user uses the --title=<value> form. Malformed inputs return null,
which the CLI surfaces as a usage error.

Web: commitRename closed the modal unconditionally, but updateDesignSystemDraft
returns null on any non-OK response or fetch failure, so a transient error
dropped the typed title with no feedback. The modal now stays open with the
title intact and shows an inline error on failure, matching the existing import
error pattern in this component. Added tests for the flag-as-value rejection
and for the failed-update modal state.

* Gate the rename completion on the active modal session

commitRename mutated the shared modal state after awaiting the PATCH, so a
slow rename for system A could resolve after the user cancelled and opened a
rename for system B, then close B's modal or show A's failure inside B's
dialog.

A monotonic session token (bumped whenever the modal opens or closes) is now
captured before the request and rechecked after it resolves. A stale
completion skips all modal-state updates. The list update for a successful
rename still applies, since that reflects a real server-side change regardless
of which modal is open. Added a regression test that opens a second rename
before the first PATCH settles and confirms the newer modal is untouched.

* Localize the rename-failed error instead of hardcoding English

The inline rename error was hardcoded English on a Settings surface that
otherwise runs through useT(), so non-English users saw English while the
rest of the panel was localized.

Added settings.designSystemRenameFailed to the typed dictionary and all 19
locale files, and the modal now reads it through t(). The translations are
adapted from each locale's existing settings.rescanFailed string ("X failed.
Check the daemon and try again."), swapping the verb to rename, so the daemon
and retry wording matches what those locales already ship.
2026-05-25 03:13:35 +00:00
zqyaym
b22c7713db
fix(web): prevent preview iframe from stealing focus on load (#2792)
* Fix preview iframe focus stealing

* Fix preview focus guard for URL-loaded HTML previews

Focus guard was only injected via the srcdoc path, but the default
URL-load path bypasses buildSrcdoc entirely. Add htmlNeedsFocusGuard
detection so focus-stealing HTML is routed through srcdoc where the
guard can suppress window.focus/element.focus calls.

* Widen focus guard detector to cover all .focus() call patterns

The previous regex only matched window.focus() and document.focus(),
missing document.body.focus(), querySelector().focus(), and other
chained focus calls. Broaden to match any `.focus(` so the default
URL-loaded preview path is forced to srcDoc for all focus-stealing HTML.

* Conservatively force srcDoc for HTML with external script references

When the HTML contains <script src=...>, we cannot inspect the linked
file for focus-stealing calls. Force the srcDoc path so the focus guard
intercepts any .focus() calls from external scripts.

---------

Co-authored-by: JoeyZhu <15500388+acthenknow@user.noreply.gitee.com>
2026-05-24 14:37:08 +00:00
张东明
53dfb8808c
fix(#2361): patch both causes that present as blank preview (#2805)
* fix(web): treat external <script src> as needing the sandbox shim (#2361)

Agent-emitted HTML artifacts that read localStorage from an external
boot.js / app.js currently render blank in the preview pane because the
URL-load iframe's sandbox lacks allow-same-origin and htmlNeedsSandboxShim
only scans the HTML string. The "Known limitation" comment already
anticipated this case; #2361 is the reported case justifying the cost.
Conservatively route any HTML with an external <script src=> through the
srcDoc path so injectSandboxShim is in place before the script runs.

* fix(web): stop infinite srcDoc re-activate loop that blanks animated previews (#2361)

The lazy srcDoc transport iframe fires its 'load' event twice for one successful activation: once when the empty transport shell HTML loads, and again when our own document.open()/write()/close() inside the shell finishes. PR #2699 made the onLoad handler unconditionally reset activatedSrcDocTransportHtmlRef.current = null so that switching preview -> source -> preview (which remounts the iframe as a brand new DOM node) would re-activate the new shell. But that reset also fires on the second load of an unchanged frame, which re-triggers activateSrcDocTransport, which re-runs document.open/write/close, which re-fires the load event, ad infinitum. In one local reproduction the dedupe ref was cleared and re-activated 4763 times before the test was stopped.

Each iteration rebuilds the document, which restarts every CSS animation from its 'from' keyframe. Designs that use 'animation-fill-mode: both' with 'from { opacity: 0 }' (very common for editorial hero fades) therefore stay at opacity 0 forever and the preview reads as blank. In React strict mode + HMR (pnpm tools-dev) the symptom is visible high-frequency flashing; in a packaged production build the loop runs cool enough that the user only sees a stable blank — both are the same root cause.

This change keeps PR #2699's remount-after-Source-toggle behavior by tracking which iframe DOM node we last reset for in a new srcDocFrameDedupeResetForRef. The reset runs exactly once per freshly mounted iframe (the first load is the shell HTML) and is skipped on every subsequent load of the same node (those are the document.write loads). Switching source back to preview remounts the iframe as a fresh DOM node, so the reset still happens and PR #2699's regression test still passes; ordinary srcDoc renders no longer enter the infinite loop.

Refs #2361

* chore: re-trigger CI

Upstream's fork-pr-workflow-approval check hit a transient 401 Bad credentials when calling the GitHub API on the previous run; the underlying workflow has nothing to do with the code in this PR. Pushing an empty commit to re-run the workflow chain.

* chore: re-trigger CI (retry transient checkout race)

First re-trigger surfaced a transient race in ci / Build workspaces (actions/checkout failed to fetch refs/remotes/pull/2805/merge with 'could not read Username for https://github.com'). Other concurrent fork PRs' Build workspaces all passed on the same upstream runner, so this is not a token/permission infra issue — likely just a per-PR fetch race after the previous push. Pushing a second empty commit to retry the workflow chain.
2026-05-24 14:29:36 +00:00
Chris Seifert
0afaf3bb7e
feat: pin custom design systems to top and read swatches from color tables (#2817)
* feat: pin custom design systems to top and read swatches from color tables

Two changes to Settings -> Design Systems.

Custom (user-created) systems now sort to the top of the list instead of
sitting under the built-in catalog. A small pure helper
(orderDesignSystemGroups) floats any group that holds an editable system
above the rest; everything else keeps its order.

Swatches now show for systems whose DESIGN.md keeps colors in a markdown
table. extractSwatches only understood inline forms before, so table
palettes came back empty and the cards showed no color squares. Added a
table-row pass that reads the first hex in a row as the value and the
first plain text cell as the name. Inline forms still win when a file
mixes both.

* Sort editable systems first within a category group

The group-level sort floated any category holding a user system to the top,
but items inside a group rendered in their incoming (alphabetized) order. A
user system that shares a category with built-ins (its DESIGN.md can set any
category) still landed below Apple/Airbnb in that group, which misses the
point of pinning custom systems to the top.

orderDesignSystemGroups now also sorts items editable-first within each
group, stable so built-ins keep their alphabetical order. The display order
comes from the helper output, so this covers the import path re-alphabetizing
before grouping without touching it.
2026-05-24 14:25:19 +00:00
Chris Seifert
c5ea97a0e8
feat: add "Use system fonts" dismiss to the missing-fonts banner (#2816)
* feat: add "Use system fonts" dismiss to the missing-fonts banner

The 'Missing brand fonts' banner only offered Upload fonts. Now there's
a 'Use system fonts' button next to it that hides the banner for that
project (saved in localStorage) when you're fine with the fallback. It
does not change how fonts render; it just stops nagging.

Pulled the banner into a shared MissingBrandFontsBanner component and
gave the warning card the bottom margin it was missing.

* Resync the missing-fonts banner dismissal when the project changes

FileWorkspace renders MissingBrandFontsBanner without a per-project key, so
the same instance is reused as the user moves between projects. The dismissal
state was read with useState only on first mount, so dismissing project A left
the banner hidden for project B in the same panel even though only A was
written to localStorage. That broke the per-project scoping this banner adds.

Added a useEffect that re-reads the dismissal whenever projectId changes, and a
rerender test that dismisses p1, switches to p2 (banner returns), and switches
back to p1 (stays dismissed).
2026-05-24 14:25:04 +00:00
icc
8615141f94
fix(web): preserve ingest select chevron (#2733)
Co-authored-by: icc <iccccccccccccc@users.noreply.github.com>
2026-05-24 14:23:30 +00:00
icc
a83e9cf9f1
fix(web): clip inline model switcher labels (#2732)
Co-authored-by: icc <iccccccccccccc@users.noreply.github.com>
2026-05-24 14:22:55 +00:00
kami
94421f9676
fix(web): expose hidden edit targets in layers (#2067)
* fix(web): expose hidden edit targets in layers

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): respect visibility overrides in edit layers

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): keep hidden layout targets editable

Co-authored-by: multica-agent <github@multica.ai>

* fix(web): address hidden layer review followups

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-24 14:22:29 +00:00
Chris Seifert
fb5d4bbd16
Surface GitHub repo connect/import CTA in design system projects (#2820)
* Surface GitHub repo connect/import CTA in design system projects

Design systems built from a GitHub repo used to show a dead-end banner
("Waiting for GitHub connector evidence") when the repo files were never
pulled in. It said nothing actionable and blocked publishing with no way
forward.

That state now reads as a CTA, in both the Design System review banner and
the project's Start-a-conversation empty state. When GitHub is not
connected, both say "Connect your repo to pull aspects of your design
system" with a Connect GitHub button that opens Connectors. Once GitHub is
connected the copy switches to "GitHub is connected" with an Import repo
button that drops the bounded github-design-context intake instruction into
the chat composer, so you review it and send. The agent runs the pull, the
snapshots land, and the prompt clears itself.

The copy lives in one helper so the banner and the chat card never drift,
and ProjectView reads connector status (refreshing on window focus) only
while the CTA is showing.

* Build the import prompt from linked repo URLs, not the manifest

The Import repo prompt hard-coded "read context/source-context.md first,"
but the CTA shows for any incomplete GitHub-backed system, and that manifest
is not always written yet. So clicking Import repo could start the recovery
on a file that does not exist.

buildRepoImportPrompt now builds the instruction from the design system's
provenance.githubUrls, which is always present when the CTA shows (the CTA
gates on a non-empty repo list). It points at context/source-context.md only
when that file is actually present, and otherwise tells the agent to run the
bounded github-design-context intake for the linked repos directly.

* Hold the connect-repo CTA neutral until the status resolves

githubConnected seeded as false, so on first paint every GitHub-backed
project rendered the CTA as Connect GitHub and routed clicks to Connectors
until /api/connectors/status came back. An already-connected user who
clicked fast got bounced to Connectors instead of the Import repo handoff.

githubConnected is now tri-state (undefined while loading). repoConnectCopy
returns a neutral "Checking GitHub..." label for that window, the CTA button
is disabled until the status resolves, and handleConnectRepo guards the
undefined case. Once the fetch lands the button flips to Connect or Import.
Tests cover the pending label, the disabled button, and the no-op click.
2026-05-24 14:20:35 +00:00