* fix(deploy): move stage-2 asset copies into build stage
Stage 2 COPY commands for skills, design-systems, craft,
prompt-templates, assets, and plugins/_official previously read
from the build context, which couples the Dockerfile to the repo
root. Platforms like Railway that require the Dockerfile directory
to equal the build context could not find these paths.
Move the asset directories into the build stage and let Stage 2
pull them via --from=build, so the Dockerfile is fully
self-contained and works from any context directory.
* fix(deploy): chown volume at runtime so containers work with root-owned mounts
Platforms like Railway mount volumes as root, which overrides the
build-time chown on /app/.od. Run the container as root with tini,
chown the volume directory on start, then drop privileges to the
open-design user via su-exec before launching the daemon.
* Revert "fix(deploy): chown volume at runtime so containers work with root-owned mounts"
This reverts commit f32655d9c2.
- apps/daemon/src/pi-rpc.ts
- apps/daemon/tests/pi-rpc.test.ts
Surface Pi RPC assistant turn_end stopReason=error messages as daemon error events so upstream provider failures do not complete as empty zero-token runs.
The first session of a task does intent clarification by raising an
AskUserQuestion card, which inherently produces no artifact. These
clarification turns were indistinguishable from artifact-generation
failures on the dashboard, inflating the "run finished -> has artifact"
dropoff.
Add a required `asked_user_question: boolean` to RunFinishedProps,
populated daemon-side via a new `runAskedUserQuestion` helper that scans
the run's agent events for an AskUserQuestion / ask_user_question
tool_use. The dashboard can now exclude clarification turns from the
artifact funnel instead of scoring them as failures.
Co-authored-by: qiongyu1999 <2694684348@qq.com>
* test(daemon): stabilize artifact-manifest reconcile mtime test on slow CI
The reconcile-on-run-end regression test compared `fs.statSync(...).mtimeMs`
against a `Date.now()`-captured run-start timestamp using strict `<`, so if
the new file's ms-truncated mtime tied with runStartTimeMs (or fell a hair
below it on a runner with NTP jitter) the new-file branch was incorrectly
skipped and the sidecar-existence assertion flaked.
Force the new file's mtime to runStartTimeMs + 1 000 ms after the write so
the ordering is deterministic regardless of system-clock vs filesystem
precision differences. The old file already uses utimesSync for the same
reason — this just makes both ends of the comparison explicit.
* test(web): wait for AMR email after late login reconcile
---------
Co-authored-by: mrcfps <mrc@powerformer.com>
Routine failure banners surfaced daemon English strings directly.
Map the known empty-output message to i18n keys so zh locales render
localized copy while other locales inherit the English fallback.
Fixes#2902
* feat(web): enable mid-chat design system switching (#498)
Wires the previously disabled "Skills and design systems" item in the
ChatComposer Tools popover (`apps/web/src/components/ChatComposer.tsx`).
Clicking it now slides the popover over to a scoped picker
(`DesignSystemSwitchPicker`) that lists the workspace's design systems
grouped by category, plus a "None — freeform" entry, with search and an
"Active" marker on the project's current DS. Selecting an entry calls
the existing `PATCH /api/projects/:id` route, mirrors the new id back
through the ProjectView -> ChatPane -> ChatComposer chain so the next
chat turn composes with the new DESIGN.md, and emits a transient toast
via the existing `projectActionsToast` surface.
This is the v1 slice of #498. Skill switching, the cross-mode
prototype<->deck compatibility warning, and dedicated unit/E2E coverage
land in a follow-up (#498 v2).
i18n: reuses the `chat.importDesignSystem*` vocabulary that was already
present in all 19 locales + `i18n/types.ts`; no new keys.
Backend: zero daemon changes -- `PATCH /api/projects/:id` already
accepts `designSystemId` and the daemon re-reads the project's DS on
every chat run.
Co-authored-by: multica-agent <github@multica.ai>
* test(web,e2e): cover mid-chat design system switcher (#498)
Pins the v1 mid-chat DS switcher with the regression boundaries the
KAM-22 acceptance criteria flagged as follow-ups:
- apps/web/tests/components/ChatComposer.designSystem.test.tsx — four
scenarios. Asserts the disabled "Coming soon" affordance flips to a
picker after the Tools popover opens, the PATCH /api/projects/:id
payload is exactly `{ designSystemId }`, the success toast names the
new system, the failure path emits the localized error toast and
leaves the picker open for retry, the row stays disabled when no
project is active, and a redundant click on the current system is a
no-op (no PATCH, no toast).
- e2e/ui/chat-design-system-switch.test.ts — Playwright flow that
creates a prototype project, opens the import menu, searches the
picker, picks a different DS, and confirms PATCH landed via a
follow-up GET /api/projects/:id.
No source code changes: the picker, PATCH wiring, and toast emission
all sit on the earlier feature commit.
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): surface load failures in mid-chat design-system picker (#498)
Reviewer @nettee (Looper / opencode) flagged that
`DesignSystemSwitchPicker` could not distinguish "registry truly has no
systems" from "/api/design-systems failed to load":
`fetchDesignSystems()` collapses non-OK responses + network errors into
`[]`, the effect stored that array in `items`, and the render fell
through to the normal picker UI with only the "None — freeform" row
visible. That hid broken integrations and looked like a legitimately
empty catalog.
Switch the picker to a new discriminated `fetchDesignSystemsResult()`
helper that preserves the fetch outcome, track an explicit `loadError`
state, and render a dedicated `role="alert"` banner
(`composer-ds-picker-load-error`) with the localized
`chat.importDesignSystemLoadFailed` copy in place of the catalog rows
when the initial fetch fails. The "None — freeform" row stays clickable
in that state so users can still clear their DS while the catalog is
unreachable.
i18n: adds `chat.importDesignSystemLoadFailed` to `i18n/types.ts` and
all 18 locale files (EN/ZH/JA/KO/DE/FR/ES/PT-BR/RU/UK/PL/TR/AR/FA/HU/ID/TH
hand-translated; ZH-TW translated too).
Tests: adds a fifth case to `ChatComposer.designSystem.test.tsx` that
mocks `fetchDesignSystemsResult` to `{ ok: false }`, asserts the alert
banner renders with the localized copy, asserts the catalog rows are
suppressed, and asserts no PATCH / toast / parent-callback fires. The
existing four cases switch their mock target from `fetchDesignSystems`
to `fetchDesignSystemsResult` to match the picker's new dependency.
Re-validation on the new HEAD:
- `pnpm guard` — green
- `pnpm typecheck` (repo-wide) — green
- `pnpm --filter @open-design/web exec vitest run -c vitest.config.ts tests/components/ChatComposer.designSystem.test.tsx` — 5/5 green
- `git diff --check` — clean
The existing `fetchDesignSystems()` is left in place as a thin wrapper
over `fetchDesignSystemsResult()` so other callers
(`NewProjectPanel`, etc.) that intentionally want the silent-empty
fallback keep their current contract.
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): keep freeform fallback reachable in DS picker on load failure (#498)
Review follow-up (@nettee on DesignSystemSwitchPicker). The
`showNoneRow` guard hid the "None — freeform" action entirely once the
search box was non-empty. Combined with a `loadError` that suppresses
every catalog row, a user who typed anything was left with no clickable
fallback — breaking the load-failure contract that the project DS can
still be cleared when `/api/design-systems` is down.
The freeform row is an action, not a catalog entry, so it now:
- stays visible whenever the typed query matches its own label/summary
(filters independently of the catalog), and
- is always shown while `loadError` is set, so a broken registry can
never strand the user without a reachable fallback.
Adds a regression test that types a non-matching query under a failed
catalog load, then asserts the freeform row is still enabled and still
PATCHes `{ designSystemId: null }`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): forward full patched project from mid-chat DS switch (#498)
Addresses two follow-ups from @nettee's review of PR #1672.
1. `onActiveDesignSystemChange` only forwarded `designSystemId`, so
`ProjectView` rebuilt its mirror as `{ ...project, designSystemId }`
and dropped server-owned fields the daemon refreshes on every PATCH
(notably `updatedAt`) — leaving recent-project / last-updated UI
stale until some later refresh. The callback now carries the full
patched `Project` straight from the `patchProject()` response and the
parent replaces its mirror wholesale. Threaded through
ChatComposer → ChatPane → ProjectView.
2. The e2e spec claimed to verify the post-switch chat turn but stopped
after re-fetching `/api/projects/:id`, so it stayed green even if the
first run was composed from stale in-memory state. It now sends a
chat turn after the switch and asserts the captured `POST /api/runs`
body carries `designSystemId: "editorial-noir"`, matching the
regression boundary the PR body promises.
The `ChatComposer.designSystem.test.tsx` callback assertions move from
the bare id to `expect.objectContaining({ ..., updatedAt })` so a
regression back to id-only forwarding is caught.
Verified: pnpm guard, pnpm typecheck (repo-wide), web + e2e typecheck,
ChatComposer.designSystem.test.tsx 6/6 green, git diff --check clean.
Co-authored-by: multica-agent <github@multica.ai>
* test(e2e): align mid-chat DS switch spec with current shell (#498)
---------
Co-authored-by: multica-agent <github@multica.ai>
The 2026-05 plugins rebuild left three homepage/library surfaces still
pointing at the retired `/skills/`, `/systems/`, and `/craft/` route
trees, so visitors hit a 301 hop (or a stale facet) instead of landing
directly on the new `/plugins/*` pages. Repoint them at the canonical
destinations and align the homepage Labs pills with the real library.
- system-card: link straight to `/plugins/design-system-<slug>/` via a
new `detailHrefForSystemSlug` resolver instead of hard-coding
`/systems/<slug>/` and relying on the redirect. The ~8 systems that
ship no manifest (hence no detail page) degrade to `/plugins/systems/`,
the same destination the legacy 301 produced, minus the hop and with no
risk of linking at a page that doesn't exist.
- homepage Labs pills: replace the hard-coded prototype/deck/mobile/office
facets (mobile/office had drifted to stale or empty counts) with a live
top-4 of `PLUGIN_CATEGORIES`, counted with the same `categorizePlugin`
rule `/plugins/templates/` uses and labelled from `pcopy.category`, so
the homepage stays in lockstep with the library and never shows a dead
chip. Counts are surfaced through a new `CatalogCounts.templateCategories`.
- remove the Craft entry points from the homepage footer, sub-page footer,
header Library dropdown, and the plugins hub tile grid. The `/craft/`
pages stay live; they're just no longer surfaced in site chrome.
The legacy `/skills/`, `/systems/`, `/templates/` 301s added in the prior
PR stay in place for inbound links and search equity.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
When the design files preview pane was closed, a very long filename
would expand its `<td.df-cell-name>` and push the kind / mtime / menu
columns off-screen — the auto-layout `<table>` had no width
constraint on the cell, so the existing `text-overflow: ellipsis`
on `.df-row-name` never engaged.
The `:not(.no-preview)` overrides in `routines.css` already pinned
the row's children to `width: 100%; max-width: 100%` when a preview
pane was open, but the no-preview state — the one shown in the issue
screenshot — had no equivalent guard.
CSS:
- `.df-cell-name`: add `max-width: 0; min-width: 0` so the table
cell collapses to its column allocation in auto-layout.
- `.df-row-name-wrap`: add `max-width: 100%`.
- `.df-row-name`: add `max-width: 100%; min-width: 0` so the flex
child clamps to the wrap and the existing ellipsis engages.
JSX (DesignFilesPanel.tsx):
- Add `title={f.name}` (and the equivalent for directory rows and
live-artifact rows) on `.df-row-name`. The browser surfaces the
full filename on hover even when the visible text is truncated, so
users can read the leading characters without opening the preview
pane. `<DfPreview>` already renders the full name with
`word-break: break-word`.
Closes#3260
Validation:
- pnpm exec vitest run tests/components/DesignFilesPanel.long-name-truncate.test.tsx
→ 3/3 passed (1 was red on main: title attr was absent)
- pnpm --filter @open-design/web test → 2501/2501 passed (260 files)
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
Note: jsdom does not measure layout, so the truncation itself can't
be asserted directly. The specs encode the structural contract the
CSS depends on (cell / wrap / name nesting + the `title` attr) so a
JSX shape change won't silently regress the fix.
* feat(daemon): implement fal.ai renderer for image + video generation
Adds renderFalImage and renderFalVideo backed by the fal queue API
(queue.fal.run). Any fal-ai/* model path can be used directly without
a catalog entry, enabling the full fal model library without code
changes. Catalogued shortcuts are mapped via FAL_ENDPOINTS to their
fal-ai/* paths; OD_FAL_MAX_POLL_MS controls the poll ceiling.
Expands the fal model catalog with flux-pro-ultra, flux-dev-fal,
flux-schnell-fal, ideogram-v3-fal, recraft-v3-fal (images) and
veo-3-fal, veo-2-fal, wan-2.1-t2v, wan-2.1-i2v, seedance-1-pro-fal,
kling-2.1-t2v-fal (video). Marks fal provider as integrated: true in
both daemon and web model registries.
* fix(daemon): address fal renderer review comments
- Correct Wan 2.1 endpoints: wan-video/v2.1/* → fal-ai/wan-t2v / fal-ai/wan-i2v
- Correct Kling 2.1 t2v endpoint: .../pro/... → .../master/text-to-video
- Add FAL_IMAGE_USES_ASPECT_RATIO: flux-pro-ultra sends aspect_ratio not image_size
- Add FAL_VIDEO_NO_DURATION: Wan models reject the duration field
- Add FAL_VIDEO_STRING_DURATION: Veo expects duration as "5s" not 5
- Fix falQueueBase() to use anchored regex replace, avoiding mangled
custom base URLs
- Do not wrap payload under input — raw fal queue HTTP API expects flat
body; the input wrapper is an SDK abstraction only (confirmed by 422
validation error from fal showing prompt missing at body.prompt)
* fix(daemon): correct fal queue protocol comment (flat body, no SDK input wrapper)
* fix(daemon): clamp Veo duration to valid fal buckets (4s/6s/8s)
* fix(daemon): report effective fal Veo duration in providerNote (with snap warning)
* fix(daemon): reduce image generation latency from 4m37s to ~73s
Five layered fixes targeting the overhead that padded a ~10s fal API call
into a 4m37s user-facing wait:
1. Skip DISCOVERY_AND_PHILOSOPHY for media surfaces (image/video/audio).
The ~3000-token HTML-artifact discovery layer is irrelevant for media
generation and forced the agent to parse and override all its rules
before dispatching. Removes it from the system prompt entirely for
these surfaces; MEDIA_GENERATION_CONTRACT is the sole authority.
2. Broaden the wait-loop contract to cover ALL slow models, not just
"Volcengine i2v / hyperframes-html". Any model whose generation
exceeds 25s — including fal flux-pro-ultra, Veo, Sora — returns exit 2
from od media generate. The contract now makes this universal and
provides a python3-based bash pattern (jq is not guaranteed to be
installed on all agent runtimes).
3. Increase od media wait polling budget from 25s to 120s. od media
generate keeps its 25s budget for fast feedback; od media wait is
purpose-built to sit and poll, so it can safely use the full 2-minute
bash-tool window. Reduces re-entries for a 3-minute generation from
~7 to ~2.
4. First fal poll is now immediate instead of always sleeping 3s before
the first status check. Saves 3s for all fal jobs.
5. Project metadata no longer emits "(unknown — ask)" for imageModel and
aspectRatio when unset. Emits the actual defaults (gpt-image-2,
aspect-ratio scene heuristic) so the agent can dispatch without
extended reasoning about model selection. Also adds dispatch-immediately
defaults and a brief-reply rule (2–3 sentences max after generation).
Measured end-to-end on the exact problem prompt before/after:
Before: 4m37s (discovery form + 7x LLM re-entries + jq failure)
After: ~73s (single bash loop, no question turn, image delivered)
* feat(daemon): inject media dispatch hint for non-media project surfaces
Agents running inside prototype, deck, and other non-image/video/audio
projects previously had no knowledge of `od media generate`, so when
asked to create an image with fal they would try to call provider REST
APIs directly and ask the user for API keys — even though the daemon
already holds credentials in .od/media-config.json.
Add MEDIA_DISPATCH_HINT to composeSystemPrompt for all non-media
surfaces. The hint tells the agent to always route media generation
through the daemon dispatcher, and explicitly forbids prompting for
API keys.
Verified end-to-end: a prototype project generates a 952 KB image via
flux-pro-ultra in ~52s with no key errors.
* fix(daemon): prevent agent from converting bash env vars to PowerShell syntax
MEDIA_DISPATCH_HINT now explicitly labels the shell as POSIX bash and
shows the correct $VAR form side-by-side with a warning NOT to use
PowerShell $env:VAR. Without this, claude-sonnet running on a Windows
host converts the example to PowerShell syntax (`& $env:OD_NODE_BIN`)
which then fails at the bash executor with 'syntax error near unexpected
token &'.
* fix(daemon): add generate→wait loop to MEDIA_DISPATCH_HINT for slow models
MEDIA_DISPATCH_HINT previously showed only a bare call.
flux-pro-ultra and other slow models always exit 2 after ~25s — without
the wait loop the agent would treat exit 2 as a failure and report an
error to the user.
Replace the single-command example with the canonical generate→wait loop
(matching media-contract.ts), add an explicit note that exit 2 means
'keep polling', and reinforce the POSIX bash / no-PowerShell rule
directly inside the code block.
* fix(daemon): allow fal-ai/* passthrough in media-agent contract
The media-agent prompt instructed the agent to warn and substitute the
default model for any ID not in the catalogue. This blocked the custom
fal-ai/* passthrough path the daemon already supports, so users could
not reach uncatalogued fal models from the normal chat flow.
Carve out the fal-ai/* exception so the agent passes those IDs through
directly instead of warning or substituting.
* fix(daemon): align MEDIA_DISPATCH_HINT with exit-0 generate contract
media generate now always exits 0 (handoff included). The non-media
agent hint still checked ec==2 to decide whether to keep polling, so
slow fal models (flux-pro-ultra, veo-3-fal) would stop after printing
the handoff JSON instead of entering the wait loop.
- generate error check: drop the ec!=2 exception (exits 0 always)
- while loop: drive on taskId presence, not ec==2; stop on ec==0/5
- footer: remove --surface inference claim; CLI requires it explicitly
* fix(guard): add test-fal-webui.ts to e2e scripts allowlist
CI failed: guard flagged e2e/scripts/test-fal-webui.ts as an
unapproved package-owned entrypoint. Add it to allowedE2eScripts.
* fix(daemon): update prompt test expectations to match exit-0 handoff wording
The two stale assertions checked for the old generate-exits-2 copy which
no longer exists in the contract. Update them to match the current
always-exits-0 wording.
* fix(daemon): move skipDiscoveryBrief override before discovery block
* chore(e2e): remove ad-hoc fal webui test script
The script was a one-time developer helper used to manually validate fal
image generation through the live UI. It relied on a real fal API key and
hardcoded local port, so it cannot participate in the e2e package's
fixture/reporting/CI conventions. Removing it per reviewer feedback.
- Delete e2e/scripts/test-fal-webui.ts
- Remove its guard.ts allowlist entry
- Gitignore the file and its screenshots to prevent accidental re-addition
* chore: remove accidental local scratch files from branch
Remove bash.exe.stackdump (MSYS crash dump) and fix_loop.py (one-off
local rewrite helper) — neither is a repo-owned source artifact.
* fix(prompts): document fal-ai/* passthrough in non-media dispatch hint
Prototype/deck agents now know arbitrary fal-ai/* model ids are valid
--model values and should be forwarded as-is, mirroring the exception
already present in media-contract.ts. Adds a prompt regression test.
* fix(daemon): use renderMediaGenerationContract(mediaExecution) for media surfaces
---------
Co-authored-by: mrcfps <mrc@powerformer.com>
* fix: link od bin after fresh install
* test: lock root od bin shim path
* test: cover root workspace deps in postinstall scan
* chore(nix): refresh pnpm deps hash
* fix: dock comment board without clipping inspect
When the comment-side dock falls back to the stacked layout in narrow
panes, collapsing the side panel now shrinks the bottom strip to a
horizontal rail height instead of keeping the full panel-height row.
commentPreviewCanvasSize() also stops over-deducting the expanded
panel height in the stacked-collapsed path so the canvas sizing stays
in sync with what is rendered.
* fix(web): address docked comment panel review follow-ups
* Fix non-docked comment tool tablet scaling
* test(web): align comment panel tests with collapse API
The right-side @-button tools popover (ToolsPluginsPanel,
ToolsSkillsPanel, ToolsMcpPanel) inserts text into the composer
draft using the textarea's selectionStart at click time, but the
picker rows had `onClick` without `onMouseDown={(e) =>
e.preventDefault()}`. On a real mouse, mousedown fires first, the
textarea loses focus before the click handler runs, and
selectionStart resets — so the inserted token lands at offset 0
instead of at the user's cursor.
The @-mention popover already prevents this by calling
preventDefault on mousedown for every picker row (the comment at
ChatComposer.tsx:3039-3043 explains the reason). This change
mirrors that protection on the three tools-menu pickers.
The mention popover itself was unaffected, so design-file
mentions (which only flow through the @-popover via
`replaceMentionWithText`) are not impacted by this issue. The
reporter's mention of "design files" appears to refer to picking a
file via the @-popover, where the protection was already in place.
Closes#3195
Validation:
- pnpm exec vitest run tests/components/ChatComposer.tools-menu-caret.test.tsx
→ 3/3 passed (red on main, asserts each picker calls
preventDefault on mousedown)
- pnpm --filter @open-design/web test → 2501/2501 passed (260 files)
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
The 2026-05 plugins library rebuild introduced /plugins/skills/,
/plugins/systems/, /plugins/templates/ and a unified detail route
/plugins/<manifest-slug>/, but the old /skills/, /systems/, /templates/
catalogs were left live in parallel. Two equivalent page trees split SEO
equity, and the homepage, footer, quickstart, agents, official and blog
pages all still linked to the old routes.
Retire the legacy generators and 301 every old URL to its new plugins
equivalent so inbound links and search equity are preserved:
- Remove the /skills, /systems, /templates page generators (English +
[locale] wrappers) and the now-orphaned skill-row component, and prune
the skills/systems/templates branches from the [locale]/[...path]
catch-all (it now renders only craft + blog).
- Add the migration block to public/_redirects. Detail slugs differ from
the old folder names (new slugs are manifest-name based, e.g.
design-system-<x>, example-<x>), so systems/templates use a prefixed
splat plus a short degrade list, and skills map the 27 with a template
equivalent explicitly while the ~110 instruction-only skills and all
mode/scenario/category facet pages degrade to the section landing.
'replicate' is forced to the section to avoid colliding with the
design-system of the same name. Locale variants (zh, zh-tw, ja, ko)
strip to the section.
- Repoint in-site links to /plugins/* across page.tsx (footer, work,
labs pills), info-page-i18n.ts (en + zh + sourceNames), official,
quickstart, agents, blog and html-anything, and update the sitemap
serialize priority list. The system-card keeps linking through
/systems/<slug>/ so the 8 systems without a detail page ride the
redirect's degrade rather than pointing at a missing page.
Verified with a full astro build: old routes no longer emit any HTML,
the new section pages exist, _redirects is copied verbatim, and no
in-site link targets a removed route (the remaining /systems/<slug>/
hrefs are the system cards that 301 by design). astro check passes.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
ChatComposer tracks the `@…` tokens this surface authored via the
@-mention popover plugin-pick path. When PluginsSection's chip strip
clears, we wire its `onCleared` and prune *only* those tracked
insertions from the draft so the textarea no longer holds orphaned
styled mentions whose chips just unmounted.
Architecture summary (rounds 1–9 collapsed; round 10 detailed below):
- `Array<{token, start, pluginId, insertionId?}>` tracking with
start offsets reconciled across each keystroke via an LCS+LCP
edit-range diff in
`apps/web/src/utils/pluginInsertionTracking.ts` (round 3-4).
`insertionId` is forwarded by `reconcileInsertions` so the
producer can locate its own entry across reconciles
(round 10).
- All draft mutations route through a single `updateDraft`
chokepoint that runs `reconcileInsertions` outside the
`setDraft` updater so React StrictMode's double-invoke is
harmless (round 4-5).
- Boundaries delegate to the shared
`inlineMentions.isMentionBoundary` /
`inlineMentions.isMentionRightBoundary` helpers so the
tracker can never diverge from the parser (round 5).
- `setActivePlugin` is a chokepoint for every applyById path,
filtering tracked entries to those matching the new active
plugin so a replace-plugin flow can never let stale entries
survive (round 6).
- Picker rollback double-snapshots draft + tracker so apply-
failure restores the tracker but only rewrites the draft
when no user keystrokes arrived during the await
(round 7-8).
- `stripPluginInsertedTokens` collapses whitespace seam-local
so user-authored multi-space spans elsewhere are preserved
(round 8).
- `setActivePlugin` is deferred past `await applyById` on
every path, and `onCleared` filters by
`pluginsSectionRef.current?.getActiveRecord()?.id` so a
pending-window clear scopes to the actually-mounted
plugin's tokens (round 9).
race in the picker rollback:
Round 9 made `onCleared` mutate the tracker and the draft when
it ran during a pending replace, and added the `getActiveRecord`
filter so the strip targets the still-mounted plugin's entries
only. The picker's failure-path rollback, however, still
restored `prevEntries` / `prevActiveId` wholesale — assuming
nothing else had touched the tracker during the await. If the
user clicked the still-mounted original chip's × during the
pending replace AND the deferred `applyById` then resolved
with a 500, the wholesale restore (a) resurrected entries that
`onCleared` had legitimately stripped (now stale offsets) and
(b) left the optimistic `@<target>` orphaned in the draft with
no chip ever having mounted — the original #2881 symptom
recurring inside the failure window.
Fix splits the failure rollback into two paths:
1. **Detect "intervening clear" via `activePluginIdRef.current
=== null && prevActiveId !== null`.** `onCleared` always
nulls the active id as its last action; our deferred
`setActivePlugin` never ran in the failure branch. So the
null-while-prev-not-null state is the smoking gun for an
intervening clear during the await.
2. **On detection, surgically remove only our optimistic
entry and only its `@<target>`.** Locate the entry by
`insertionId` (added to `TrackedInsertion` as an optional
field, forwarded by `reconcileInsertions` so the id
survives offset shifts) — this disambiguates the case
where the user picked the same plugin from the @-popover
more than once during the await window. Splice that entry
out and run `updateDraft((d) => stripPluginInsertedTokens(
d, [ourEntry]))` so the draft loses `@<target>` and any
remaining tracked entries (the in-flight target would have
no others, but a co-pending second pick could) get their
offsets reconciled. `activePluginIdRef` stays at `null` —
`onCleared`'s truth, since no chip is mounted.
The "no intervening clear" branch is the round 7/8 path:
restore `prevEntries`/`prevActiveId` wholesale and rewrite
the draft only if `draftRef.current === postInsertDraft`
(no user keystrokes during the await).
Regression coverage (additions):
- `apps/web/tests/components/ChatComposer.plugin-clear-prunes-draft.test.tsx`
— 18 integration specs total (17 prior + 1 new round-10):
* `@-popover pick A → @-popover pick B (apply pending) →
clear A's chip → resolve B with 500 → assert no orphan
@<target>, no orphan @A, no chip mounted, no stale
tracker entries`. Uses a deferred `Promise<Response>` so
the apply stays in flight while the chip-clear is fired,
then resolves with a 500 to drive the failure path. Pre-
fix this would resurrect Airbnb's stale entry AND leave
`@SecondPlugin` orphaned in the draft.
PluginsSection.tsx is unchanged. The host-local tracking +
draft-update chokepoint + parser-aligned boundaries + deferred
active-plugin scoping + transactional applyById + intervening-
clear-aware rollback + filtered `onCleared` keep the cross-
component contract identical to main — only ChatComposer touches
behavior, plus the utils module and two `inlineMentions` exports.
Validation:
- pnpm exec vitest run tests/utils/pluginInsertionTracking.test.ts → 36/36 passed
- pnpm exec vitest run tests/components/ChatComposer.plugin-clear-prunes-draft.test.tsx → 18/18 passed
- pnpm exec vitest run -c vitest.config.ts (full apps/web suite, 228 files) → 2202/2202 passed
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
* feat(web): wire generation preview stage into workspace
Show a 3-step progress overlay (understand → generate → prepare) in the
preview area while artifacts are being generated, replacing the blank
empty state. Displays elapsed time, an estimated duration hint, and a
retry button on failure.
- Add GenerationPreviewStage component + CSS module + runtime helpers
- Integrate buildGenerationPreviewState into FileWorkspace
- Pass messages/artifact/error/retry from ProjectView to FileWorkspace
- Register i18n keys for en and zh-CN locales
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): keep generation preview alive and persistent across waiting states
Address UX feedback on the generation preview surface:
- Make the waiting card feel alive instead of frozen: breathing mark,
sweeping progress shimmer, pulsing running-step dot, and a live
activity snippet pulled from streamed events (respects
prefers-reduced-motion).
- Add an `awaiting-input` phase so the preview no longer reverts to the
empty "design will appear here" placeholder when the agent asks the
user a clarifying question (detects inline <question-form>).
- Add a `stopped` phase so a canceled/paused run keeps a contextual
paused card instead of blanking the surface.
- Fix workspaceHasPreviewSurface live-artifact tab match (was reading a
non-existent `tabId` field) and correct the unit assertion that
contradicted the helper's `thinking` handling.
- Populate generationPreview.* keys (incl. new awaiting/stopped strings)
across all locales.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): reveal generation steps progressively as the agent reaches them
- Only render steps the agent has actually reached (drop pending pills)
with a slide/fade entrance, so the card visibly evolves 1->2->3 instead
of always showing the same fully-populated row.
- Keep the "understand" step in progress during requesting/starting so a
fresh run opens with a single step rather than a pre-filled set.
- Stop surfacing status detail (e.g. the model slug from `requesting`) as
the live activity line; only genuine thinking/output text is shown.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): add dynamic sub-status to the generating step
Keep 3 high-level steps but give the long "generating" phase concrete,
moving feedback (option A) instead of splitting into more, less-reliable
steps:
- Derive a sub-status from the agent's TodoWrite plan: the in-progress
task label (activeForm) plus a done/total count, falling back to the
latest write/edit target file when no plan was emitted.
- The count counts the in-progress task toward `done` to match the
chat-side todo card (e.g. 3/7 on both sides).
- Suppress the higher-level narration line while the sub-status is shown
so only one dynamic line appears at a time (early phase = narration,
writing phase = concrete task + count).
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(web): drop elapsed timer and duplicate estimate from generation preview
The "usually 2–5 minutes" estimate showed twice (lead footnote + meta row)
and the elapsed counter added little signal, so remove both: delete the
meta row, stop falling back to the estimate footnote in the generating
lead (render the lead only when live narration exists), and drop the now
unused elapsed timer/util.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): make hand-off no-editors fallback perform a real reveal
The Finder/Explorer/File Manager fallback button was only calling an
optional onRequestRevealInFinder prop that the actual caller never
passes, so the surface advertised an action it never performed.
finder, explorer, and file-manager are real entries in the daemon's
open-in catalogue (open / explorer / xdg-open), so route the fallback
through openProjectInEditor(projectId, fallbackId) for a genuine
reveal. Keep the renderer reveal bridge as a secondary fallback if
the daemon spawn fails, and disable the button while busy so a double
click can't queue two reveals.
Adjacent: PreviewDrawOverlay's Send-while-streaming behavior is
intentional (sending is queued downstream, not blocked), and the
button already carries sendDisabledReason as its tooltip. Cover that
contract with a regression test so a future change can't silently
re-disable the control or drop the localized reason.
Scope note: the i18n hand-off key migration that previously rode on
this branch landed on main via a different key set, so this PR is
narrowed to just the fallback wire-up and the two regression tests.
* fix(web): surface daemon spawn failure inline in zero-editors fallback
The zero-editors HandoffButton fallback called setError() on a rejected
openProjectInEditor but returned only the <button>, so the error never
rendered. Production callers (ProjectView) mount the component without
onRequestRevealInFinder, so a daemon spawn failure became a silent no-op
— exactly the failure mode the PR was meant to cover.
Wrap the solo button in a handoff-wrap container and render the error
inline next to it. Adds a regression test for the rejected-spawn path.
* fix(web): align preview draw send-disabled test
* fix(web): show handoff fallback for zero editors
---------
Co-authored-by: nicejames <nicejames@gmail.com>
Co-authored-by: mrcfps <mrc@powerformer.com>
`opencode run`'s `-f, --file` is a yargs array option that greedily
consumes every trailing non-flag token, so the memory extractor's
`--file <prompt-file> "<message>"` invocation made OpenCode treat the
message text as a second attachment and exit 1 with "File not found".
Every LLM memory extraction failed for OpenCode Local CLI users.
Deliver the prompt on stdin like the chat-run path (def.promptViaStdin)
and drop the --file attachment. The connector-memory test now models
the real yargs --file array-greediness so it would catch a regression.
Long project names in the "Existing projects" section of the
automation project picker rendered verbatim with no truncate styling,
so a single name like "A very long project name that would otherwise
wrap onto several lines" blew up the row height and made the dropdown
messy to scan. The expected behavior is a single-line label with
ellipsis, with the full name still discoverable on hover.
Add the standard truncate triad (`white-space: nowrap`,
`overflow: hidden`, `text-overflow: ellipsis`) to
`.automation-popover__label`. The parent
`.automation-popover__body` already sets `min-width: 0`, so the
ellipsis renders cleanly. Thread an optional `title` prop through
`PopoverItem` and pass each project's full name from the picker
call site, so the native hover tooltip carries the unclipped name.
Other PopoverItems with fixed in-product copy (e.g. "New project
each run") deliberately omit the title — they never exceed the row
width and the redundant tooltip would be noise.
Regression test covers the DOM contract (every project row has
`title=<full name>`, fixed rows do not); the CSS half is verified by
code review since jsdom does not apply stylesheets.
- apps/web/src/components/ChatComposer.tsx
- apps/web/tests/components/ChatComposer.context-pickers.test.tsx
Clear stale absolute anchors when the pet composer menu is positioned fixed so the popover wraps its content instead of collapsing over the composer textarea.
* fix(daemon): detect and strip fabricated role markers in model output (#3247)
Three-layer defence against models emitting `## user` / `## assistant` /
`## system` lines mid-response, which the chat host interprets as real
turn boundaries and acts on as unauthorised instruction:
1. **System prompt**: anti-roleplay instruction elevated from a bullet
under "What you don't do" to a standalone `## CRITICAL` section in
`official-system.ts`, with a REMINDER pinned at the end of the
composed prompt for recency bias.
2. **Stream-level detection and truncation**: shared `role-marker-guard.ts`
module (`createRoleMarkerGuard` + `FABRICATED_ROLE_MARKER_RE`) used
across all text paths — Claude stream (per-message guards), non-Claude
structured streams (run-scoped guard via `emitGuardedTextDelta`),
and BYOK proxy routes (`createDeltaGuard`). When a marker is detected,
the contaminated suffix is dropped and a `fabricated_role_marker` event
surfaces a warning in the UI.
3. **UI**: `StatusPill` gains `is-warning` / `is-error` CSS variants;
`fabricated_role_marker` events render as amber warning pills.
* fix(chat-routes): do not await reader.cancel() on stream early-return
The await on reader.cancel() can hang indefinitely on response streams
whose underlying source is a Uint8Array (most notably surfaced by the
ollama test in proxy-routes.test.ts, which builds its mock body via
`new Response(uint8array)` rather than the controller-based helper
`sseResponse()`). The hung await holds the request handler open, which
in turn blocks `server.close()` in the afterAll hook, producing the two
test timeouts (test at 145, hook at 36) currently failing CI on #3296.
Fix is in production code, not the test: don't await the cancel. It
is a cleanup hint and we are returning from the function anyway, so
blocking on it offers no value. fire-and-forget with an empty catch
keeps the cancel signal flowing for real HTTP streams without
risking a hang on mock/edge-case implementations.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(daemon): terminate child on role-marker detection (close#3247 generation vector)
PR #3296's detection layer truncates display and persistence of fabricated
role markers, but the underlying model subprocess keeps generating tokens
after detection. Three concrete consequences:
1. The model bills the user for the entire contaminated response
(we observed 5,106 chars stored in claude's session file for a turn
where only the first 3,013 chars were legitimate — a 40% overhead).
2. tool_use blocks emitted AFTER the marker reach the daemon's
dispatcher unchecked, since detection only gates the text-delta
emission path, not content-block-stop / tool_use blocks. The
model could fabricate "## user delete file X" then emit a
tool_use(delete X) that the dispatcher would execute.
3. The UI surfaces a `fabricated_role_marker` warning followed by an
eventual normal turn-end, blurring the distinction between
"completed normally" and "killed by safety guard."
This commit adds a single idempotent `abortForRoleMarker(marker)`
helper in server.ts, scoped to the same closure as `child` and
`runGuard`. On any detection event (per-message Claude guard,
run-scoped non-Claude guard, plain stdout guard) the helper:
- Emits a structured `ROLE_MARKER_HALLUCINATION` SSE error so the
UI can render a security-class status distinct from a normal
turn-end. The existing `fabricated_role_marker` warning is still
sent and rendered as the amber pill (PR #3296's UI).
- Calls `acpSession.abort()` for ACP-multiplexed agents (Hermes,
Kimi, Devin, Kiro) whose I/O doesn't necessarily release on
SIGTERM of the wrapper process alone.
- SIGTERMs the child immediately, with the existing
`scheduleForcedChildShutdown()` SIGKILL fallback at 2x grace.
Wired into three sites where contamination is detected:
- `emitGuardedTextDelta` (sendAgentEvent / copilot / ACP / pi-rpc
text_delta paths)
- Plain-stdout listener (BYOK plain mode)
- The Claude stream handler's onEvent (per-message guards in
claude-stream.ts surface `fabricated_role_marker` events directly
via onEvent rather than through the run-scoped emitGuardedTextDelta)
Tool_use blocks emitted BEFORE the marker still flow through normally
— this guard can't help with those, since by the time we observe a
text marker the prior content block has already finished. Closing
that gap requires speculative cancellation of in-flight tool calls
when a downstream text block contains a marker; that's tracked as
follow-up work, not included here.
Co-Authored-By: roverkai <2196140098@qq.com>
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* refactor(role-marker-guard): bounded tail + drop chat-style markers
Addresses two review comments on #3303:
(1) O(1) memory + per-delta work (review r3323982225)
Replace the unbounded `accumulated` string with a rolling tail capped
at TAIL_BUFFER_SIZE (64 chars — comfortably exceeds the longest
marker prefix `\n<whitespace>## assistant` ≈ 16–24 chars in practice).
A 50 KB assistant response delivered in 1000 chunks of 50 bytes was
previously O(n²) on string concatenation alone; now it is O(1) per
delta regardless of message length. The `tail.length` value carries
the "already emitted" offset that the cut-point math needs, so the
offset semantics at L74–78 of the prior implementation are preserved
without re-introducing the full-text buffer.
(2) Drop chat-style markers entirely (review r3323982234, option (a))
`User:` / `Assistant:` / `Human:` / `AI:` are removed from the regex.
Rationale:
- The host parses ONLY `## user` / `## assistant` / `## system`
lines as turn boundaries (see `buildDaemonTranscript` in
apps/web/src/providers/daemon.ts). A model emitting chat-style
markers does NOT cause the original #3247 security failure.
- With kill-on-detection wired in this PR (`abortForRoleMarker`
in server.ts), a false positive aborts the whole run — far
more expensive than a stray unflagged `User:` line in chat
scrollback. Chat-style markers collide with legitimate output
(form labels, email contacts, JSDoc) often enough that pairing
them with kill-semantics is the wrong tradeoff.
The tradeoff is now documented in the regex docblock so the
kill-on-match behaviour is justified against the false-positive
surface.
Also aligns the prompt-side CRITICAL block in system.ts: drop the
"don't emit User: / Assistant: / Human: / AI:" bullet, since we no
longer enforce it. Less ambiguity for the model and the operators.
Test file updated:
- Chat-style positive tests flipped to negative ("does NOT match
User: — chat-style out of scope") so the intentional exclusion
has a permanent regression test.
- Two new tests cover the bounded-tail behaviour: a marker arriving
after 10 KB of clean text in small chunks, and a marker
straddling a chunk boundary after 100 prior chunks.
- Added test for legitimate `User: bob@example.com`-style content
not triggering contamination.
Test count is now 35 (up from 25); two of the new ones explicitly
exercise the new bounded-tail path.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): drop \`^\` anchor after first chunk (review r3324060995)
Blocking correctness bug introduced by commit 4 (bounded-tail refactor):
once \`tail\` is a rolling slice of mid-stream text, \`^\` in the
canonical regex \`(?:^|\\n)\\s*##\\s+(?:user|...)\` no longer represents
the genuine message start. As the rolling window slides forward chunk
by chunk, a sliced tail can begin with whitespace + \`##\` (or just
\`##\`), letting \`^\` anchor a match against text that the
full-buffer implementation correctly ignored. With kill-on-detection
wired in commit 3, that false positive now SIGTERMs the run and emits
a \`ROLE_MARKER_HALLUCINATION\` error — exactly the failure class
called out in the docblock at L22–29.
Reviewer's evidence (PerishCode, r3324060995): streaming
"…take a look at the ## user content section…" one character at a
time reports \`contaminated: true\` post-refactor; the same text in a
single feed stays clean.
Fix: keep the canonical \`FABRICATED_ROLE_MARKER_RE\` for the very
first non-empty feed (where \`^\` legitimately points at the message
start), and switch to an internal \`NEWLINE_ANCHORED_ROLE_MARKER_RE\`
(\`\\n\\s*##\\s+(?:user|...)\` — drops the \`^\` alternative) for all
subsequent feeds. A \`firstChunk\` boolean tracks the state. Real
newline-preceded markers straddling chunk boundaries are still caught
because the preceding \`\\n\` is retained inside the 64-char tail.
Regression tests added (\`apps/daemon/tests/role-marker-guard.test.ts\`):
- mid-line \`## user\` streamed char-by-char with no preceding \\n
(mirrors the reviewer's repro)
- space-preceded mid-line \`## user\` in a >130-char stream, which
long enough to force the rolling window past the marker — exercises
the exact slice condition that triggered the bug
- real \\n-preceded \`## user\` still caught after a long preamble
(positive case must not regress)
- \`## user\` as the very first chunk still caught (\`^\` legitimately
anchors on the first feed)
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): case-sensitive + tighter prefix scope (reviews r3324151877 / r3324151882)
Two refinements addressing the third review on #3303:
== Blocking (r3324151877) ==
The regex over-matched legitimate Markdown headings, and with
kill-on-detection wired in commit 3 each false positive
deterministically aborts a real run. Three changes tighten the match
to the actual security surface — `## user` / `## assistant` /
`## system` lines the chat host parses as turn boundaries — without
losing any real attack pattern:
1. CASE-SENSITIVE. Dropped the `/i` flag. The host's turn-boundary
delimiter is lowercase (see `buildDaemonTranscript` in
apps/web/src/providers/daemon.ts), and the `## CRITICAL`
system-prompt block already forbids only the lowercase forms.
Title-Case headings like `## User Guide`, `## System Architecture`,
`## Assistant settings` are now ignored — these are legitimate
technical writing patterns LLMs emit constantly. `## USER NOTES`
(all-caps) likewise no longer flags.
2. POSITIVE LOOKAHEAD `(?=[^a-z])` after the role keyword. Without it,
`## userland`, `## userspace`, `## users guide`, `## systemd`,
`## assistance` all match via prefix in the alternation. The
lookahead requires the next character to exist and to not be a
lowercase letter, so:
- `## user\\n…` → match (newline is not lowercase)
- `## assistantR…` → match (R is uppercase; the glued-form
attack pattern still gets caught)
- `## assistant.` → match (. is not a letter)
- `## users guide` → no match (s is lowercase letter)
- `## userland` → no match (l is lowercase letter)
POSITIVE rather than NEGATIVE `(?![a-z])` because the negative
form is satisfied at end-of-string, which in a streaming context
means "we have `## user` but don't know what comes next yet" —
would fire prematurely if `land` arrives in a later chunk. The
positive form delays detection by one character in that edge
case, traded for correctness.
3. `[ \\t]` instead of `\\s` for inner whitespace. Markdown role
markers are single-line by convention; restricting to space/tab
prevents oddities like `##\\nuser` from matching across lines.
Test file: added Title-Case fixtures (`## User Guide`,
`## System Architecture`, `## Assistant settings`, `## USER NOTES`)
and prefix-of-longer-word fixtures (`## users guide`, `## userland`,
`## systemd`, `## assistance`) — each asserting NO contamination.
The existing `## usability` negative test gave false confidence as
the reviewer noted (only failed via alternation-miss, not via
word-boundary semantics); the new fixtures actually exercise the
lookahead. Also added a positive test for `## assistant.` (glued
punctuation) to balance the existing `## assistantReading`
(glued uppercase) coverage. Total tests: 35 → 50.
== Non-blocking (r3324151882) ==
Added `ROLE_MARKER_HALLUCINATION` to `API_ERROR_CODES` in
`packages/contracts/src/errors.ts` alongside the existing agent/AMR
codes, with a docblock comment explaining the emission contract:
emitted by `server.ts::abortForRoleMarker` alongside the existing
`fabricated_role_marker` warning event when the daemon detects a
fabricated Markdown role marker in agent output; retryable. The code
was already being emitted over the wire but unregistered — landing
the registration here keeps the contract and emitter in sync as
reviewer requested.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): defer complete-but-unconfirmed marker suffix
Addresses review r3324277xxx — the boundary case where a stream chunk
boundary lands between the role keyword and its lookahead character
violated the documented "everything from the marker onward is silently
dropped" contract. With (?=[^a-z]) as the lookahead, `feedText('## user')`
returned `## user` as safe (no char to satisfy the lookahead → no match
→ pass through), so the fabricated marker line leaked into UI and
app.sqlite before the next chunk confirmed contamination on the next
SIGTERM cycle.
Fix: introduce a `pending` state variable holding bytes that match the
COMPLETE-but-unconfirmed marker prefix at end of buffer
(/(?:^|\\n)[ \\t]*##[ \\t]+(?:user|assistant|assist|system)$/, no
lookahead, $ anchor instead). When the no-match branch detects this
suffix, withhold it from emission until the next feed either:
- Confirms it (next char non-lowercase) → main regex matches →
contaminated → withheld bytes dropped along with `## user`.
- Denies it (next char lowercase, e.g. `userl…`) → main regex no
longer matches the role keyword → withheld suffix is released
and emitted alongside the new continuation.
Also tied the firstChunk transition to actual byte emission rather
than feed count. Previously a message that starts with `## system`
followed by a separate `\\n` chunk would lose the `^` anchor on the
second feed (firstChunk had flipped after the first feed even though
nothing was emitted yet), silently breaking detection for that edge
case. Now `firstChunk` stays true until at least one byte has crossed
the emission boundary, matching the conceptual definition of "message
start".
Tests added (apps/daemon/tests/role-marker-guard.test.ts):
- `## user` deferred at chunk boundary, confirmed by `\\n` in next
- `## user` deferred at chunk boundary, denied by `land` continuation
- `## assistant` deferred, confirmed by punctuation
- `## User` Title-Case still passes through unconditionally
- `## system` as the very first chunk: deferred, confirmed by \\n
in next chunk (tests the firstChunk-stays-true-when-nothing-
emitted invariant)
Total tests: 50 → 55.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(claude-stream): scope role-marker guard to text_delta only, not thinking_delta
Addresses review r3324xxxxxx — guarding the thinking channel buys no
security and causes legitimate aborts.
Why thinking is NOT a #3247 vector:
- `buildDaemonTranscript` in apps/web/src/providers/daemon.ts only
re-serializes `m.content` as `## ${m.role}\n...`.
- Extended-thinking content is rendered to a separate
`kind: 'thinking'` payload (daemon.ts:857-858) and never folded
into `m.content`.
- So a `## user` line in the thinking channel CANNOT become a
fabricated turn boundary on the next round-trip.
Why guarding it is harmful:
- Models routinely emit literal `## user` / `## assistant` lines
in chain-of-thought when reasoning about conversation structure
("Let me think about this. The user might phrase it as:\n## user\n
…"). Common pattern in production traces.
- With `abortForRoleMarker` wired in server.ts, a guard match on
thinking SIGTERMs the run and surfaces a security error to the
UI. The user paid for the reasoning, never sees the answer, and
gets a confusing "fabricated role marker" warning for what was
actually legitimate metacognition.
- This directly contradicts the module's own stated philosophy
("a false positive aborts the whole run — a much more expensive
failure than a stray unflagged ... line", role-marker-guard.ts).
Fix: `emitSafeText` now passes thinking_delta through unconditionally,
skipping both the guard and the contamination check. text_delta
remains fully guarded. The single-line change at the top of
emitSafeText preserves all other channels' behavior.
Regression tests added (apps/daemon/tests/claude-stream-thinking.test.ts):
- `## user` / `## assistant` lines in a thinking_delta — must NOT
fire fabricated_role_marker, the thinking content streams intact
including the marker text, and the subsequent text_delta answer
still reaches the consumer (run not aborted).
- Sanity check: same `## user` pattern in a text_delta DOES fire
fabricated_role_marker and truncates emission at the marker. Locks
in the channel-discriminated behavior.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(role-marker-guard): tie firstChunk to slicing, not byte emission
Blocking review r3324xxxxxx: under the prior firstChunk transition
("any byte emitted"), a role marker that arrived at the very start of
a message with its prefix split across multiple chunks bypassed
detection — reopening the #3247 vector on the Claude path.
Concrete cases that were missed (all are routine provider
tokenizations of \`## user\n…\` at message start):
- \`##\` | \` user\nDELETE…\`
- \`## us\` | \`er\nDELETE…\`
- \`## \` | \`user\nDELETE…\`
Mechanism: the pending-deferral regex only catches COMPLETE role
keywords, so a first chunk ending in a partial prefix (\`##\`, \`## \`,
\`## us\`) was emitted in full. That emission flipped firstChunk to
false. From that point only NEWLINE_ANCHORED_ROLE_MARKER_RE was used,
which requires a literal \n before \`##\`. A marker at buffer
position 0 has no preceding \n, so it could no longer match.
abortForRoleMarker never fired and tool_use blocks emitted after the
fabricated turn boundary reached the dispatcher.
Fix: change firstChunk to track "tail has not been sliced yet" rather
than "any byte emitted". While total emitted bytes <= TAIL_BUFFER_SIZE,
tail still represents the entire emission so far and \`^\` in the
canonical regex genuinely anchors at byte 0 of the stream — so the
\`^|\n\` alternation safely catches a chunk-split message-start
marker. The transition happens at the moment we would slice: once
emitted > TAIL_BUFFER_SIZE, tail becomes a mid-stream window, \`^\`
becomes meaningless, and we switch to the newline-only variants.
Earlier iterations of this code tried two other definitions, both
unsound:
- "any byte emitted" (this commit fixes) — lost \`^\` before a
chunk-split message-start marker could finish arriving.
- "newline emitted" (briefly considered as the reviewer's
alternative suggestion) — left \`^\` valid on a sliced buffer
when streams hadn't emitted a newline yet, re-introducing the
rolling-tail mid-stream false positive from review r3324060995.
The slice-based invariant satisfies both: while we have not sliced,
\`^\` is correct; once we slice, it is not.
Regression tests added (apps/daemon/tests/role-marker-guard.test.ts):
- \`##\` | \` user\nDELETE…\` → contaminated, marker=\`## user\`
- \`## us\` | \`er\nDELETE…\` → contaminated, marker=\`## user\`
- \`## \` | \`user\nDELETE…\` → contaminated, marker=\`## user\`
- \`#\` | \`# user\nDELETE…\` → contaminated, marker=\`## user\`
The fourth case (single \`#\` first chunk) exercises an even more
adversarial tokenization than the reviewer's examples; it is also
caught.
Total tests: 55 → 59.
Co-Authored-By: JasonBroderick <jason@buddyboss.com>
* fix(tests): wrap events in stream_event envelope in thinking test
feedJsonl was feeding raw events without the `{ type: 'stream_event',
event: ... }` wrapper that createClaudeStreamHandler requires (line 141
of claude-stream.ts). Events silently fell through all branches, making
both tests pass vacuously. Also fix TS2532 on warnings[0].marker with
non-null assertion (safe after the toHaveLength(1) guard).
Co-Authored-By: RoverKai <roverkai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: roverkai <2196140098@qq.com>
Co-authored-by: JasonBroderick <jason@buddyboss.com>
Co-authored-by: RoverKai <roverkai@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(web): preserve preview scroll across tools
Capture URL-loaded preview scroll state before tool handoff and restore it through an opt-in raw HTML bridge to avoid jumping back to the top.
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
* test(daemon): cover scroll bridge injection paths
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.6
---------
Co-authored-by: Codex <gpt-5@openai.com>
* fix(web): theme home hero select menu
Use theme tokens for HomeHero footer select dropdown panels so dark theme menus do not render light-only white backgrounds with low-contrast text.
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.3
* test(web): cover dark model logo inversion
Agent-Model: gpt-5
Agent-Family: openai
Agent-Session: 019e6ceb-c33d-7cd3-bff0-cbc20c642197
Agent-Step: 0.0.7
---------
Co-authored-by: Codex <gpt-5@openai.com>
* fix: insert skill reference when selecting from tools panel
When selecting a skill from the tools panel (not via @ mention), the skill
reference was not being inserted into the input. The tools panel only called
applyProjectSkill() without inserting the text token.
This fix makes the tools panel skill picker behave consistently with other
reference types (MCP, connectors) by:
- Inserting the @skill token at the current cursor position
- Setting focus and cursor position after the inserted reference
- Closing the tools panel after successful insertion
Now users can:
1. Manually delete an @skill reference
2. Open the tools panel and select a skill
3. See the new @skill reference inserted at the cursor
Fixes#3188
* fix(web): preserve draft edits when inserting skill
---------
Co-authored-by: mrcfps <mrc@powerformer.com>
* fix(web): persist design-files view state across navigation
pageSize, sortKey, sortDir, and kindFilter reset on every navigation
because DesignFilesPanel remounts via key={projectId}. Persist them to
localStorage under od:design-files:view-state:v1:<projectId> so each
project's view prefs survive tab-switching.
- Read persisted state via lazy useState initializers (SSR-safe try/catch)
- Write back in a single useEffect keyed on all four values
- Scoped per-project so proj-a settings never bleed into proj-b
- Schema-guarded: invalid/missing fields fall through to defaults
- Red spec: apps/web/tests/components/DesignFilesPanel.view-state-persist.test.tsx
* fix(web): address review feedback on view-state persistence
- Add typeof window guard in readViewState for explicit SSR safety
- Consolidate 4 separate localStorage reads into a single useRef read at
mount time; each lazy useState initializer now reads from savedViewState.current
instead of re-parsing localStorage independently
* fix(web): harden design-files view-state persistence
- Validate restored kindFilter values against the current ProjectFileKind
union via isProjectFileKind() so stale stored values from a prior schema
are dropped silently instead of being cast unchecked.
- Introduce DEFAULT_SORT_KEY/SORT_DIR/PAGE_SIZE constants so the useState
initialisers and the new validation guard share a single source of truth.
- Add viewStateHasMounted ref to skip the first-render write in the persist
useEffect. Without this guard every project the user visits accumulates a
default-value entry in localStorage on mount, growing stale-key garbage
unboundedly and making future field additions silently inject defaults into
every existing entry.
- Harden kindFilter test: replace the silent early-return-on-missing-trigger
with expect(filterTrigger).not.toBeNull() so a render failure surfaces as
a real test failure rather than a passing no-op.
* test(e2e): design files view state persists across navigation and reload
Adds a Playwright UI smoke test in e2e/ui/ that exercises the three key
guarantees of the view-state persistence fix:
(a) Tab-away / tab-back: navigating to a file tab and returning remounts
DesignFilesPanel (conditionally rendered); all four prefs (sortKey,
sortDir, pageSize, kindFilter) are restored from localStorage.
(b) Hard reload: localStorage survives page.reload(); prefs are intact on
the next mount.
(c) Per-project key isolation: a second project starts with defaults and
does not inherit values from the first project's localStorage entry.
The test uses OD_PORT=18011 / OD_WEB_PORT=18012 to avoid port conflicts with
the default development ports.
Also fixes a race in DesignFilesPanel: the stale-kind cleanup useEffect was
running against an empty availableKinds set before the async file list arrived
on mount, which cleared a kindFilter correctly restored from localStorage.
Guard added: skip the cleanup when availableKinds is empty.
Red on origin/main (no persistence logic exists there); green on this branch.
* fix(e2e): address code-reviewer feedback on view-state-persist test
- Add data-testid='df-page-size-select' to per-page <select> in
DesignFilesPanel (W2: decouple test from i18n string 'Show')
- Add StrictMode comment to viewStateHasMounted guard explaining
the dev-mode double-write behaviour (W1: document the invariant)
- Switch nav-away from dblclick to single-click + Open button,
matching the pattern used in app-design-files.test.ts (W4)
- Raise timeout from 60s to 90s for cold CI runners (W3)
- Unify seedTextFile/seedPngFile into shared seedFile helper (N3)
- Add home-hero-input assertion in gotoEntryHome (N2)
- Switch waitForPageSizeSelect to use data-testid (W2)
* test(e2e): split design-files persist into nav, reload, and per-project scenarios
* fix(web): tighten isPageSize to discrete option set, add invalid-value regression test
* fix(web): isolate DesignFilesPanel.test.tsx from persisted view-state key
* fix(chat): surface OpenCode provider failures from its log on a silent stall
OpenCode's headless `run --format json` mode swallows provider failures: a
429 usage-limit is marked retryable and retried silently with nothing on
stdout/stderr, so the chat run only dies via the inactivity watchdog and the
daemon shows a bare "request timed out" with no reason. The real error
(statusCode + "Monthly usage limit reached…") is recorded only in OpenCode's
own session log.
On a failed OpenCode close where stdout/stderr carry no signal, read the
newest OpenCode session log, extract the latest `service=llm` provider error
(scoped to that one line so the embedded request body can't contaminate the
classification), and emit a structured, retryable SSE error (RATE_LIMITED /
AGENT_AUTH_REQUIRED / UPSTREAM_UNAVAILABLE) carrying the provider's message.
Refs #982.
* fix(chat): emit recovered OpenCode failure from the watchdog path, bound to the run
Addresses review on #3316.
Blocking: the recovery previously ran only in the child-close handler, but in
the inactivity-watchdog stall path (the exact case this targets)
failForInactivity sends its error and finish()es the run — which clears
run.clients — before the child closes. So the structured error reached zero
live SSE clients and only surfaced on reload. Recover and send the OpenCode
failure inside failForInactivity, before finish(), on the same pre-teardown
send path the generic stall message already uses. Keep the close-handler
branch for the case where OpenCode exits non-zero on its own (clients still
attached).
Non-blocking: bind the log lookup to the current run via an mtime gate
(since=run.createdAt) so a stale or concurrent session's error can't be
misattributed — skip log files last written before the run started.
* docs(opencode-log): note the concurrent-run limitation of the mtime gate
* fix(chat): skip close-handler failure emit when the watchdog already finished the run
Non-blocking review follow-up on #3316: on the silent-stall path both
failForInactivity and the child-close handler fired for the same run, so the
recovered RATE_LIMITED error was sent twice and the events-log stream was
reopened after finish() had closed it. Guard the close-handler failure emit
with !design.runs.isTerminal(run.status) — the watchdog already sent the
error and finalized the run; finalization below still runs (finish() no-ops
once terminal).
* fix(daemon): validate skillId on POST /api/projects against runtime source of truth
* fix(daemon): validate skillId on PATCH /api/projects/:id, sharing the POST validator
* test(daemon): cover skillId canonicalization, design-template ids, empty-string + null normalization, type rejection
* fix(platform): search mise shims dir so mise-installed CLIs are detected
- Add ~/.local/share/mise/shims (and MISE_DATA_DIR override + legacy ~/.mise/shims) to wellKnownUserToolchainBins.
- This makes Pi, Kimi, and other mise-managed coding agents visible to the daemon even when launched from GUI contexts with stripped PATH.
- Added tests for default and MISE_DATA_DIR cases.
- Also pinned pnpm@10.33.2 in root mise.toml for better mise ergonomics.
Before/after: more local CLIs now appear in the runtime picker (Kimi, Pi, Antigravity, Kilo, etc.).
Refs: discussion in session around improving detection for common mise users.
* fix(platform): address Copilot review on mise shims logic
- Generalize the shims comment (no hard-coded CLI examples).
- Make per-version Node toolchain scanning respect MISE_DATA_DIR
(use the same mise root for installs as for shims).
- Avoid duplicate shims entries when MISE_DATA_DIR makes legacy path
identical to the primary one.
Addresses the three inline comments from copilot-pull-request-reviewer
on PR #3319.
* test(platform): extend MISE_DATA_DIR test to cover installs scanning
Addresses non-blocking review feedback from @nettee on PR #3319.
The previous test only asserted shims behavior under a custom
MISE_DATA_DIR. This extends it to also create fixture trees under
customMise/installs/node/... and customMise/installs/npm-openai-codex/...
and assert that the install paths are discovered while default-root
paths are excluded.
This makes the test robust against regressions in the installs
scanning logic (existingMiseNpmPackageBinDirs + node version dirs).
* fix(platform): only fall back to ~/.mise/shims when no MISE_DATA_DIR is set
Addresses the remaining non-blocking review comment from @nettee on PR #3319.
When an explicit MISE_DATA_DIR is provided, we no longer inject the
legacy ~/.mise/shims path. This prevents stale shims from a previous
mise layout from being re-introduced into detection.
Also added a regression assertion in the MISE_DATA_DIR test.
* fix(daemon): make claude-stream dedup robust when final assistant wrapper lacks msgId
Prevents duplicated text and thinking output (especially visible during
design system generation with AMR/Vela).
Root cause: the textStreamed guard fell back to whenever the
final message arrived without a string uid=501(ramarivera) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),79(_appserverusr),80(admin),81(_appserveradm),399(com.apple.access_ssh),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),400(com.apple.access_remote_ae) (common in some
AMR flows and design system tasks), causing the full content to be
re-emitted even if it had already been delivered via streaming deltas.
Fix: track whether any text or thinking was streamed via deltas for the
current message and use that as a reliable fallback for the final wrapper
instead of only trusting presence.
* revert: remove dedup from claude-stream (PR #3319 should stay clean)
The Mark tool (#3081/#3277) captured the preview via the *active* iframe. For
URL-load previews — decks especially — the active frame is the bridgeless URL
iframe, while the snapshot bridge lives only in the (mounted but hidden) srcDoc
transport frame. So Send on a deck timed out and showed 'Could not capture the
preview. Try again to avoid sending only ink.'
Snapshot the srcDoc-render-mode frame instead (capture mode already keeps it on
full content, so it carries the bridge), with a short retry while it finishes
swapping to full content. Falls back to the active frame for the non-URL-load
case where they are the same.
Red spec: PreviewDrawOverlay.test 'snapshots the srcDoc bridge iframe, not the
visible URL-load frame' fails on main (targets the URL frame), passes here.
The homepage runs its own inline header enhancer instead of importing
the shared header-enhancer.astro component, and that inline copy only
ported the scroll-headroom and GitHub stars/version logic — it never
included the hamburger toggle handler. As a result the mobile menu
button rendered (and animated to an X via CSS) but clicking it did
nothing on / and /<locale>/, while sub-pages that do import the shared
enhancer worked fine.
Port the same toggle handler into the homepage inline enhancer: click
flips .is-open on header.nav (which CSS expands into the dropdown panel
below 1080px), and outside-click, Escape, and any in-menu link close it,
keeping aria-expanded in sync.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* fix(pack): add missing download and host packages to Linux INTERNAL_PACKAGES
The native (non-containerized) Linux AppImage build fails with npm 404
errors because @open-design/download and @open-design/host — runtime
dependencies of @open-design/desktop and @open-design/web — were not
included in the INTERNAL_PACKAGES list. Without tarballs for these two
packages, npm install in the assembled app directory tries to resolve
them from the public registry where they don't exist.
Add both packages to INTERNAL_PACKAGES and their build steps to
buildWorkspaceArtifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pack): apply same download/host fix to mac and win lanes, add regression test
1. Extend the INTERNAL_PACKAGES fix to mac/constants.ts, mac/workspace.ts, win/constants.ts, and win/app.ts so all three pack lanes produce tarballs for @open-design/download and @open-design/host.
2. Add internal-packages-coverage.test.ts that derives required workspace runtime deps from apps/desktop and apps/web package.json files and asserts every pack lane's INTERNAL_PACKAGES includes them. This prevents the same drift from recurring when a new workspace dependency is added.
3. Update win-app.test.ts and workspace-build.test.ts mock directory lists to include the two new packages.
* fix(pack): include runtime packages in workspace build cache
* fix(pack): install platform with desktop prebundle packages
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
* fix(web/router): defer popstate dispatch to microtask
navigate() previously dispatched a synchronous popstate event after
mutating window.history, which caused React 18 to emit:
Cannot update a component (Router) while rendering a different
component (App). To locate the bad setState() call inside App,
follow the stack trace as described in
https://react.dev/link/setstate-in-render
This happens whenever a caller invokes navigate() from inside a
useState updater (e.g. App.tsx:479 routing first-run users through
the onboarding panel from inside the setConfig() update). The
synchronous popstate dispatch reaches useRoute() subscribers which
then call setRoute() while the parent component is still rendering.
Defer the popstate dispatch to a microtask. The window.history call
itself stays synchronous so the URL bar updates immediately; only
subscriber updates are pushed past the current render commit, which
removes the warning without changing observable behaviour for any
existing caller.
* fix(web/router): cover deferred navigation timing
---------
Co-authored-by: Visionboost <contact@visionboost.fr>
Co-authored-by: Siri-Ray <2667192167@qq.com>
Adds two new entry points and reworks the community page chrome to
match the wider landing-page direction (PRs #3222 and #3230).
Showcase / Plugin Everything (above Ambassadors)
Pitches Open Design as 'studio and gallery' in one address: anything
shipped through the studio (content, products, templates, Skills,
workflows) can return as a plugin, and the strongest pieces are
carried out to the registry, to X, to Discord's #showcase channel,
to the newsletter, and to the video reels. Right column holds a
zero-code Contribute card with a curl installer, a copy-to-clipboard
button, and a three-step flow for the od-contribute Skill.
Hero CTA row
Three buttons, in a single row: Stage your masterpieces (Showcase),
Become an ambassador (Ambassadors), Contributors hall of fame
(Maintainers).
Top nav
Pulls the breadcrumb out of the brand mark, surfaces Contributors /
Ambassadors / Showcase as anchors, and adds GitHub + X icon buttons
next to the Join Discord pill (mirrors PR #3230).
Footer
Restructured into columnar layout with brand summary plus Products,
Plugins, and Community columns; copyright moves to a bottom rule.
Ambassadors
Renaissance-voice three-column program (Vocation / Patronage /
Covenant) with an Apply on Discord CTA to the ambassador channel.
Discord
Card spans wider (max-width 1440px), copy reframed as 'the front
line of the agent-design era', two moderator profiles on the right
(Koki from the founding team, Victor as Discord steward), channel
list and CTAs on the left.
Recent signal
Kicker and headline framed as this week's leaderboard; backed by a
hand-curated RANKING_SNAPSHOT. A real refresh pipeline remains a
follow-up; data is hand-updated until then.
Other notes
Punctuation pass: replaced most em-dashes in prose with colons,
periods, commas, semicolons, parentheses; em-dashes only remain in
data placeholders, page title, and HTML comments. Logo size bumped
to 32px and now uses an alt of 'Open Design'.
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
* fix: pet hover card gets cut off at screen edges
* fix: address review feedback - viewport clamping + unadopted pet wake
- Add window.innerHeight check to prevent bottom-edge clipping
- Increase menuH estimate for safer positioning
- Open pet settings instead of no-op Wake for unadopted pets
* fix: address review feedback on pet menu positioning and wake action
- Add viewport height check (viewH) to prevent bottom-edge clipping
- Increase menuH estimate for safer positioning
- Open pet settings instead of no-op Wake for unadopted pets
* fix(daemon): grok-build runtime — pass prompt inline as -p value, drop stdin
Grok Build CLI 0.1.212 enforces `-p, --single <PROMPT>` as a value-requiring
flag — invoking with bare `-p` and piping the prompt to stdin now fails with:
error: a value is required for '--single <PROMPT>' but none was supplied
The previous runtime def used `promptViaStdin: true` + `buildArgs` returning
`['-p']`, which only worked against earlier grok builds that read the prompt
from stdin when `-p` had no inline value.
This change inlines the prompt as the `-p` argument value and flips
`promptViaStdin: false`. Linux `MAX_ARG_STRLEN` (128 KB) is enough headroom
for typical Open Design prompts; if we ever hit `E2BIG` on a very large
brief, a follow-up could shell out to `--prompt-file <tempfile>`.
Verified against grok 0.1.212 (b7b8204a4) — single-turn invocations now
return clean text replies instead of exit 2.
* fix(daemon): declare grok-build argv prompt budget + regression coverage
@mrcfps' review on #2259 flagged that moving the Grok Build adapter from
the (no-longer-working) stdin path to argv would regress oversized
composed prompts from the actionable AGENT_PROMPT_TOO_LARGE error we
already emit for DeepSeek to a raw spawn ENAMETOOLONG / E2BIG instead.
Fixed by mirroring the DeepSeek argv-budget shape:
- grok-build.ts: `maxPromptArgBytes: 30_000` (same headroom as DeepSeek,
~2.7 KB under the Windows CreateProcess 32_767-char cap) so
`checkPromptArgvBudget` pre-flights composed prompts (system + history
+ skills + design-system content + user message) before spawn.
- prompt-budget.ts: Grok-Build-specific message — names the `-p /
--single` flag, the xAI CLI 0.1.212+ behavior change, and points the
user at stdin-capable adapters (claude / codex / hermes) when they
need to ship large local context.
- Tests: 3 new vitest cases in prompt-budget.test.ts — pin the budget
field, exercise the strict-overrun + at-limit + CJK byte-count guards
exactly like the DeepSeek regression set, and assert the Grok-named
diagnostic copy. New `grokBuild` + `grokBuildMaxPromptArgBytes`
helpers exported alongside the existing `deepseek*` ones.
All 23 prompt-budget tests pass locally (`pnpm exec vitest run
tests/runtimes/prompt-budget.test.ts`).
---------
Co-authored-by: Sriram Sivakumar <sriram155@gmail.com>
Co-authored-by: Siri-Ray <2667192167@qq.com>
Resolve Comment picker hit testing against meaningful visible DOM leaves before falling back to annotated ancestors, while preserving Inspect mode's annotation-first selector behavior.
Filter generated React root annotations from Comment targets, keep real element bounds separate from hoverPoint, and avoid rendering the comments drawer inline when a configured dock portal is not mounted.
Inline @-mentioned filenames in the composer can be very lengthy,
causing line wrapping and visual crowding in the input area.
- Switch display from inline to inline-block for max-width
- Cap width at min(240px, 25vw)
- Apply overflow: hidden + text-overflow: ellipsis + white-space: nowrap
- Remove box-decoration-break (unused since content won't wrap)
- Tweak vertical-align for consistent inline-block alignment
Closes#3261
Co-authored-by: freshtemp-labs <freshtemp-labs@users.noreply.github.com>
Move the three buttons (settings, handoff, avatar) from fileActionsBefore
to the actions slot so they always stay pinned to the right edge of the
header, regardless of how many extra controls (Share, Present, etc.) are
injected via portal during HTML preview.
Co-authored-by: qiongyu1999 <2694684348@qq.com>
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
The Linux AppImage path assembles INTERNAL_PACKAGES as `file:` tarballs
and runs `npm install --omit=dev` in an isolated app directory. `pnpm
pack` rewrites each tarball's `workspace:*` refs to a concrete version,
so any runtime @open-design/* dependency missing from INTERNAL_PACKAGES
is resolved from the public npm registry and 404s.
Linux ships webOutputMode "server" and tarball-installs every
INTERNAL_PACKAGES entry, including @open-design/desktop and
@open-design/web. @open-design/host (dep of web + desktop, added in
#2246) and @open-design/download (dep of desktop, added in #2677) landed
after the Linux package list was written and were never added to it, so
`pnpm exec tools-pack linux build --to appimage` fails with:
npm error 404 Not Found - GET .../@open-design%2fdownload
mac/win default to "standalone", where desktop/web/packaged/daemon are
prebundled with esbuild and excluded from the tarball install
(shouldInstallInternalPackageFor{Mac,Win}Prebundle). The packages they
do install have no download/host dependency, so those lanes correctly
omit them and need no change — this fix stays scoped to linux.ts and
touches no mac/win or workspace-build code.
Add both packages to the Linux INTERNAL_PACKAGES and build them in
buildWorkspaceArtifacts (download depends on platform). Add a cross-lane
regression test that, for each lane, derives the set it actually installs
(honoring the standalone prebundle exclusion) and asserts that set is
closed under its runtime @open-design/* dependencies. The test is red on
the linux lane without this fix and green with it, while mac/win pass
either way — encoding why only Linux needs these packages.
* feat(mocks): replay-based mock CLIs for opencode/claude/codex/deepseek/qwen/grok
Drops in a `mocks/` top-level dir that pretends to be the real agent
CLIs by streaming pre-recorded sessions in each CLI's native stdout
protocol. Zero LLM tokens.
## Use cases
- **E2E tests** in `apps/daemon/tests/` — exercise the full chat-server
pipeline against a known trace, assert UI events / artifacts.
- **Self-validation during dev** — iterate on `claude-stream.ts` /
`json-event-stream.ts` parser changes without burning provider budget.
- **Regression harness** — replay the same trace before and after a
charter / parser change; diff the daemon events the UI surfaces.
- **Demo / onboarding** — show what a 17-tool claude editing session
looks like end-to-end, offline.
## How
- 6 bash wrappers (`mocks/bin/`) shadow the real CLIs when PATH-overlaid.
- `mocks/mock-agent.mjs` reads `mocks/recordings/<trace>.jsonl`, picks
one via env var (`SYNCLO_EXPLORE_MOCK_TRACE` / `_POOL` /
`_BY_PROMPT_HASH`), streams the trace in the requested format.
- Each format renderer matches the EXACT JSON shape the OD daemon
parser expects, verified line-by-line against
`apps/daemon/src/{json-event-stream,claude-stream}.ts`:
| CLI | streamFormat | parser source |
| ------------------------- | ------------------------- | ------------------------------------------ |
| `opencode` | `json-event-stream` | `handleOpenCodeEvent` |
| `codex` | `json-event-stream` | `handleCodexEvent` |
| `claude` | `claude-stream-json` | `createClaudeStreamHandler` |
| `deepseek` `qwen` `grok` | `plain` | `server.ts` (raw stdout) |
## Quick start
```bash
export PATH="$PWD/mocks/bin:$PATH"
export SYNCLO_EXPLORE_MOCK_TRACE=04097377 # 8-char prefix OK
export SYNCLO_EXPLORE_MOCK_NO_DELAY=1
echo "any prompt" | opencode run
echo "any prompt" | claude -p --output-format=stream-json
echo "any prompt" | codex exec
```
The mock binary announces the picked trace id on stderr:
`[mock-opencode] picked 04097377… via fixed`.
Recording selection (env, in priority order):
- `SYNCLO_EXPLORE_MOCK_TRACE=<id>` — fixed (prefix OK)
- `SYNCLO_EXPLORE_MOCK_BY_PROMPT_HASH=1` + stdin prompt — `sha256(prompt) % N`
- `SYNCLO_EXPLORE_MOCK_POOL=<tag>` — random within `agent:claude` /
`skill:agent-browser` / `outcome:failed` / etc.
- (default) uniform random
- `SYNCLO_EXPLORE_MOCK_SEED=<str>` — reproducible "random"
- `SYNCLO_EXPLORE_MOCK_NO_DELAY=1` — skip inter-event waits
## Dataset
179 anonymized Langfuse traces from this project's own production
telemetry:
- 9 agents: claude 57 · opencode 41 · codex 38 · gemini 25 ·
cursor-agent 11 · qwen 2 · copilot 2 · deepseek 2 · antigravity 1
- outcomes: succeeded 144 · failed 35
- skills: default 71 · ad-creative 50 · algorithmic-art 30 ·
agent-browser 22 · video-hyperframes 2 · plus magazine-web-ppt /
brainstorming / data-report / penpot-flutter-design-source 1 each
- 124 multi-turn (sessions with ≥2 turns)
- 18 produce `<artifact>` output
- ~4.5 MB on disk total
Anonymization: `/Users/<name>/` → `${HOME}/`,
`C:\Users\<name>\` → `%USERPROFILE%\`, project UUIDs →
stable `proj-001`, `proj-002`, …. Tool input/output payloads
preserved verbatim (templated UI, no cell-level PII).
## Smoke test
`bash mocks/scripts/smoke-test.sh` — 6 checks across all 6 agents.
All pass on this branch (verified locally):
```
✓ opencode first event = step_start
✓ codex first event = thread.started
✓ claude first event = system
✓ deepseek emitted plain text (144 chars on first line)
✓ qwen emitted plain text (144 chars on first line)
✓ grok emitted plain text (144 chars on first line)
All mock CLIs working. ✅
```
## Adding more recordings
The exporter that produced this set lives in
[nexu-io/agent-pr-explore](https://github.com/nexu-io/agent-pr-explore)
(see `cli/src/local/orchestrator/langfuse-import.ts` + the `local
langfuse-import` CLI command). Operators with the Langfuse keys can pull
more by tag / outcome / artifact / multi-turn filter, then run
`local recordings anonymize --out-dir ~/Documents/open-design/mocks/recordings`.
`mocks/README.md` has the full instructions.
## Out of scope (follow-ups)
- **ACP agents** (`devin`, `hermes`, `kilo`, `kimi`, `kiro`, `vibe`) need
a JSON-RPC server on stdio rather than a one-shot stream — separate
`format-acp.mjs` module not yet written.
- **Per-agent json-event-stream variants** (`cursor-agent`, `gemini`,
`qoder`, `copilot`, `pi`) currently fall back to the `plain` renderer;
their parsers are in `apps/daemon/src/json-event-stream.ts` and follow
the same template as `format-codex.mjs`.
## AGENTS.md updates
- Added `mocks/` to the top-level content directories listing
- Added a Validation strategy bullet pointing here for agent-stream /
parser changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mocks): add opencode-cli/kiro-cli/vibe-acp bin aliases and unref ACP timeout
- Add mocks/bin/opencode-cli, kiro-cli, vibe-acp wrappers for the primary
RuntimeAgentDef bin names OD resolves before any fallback. Without these,
a PATH-overlaid OD daemon run bypasses the mock entirely (opencode-cli,
kiro-cli) or cannot find the mock at all (vibe-acp, which has no fallback).
- Include opencode-cli, kiro-cli, vibe-acp in the smoke-test ACP/JSON loop
so coverage is verified end-to-end.
- Call .unref() on the 30s safety timeout in format-acp.mjs so a completed
ACP session exits promptly instead of waiting the full 30 seconds.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* feat(mocks): add vela (AMR) — login / models / ACP with strict set_model gate
Extends mocks/ to cover OD's own AMR runtime. `vela` is the bin name
`apps/daemon/src/runtimes/defs/amr.ts` specifies (`bin: 'vela'`,
`streamFormat: 'acp-json-rpc'`). It's richer than the generic ACP
agents — covers full login + models + chat-session lifecycle.
### What vela does (mirrored from apps/daemon/tests/fixtures/fake-vela.mjs)
1. `vela login` — writes ~/.amr/config.json with a fake profile (controlKey,
runtimeKey, user{email,name,plan}, profile-specific apiUrl/linkUrl).
The on-disk projection is what OD's daemon login route + AmrLoginPill
poller read; production goes through device-auth, the mock skips
straight to the file write.
2. `vela models` — prints the production-shaped public model catalog as
newline-separated `public_model_* vela` lines. Override via
FAKE_VELA_MODELS env.
3. `vela agent run --runtime opencode` — ACP JSON-RPC server with three
vela-specific protocol extensions:
a. `initialize` response carries `agentCapabilities`
(`promptCapabilities.embeddedContext`) + `models`
(`currentModelId` + `availableModels`).
b. `session/new` response carries the same `models` block.
c. **Strict set_model gate**: `session/prompt` is rejected with
JSON-RPC -32602 ("session/set_model must be called before
session/prompt") UNLESS `session/set_model` (or
`session/set_config_option`) has been called for the current
sessionId. Mirrors real vela 0.0.1 contract; catches regressions
in `attachAcpSession` that silently skip set_model.
### Error injection envs (in sync with fake-vela.mjs)
FAKE_VELA_SESSION_ID - sessionId returned by session/new
FAKE_VELA_TEXT - override assistant text
FAKE_VELA_THOUGHT - optional thought_chunk before text
FAKE_VELA_SESSION_NEW_ERROR - fail session/new
FAKE_VELA_SET_MODEL_ERROR - fail session/set_model
FAKE_VELA_PROMPT_ERROR - fail session/prompt
FAKE_VELA_REQUIRE_SET_MODEL='0' - disable the strict gate (legacy)
FAKE_VELA_LOGIN_USER_EMAIL - email written into config profile
FAKE_VELA_LOGIN_USER_PLAN - plan written into config profile
FAKE_VELA_LOGIN_DELAY_MS - sleep before write (test in-flight)
FAKE_VELA_LOGIN_FAIL - print + exit 1
FAKE_VELA_MODELS - override models stdout
VELA_PROFILE - profile slot (prod | test | local)
### Components
`mocks/lib/format-vela.mjs` (~205 LOC)
- Full ACP server with vela protocol extensions
- Strict set_model gate
- Error injection plumbing
`mocks/lib/vela-subcommands.mjs` (~90 LOC)
- runVelaLogin() — writes ~/.amr/config.json
- runVelaModels() — prints catalog
`mocks/bin/vela` — dispatcher wrapper. Forwards `vela <subcmd>` to
mock-agent.mjs which routes to login/models or falls through to ACP.
`mocks/mock-agent.mjs` — parseArgs now collects positionals so the vela
dispatcher can read subcommand from there; switch case added for vela.
`mocks/scripts/smoke-test.sh` — +4 assertions:
vela models prints ≥10 catalog lines
vela login writes ~/.amr/config.json with the requested email
vela agent run ACP roundtrip (initialize+models+set_model+stream+result)
vela strict set_model gate rejects prompt without prior set_model
### Verified locally
✓ vela models printed 15 catalog lines
✓ vela login wrote ~/.amr/config.json with profile.prod.user.email
✓ vela agent run ACP roundtrip (initialize+models, set_model accepted, prompt streamed)
✓ vela strict set_model gate rejects session/prompt without prior set_model
All 21 smoke checks pass (up from 17 with previous P3 ACP commit).
### AGENTS.md + README updates
AGENTS.md — mention `vela (AMR — vela CLI)` alongside ACP agents in
the directory listing entry.
mocks/README.md — protocol table row + dedicated vela section with
subcommand contract, strict gate explanation, env-injection cheat
sheet. Mock-tree listing updated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mocks): honor REPORT_FILE env when --report-file flag not given
Harnesses that spawn the mock without translating their report-path
contract to the mock's CLI flag (notably nexu-io/agent-pr-explore's
orchestrator, which passes REPORT_FILE as env per the existing
opencode/claude/codex agent launchers) wouldn't get a report file
written, so the harness's "agent exit 0 but produced no report"
check would always fire and mark mock runs as failure even though the
stdout stream was complete.
Fix: in mock-agent.mjs parseArgs, fall through to process.env.REPORT_FILE
when --report-file wasn't provided on argv. Each format renderer already
accepts opts.reportFile and writes the recording's final assistant text
to it (`format-*.mjs` already had this — only the wiring was missing).
Verified: synclo-explore run with `mock=true, mock_trace=04097377`
against the opencode wrapper now produces a plan.md with the recording's
17-tool claude editing session report. ~1.5s per run vs ~70s real opencode.
* mocks: move recordings to Cloudflare R2; PR→main→Action upload path
The 179-recording corpus (~4.5 MB raw, ~280 KB after compression) has
been moved off git into Cloudflare R2 at the bucket open-design-mocks
under recordings/v1/. The repo now ships:
- mocks/manifest.json — the canonical catalog (renamed from
recordings/index.json) with sha256 + storage hints; consumers
fetch this to discover what exists, then pull individual jsonl
files on demand
- mocks/scripts/fetch-recordings.sh — parallel, sha256-verified,
idempotent puller for the public r2.dev URL
- mocks/scripts/add-recording.sh — local maintainer helper that
validates a new .jsonl and copies it into recordings-staging/
(no R2 calls; no credentials needed)
- mocks/scripts/upload-to-r2.mjs — called only by the CI workflow
- mocks/scripts/lib/manifest-utils.mjs — shared sha256/meta/
rebuild-histograms logic, used by both add-recording (preview)
and upload-to-r2 (actual write) so the entry shape never drifts
- .github/workflows/sync-mocks-to-r2.yml — fires on push to main
when mocks/recordings-staging/ changes; uploads to R2, updates
manifest, commits cleanup back; serialized via concurrency group
Trust model: R2 write credentials (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID) are repo secrets; nobody can push from a
laptop. Read stays public via the r2.dev URL.
Why not pnpm install integration: contributors who do not touch
agent code do not pay the fetch cost. Fetch happens on first
smoke-test run (auto-fallback) or when a mock spawn needs data.
Repo size: -4.55 MB net (delete 179 jsonl, +280 KB manifest +
scripts). Smoke test (21 checks) still green against the fetched
corpus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: scope R2 write token to a dedicated secret name
Use CLOUDFLARE_R2_MOCKS_TOKEN (instead of reusing the shared
CLOUDFLARE_API_TOKEN that landing-page-*.yml uses for Pages deploys)
so the R2 write capability can be scoped to just the
open-design-mocks bucket without bleeding extra capability into the
Pages workflows.
Also hardcode the powerformer CF account_id directly in the workflow
(account IDs are not secret and the shared CLOUDFLARE_ACCOUNT_ID
secret may point at a different account).
Workflow now fails fast with an actionable error message + dashboard
link if the secret is unset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: switch R2 sync to S3-compat API (wrangler getMemberships gate)
wrangler 4.x calls /memberships before any r2 action, requiring
user:read scope. R2 "Object Read & Write" tokens deliberately lack
that scope (defense in depth — a leaked token should not enumerate
account-level resources). The workflow now uses the aws CLI talking
straight to the R2 S3-compatible endpoint with SigV4, no membership
lookup.
Secret rotation: CLOUDFLARE_R2_MOCKS_TOKEN (Bearer) is replaced by
CLOUDFLARE_R2_MOCKS_AK / CLOUDFLARE_R2_MOCKS_SK (matching the
existing CLOUDFLARE_R2_RELEASES_AK/SK naming convention). End-to-end
tested locally: PUT recording → manifest rebuild → manifest PUT →
staging cleanup all green.
aws CLI is pre-installed on ubuntu-latest, so no install step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: scrub synclo namespace; use OD_MOCKS_* env prefix throughout
These mocks were copy-pasted from synclo-explore, where they
originated, and inherited the SYNCLO_EXPLORE_MOCK_* env-var
convention. That brand-bleed is not appropriate in OD: rename the
public env surface to OD_MOCKS_* (matching OD-native prefixes like
OD_MOCKS_CACHE_DIR, OD_TRACE_R2_UPLOAD, OD_EXPECT_TIMEOUT_SECONDS).
Renames:
SYNCLO_EXPLORE_MOCK_TRACE → OD_MOCKS_TRACE
SYNCLO_EXPLORE_MOCK_BY_PROMPT_HASH → OD_MOCKS_BY_PROMPT_HASH
SYNCLO_EXPLORE_MOCK_POOL → OD_MOCKS_POOL
SYNCLO_EXPLORE_MOCK_SEED → OD_MOCKS_SEED
SYNCLO_EXPLORE_MOCK_NO_DELAY → OD_MOCKS_NO_DELAY
SYNCLO_EXPLORE_MOCK_RECORDINGS_DIR → OD_MOCKS_RECORDINGS_DIR
SYNCLO_EXPLORE_MOCK_SMOKE_TRACE → OD_MOCKS_SMOKE_TRACE
SYNCLO_OD_MOCKS_I_KNOW_WHAT_IM_DOING → OD_MOCKS_ALLOW_LOCAL_UPLOAD
Also drop the inline harvester usage from README. The harvester is an
external CLI in nexu-io/agent-pr-explore — its README is the right
place for langfuse-import flags, anonymization options, etc. OD only
documents its own staging→PR→Action workflow.
Smoke test (21 checks) still green; OD_MOCKS_TRACE end-to-end
verified to route correctly.
Consumers of the OLD env names (notably the orchestrator in
nexu-io/agent-pr-explore) need a matching rename. No back-compat
shim here — the explore side has zero external users today and a
one-line follow-up is cleaner than a permanent deprecation layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* AGENTS.md: align mock env names with mocks/ rename (SYNCLO_* → OD_MOCKS_*)
Missed in the prior commit (a30b868a) — only grepped mocks/ subdir.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: drop staging dir + GH Action; back to local-script upload
The staging-dir + Action design (added earlier in this PR) had a flaw
the user caught: new recordings briefly entered the repo on their way
through staging, leaving them in git history forever even after the
Action cleanup commit removed them from HEAD. That defeats the whole
point of moving recordings to R2.
Replace with the simpler local-maintainer flow:
bash mocks/scripts/upload-recording.sh /path/to/<trace>.jsonl
# → validates, wrangler r2 put, updates manifest.json, wrangler r2 put manifest
git add mocks/manifest.json && git commit && git push
# → only the ~200B manifest delta enters git
The wrangler-OAuth gate replaces the CI secret + Action duo. For a
solo / small maintainer team this collapses the trust chain down to
"do you have wrangler login to the powerformer account?" — no GH
secrets to rotate, no concurrency window to worry about, no
inevitable repo-history bloat.
Deletes:
- .github/workflows/sync-mocks-to-r2.yml
- mocks/scripts/upload-to-r2.mjs (CI-only)
- mocks/scripts/add-recording.sh (staging helper, now obsolete)
- mocks/recordings-staging/ (empty dir, never to be repopulated)
Adds:
- mocks/scripts/upload-recording.sh
Kept:
- mocks/scripts/fetch-recordings.sh
- mocks/scripts/lib/manifest-utils.mjs (still used by upload-recording.sh)
- mocks/manifest.json (committed; the only mocks artifact in git)
End-to-end tested locally: re-upload an existing recording is
idempotent, manifest math is stable, fetch + smoke test still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: address review — guard allowlist + safe ~/.amr + loud OD_MOCKS_TRACE typo
Three concrete issues raised across recent Siri-Ray (Looper) review
threads on #3241:
1. scripts/guard.ts only allowlisted mocks/lib/ + mocks/mock-agent.mjs,
leaving mocks/scripts/lib/manifest-utils.mjs outside the residual-
JS guard. Result: Preflight fail on every push. Extend the allowlist
to mocks/scripts/ — same precedent as the lib/ entry directly above.
2. mocks/scripts/smoke-test.sh moved the caller real ~/.amr to
~/.amr-smoke-backup, ran vela login (which writes a fake config),
then rm -rf the .amr and restored the backup. Two failure modes:
crash mid-run loses the user real config, and re-running before
restore overwrites the backup with the fake login. Fix: sandbox
vela login into a mktemp -d HOME via env (HOME=$amr_sandbox vela
login). Never touches the real ~/.amr at all. trap cleans up.
3. mocks/lib/recording-picker.mjs silently fell through to
prompt-hash → pool → random when OD_MOCKS_TRACE was set but did
not match any recording (typo, prefix too short, corpus not
fetched). Tests using a pinned trace would silently get a
different trace, hiding regressions. Fix: throw an explicit error
with the failing value + a pointer at fetch-recordings.sh.
Verified locally: pnpm guard prints "Residual JavaScript check
passed", smoke-test still 21/21, ~/.amr mtime unchanged after run,
typo on OD_MOCKS_TRACE now produces "mock-agent: OD_MOCKS_TRACE=...
set but no matching recording in <dir>" on stderr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fetch-recordings: detect empty filter result before line-counting
printf '%s\n' on an empty string emits a single empty line, so the
previous TOTAL=$(printf ... | grep -c "") math returned 1 on an
empty $ENTRIES_TSV — a typo like `--agent no-such-agent` printed
"Fetching up to 1 recordings", downloaded zero, and exited 0
("ready"). Check `-z $ENTRIES_TSV` first.
Reproduced + fix verified per the reviewer thread.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mocks: address mrcfps review — goldens + provenance + contract check
Three durability improvements suggested in the PR #3241 top-level
review:
## 1. Golden daemon-event snapshots (mocks/golden/*.events.json + apps/daemon/tests/mocks-golden.test.ts)
Smoke-test verified that mocks RUN; that catches crashes but not a
parser change that semantically reshapes the events the daemon emits.
Commit the daemon-event sequence for 3 representative traces:
- claude 314d6833 — median-complexity agent-browser session
- codex dcdff3b3 — 14-tool refactor
- opencode 9a9522ec — 7-tool data-report
apps/daemon/tests/mocks-golden.test.ts spawns the mock, feeds stdout
through the real createClaudeStreamHandler / createJsonEventStreamHandler,
normalizes per-spawn volatile fields (only sessionId today, only on
claude), and deep-equals against the committed snapshot. A parser
regression fails the test loudly.
After an intentional parser change, regenerate:
MOCKS_GOLDEN_UPDATE=1 pnpm --filter @open-design/daemon test mocks-golden
git diff mocks/golden/
# eyeball; commit if shapes match intent
## 2. Provenance fields on every manifest entry (mocks/scripts/lib/manifest-utils.mjs + mocks/manifest.json)
Augment inspectRecording() to write:
captured_at — ISO 8601 from existing meta.timestamp
cli_version — null until harvester writes it
protocol_version — null until harvester writes it
anonymization_version — null until harvester writes it
captured_at is now populated for all 179 existing entries from the
meta event the harvester already emits. The harvester in
nexu-io/agent-pr-explore is the next step for cli_version /
protocol_version / anonymization_version — once those are
populated, consumers can detect when a recording is older than ~1
minor version behind the live CLI and flag for re-harvest.
No matrix of (cli_version × agent) recordings — that explodes
maintenance. Just metadata per recording so trust decay is visible.
## 3. Real-CLI contract check (mocks/scripts/contract-check.sh + docs/MOCKS-CONTRACT-CHECK.md)
Mocks catch parser regressions against recordings; they do NOT
catch recordings drifting away from the live agent CLI as that CLI
evolves. The contract check spawns the real CLI alongside the mock
with a fixed deterministic prompt + diffs top-level event-type
distributions.
Deliberately human-driven, not cron-scheduled:
- costs real LLM tokens per invocation
- requires real CLI auth
- maintainer reads the output, not a regex
Suggested triggers per doc: real-CLI release notes mentioning
"output format" / "stream" / "JSON" / "events"; before a parser
refactor; ad-hoc when something looks off.
## Coverage note
README updated to position mocks as "deterministic protocol/parser
coverage" (not "e2e replacement") per mrcfps framing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mocks-golden test): drop import of non-exported ParserKind
Use plain string (the type alias is `string` anyway) — Preflight
typecheck on a31fa71a failed:
tests/mocks-golden.test.ts(29,8): error TS2459: Module
"../src/json-event-stream.js" declares "ParserKind" locally, but
it is not exported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* recording-picker: structured OD_MOCKS_POOL + hard-fail no-match
Siri-Ray review: \`OD_MOCKS_POOL=outcome:failed\` was documented as a
supported selection knob, but the matcher only checked tags and
\`meta.agent\` — so the negative-path pool found 0 candidates and
silently fell through to global random, validating against any
recording instead of a failed trace.
Fix:
- Parse \`<dim>:<value>\` shape and route each dim to the right meta
field: \`outcome\` → \`meta.outcome\`, \`agent\` → \`meta.agent\`,
\`skill\` → \`tags[]\`. Bare values still fall back to tag substring.
- If the env was set and matched nothing, throw with the failing
value and a jq one-liner for inspection. Same loud-fail policy as
OD_MOCKS_TRACE — silent fallback was the original bug.
Verified locally: outcome:failed, agent:codex, skill:agent-browser
all route correctly; outcome:nonsense throws the explicit error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* contract-check.sh: fix lost $PROMPT in mock invocation
Siri-Ray review on e576074a: the mock side wrapped its pipeline in
`bash -c "printf %s \"\$PROMPT\" | ..."` — but $PROMPT was a parent
shell variable, not exported, so the child bash expanded it to an
empty string. Result: the contract check sent the real prompt to the
real CLI and an empty string to the mock, defeating the
same-input invariant the whole script rests on. Also let the mock
randomly select a different trace whenever a maintainer happens to
have OD_MOCKS_BY_PROMPT_HASH=1 in their env.
Fix: drop the inner bash -c entirely; use a subshell that scopes the
PATH overlay and pipes printf into the PATH-resolved mock binary
directly. The subshell limits the PATH change without var-passing.
Verified locally: with prompt-A the mock picks trace 54ec02ee via
hash; prompt-B → 2667e851 via hash; empty prompt (old broken
behavior) → random — confirms the prompt is now actually reaching
the mock under PATH overlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(contrib): add od-contribute skill for non-coder contributors
Adds a Claude Code skill at .claude/skills/od-contribute/ that walks any OD
user — including non-coders — through a first-PR contribution flow:
- Ship a Skill / Design System made with OD
- Translate README / QUICKSTART / CONTRIBUTING to a new language
- Fix a typo / dead link / write a use-case blog post
- Report a high-quality bug (issue path, no PR)
The skill replaces the test-driven dev-loop of auto-github-contributor with
type-specific no-code validators (frontmatter parse, markdown link check,
code-fence balance, structural overlap with reference DESIGN.md files), so
artifact-only contributions don't have to pretend to be code.
This commit only adds files under .claude/ — no product code, no build
config, no runtime dependencies. .gitignore is amended with three explicit
exceptions so the skill is tracked while personal Claude state (sessions,
settings, etc.) stays ignored as before.
Next steps (separate PRs):
- Wire the OD app to mount this skill for its embedded agent
- Add a "Ship to GitHub" UI button in OD that invokes /od-contribute
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* feat(contrib): English-by-default skill + zip installer for non-coders
Two follow-ups to the initial od-contribute skill:
1. Skill content is now English with an explicit instruction at the top
telling the agent to mirror the user's chat language for every user-
facing prompt. Generated artifacts (PR titles, commit messages, PR/
issue body) stay English regardless — GitHub convention.
2. tools/od-contribute-installer/ ships a cross-platform installer that
drops the skill into every supported agent's home dir without the
user opening a terminal:
install.command macOS double-click
install.bat Windows double-click
install.sh Linux
Targets covered:
~/.claude/skills/od-contribute/ Claude Code (native)
~/.claude/commands/od-contribute.md Claude Code slash command
~/.agents/skills/od-contribute/ Codex CLI (canonical)
~/.codex/skills/od-contribute/ Codex CLI (legacy, only
written if ~/.codex/ exists)
Verified Codex CLI reads the same SKILL.md frontmatter format as
Claude Code (source: openai/codex codex-rs/core-skills/src/loader.rs).
Added agents/openai.yaml sidecar inside the skill for Codex picker UX.
3. build-zip.sh produces od-contribute-installer.zip (~37KB) from the
in-repo skill. The zip is meant to be hosted as a GitHub Release
asset; the marketing site button points at:
github.com/nexu-io/open-design/releases/latest/download/od-contribute-installer.zip
(See tools/od-contribute-installer/HOSTING.md for the manual release
recipe; CI workflow can come later.)
The zip itself is gitignored — distribute via Releases, not source.
Still no product code touched, no build config changed.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* refactor(contrib): drop zip installer; ship single curl one-liner
Replace tools/od-contribute-installer/ (4 install scripts + zip build
machinery) with a single self-bootstrapping tools/install-od-contribute.sh.
User flow becomes:
1. Click button on opendesign.so
2. Modal shows: paste this into your AI agent's chat:
curl -sSL https://raw.githubusercontent.com/nexu-io/open-design/main/tools/install-od-contribute.sh | bash
3. Agent runs it via its Bash tool. User never touches a terminal.
4. /od-contribute is live in their next chat.
Why this is better than the zip approach:
* Zero downloads visible to the user — no .zip in their Downloads folder
* Zero unzip step
* Zero terminal window flash (the agent's Bash tool runs in-process)
* Zero per-OS installer files (.command/.bat/.sh) to maintain
* Auto-updates: re-running the one-liner pulls the latest skill from main
The script downloads only the skill subtree (.claude/skills/od-contribute/
and .claude/commands/od-contribute.md) from a GitHub tarball — no `git`
dependency, just curl + tar (universally available).
Targets remain the same:
~/.claude/skills/od-contribute/
~/.claude/commands/od-contribute.md
~/.agents/skills/od-contribute/
~/.codex/skills/od-contribute/ (only if ~/.codex/ exists)
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* chore(contrib): remove leftover zip artifact
Build artifact accidentally committed in the previous commit.
Cleaning up so the binary doesn't live in git history.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): make skill work in sandboxed agents (Codex.app, Cursor)
macOS App Sandbox apps like Codex.app cannot reach the system keychain
where `gh auth login` stores the GitHub token by default. Result: the
skill's check-prereqs.sh fails on `gh auth status` with a misleading
"not authenticated" error, even when gh works fine in the user's regular
shell.
Two changes:
1. config.sh: if GH_TOKEN isn't set in the env, fall back to reading a
.gh-token file at the skill root. Lets a user (or the OD app, or a
future OAuth Device Flow bootstrapper) drop a token there once and
have every skill script pick it up automatically.
2. check-prereqs.sh: accept GH_TOKEN-from-env as a valid auth path
alongside `gh auth status`. When neither works, the error hint now
shows BOTH options:
A) gh auth login from a regular terminal (any agent)
B) gh auth token > <skill>/.gh-token (sandboxed agents)
Verified: in my local Claude Code (where gh has keychain access), the
keychain path still wins and nothing changes. With GH_TOKEN exported,
check-prereqs.sh succeeds without even consulting gh auth status.
Future: implement OAuth Device Flow inside the skill so non-coder users
hitting this in Codex.app can authenticate by clicking a link, no
terminal involved. That's a separate PR.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* chore(contrib): move install script into skill folder (CI policy fix)
The repo's tools/ directory has a strict allowlist policy enforced by
scripts/guard.ts — only AGENTS.md, dev/, pack/, and serve/ are permitted
top-level entries. Moving install-od-contribute.sh out of tools/ and into
.claude/skills/od-contribute/install.sh:
- Satisfies the guard policy (no scripts/guard.ts edit needed)
- Co-locates the install script with the skill it installs (cleaner
mental model: skill folder is self-contained)
- The install URL stays inside the gitignore exception we already
established for .claude/skills/od-contribute/
Public install URL changes from
raw.githubusercontent.com/.../main/tools/install-od-contribute.sh
to
raw.githubusercontent.com/.../main/.claude/skills/od-contribute/install.sh
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): address @nettee/looper review feedback (3 blocking issues)
Three real bugs caught by the looper review bot, all fixed:
1) create-pr.sh:48 — git diff missed untracked files
`git diff --quiet || git diff --cached --quiet` ignored untracked paths,
so the most common contribution shape (a brand-new Skill folder, a new
translation file, a new doc) hit the else branch and pushed an empty
commit. Replaced with `git status --porcelain` which sees untracked,
plus a post-stage sanity check via `git diff --cached --quiet` so we
skip the commit cleanly if everything turned out to be in .gitignore.
2) validate-skill-submission.sh:34 — frontmatter parse too lenient
The awk fence-counter accepted `---` anywhere in the file as the
opening fence. A SKILL.md with prose before the YAML block parsed as
"valid frontmatter" by this script while the actual loaders (Claude
Code + codex-rs/core-skills) required the fence on line 1 and would
reject it. Added an explicit head -n 1 check so leading prose is
rejected with a clear error before awk runs.
3) check-prereqs.sh:87 — gh api user failure swallowed
`GH_USER="$(gh api user --jq .login 2>/dev/null || echo '?')"` set
GH_USER to literal "?" when the API call failed (revoked token,
missing 'repo' scope, network), then the script exited READY=1.
Downstream that propagated to TARGET_FORK="?/open-design" and
blew up at push time.
Dropped the `|| echo '?'` fallback. An empty GH_USER now triggers a
structured error with three common causes and the recovery command,
and exits 2.
While here, also fixed a related bug: this script sources config.sh
which has `set -euo pipefail`, so -e leaked in and aborted the
script silently the moment any check failed (instead of accumulating
diagnostics like the original auto-github-contributor design
intended). Added explicit `set +e; set -uo pipefail` after sourcing
to restore the "keep checking past failures" behavior the comment
on line 7 promised.
Smoke-tested all four fixes locally:
- create-pr.sh: git status --porcelain correctly sees untracked files
- validator: rejects SKILL.md starting with prose, passes well-formed
- check-prereqs.sh: with stubbed gh that fails `gh api user`, now
exits 2 with the structured error (was: silent exit 1)
- check-prereqs.sh: happy path on real machine unchanged
Thanks @nettee for the careful review.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): macOS Bash 3.2 + over-strict link validator (review round 2)
Two more blocking issues from the looper review, plus one related bug I
caught while re-testing on real OD docs.
1) discover-i18n-gaps.sh: removed Bash 4 dep (declare -A)
macOS still ships Bash 3.2.57 by default and most agent-spawned bash
subprocesses inherit that. `declare -A SEEN_LANG=()` failed with
`declare: -A: invalid option`, crashing Step 3b before any translation
target could be shown.
Replaced the associative array with a newline-delimited string set
(\n<lang>\n bracket form to avoid prefix-overlap false matches like
zh vs zh-CN). Verified end-to-end on /bin/bash 3.2.57 against the
actual OD repo: returns the correct 28 stale-translation rows
across the four English source docs.
Also fixed a latent path-stripping bug in the same loop: `find`
emits `./README.zh-CN.md` with leading `./`, so `${path#README.}`
wasn't stripping the prefix at all. Switched to basename-first.
2) validate-markdown.sh: --reference flag for i18n / docs-edit flows
The validator was treating every relative link target as a file path
and failing on slugs like `skills/blog-post/` that are website
router routes, not files in the checkout. A structure-preserving
translation of README.md couldn't pass even when the user changed
nothing except language.
Added --reference <orig> flag. The validator now builds a "known
already-broken" set of refs from the source file and excuses those
in the new file. Newly-introduced broken refs still fail.
Without --reference (e.g. brand-new blog file with no prior version),
the relative-ref check is skipped entirely with a SKIP note — since
we can't tell route slugs from file paths in isolation, failing
would be wrong. Code-fence balance + external-link health still run.
Updated SKILL.md so the i18n branch (3b.6) and the docs branch
(3c.6) call validate-markdown.sh with --reference pointing at the
English source / HEAD revision respectively.
3) (caught while testing) URL extraction regex too loose
`grep -oE 'https?://[^) ]+'` was capturing trailing quotes from HTML
<img src="..."> tags in OD's README, e.g.
https://cms-assets.youmind.com/.../foo.jpg"
The trailing `"` made the curl HEAD return 404. Tightened the
character class to also stop at `"`, `'`, `<`, `>`, `[`, `]`.
With this fix, README.md now passes all checks (20 external links
verified 2xx/3xx).
Smoke-tested on macOS /bin/bash 3.2.57 with the actual nexu-io/open-design
working copy. All four scenarios behave correctly:
- README.md without --reference → SKIP relative-ref check, PASS overall
- README.md with --reference itself → 34 refs excused as pre-existing, PASS
- Newly-introduced broken ref → FAIL (regression catch preserved)
- Old test cases (skill validator, prereq check) → still pass
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): preserve .gh-token across install.sh reruns
`install_skill_to()` did `rm -rf $dest` before copying in the new skill,
which wiped any user-local state files. The most consequential one is
`.gh-token` — sandboxed agents (Codex.app, Cursor) write a GitHub token
there because they can't reach the macOS keychain (see check-prereqs.sh's
hint and config.sh's fallback path).
Effect: the documented upgrade path ("re-run the curl one-liner to pull
the latest skill") would silently lose the token on every refresh, and
the very next /od-contribute run would fail at the prereq gate with
"no GitHub credentials available", forcing the user back through manual
token setup. This affects exactly the audience the PR is aimed at.
Fix: stash any file in PRESERVE=(.gh-token) to a tempdir before rm -rf,
restore after the copy, re-chmod 600 on the way back. Test:
1. Pre-seed .gh-token in all three target dirs
2. Run installer
3. Verify all three tokens still present, contents unchanged, perms 600
Centralized the preserved-state list as PRESERVE=() so future per-user
state (e.g. an OAuth-flow-saved refresh token) only has to be added in
one place.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): i18n stale false-positive + tier markdown link check (round 4)
Two more blocking issues from looper, both real.
1) discover-i18n-gaps.sh: false-stale on same-commit translations
`git log --since=@<epoch>` is INCLUSIVE of the boundary epoch, so when
the English source and a translation get touched in the SAME commit
(a very common pattern: bulk i18n refresh, structural edits applied
across all locales), the shared commit was counted toward
english_commits_since_translation. Result: an already-current
translation was reported with `status="stale", english_commits_since_
translation=1`, and Step 3b would suggest it for refresh — driving
users into no-op PRs.
Reproduced exactly per looper's case: README.md and README.uk.md both
have last commit 338cb4d at epoch 1779948707; the OLD predicate
returned 1, the NEW predicate returns 0.
Switched commits_between() from `--since=@<epoch>` math to commit
ancestry: `git rev-list <tr_sha>..HEAD -- <newer>`. tr_sha..HEAD reads
"commits reachable from HEAD but not from tr_sha", which correctly
excludes the shared tip when both files were last touched together.
2) validate-markdown.sh: brand-new files bypassed local link check
The previous fix skipped relative-ref validation entirely when
--reference was absent. That covered slug-style refs (good) but also
covered explicit `./foo.md` and `../bar/baz.md` style refs (bad).
Step 3c (new blog post) doesn't pass --reference, so a contribution
could ship with `[broken](./missing.md)` and pass the validator.
Tiered the relative-ref check:
- Image refs (``) — ALWAYS validated. Markdown image
syntax is never a website route.
- Refs starting with `./` or `../` — ALWAYS validated. Explicit
relative paths are unambiguous file references.
- Other link refs (`skills/blog-post/` style) — only validated
when --reference is supplied; otherwise skipped (could be route).
In all cases, refs already broken in --reference (when supplied) are
excused as pre-existing rather than reported as regressions.
Verified against looper's exact repro (`[new broken](./missing.md)`
in a brand-new file with no --reference): now correctly fails. Also
verified ambiguous-slug test (`skills/blog-post/`) still skips
without --reference, image refs always check, and README.md regression
tests both with and without --reference still pass.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
* fix(contrib): catch bare-path refs in validators (review round 5)
Two narrow follow-ups to the round-4 tiered link checks:
- validate-skill-submission.sh: scan every non-URL, non-anchor markdown
link target in SKILL.md (not just `./` / `../` prefixed paths). Plain
intra-skill refs like `[ref](references/foo.md)` were previously
ignored by the regex, letting broken bundles pass. Escape detection
switches to lexical (segment count) instead of `cd … && pwd -P`, so a
missing intermediate directory no longer masquerades as an escape.
- validate-markdown.sh: treat file-like targets (`*.md`, `*.png`,
`*.svg`, image/asset/script extensions) as on-disk refs even without
`--reference`. `[doc](missing.md)` is unambiguously a sibling file,
not a website route, and Step 3c (new docs/blog) had no `--reference`
to fall back on. Slug-style refs without an extension still get
skipped without `--reference`.
Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code)
* fix(contrib): scratch leak + dedupe gate + workdir reuse (review round 6)
Three blocking issues from looper round 6, all fixed.
1) create-pr.sh + setup-workspace.sh: .od-contrib/ scratch leaked into PR
`git add -A` in create-pr.sh staged everything in the worktree, including
the skill's internal scratch dir (.od-contrib/type.txt, .od-contrib/
slug.txt, .od-contrib/PR-BODY.md created by setup-workspace.sh and the
render step). OD's .gitignore doesn't exclude .od-contrib/, so every PR
opened through this flow shipped those bookkeeping files in the user's
contribution diff.
Two layers of defense:
- setup-workspace.sh now writes `.od-contrib/` to .git/info/exclude
when preparing the workdir (repo-local exclude, not committed).
- create-pr.sh now uses an explicit pathspec `:!:.od-contrib` on its
git status / git add calls. So even if a workdir was prepared
differently, this script alone refuses to stage the scratch dir.
Verified with a temp repo containing both .od-contrib/PR-BODY.md and a
user file: only the user file lands in the index after `git add -A
-- . :!:.od-contrib`.
2) create-issue.sh: dedupe gate didn't actually gate
The --dedupe-keywords flag printed search hits to stderr but then
unconditionally fell through to `gh issue create`. The `|| true` after
the gh search pipeline also swallowed network/jq failures, so a broken
search looked identical to "no duplicates found" — and the issue got
created either way. The user never got a real chance to choose
"comment on existing / open anyway / cancel".
Now:
- Run gh search and jq as separate steps; either failure exits 2 with
a structured REASON=search_failed/parse_failed.
- If matches > 0 AND --allow-duplicates was NOT passed, exit 3 with
REASON=duplicates_found and MATCH_COUNT=N. Caller must explicitly
re-run with --allow-duplicates after surfacing matches to the user.
- The script now requires `jq` (added od::require jq) since we
actually parse JSON.
- Updated the docstring at the top so the caller contract (ask the
user, then re-invoke with --allow-duplicates) is explicit.
Verified: searching keyword "preview" against nexu-io/open-design
matches 5 open issues; the script exits 3 and never calls
`gh issue create`.
3) setup-workspace.sh: same-day workdir reuse leaked stale state
`SESSION_DIR=<TYPE>-<SLUG>-<YYYYMMDD>` reused the same directory for
every same-day, same-(type,slug) invocation. The most acute case:
SKILL.md 3b.1 calls `setup-workspace.sh i18n translate` BEFORE the
user has picked a doc/language, so every i18n attempt on the same
day landed in `i18n-translate-<date>/` — and untracked files from an
abandoned earlier translation survived `git checkout`/`pull` and
leaked into the next user's run.
Two changes:
- Bumped tag to second precision: `<YYYYMMDD>-<HHMMSS>`. Two human-
paced sessions in the same second is vanishingly rare. Verified
two rapid runs produce different tags (114208 vs 114209).
- When a workdir IS reused (same SESSION_TAG passed in explicitly,
or rare clock collision), now does `git reset --hard HEAD` and
`git clean -fdx` first so the run starts from a known-good base
instead of inheriting prior occupant state.
The branch name now also tracks the timestamp tag, so two runs can't
accidentally end up on the same feature branch either.
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: leilei926524-tech <leilei926524-tech@users.noreply.github.com>
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
Forward-ports chaoxiaoche's Studio toolbar work from #3081 onto current
main. The preview toolbar drops to 4 controls — Comment, Mark (the merged
Draw/Screenshot tool with box-select + pen sub-tools), Edit, Comments —
matching the latest design. The standalone Screenshot button and its
copy-to-clipboard path are removed; capture now flows through the mark
overlay. Also carries #3081's comment select-all/clear-selection panel and
keeps the Draw send guard added in #3270 (Send disabled mid-run, Queue stays).
Reconciled with main work that postdates #3081's base so nothing is lost:
- Preserves #2190's preview iframe keep-alive pool and the AnnotationHoverPopover
hover card (re-added on top of #3081's BoardComposerPopover, with its own
anchor helper so it doesn't clash with the composer popover anchoring).
- i18n: keeps every locale key main added; adopts #3081's mark wording.
Behavior change: the comment side-panel Clear now deselects instead of
batch-deleting selected comments (per #3081); per-comment delete and
send-selected remain.
Validation: pnpm --filter @open-design/web typecheck (clean),
full web vitest (2354 passed), pnpm guard.
Co-authored-by: chaoxiaoche <fanzhen910412@gmail.com>
* chore: bump vela cli to 0.0.4-test.0
* chore: refresh lockfile for vela cli 0.0.4-test.0
* chore(nix): refresh pnpm deps hash
* fix: materialize electron before mac release checks
* fix: rebuild electron when mac framework links are invalid
* revert: drop release workflow experiments
* chore(nix): refresh pnpm deps hash
* fix: stop blocking beta mac release on electron symlink preflight
* fix: stop using custom electron dist for beta mac packaging
* fix: guard oversized chat images and opencode overflow
* chore: bump vela cli to 0.0.4
* chore(nix): refresh pnpm deps hash
* fix(daemon): surface prompt-image stat failures instead of dropping them
resolveSafePromptImagePaths only swallowed unresolvable path input; once a
path was confirmed inside UPLOAD_DIR and existed, a statSync failure
(EACCES/EPERM, a file vanishing mid-run) silently dropped the image and let
the run continue without that prompt context. Since this helper is now also
the 1 MB enforcement point, that turned an infra/validation failure into a
'successful' run with missing required context.
Collect those into a new failedImages bucket and fail the run with
INTERNAL_ERROR at the call site, mirroring the oversized-image guard. Add a
unit test covering statSync throwing.
---------
Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
* feat(landing-page): surface Discord + X in header, restructure site footer
Two related public-chrome adjustments:
- **Header gains compact Discord + X icon buttons.** Both community
channels were previously buried in the footer, so the typical
visitor never saw them on a page-deep scroll. They now sit before
the Download / Star CTAs in `nav-side`, share the ghost-button
outline language, and stay icon-only with `aria-label` so they
read as social affordances rather than competing with the text
CTAs. At ≤1080px the icon buttons hide alongside the existing
ghost CTA, so the bar still collapses cleanly into the hamburger
panel — Star stays in the bar at every breakpoint.
- **Footer restructured into 4 columns: Products / Plugins /
Resources / Connect.** The old `Plugins / Open Design / Connect`
three-column layout muddled three different things — sister
products, the artifact catalogue, and contributor channels —
under one roof, so visitors hunting for "the other thing this
team makes" had nowhere obvious to go.
- **Products** (new) lists the team's apps: Open Design (links
to homepage) and HTML Anything. Two entries by design — adding
more products without an editorial pass would dilute the
column.
- **Plugins** mirrors the topbar `Plugins` dropdown verbatim:
Templates / Skills / Systems / Craft, with no count prefix on
Systems / Craft so it reads identically to the nav.
- **Resources** (renamed from `Open Design`) carries the
docs-style links: Official source / Quickstart / Agents locaux
/ Compare / Claude Design alternative. The old column heading
was confusing because the OD logo + brand name already sit
under the column.
- **Connect** gains an X / Twitter row pointing at
`@nexudotio`. The brand entries on this column are
contributor / community surfaces only — code, releases,
chat, social, RSS, contact form.
Implementation:
- `_components/header.tsx` — `DISCORD` and `X_TWITTER` consts at
the top alongside `REPO`. Two `<a class="nav-icon">` blocks with
inline SVG before the existing Download / Star CTAs.
- `_components/site-footer.astro` — `HTML_ANYTHING` and `NEXU_IO`
consts. `<div class="sub-footer-col">` re-ordered to put
Products first, Plugins second (no longer carries `counts.*`
values), Resources third, Connect fourth (with the new X / Twitter
row).
- `globals.css` — `.nav-icon` rule cloned from the ghost CTA's
visual language (transparent + 1px line, fills on hover) but
square (36×36 round) so it reads as a social-icon affordance.
Added `display: none` for `.nav-side .nav-icon` to the existing
≤1080px and ≤880px media queries so the icons follow the same
collapse behaviour as the Download CTA.
- `sub-pages.css` — `.sub-footer-grid` switches from
`1.6fr 1fr 1fr 1fr` to `1.4fr 1fr 1fr 1fr 1fr` (brand + 4
columns). At ≤1080px it falls back to a 3-column shape so each
column has room to breathe; at ≤720px it stays a single column
(existing behaviour).
- `i18n.ts` — adds `products`, `resources`, `xTwitter`,
`sisterProjects`, `htmlAnything`, `nexuIo` to `LandingUiCopy.footer`
(the last three are kept around even though `sisterProjects` is no
longer rendered after the column was renamed Products — they're
harmless and avoid churning the type if a future iteration brings
the Sister-projects framing back). All 17 non-English landing
locales gain translations for the new keys via the existing
`LOCALIZED_LANDING_FOOTER_COPY` map (and the `LANDING_UI_COPY_OVERRIDES`
block for `zh` / `zh-tw`). Translations were generated with
`claude-haiku-4-5` over OpenRouter, with explicit instructions
to keep "Open Design", "HTML Anything", and "X / Twitter" in
English and to render "Products" / "Resources" in sentence case
per locale convention. Spot-checked against rendered pages on
`/zh/`, `/zh-tw/`, `/ja/`, `/ko/`, `/de/`, `/fr/` (and `/ar/` for
RTL) for natural phrasing.
Validation: `pnpm --filter @open-design/landing-page typecheck` ->
0 errors / 0 warnings; local dev server smoke-tested on en root
(`/html-anything/`) and 5 locale variants (`/zh/`, `/zh-tw/`,
`/ja/`, `/de/`, `/fr/`) — header renders 2 nav-icon buttons,
footer renders 4 localized column headings in the correct order
with the right link targets.
* fix(landing-page): address PR #3230 review — locale-aware HTML Anything link + drop unused const
Two non-blocking inline review points from @PerishCode on PR #3230:
- The HTML Anything entry in the new Products column hardcoded
`https://open-design.ai/html-anything/` via a top-level
`HTML_ANYTHING` const, but `/html-anything/` is a real localized
route in this app (`pages/[locale]/html-anything/index.astro`)
and `open-design.ai` is the same site's live domain. A visitor
on `/zh/…` clicking through landed on the English route and lost
locale context, and hardcoding the production domain meant a
preview build would surface a link that bounces visitors back
to prod. Switch to `href('/html-anything/')` so the locale prefix
+ the current site's domain (resolved by `localizedHref`) are
honored, matching every other footer link.
- `NEXU_IO` was declared at the top of the component but never
referenced — leftover from an earlier iteration that listed
`nexu.io` as a Sister-projects entry before the column was
renamed Products and reduced to OD + HTML Anything. Removed.
No behavior change beyond the locale routing fix; the i18n keys
and column structure stay as they landed in the original commit.
* fix(landing-page): correct nav-icon comment to match actual responsive behaviour
The JSX comment introduced for the new Discord + X icon buttons in
PR #3230 claimed the icons "survive at narrow widths while text-only
nav items get pushed off". The CSS that shipped in the same PR does
the opposite: both `@media (max-width: 1080px)` and `@media (max-width:
880px)` blocks add `.nav-side .nav-icon { display: none; }`, so at
narrow widths the icons collapse alongside the ghost Download CTA
while the text nav <ul> moves into the hamburger panel — only the
Star CTA remains visible in the bar.
Rewrite the comment to describe the actual responsive contract so
the next reader of `header.tsx` doesn't have to cross-reference
`globals.css` to figure out which surface stays. Reviewer flag from
@PerishCode on PR #3230.
No code-path change; comment-only.
* fix(landing-page): correct sub-footer 1080px comment to describe actual 3-column grid
The CSS comment introduced for the new sub-footer grid claimed the
≤1080px breakpoint drops to "brand + 2x2 grid of columns" — but the
rule produces a 3-column grid, not a 2x2.
`.sub-footer-grid` has 5 children at this breakpoint (the brand
block + the four footer columns) and `.sub-footer-brand` carries
no `grid-column` span, so with `grid-template-columns: 1.6fr
repeat(2, 1fr)` they flow as: row 1 = brand · Products · Plugins,
row 2 = Resources · Connect · empty cell. The brand sits inline
with two columns rather than on its own, and the four content
columns are not a clean 2x2.
The layout itself is fine; only the comment misleads the next
reader about how the columns wrap. Same flavor as the `header.tsx`
icon comment fixed in 744daec — describe what the rule actually
does so the comment doesn't drift from the CSS. Reviewer flag
from @PerishCode on PR #3230.
Comment-only change.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* 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>
* 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: portal staged-preview-modal to document.body to escape .composer stacking context
The .staged-preview-modal was rendered inside .composer (position:relative;
z-index:1), which created a local stacking context. The modal's z-index:1200
was scoped within .composer's context instead of the root stacking context,
preventing it from properly covering .app-chrome-header when active.
Fix by rendering the modal via createPortal to document.body, consistent
with other modals in the codebase (ConversationsMenu, FileViewer, etc.).
related issue 3154
The scroll handler in ChatPane called setScrolledFromBottom on every
scroll event. During streaming, programmatic scrollTop assignments
(followLatestIfPinned via rAF + ResizeObserver) emit synchronous scroll
events while React is still committing prior updates, scheduling a
setState per tick. React eventually trips its update-depth guard with
'Maximum update depth exceeded' (issue surfaces in Next.js 16
Turbopack dev consoles).
Use a functional updater that returns prev when the boolean is
unchanged so React skips the re-render entirely. This breaks the
cascade at the call site without touching the auto-follow logic.
Co-authored-by: nicejames <nicejames@gmail.com>
* fix(platform): support live system proxy changes
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Honor lowercase proxy env vars within a single source before merging proxy-aware envs.\n\nGenerated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Refresh provider request proxy env on each dispatcher creation and cover it with a focused regression test.
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): enable node env proxy for user proxy vars
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* fix(platform): support live system proxy changes
Generated-By: looper 0.9.1 (runner=fixer, agent=opencode)
* 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>
Use danger-full-access when WSL_DISTRO_NAME is set and pass
default_permissions=":workspace" so newer Codex builds can write
inside the project directory instead of staying read-only.
* fix(daemon): reconcile missing artifact manifests on run end (#2893)
When an agent writes HTML via write_file instead of create_artifact,
no .artifact.json manifest sidecar is created. If the run then
terminates (inactivity watchdog, user cancel, or process exit), the
HTML file exists on disk but the manifest is missing — breaking the
artifact panel, finalize, and export flows.
Add a best-effort reconciliation step in the child.on('close') handler
that lists project HTML files and calls reconcileHtmlArtifactManifest
for any missing sidecars. The IIFE runs asynchronously after
design.runs.finish() so it never blocks run finalisation.
* fix(daemon): scope run-end reconciliation to files modified during the run
The review on #3110 flagged that listing the entire project tree and
reconciling every HTML file without a sidecar is too broad — for
imported-folder projects (metadata.baseDir), pre-existing HTML files
would receive spurious manifests.
Record runStartTimeMs at the beginning of startChatRun and filter the
reconciliation loop to only touch HTML files whose mtime >= that
timestamp. Add a regression test that backdates a pre-existing HTML
file and verifies it is skipped while a new file is reconciled.
* test(daemon): fix mtime ordering in reconciliation regression test
The runStartTimeMs was recorded after writing the new file, so its
mtime fell before the threshold and the reconciliation filter skipped
it. Move the timestamp capture to before the write to match the real
startChatRun semantics.
Increase gap from 2px to 4px in .subtab-pill to prevent the active
tab's box-shadow from visually overlapping adjacent tabs. The previous
2px gap was insufficient for the shadow to render cleanly between tabs,
causing the Style tab to obscure neighboring Content/Attributes/HTML tabs.
Closes#2904
The "Lint changed blog SEO" and "Guard blog URL changes" steps in
landing-page-ci.yml fell back to the literal "HEAD^" when no base SHA was
available. On merge_group (and first-push) events there is no
pull_request.base.sha / before, so BASE became "HEAD^", and the
blog-indexing scripts' assertSafeGitRef (regex /^[A-Za-z0-9_./:-]+$/,
which forbids "^") threw `Unsafe git ref for base: HEAD^`, failing the
merge queue for every PR.
Resolve the fallback to a concrete commit with `git rev-parse HEAD^`
(checkout uses fetch-depth: 0, so it is always available), mirroring
blog-indexing-on-deploy.yml. The resulting 40-hex SHA passes the ref guard.
* ci: trigger PR exploration via maintainer "/explore" comment (no approval)
Add a low-friction way to run the sandbox exploration: a maintainer
comments "/explore" on a PR.
- on: issue_comment (kept workflow_dispatch). The job `if` allows the
comment path only when it is on a PR and the commenter has write access
(author_association OWNER/MEMBER/COLLABORATOR), so randoms cannot trigger
it; untrusted PR code still runs only inside the Docker sandbox.
- Drop the agent-pr-explore environment approval gate: both triggers are
already write-gated and there is no auto-trigger, so the extra manual
approval is redundant. R2 creds are repo-level secrets (no env-scoped
secrets), so they stay available without the environment.
- Feedback: 👀 reaction on the command + a placeholder comment carrying
the report marker (so the run yields one evolving comment), 🚀 on
success, and 👎 + a failure note (with the run link) on failure.
Does not auto-run on every PR, so unrelated PRs stay clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: don't clobber a produced report with the /explore failure note
Review: the failure-feedback step ran after the always() report step, so
on the failure-with-report case (sandbox wrote a report then exited
non-zero) it overwrote the just-posted report with the generic "failed
before producing a report" note — losing the useful output.
Guard it: if the report file exists, leave the posted report in place and
skip the failure note/reaction. Only post the short failure note when no
report was produced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add tracking for Automations, Plugin Detail, and Plugin Loop
- RoutinesSection: track new_automation, create, save, cancel, run_now,
edit, pause, resume, delete, history button clicks
- PluginDetailView: track back navigation and use_plugin action
- PluginLoopHome: track clear_active, submit, card_details, card_use
- Extend AutomationsClickProps with new CRUD elements
- Add PluginDetailClickProps and PluginLoopClickProps contracts
* fix: address review comments on plugin/automation tracking
- Extract onBack handler in PluginDetailView to cover both error-path
and success-path back buttons with tracking
- Move create/save tracking from submit button onClick into the form
submit handler to capture keyboard submissions and avoid false
positives from validation failures
* fix: move submit tracking into submit() handler in PluginLoopHome
Same fix as RoutinesSection: tracking now fires inside submit() so
keyboard Enter submissions are captured and the !trimmed guard
prevents false positives.
---------
Co-authored-by: qiongyu1999 <2694684348@qq.com>
* fix(amr): close ACP stdin on abort so vela tears down OpenCode
When an AMR (vela) run is cancelled, attachAcpSession.abort() sent a
`session/cancel` RPC but left the child's stdin open. The vela ACP bridge
keeps running until it sees EOF (or is signaled), and it only shuts down
its private OpenCode `serve` process on a clean exit — so on abort the
OpenCode server lingered until the caller's SIGTERM fallback, and leaked
entirely if the parent was killed before cleanup ran.
End stdin after sending the cancel (mirroring the clean-completion path)
so the agent receives EOF and shuts down its own runtime promptly,
independent of signal timing.
* fix(amr): end stdin on abort even before session/new resolves
Addresses review on #3097: abort() still returned early when sessionId
was unset, so the stdin EOF only happened after session/new completed.
Cancelling during ACP startup (before the session exists) left the
OpenCode-teardown window open until the caller's SIGTERM fallback — and a
parent hard-kill before that could still strand the private OpenCode
process.
Move stdin.end() out of the sessionId guard so abort always closes stdin
when the pipe is writable; gate only the session/cancel RPC on sessionId.
Add a regression test that aborts during startup and asserts stdin is
ended with no session/cancel emitted.
* 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): directly import remixicon.css in layout to fix icon font loading
* fix(web): resolve remixicon font not rendering via public/ directory
RemixIcon icons were blank in Chrome and showed garbled PUA codepoints
in Firefox. The font file was referenced with a relative URL in CSS
loaded through @import, which Turbopack does not resolve.
Fix: copy remixicon.woff2 to public/ and change the @font-face src
to an absolute URL (url("/remixicon.woff2")), so the browser fetches
it directly from the server root without relying on the bundler.
Per product feedback: the Homepage button (rendered when a plugin's
manifest carries a `homepage` field that differs from its
`sourceUrl`) sent visitors off to upstream-author or original-source
URLs before they had a chance to explore the plugin via Open Design
itself. On a marketing detail page that's a leak, not a feature.
Removes the conditional block that rendered
`<a class="btn btn-ghost">Homepage ↗</a>` between the GitHub link
and the Share button. The header-action row is now exactly three
controls everywhere: Use this plugin → · Find on GitHub → · Share ↗.
The `plugin.homepage` data field stays available on the
`BundledPluginRecord` shape since the in-app catalog row, JSON-LD,
and any future author-bio surface can still consume it. The
`pcopy.detailHomepage` i18n key (with full 18-locale coverage) stays
for the same reason — `PluginsCopy` is `Partial<>` everywhere and
removing it would mean a 18-locale block edit for zero functional
gain.
apps/landing-page typecheck stays at 0 errors.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* ci: add idempotent provision script for the agent-pr-explore runner
The self-hosted runner's setup was hand-assembled and easy to lose on a
rebuild — most dangerously the codex-acp pin: expect-cli bundles
codex-acp 0.10, which is incompatible with ChatGPT-account auth (every
model rejected); we run 0.15, but any expect-cli reinstall silently
reverts it and breaks the agent.
Add a self-contained, idempotent provision script that brings the
runner's config layer back to a working state and is safe to re-run:
codex model pin (gpt-5.4), the codex-acp 0.15 pin (npm pack + extract +
chmod), deploy-key generation, base-repo git mirror seed/refresh,
pnpm-store/reports dirs, the weekly image-refresh helper + cron, and the
readiness self-check helper. The header documents the manual/secret
steps it intentionally does not automate (base toolchain + colima, the
interactive `codex login`, registering the deploy key on the repo, and
registering the Actions runner service).
Verified idempotent against the live runner (all checks pass, no config
disturbed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: provision — update codex model key in place, don't truncate config.toml
Review: step 2 overwrote the whole ~/.codex/config.toml with just the
model line whenever the exact pin wasn't already present, dropping any
other Codex settings on a re-run — destructive, contradicting the
idempotent goal. Now: replace an existing `model =` line in place (sed),
append only when the key is absent, and leave the rest of config.toml
untouched. Verified preservation locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: provision — create ~/.ssh before ssh-keygen on fresh host
Review: on the fresh-rebuild path this script targets, ~/.ssh usually
does not exist, so `ssh-keygen -f ~/.ssh/od_agent_deploy` fails with
"No such file or directory" and the deploy key (and downstream mirror
bootstrap) never gets created. mkdir -p the key's parent dir (chmod 700)
before keygen, and only print the pubkey when it actually exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Packaged diagnostics bundles never contained the daemon or web
`latest.log` — the very logs that hold the agent/critique run flow — so
support exports could not explain "sent prompt to the agent, then
nothing happened" reports.
Root cause: the sidecar `base` means different things per launch path.
tools-dev passes the pre-namespace source root, so
`resolveNamespaceRoot(base, namespace)` is correct. But the packaged
orchestrator launches every child with `base = <namespaceRoot>/runtime`
(apps/packaged/src/{paths,sidecars}.ts) while logs live a level up at
`<namespaceRoot>/logs`. The diagnostics builders re-appended the
namespace and resolved every log to
`<namespaceRoot>/runtime/<namespace>/logs/...` → ENOENT. renderer.log
only survived by accident: the desktop main process wrote it to the
same wrong path the reader looked in.
Add `resolveRuntimeNamespaceRoot(runtime, contract, runtimeMode)` to
`@open-design/sidecar` which walks up out of the `runtime/` dir in
packaged (runtime-mode) launches and falls back to the dev layout
otherwise. Route the desktop renderer-log path and both diagnostics
exporters (desktop IPC + daemon HTTP) through it so writer and reader
stay in lockstep and renderer.log lands next to the desktop log dir.
Tests: sidecar unit specs for both layouts; a daemon export spec that
writes a real `<namespaceRoot>/logs/daemon/latest.log` and asserts the
bundle captures its contents (red on main → ENOENT placeholder, green
here).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
* 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>
* ci: retry PR-context gh calls so a transient API blip doesn't abort the run
The early PR-context gathering calls `gh pr diff`, `gh pr view`, and
`gh api .../files`. gh hits api.github.com under the hood, and a single
transient timeout/5xx there aborts the whole run before any exploration
(seen on #3083: "could not find pull request diff: Get \"https://api.
github.com/...\": net/http timeout"). These were the only network calls
in the run without a retry (source fetch + npm already retry).
Add a small gh_retry helper (4 attempts, linear backoff) and wrap the
three read-only context calls. gh writes nothing to stdout on a failed
API call, so retrying is safe even for the calls piped into the context
file; the retry warning goes to stderr.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: address review — buffer gh retries to files (no paginated duplication)
Review (Siri-Ray): wrapping `gh api --paginate` retries inline while the
context block is redirected to the file means a mid-pagination failure
leaves partial pages in the context, and the retry appends them again —
duplicating the patches section and burning the context budget the agent
reads.
Replace the in-pipe gh_retry with gh_retry_file: each call buffers to its
own file per attempt (`>` truncates on open, so a failed/partial attempt
is discarded before the next), and the context block just cats the
finished files. Fetch PR body + patches to files up front, then assemble.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewers asked for at-a-glance outcomes. Instruct the agent to begin each
"Cases Tested" bullet with a status emoji (✅ pass / ❌ fail / ⚠️ warning /
⚪ inconclusive) and a bold case name, so the report shows which checks
passed or failed without reading each line.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: clean agent report (write-to-file) + slim artifacts/uploads
Four related cleanups to the agent PR exploration output:
1. Clean report. The PR comment / report.md was assembled by dumping the
entire verbose expect.log (ACP init logs, "Git failed" warnings, the
~24KB echoed prompt, ANSI codes, progress checklist) under the trace
header -- ~28KB of noise. Instead, instruct the agent to write its
final Markdown report to a file via its file-write tool, and have the
runner read that file directly. Verified: Codex writes a clean report
to the given absolute path. Falls back to an inconclusive note if the
agent did not finish.
2. Drop duplicate trace/video. The script copied
playwright-smoke-trace.zip -> playwright-trace.zip (a ~28MB legacy
duplicate) and the webm likewise, and uploaded both to R2. Keep only
the canonical smoke-named artifacts.
3. Slim the GitHub artifact. The trace zips and videos are already on R2;
exclude *.zip / *.webm from the uploaded artifact so it drops from
~56MB to <1MB (report + logs only).
4. Persist report on the runner. Copy the report / agent-report /
expect.log / trace URL to a stable host dir
($HOME/.cache/agent-pr-explore/reports/pr-<n>) so dry runs
(skip_comment) can be inspected without downloading the artifact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: address review — keep advisory reports + recursive artifact excludes
Review findings on the report/artifact cleanup:
1. Regression fix: the non-app-surface and deterministic-verifier branches
write their pre-baked advisory report (Inconclusive / Pass / Fail) and
never run the agent, so they don't produce agent-report.md. After
switching write_agent_report_artifact to read only agent-report.md they
fell through to the "agent did not write a final report" fallback,
dropping the real advisory (and mis-reporting on .github-only PRs like
this one). Fix: those branches now write their advisory directly to
$agent_report_file — single source of truth for the report body.
2. Recursive artifact excludes: the source Playwright recording lives at
artifacts/playwright-video/<uuid>.webm; non-recursive !*.webm / !*.zip
didn't match the subdirectory. Use **/*.zip and **/*.webm so the slim
actually holds.
3. Drop the now-dangling summary.legacyTrace field (the legacy trace copy
is no longer produced), matching the legacyVideo removal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(landing): add Community link to top nav
Adds a 'Community' entry to the landing-page nav between Blog and the
Star CTA, linking to /community/ which Cloudflare Pages 302-redirects to
the contributors honor-cards page (currently a Vercel deploy).
Translations added for all 18 locales. The nav slot was previously empty
after Blog because Contact had been intentionally pulled from the bar
(left as a footer + #contact anchor).
* fix(landing): use literal /community/ href so non-default locales don't 404
PerishCode review caught that href('/community/') routes through
localizedHref and produces /zh/community/, /ja/community/, etc. The
_redirects rule only matches the literal /community/ and the
[locale]/[...path].astro catch-all does not generate community pages,
so 17 of the 18 translated locales would have hit a Cloudflare 404.
The destination is a single non-locale-aware external page, so skip
the locale prefix entirely — same shape as the GitHub Star and
Download CTAs.
* feat(landing): host community + ambassadors page first-party
Lands the contributors / ambassadors page as a static asset at
apps/landing-page/public/community/index.html, served at /community/
on open-design.ai. Drops the temporary 302 to the Vercel preview URL
(d5458c46-…vercel.app) — that hostname was a deploy-time UUID Vercel
could recycle, which the reviewer correctly flagged as a follow-up.
The page now opens with an Ambassadors section: vocation, patronage,
covenant — three columns of the program in Renaissance-atelier voice,
with a single Apply on Discord CTA pointing at the ambassador channel
(discord.gg/2p7Ajbxw3h). Maintainers / leaderboards / good-first-issues
sit below as before. Header.tsx comment updated to point at the new
source of truth instead of the deleted redirect rule.
* fix(community): drop time-bound claims, tighten bot heuristic, drop dead CORE_TEAM entry
PerishCode review on ff1cd44b flagged three correctness issues with
the static community page. Addressing each:
* The 'This week's signal' / 'This week's leader' / 'Last 7 days' /
'PRs · 7d' framing made promises a frozen RANKING_SNAPSHOT can't
keep — three weeks from build, the page would be calling the
2026-05-26 leaderboard 'this week's leader.' Renamed to time-neutral
copy ('Recent signal', 'A recent leader', 'Snapshot', 'Recent PRs')
and dropped the snapshot's 'since' field so we don't pin a window
we can't honour. Real refresh pipeline is a follow-up.
* 'Showing first N · resets every 30 minutes' didn't describe the code
(no caching of any kind exists; each page load re-hits /search/issues
and /users/:login). Replaced with a truthful 'Showing first N open
good-first-issues.'
* The bot exclusion heuristic used substring match on bot/cursor/agent,
which would silently drop real logins like 'agentina', 'cursorsmith',
'robothai'. Tightened to a whole-token regex (/(?:^|[-_])(bot|cursor|
agent)(?:$|[-_])/) and dropped 'leon wang' from CORE_TEAM — it had
an embedded space, which GitHub logins never do, so the entry was
unreachable dead code.
---------
Co-authored-by: koki yanlai xu <koki@kokideMacBook-Air.local>
* 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
* docs(creative-memory): add integration-shape proposal
Captures the integration-boundary decisions the product/pipeline team needs to make before any creative-memory implementation lands in the live generation loop. Covers the four areas lefarcen requested in the issue thread: signal capture points, retrieval insertion into generation/critique, user controls and escape hatches, and how Section 11's raw-events + content-addressed-derivations contract reshapes the boundary.
Doc-only. No code lands with this PR. Each section enumerates the option space, names a working lean, and flags the explicit decision that needs a product/pipeline call. Written to apply to either implementation (PR #1746 or a team-internal one), per the resolved foundation-vs-reference question.
Refs: #1637
* docs(creative-memory): self-contain the integration-shape doc
Per @mrcfps's review on PR #3021: the doc was referencing files that live on the parked add/creative-memory-system branch (PR #1746), not on main, which left the primary references dead from main's perspective.
Fix: inline the essential engine context (external API, storage shape, raw-events / content-addressed-derivations contract) as a Background section, drop the dead links to packages/creative-memory-system/, and replace inline open-questions.md cross-references with terse parenthetical statements that stand on their own.
The doc is now self-contained at the level reviewers need: the §5 decision summary stays the entry point, and no reference depends on an external file. PR #1746 is still linked as the RFC/prototype thread for anyone who wants the full simulation suite or open-questions ledger.
* docs(creative-memory): align §2 with the live composer, declare relationship to ## Personal memory
Per Looper review on PR #3021: §2's 'Generation prompt today' stack did not match apps/daemon/src/prompts/system.ts:454-483, which was a real correctness issue because §2 is the integration contract for prompt placement. As written, an implementer could ship a second memory layer or move the existing personal-memory block without deciding how the two memories interact.
Patch:
- Replace the simplified five-line stack with the live thirteen-block order, gated as the composer gates each block.
- Add Decision A: replace, append, or sibling block relative to the existing ## Personal memory slot. Lean A3 (sibling block, shared precedence).
- Reframe Decision B (insertion point) against the live order. Lean B1 (after ## Personal memory, before custom instructions).
- Add Decision C: precedence wording for the new block, mirroring the existing 'brand wins / skill wins' clause.
- Update §5 decision summary: items renumbered, three new explicit calls (relationship, insertion point against live order, precedence wording).
- Touch the Purpose paragraph to mention the existing ## Personal memory slot, since it is now the headline §2 question.
* docs(creative-memory): name the §2 prefix scope and route raw-events through OD_DATA_DIR
Per Looper review on PR #3021. Two doc-accuracy fixes:
1. §2 'Live composer order' stopped at ## Active skill (block 13), but composeSystemPrompt keeps appending mode-specific blocks after that (plugin / per-stage atom / metadata / deck framework / media contract / Codex imagegen / critique / visual-direction override / connected-MCP / Claude AskUserQuestion). As written, an implementer could treat block 13 as the tail of the stack and miss precedence layers a creative-memory block has to coexist with. Fix: rename the subsection to 'Memory-relevant prefix of the live composer order', add a 'Tail blocks (mode-specific, after ## Active skill)' note that summarizes the tail without listing every gate, and call out API_MODE_OVERRIDE as block 0. Source line numbers cited as a range with the disclaimer that gating + order are the durable shape.
2. §4 raw-events ownership option B wrote '.od/memory/raw_events.jsonl' literally, which hard-codes a repo-rooted path and bypasses the OD_DATA_DIR precedence documented in AGENTS.md FAQ. Packaged installs and NixOS / Home Manager modules already point OD_DATA_DIR at a writable directory because the install root may be read-only. Fix: rewrite option B as '<OD_DATA_DIR>/memory/<userId>/raw_events.jsonl' or a 'raw_events' table inside '<OD_DATA_DIR>/app.sqlite'; update option C wording for consistency; add a paragraph below the table explaining the OD_MEDIA_CONFIG_DIR > OD_DATA_DIR > <projectRoot>/.od precedence so this section cannot be read as endorsing a hard-coded .od/ path.
* docs(creative-memory): correct the raw-events precedence — OD_DATA_DIR only
Per Looper review on PR #3021. The previous patch's precedence paragraph copy-pasted the full chain (OD_MEDIA_CONFIG_DIR > OD_DATA_DIR > <projectRoot>/.od) from AGENTS.md, but that chain is the resolution rule for media-config.json specifically — OD_MEDIA_CONFIG_DIR is a narrower override that relocates only API credentials, not the daemon data root. Applying it to raw events would route preference event data into the credentials directory, which is the wrong contract.
Fix: rewrite the paragraph so raw events follow only the daemon data-root contract (OD_DATA_DIR if set, else <projectRoot>/.od). Add an explicit follow-up paragraph that OD_MEDIA_CONFIG_DIR is NOT part of this precedence, with a one-line explanation of why mentioning it as a contrast — so anyone reading the AGENTS.md chain alongside this doc knows the difference.
* feat(landing-page): localize plugins library across 18 locales
PR #2926 shipped the new `/plugins/` library hub + four kind sub-routes
+ detail pages, but the chrome was English-only — visitors landing on
`/zh/plugins/` saw the old marketplace registry placeholder rendered
by the catch-all instead, and detail pages rendered identical English
copy regardless of locale prefix. This PR brings the plugin surface
to feature parity with `/zh/skills/`, `/zh/templates/`, `/zh/systems/`,
`/zh/craft/`.
## What changes
- New `app/_lib/plugins-i18n.ts` — single source for all plugin chrome
copy (hub, list pages, chip rails, share dialog, detail-page meta
labels). English baseline + 17 locale overrides keyed on
`LandingLocaleCode` (the same short-code shape `localeFromPath()`
returns). Missing keys per locale fall back to English so a
partially-translated locale still renders sensibly. Translations
cover hub copy, four tile titles + blurbs, seven artifact-kind
labels + descriptions, 23 scene-subcategory labels, 18 detail-page
chrome strings, and a six-key share-dialog table with a
per-locale `shareTemplate({title, url})` function (translated for
every locale where `_lib/i18n.ts` already had one — same voice).
- `app/pages/plugins/{,templates/,templates/[kind]/,skills/,systems/,
craft/,[slug]/}/index.astro` — every hardcoded English string now
reads `getPluginsCopy(locale)` keys. Page logic and routing
unchanged.
- New short-code wrappers under `app/pages/[locale]/plugins/` — six
files (hub + three sub-routes + `[kind]/` and `[slug]/`) following
the same pattern `[locale]/skills/index.astro` already uses: each
re-exports the canonical page component and adds a per-locale
`getStaticPaths()` so the build emits 17 locale prefixes per
plugin route. Total plugin-route prerender count goes from ~390 to
~7 000, matching the existing skill/template scaling.
- Catch-all (`[locale]/[...path].astro`) — old `getPublicPlugins` /
`getRegistryCounts` registry rendering removed (placeholder UI
that was never wired to a real marketplace data source). Plugin
routes now live exclusively under `[locale]/plugins/...` short-code
wrappers, so the catch-all stops claiming `'plugins'` as a route
root. The dead-code path also drops a `pluginCounts.all` reference
the title row was reading.
- `.plugins-tile-grid` styles promoted from a scoped `<style>` in the
default-locale hub to global `app/sub-pages.css` so the
short-code wrapper renders the same hub markup without re-mounting
per-page CSS — `display: contents`-style scoping pitfalls in
Astro's per-component CSS scoping made this the cleanest fix.
## Surface area
- [ ] **UI** — new page / dialog / panel / menu item / setting / empty state in `apps/web` or `apps/desktop`
- [ ] **Keyboard shortcut** — new or changed
- [ ] **CLI / env var** — new `od` subcommand or flag, new `tools-dev` flag, or new `OD_*` env var
- [ ] **API / contract** — new `/api/*` endpoint, new SSE event, or changed shape in `packages/contracts`
- [ ] **Extension point** — new entry under `skills/`, `design-systems/`, `design-templates/`, or `craft/`, or change to the skills protocol
- [ ] **i18n keys** — new translation keys (full plugin chrome added across all 18 locales)
- [ ] **New top-level dependency** — adding any new entry to the **root** `package.json`
- [ ] **Default behavior change** — changes what existing users experience without opting in
- [x] **None** — landing-page-only restoration of i18n parity for the plugin surface
## Validation
- `pnpm --filter @open-design/landing-page typecheck` → 0 errors
- `pnpm --filter @open-design/landing-page build:static` → 16 127 pages
built (+6 584 over current main: ~388 plugin detail pages × 17
locale prefixes plus the hub + four sub-routes × 17 locales).
- `copy-example-html.ts` reports `266 entry files + 65 referenced
files`, identical to before — no regression in the asset-mirroring
pipeline.
- Local Playwright smoke (`/zh/plugins/...`):
- `/zh/plugins/` renders `<title>插件库 · Open Design</title>`,
label `插件库`, h1 `407 个可组合的构件。`, four tiles labelled
`模板 / 技能 / 设计系统 / 工艺`.
- `/zh/plugins/templates/video/` renders h1 `48 视频`, scene chips
`全部 / 动效 / 短视频 / 营销 / 产品 / 数据讲解`.
- `/zh/plugins/example-article-magazine/` share dialog renders
`复制下面的文案、然后跳到你想分享的平台粘贴即可` etc., share
template auto-interpolates plugin title + URL into Chinese voice.
- All 18 locale prefixes (`/zh`, `/zh-tw`, `/ja`, `/ko`, `/de`,
`/fr`, `/ru`, `/es`, `/pt-br`, `/it`, `/vi`, `/pl`, `/id`, `/nl`,
`/ar`, `/tr`, `/uk`) → 200 across hub + four sub-routes + sample
detail page.
- English `/plugins/` unchanged (default-locale path bypasses the
`[locale]/...` wrapper).
* feat(landing-page): finish plugins i18n chrome across 18 locales
The first localization pass shipped a partial fix: hub headings, lead
copy, two-level page chrome, detail-page metadata labels, the share
dialog, and the chip rail were still falling back to English on every
non-English locale because plugins-i18n.ts only filled a chrome slice
for `zh` and the file header even claimed "7 artifact-kind labels and
25 scene-subcategory labels are translated" for every locale that did
not yet have those blocks.
Three changes close the visible gap:
1. plugins-i18n.ts: fills the 27 still-missing chrome fields per locale
for zh-tw / ja / ko / de / fr / ru / es / pt-br / it / vi / pl / id /
nl / ar / tr / uk. Includes the 7-key category map, the 23-key
subcategory map, hubHeading / hubLead, the 4 *Label / *Heading /
*Lead triples for the templates / skills / systems / craft hub
pages, the 4 tile blurbs, the 4 browse buttons, sceneLabel, allChip,
the 12 detail-page metadata labels (mode / scenario / platform /
surface / author / manifest id / tags / preview caption / find on
GitHub / homepage / open in new tab) and bucket label map, the
detail share dialog (title / copy link / jump-to), and the
header-side nav.plugins entry. zh receives the same 11 detail-page
and share-dialog labels it was also missing.
2. header.tsx + site-footer.astro: routes the hardcoded "Plugins /
Templates / Skills / Systems / Craft" labels through `nav.*` from
HeaderCopy, so every locale gets its own dropdown trigger and
footer column. Adds `nav.plugins` to HeaderCopy and fills it in 18
locales with the local form ("插件" / "プラグイン" / "Plugins" /
"Plug-ins" / "Plaginy" / "الإضافات" / etc).
3. plugin-row.astro + content-i18n.ts: chip rail. The bundled-plugin
branch now runs raw `mode` / `scenario` slugs through the shared
localizeTaxonomyValue, and that helper now also consults the
plugins-i18n subcategory map before giving up. localizeTaxonomyValue
now returns undefined on a true miss instead of the unknownTag
placeholder, so chips drop quietly instead of showing "Category" /
"分類" / "Categoría" for taxonomy slugs we have not localized yet.
Callers that genuinely want the placeholder (`localizeContentTag`,
blog `category`, system noun) still keep the explicit fallback.
Out of scope and tracked separately: per-plugin title and description
in plugins/_official/* (author-supplied English metadata, ~401 plugins
without an i18n schema in the manifest yet — needs RFC + tooling
before the manifests can be expanded), and adding the long tail of
mode / scenario / category slugs (`code-migration`, `plugin-sharing`,
`tune-collab`, `live-artifacts`, `engineering`, ...) to TAXONOMY_TERMS
so chips render localized labels for every taxonomy value rather than
dropping silently.
* feat(landing-page): cover plugins chip rail long-tail taxonomy slugs
PR #3010's first round localized the high-frequency mode/scenario
chips (prototype, video, image, marketing, design, ...) but left the
~37 mode/scenario and 14 category slugs that show up in real `od.*`
metadata — code-migration, plugin-sharing, design-system, planning,
scenario, refine, discovery, handoff, token-map, tune-collab, orbit,
live-artifacts, engineering, healthcare, hr, sales, support,
default-router, downstream-export, figma-migration, media-generation,
plugin-authoring, validation, 3d-shaders, animation-motion,
audio-music, creative-direction, design-systems, diagrams, documents,
image-generation, marketing-creative, screenshots, slides,
video-generation, web-artifacts, ... — falling through to undefined
and dropping their chip silently on every non-English locale.
The data layer is the source of truth here, so this expansion lands
in `content-i18n.ts:TAXONOMY_TERMS` / `CATEGORY_LABELS` rather than
the plugins-i18n catalog: a single dictionary entry per slug fans out
to every chip-rail consumer (catalog rows, detail metadata, the
templates/[kind] facets) without each consumer touching its own copy.
Translations cover all 17 non-`en` locales. Brand and product nouns
(Figma, Open Design, BYOK, plugin) stay literal; technical taxonomy
slugs get short equivalents that read as chips rather than full
prose. The result on `/ja/plugins/skills/` matches `/plugins/skills/`
chip-for-chip (30 chips both sides) instead of dropping 27 of them
the way the previous iteration did.
* feat(landing-page): read manifest title_i18n / description_i18n on bundled plugins
PR #3010's prior rounds localized chrome and chip rails but the
catalog's most prominent text — each row's plugin name and blurb —
stayed English on every non-English locale. The plugin manifest
schema (`packages/contracts/src/plugins/manifest.ts`) has supported
`title_i18n` and `description_i18n` (Record<locale, string>) on every
manifest from spec v1; ~24 of the 401 first-party manifests already
carry one for `zh-CN`. The reader was just never wired to use them.
This change does the reader half: bundled-plugins.ts captures the
two i18n maps off each `open-design.json`, plugin-row.astro and the
detail page resolve them at render time via two new helpers
(`resolveBundledTitle`, `resolveBundledDescription`) that mirror the
short→long fallback chain documented in the manifest spec
(`htmlLang` like `zh-CN` → short `LandingLocaleCode` like `zh` →
primary tag → `en` → English baseline). The static-paths pass still
runs once for all locales — it has to, since each manifest produces
one URL — but the title/description shown on the rendered page now
reads the locale off `Astro.url.pathname` and picks the right entry
out of the maps.
Verified locally: `/zh/plugins/example-card-twitter/` now reads
"Twitter 分享卡 / 推特金句 / 数据卡, 适合配推文" from the manifest's
existing `zh-CN` block instead of the English baseline.
Plugin-data half follows in a separate commit. The 17 non-English
locales × 401 manifests need backfilling so the reader has something
to resolve to; that's data, not schema, and lands as a sequence of
manifest patches rather than tangled with this code change.
* feat(plugins): translate scenarios bucket title/description across 17 locales
Closes the first chunk of #3028. Eleven scenarios plugins (the
default-scenario bundle for each taskKind: code-migration,
figma-migration, media-generation, new-generation, tune-collab,
plugin-authoring; the default design router; the React / Vue /
Next.js downstream-export starters; and the Refine baseline) get
title_i18n + description_i18n filled for all 17 non-English locales
the landing page serves (zh-CN, zh-TW, ja, ko, de, fr, ru, es,
pt-BR, it, vi, pl, id, nl, ar, tr, uk).
The reader landed in 7ddfe36; this commit is data-only. taskKind
slugs that other docs reference by name (`code-migration`,
`figma-migration`, `tune-collab`, etc.) stay literal in the
descriptions so cross-references still resolve. Brand nouns —
Open Design, Next.js, React, Vue, Figma — also stay literal.
`/ja/plugins/od-code-migration/` now reads
"コードマイグレーション(デフォルトシナリオ)" instead of the English
baseline; `/zh/plugins/skills/` shows "代码迁移(默认场景)" in the
catalog row.
Remaining buckets (image-templates 45, video-templates 50,
examples 140, design-systems 142 = 377 plugins) follow in
subsequent commits in this PR.
* fix(landing-page): drop CJK template wrap when source name is still English
The Chinese / Japanese / Korean fallback templates for craft, skill,
template, system, plugin, and blog text splice the source `name` /
`title` into a CJK sentence frame: ``${name}工艺规则``,
``Open Design 指南:${topic}``, ``${name} は…のスキルです``. When the
underlying SKILL.md / craft markdown / blog frontmatter still ships
an English name (true for ~95% of the catalog today), that produces
mid-sentence script straddling on `/zh/...`, `/zh-tw/...`, `/ja/...`,
`/ko/...` like:
H1 : "Editorial typography hierarchy工艺规则"
Lead : "这条 Open Design 工艺规则定义 Editorial typography hierarchy
的执行标准…"
Plug : "video 插件 · 3D Animated Boy Building Lego"
That reads worse than the all-English fallback, because the visitor
parses the page in two scripts at once.
Adds a `nameNeedsEnglishFallback` guard that fires for the four CJK
locales whenever the spliced-in name has no CJK characters of its
own, and threads it through every `localizeXxxText` helper:
craft, template, system, plugin, skill, blog. When it fires the
helper returns the raw English content untouched, so the section
renders end-to-end in one language. Chrome (header, footer, breadcrumb,
buttons, share dialog) keeps its CJK rendering — only the
title-and-lead block falls back.
Side benefit: the same guard kicks in on the long tail of plugin
manifests still pending `title_i18n` / `description_i18n` backfill
(tracked in #3028), so `/zh/plugins/<bundled>/` no longer pairs a
"video 插件 · 3D Animated Boy Building Lego" title with a Chinese
breadcrumb. The page reads "3D Animated Boy Building Lego" + the
English manifest description, while header / footer / breadcrumbs
stay localized. Once a manifest ships its i18n maps, the chrome and
body re-converge automatically.
Non-CJK non-Latin scripts (ar, vi, ...) keep the previous behavior —
their templates already read tolerably with English names. If that
turns out to be wrong on a real audit, the same guard generalizes by
adding the matching Unicode range and locale set.
* feat(plugins): translate image-templates bucket title/description across 17 locales
44 of 45 image-templates plugins get title_i18n + description_i18n
filled for all 17 non-English locales (zh-CN, zh-TW, ja, ko, de, fr,
ru, es, pt-BR, it, vi, pl, id, nl, ar, tr, uk). Generated via Claude
Sonnet 4.5 over the OpenRouter gateway, ~$1.38 in API spend, 156s
wall-clock. Brand and cultural references stay literal (Open Design,
Lego, Hanfu, Showa, Pokémon, Black Myth: Wukong). Long AI generation
prompts collapse to a 1-2 sentence summary capturing what the plugin
does — the description doubles as catalog blurb on the landing site,
not as the actual generation prompt (which lives in example.html /
the manifest's preview entry).
Skipped: `profile-avatar-realistically-imperfect-ai-selfie` returned
malformed JSON on three retries; will rerun with a tighter prompt in
a follow-up commit. Catalog rows for that plugin keep falling back to
the raw English fields per #3010's reader change, so nothing breaks.
Tracking: closes the image-templates row in #3028.
* feat(plugins): translate video-templates bucket title/description across 17 locales
49 of 50 video-templates plugins get title_i18n + description_i18n
filled for the 17 non-English landing locales. Generated via Claude
Sonnet 4.5 over OpenRouter, ~$1.47 in API spend, 177s wall-clock.
HyperFrames templates, the Three Kingdoms cinematic series, the
Seedance/short-film prompts, and the K-pop / wuxia / anime variants
all get a 1-2 sentence catalog blurb in each locale; brand and
cultural tokens (Black Myth: Wukong, Hanfu, Showa, Pokémon, Three
Kingdoms / 三国志, Lego, Disney, K-pop, HyperFrames) stay literal.
Skipped: `live-action-anime-adaptation-water-vs-thunder-breathing-duel`
returned malformed JSON on three retries; will rerun in followup.
Falls back to the raw English fields per the reader landed in 7ddfe36.
Tracking: closes the video-templates row in #3028.
* feat(plugins): translate examples bucket (117/140) title/description across 17 locales
117 of 140 examples plugins get title_i18n + description_i18n filled
for the 17 non-English landing locales. Generated via Claude Sonnet
4.5 over OpenRouter, $3.94 in API spend, ~13 min wall-clock at
8-way concurrency. Existing zh-CN translations on 24 manifests are
preserved (the merge keeps author-supplied entries and only adds
missing locales).
23 of 140 returned malformed JSON on three retries — the output
likely hit the 4000 max_tokens ceiling on plugins whose description
balloons across 17 locales. Those manifests fall back to English on
non-`en` rendering per the reader landed in 7ddfe36, and will rerun
in a follow-up commit with a larger token budget and a stricter
output schema.
Tracking: closes 117/140 of the examples row in #3028; the remaining
23 stay open in that issue's failure list.
* feat(plugins): translate design-systems bucket (141/142) title/description across 17 locales
141 of 142 design-systems plugins get title_i18n + description_i18n
filled for the 17 non-English landing locales. Generated via Claude
Sonnet 4.5 over OpenRouter, $2.55 in API spend, 301s wall-clock at
8-way concurrency.
Translator script gained two improvements between examples and this
bucket:
- max_tokens bumped from 4000 to 8000 so 17-locale outputs stop
truncating on the long-tail manifests with verbose descriptions
- a balanced-brace JSON extractor that pulls the outermost `{ ... }`
from the response, tolerating trailing prose Claude occasionally
appends after the JSON object.
Result: only 1 manifest (`totality-festival`) failed parse this
batch, down from ~16% on the examples bucket. The next commit
re-runs the prior buckets' failures with the improved script.
Tracking: closes 141/142 of the design-systems row in #3028.
* fix(plugins): backfill 4 plugins that retried green after JSON extractor improvement
dcf-valuation, social-media-dashboard, wireframe-sketch (examples
bucket) and live-action-anime-adaptation-water-vs-thunder-breathing-duel
(video-templates bucket) parse cleanly under the balanced-brace
extractor introduced for the design-systems batch. The remaining
22 failures from the prior runs hit a different parse mode (Claude
emitting unescaped double quotes inside string values when the source
description contains its own English quotes like 'make it professional');
those will need a tighter prompt and rerun.
* fix(plugins): translate the last 22 plugins with quote-handling prompt fix
The 22 stuck plugins all carried English / Chinese double-quoted
phrases inside their description (\"make it professional\",
\"What's inside\", \"电子杂志 × 电子墨水\") that Claude was emitting
back inside JSON string values without escaping, breaking the parse.
Added one rule to the translator prompt — never use a straight double
quote inside a translated string, prefer single quotes / curly quotes
/ CJK 『 』 / 《 》 — and the previously stuck batch sailed through
clean: 22/22 ok, 0 retries, $0.85.
This closes the long tail of #3028:
- scenarios 11/11 ✓
- image-templates 45/45 ✓
- video-templates 50/50 ✓
- examples 140/140 ✓
- design-systems 142/142 ✓
- atoms N/A (filtered from public catalog)
All 388 catalog-visible plugins now ship title_i18n + description_i18n
for all 17 non-English locales the landing page serves.
* fix(plugins): clean up four review-flagged i18n data issues
- apps/landing-page/app/_lib/plugins-i18n.ts:759 — Polish bucket
label `examples: 'Przyklad'` was missing the diacritic; every
other Polish string in the same block uses proper diacritics.
Restore to 'Przykład'. (Reviewer: looper #4364985878.)
- video-templates/cinematic-route-navigation-guide — German
title_i18n.de was a byte-for-byte copy of en ("Cinematic Route
Navigation Guide") while the German description was already
translated. Replace with "Cinematischer Routen-Navigationsleitfaden"
to match the German voice the description sets.
- video-templates/hollywood-haute-couture-fantasy-video-prompt —
Dutch title_i18n.nl was identical to en for the same reason.
Translate the trailing noun phrase: "Hollywood Haute Couture
Fantasy Videoprompt" (mirrors the Dutch description's compound
word style).
- video-templates/video-seedance-three-kingdoms-guanyu-slaying-yanliang —
Korean Hangul `돌진` had leaked into the Turkish description (a
translation-pipeline artifact where the model copied the verb
from the Korean output without translating it). Replace
"saflarına돌진 eder" with the idiomatic Turkish "saflarına dalar".
All four are data-only fixes against existing manifests; no schema
changes, no reader changes. typecheck stays at 0 errors.
* fix(landing-page): localize aria-labels, alt text and BreadcrumbList JSON-LD on plugin detail page
The PR's prior rounds left six accessibility / structured-data
surfaces on `/{locale}/plugins/<slug>/` either entirely English or
mixing English chrome with the localized plugin title. Reviewer
flagged each one across multiple loops; this commit clears them all:
1. `aria-label` on the open-in-new-tab popout no longer reuses the
visible label `pcopy.detailOpenInNewTab` (which carries the
decorative `↗`). Added `detailOpenInNewTabAria` — same wording,
no glyph — and the `<a aria-label>` consumes that key. The
visible link text still ends in `↗`.
2. `<nav class="breadcrumb" aria-label="Breadcrumb">` now reads
`aria-label={pcopy.breadcrumbLabel}`. Eighteen locales filled
("面包屑导航", "パンくずリスト", "Brotkrumen-Navigation",
"Fil d'Ariane", "مسار التنقل", "İçerik haritası", ...).
3. Share-dialog `<button aria-label="Close">` now reads
`aria-label={pcopy.shareDialogClose}`. Eighteen locales filled
("关闭", "閉じる", "Cerrar", "Закрыть", "إغلاق", ...).
4. Three template-literal a11y strings (`${pluginTitle} preview`,
`Open interactive preview for ${pluginTitle}`, `${pluginTitle}
interactive preview`) become function calls
(`pcopy.previewImageAlt(t)`, `previewSummaryAria(t)`,
`previewIframeTitle(t)`) so the sentence frame around the
plugin title rotates with the page locale. Two `<img alt>` call
sites (the static preview at line 210 and the click-to-expand
thumbnail at line 179) both consume `previewImageAlt`.
5. `BreadcrumbList` JSON-LD position-2 now reads
`name: pcopy.hubLabel` instead of hardcoded English `"Plugins"`.
The visible breadcrumb at line 105 already renders
`pcopy.hubLabel`; this aligns the structured data with the
rendered chrome on every locale.
The new function-typed keys deliberately interpolate `pluginTitle`
(which is itself locale-resolved via `resolveBundledTitle`) so the
mixed-language guard from commit 002d457 is preserved: a manifest
without a per-locale title still flows through to a coherent
single-language a11y string because `pluginTitle` falls back to
English along with the rest of the section.
apps/landing-page typecheck stays at 0 errors.
Closes reviewer threads:
- #pullrequestreview-4364985878 (Open in new tab aria)
- #pullrequestreview-4368926224 (Polish typo + plus mixed-language alt/aria)
- #4373... (BreadcrumbList JSON-LD)
- #4374... (aria-label="Close" + aria-label="Breadcrumb")
* fix(landing-page): redirect legacy fa/hu/th /plugins/ paths to canonical
When the new `/{locale}/plugins/...` short-code wrappers landed, the
legacy catch-all `pages/[locale]/[...path].astro` dropped `'plugins'`
from its `paths` list. That intentionally avoids serving stale
marketplace-registry placeholder routes for the modern landing
locales — but it also takes `/fa/plugins/`, `/hu/plugins/`, and
`/th/plugins/` from 200 to 404, because those three legacy locales
live only in the old `_lib/i18n.ts:LOCALES` set and are not part of
`LANDING_LOCALES` (the modern 18-locale list the new wrappers serve).
Three `301`s in `_redirects` send those legacy URLs to the canonical
English `/plugins/...` so SEO and inbound links keep working until
the legacy locale set is retired entirely.
Reviewer thread (#pullrequestreview-4364052045) flagged this as a
non-blocking regression across multiple loops; this commit closes it.
* ci(landing-page): add merge_group trigger so the queue can clear PRs
`landing-page-ci.yml` only fired on `pull_request` and `push:main`,
which meant the required `Validate landing page` and
`Strict PR visual tests` checks never dispatched against the
`merge_group` ref the merge queue creates. The queue then sat at
"awaiting checks" until it timed out and ejected the PR (the
deadlock observed during the 5/26 release window).
Adding a `merge_group: { types: [checks_requested] }` trigger to
the same workflow lets the queued ref reuse the existing job graph,
matching the pattern in `ci.yml` which already wires `merge_group`.
Also drops `plugins/**` into the same paths filter as `pull_request`
since the new bundled-plugins reader (commit 7ddfe364) consumes
those manifests' `title_i18n` / `description_i18n` maps and the
landing-page CI must rerun when manifest data changes.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Track "Save comment" and "Send to chat" button clicks in the comment
popover with a new `comment_popover` area, so we can measure the
distribution of save vs send-to-chat usage.
Co-authored-by: qiongyu1999 <2694684348@qq.com>
Real Codex runs (#3060) explored correctly — verifying 3-4 UI cases with
DOM evidence — but Codex over-planned (6 steps), executed the high-value
ones, then went silent chasing a remaining planned step and tripped
expect-cli's ~180s no-output watchdog, aborting the turn before it emitted
a final report. The run then fell back to an advisory artifact, so the
real findings never reached report.md.
Tighten the prompt so Codex finishes and submits before going idle:
- cap at 3 cases (was 6) and target 2-3, quality over breadth;
- add a CRITICAL instruction stating the runner aborts with no report
after ~3 min of no output, so Codex must stop after 2-3 cases and emit
the complete report in one final turn rather than leaving planned steps
pending or retrying silently.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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.
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.
expect-cli defaults to the Claude Code ACP provider, which is not
installed on the self-hosted runner, so the exploration step errored
(AcpProviderNotInstalledError) and fell back to a reachability-only smoke
trace instead of real UI exploration.
Pass `-a codex` to expect-cli so it drives the Codex agent (installed on
the runner, authenticated via CODEX_HOME). Configurable via
OD_EXPECT_AGENT (set to empty to use expect-cli's default). When the
agent is unavailable the existing smoke-trace fallback still applies, so
this is safe even before Codex is authenticated.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Treat Claude Code stdout like "Not logged in · Please run /login." as an
auth failure in diagnoseClaudeCliFailure so connection tests and chat
runs surface actionable login guidance instead of raw CLI text.
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
After fixing source acquisition (#3078), the #3060 validation run reached
the container and got through most of `pnpm install`, then failed building
the better-sqlite3 native module: prebuild-install could not reach github
releases and the node-gyp fallback could not fetch node headers from
nodejs.org (ECONNRESET). The electron postinstall hits the same blocked
hosts, and package tarballs from npmjs were throttled to ~20 KB/s.
The runner's network to npmjs / nodejs.org / github releases is throttled
or reset by GFW; the China npm mirror (npmmirror.com) is fast and complete
(verified from the runner: registry ~2.4 MB/s, node headers ~3.6 MB/s,
better-sqlite3 prebuilt present). Point the in-container install at it via
registry + disturl (node-gyp headers) + electron / electron-builder /
better-sqlite3 binary mirrors + Playwright download host.
Package integrity is still verified against the lockfile, so the mirror
only changes transport. Once a native module builds, pnpm's side-effects
cache in the persistent store keeps it warm for later runs.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Add a `skip_comment` workflow_dispatch input (default false). When set,
the "Comment exploration report" step is skipped, so a validation/dry
run can exercise the full pipeline and produce the report artifact
without posting a public comment on the target PR (useful when testing
against an external contributor's PR). The report is still uploaded as
an artifact for review.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sandbox checked out PR code with `git fetch https://github.com/...`
*inside* the container. The self-hosted runner's bandwidth to github.com
is throttled across every transport (HTTPS/SSH/codeload/API, all
~30-90 KB/s) and the HTTPS handshake is frequently RST'd, so a
from-scratch fetch of this ~200MB repo is impractical and unreliable per
run (run 26491460889 failed here with repeated GnuTLS resets).
Move source acquisition to the trusted host and make it incremental:
- Keep a persistent bare mirror of the base repo
($HOME/.cache/agent-pr-explore/open-design.git, overridable via
OD_SANDBOX_REPO_MIRROR). Each run fetches only the PR's delta via
`refs/pull/<n>/head` over SSH -- the one transport GFW doesn't reset --
using a read-only deploy key (OD_SANDBOX_GIT_SSH_KEY).
- Take the head from the BASE repo's pull ref so fork PRs work without
depending on the head fork, and verify it equals the resolved HEAD_SHA.
- Check the PR head into a per-run worktree and mount it read-only into
the container; the container copies it into a writable workdir and no
longer needs (or has) any github access.
The deploy key stays on the trusted host and is never exposed to the
untrusted PR code. The mirror must be seeded once on the runner (the
error message prints the exact clone command if it is missing).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use DESIGN.md telemetry palette values in tokens.css and the
components.html fixture. Document the --success/--danger mapping in
DESIGN.md so spec readers and CSS consumers stay aligned.
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.
* ci: skip docker pull when agent sandbox image is already cached
The agent PR exploration script ran an unconditional `docker pull
"$image"` before `docker run`. Under `set -e`, a transient registry
timeout (the self-hosted runner's network to docker.io is unreliable)
aborts the whole run even when the base image (node:24-bookworm) is
already cached locally — which is what happened on run 26490782540.
Skip the pull entirely when the image is already present, and only pull
when it is missing. This avoids both the failure and the wasted pull
timeout on every run, and keeps a run's base image stable. Refreshing
the cached image is a separate, explicit operation on the runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: persist agent sandbox pnpm store across runs
The pnpm store was placed under $RUNNER_TEMP, which the Actions runner
wipes per job, so every agent exploration re-downloaded all dependencies
from the npm registry — slow, and as fragile as the runner's docker.io
access (the same network class that already broke the docker pull).
Move the store to a persistent host path ($HOME/.cache/agent-pr-explore/
pnpm-store, overridable via OD_SANDBOX_PNPM_STORE) so a warm,
content-addressed store is reused across runs. `rm -rf "$root"` no longer
touches it since it lives outside the per-run root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Checkout trusted base scripts" step did a full actions/checkout of
this large repo on the self-hosted runner. On a recent run it stalled in
the initial `git fetch --depth=1 origin <sha>` for many minutes before
the agent script ever started, and the run had to be cancelled.
The trusted host side only needs the self-contained
`.github/scripts/agent-pr-explore-sandbox.sh`; PR code is checked out
inside Docker and PR context is gathered via the API. Replace the full
checkout with a single-file fetch via `gh api` (raw), pinned to the same
trusted base/dispatch commit, which avoids the git-protocol fetch of the
whole repo entirely.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(daemon): detect CodeWhale as DeepSeek TUI fallback binary
The renamed CodeWhale CLI installs the `codewhale` dispatcher instead of
`deepseek`. Probe it via fallbackBins so agent detection works without
requiring DEEPSEEK_BIN overrides.
Fixes#2983
* test(daemon): align deepseek docsUrl expectation with CodeWhale metadata
Update env-and-detection coverage to match the runtime metadata URL
changed for issue #2983.
Import/install routes compared bare directory slugs against catalog ids
prefixed with user:, causing a false 500 after a successful write and
duplicate entries on retry. Normalize lookup and reserved slug ids.
Fixes#2489
* ci(landing): split landing deploy into staging gate + manual production
A merge to `main` previously published the landing page straight to
production (open-design.ai) via `landing-page-deploy`. There was no
buffer to review the rendered site, so a bad merge was live instantly.
Split deploys across two Cloudflare Pages projects so production is only
ever reached by an explicit human action:
- `landing-page-staging` (push to main) -> staging project
`open-design-landing-staging` -> staging.open-design.ai.
- `landing-page-production` (manual workflow_dispatch only) -> production
project `open-design-landing` -> open-design.ai. Only this workflow
names the production project; gate it with required reviewers on the
`production` GitHub environment.
- `landing-page-ci` now also deploys a per-PR preview into the staging
project (`--branch=pr-<n>`) for same-repo branches and comments the URL.
Fork PRs (no secrets / read-only token) skip the deploy and keep just
the build validation. Path filters already scope this to landing edits.
Decouple search-engine indexing from staging:
- `blog-indexing-on-deploy` now triggers on `landing-page-production`
(not every main push), so the test environment is never submitted to
Google/IndexNow.
- It diffs from a new `blog-indexed-prod` tag (the last indexed prod
commit) instead of `HEAD^`, and force-advances the tag after a
successful run, so a manual promotion bundling several merged posts
indexes all of them rather than only the last commit.
Staging and PR-preview builds drop `PUBLIC_GA_MEASUREMENT_ID` so test
traffic does not pollute the production GA property.
* ci(landing): keep staging + PR previews out of the search index
staging.open-design.ai mirrors production and is exposed via cert
transparency logs, so search engines can discover it. Indexing the
mirror competes with open-design.ai for the same content.
Emit `<meta name="robots" content="noindex, nofollow">` whenever
OD_LANDING_NOINDEX=1, and set that flag on the staging and PR-preview
builds (production leaves it unset and stays indexable). noindex is
used rather than a robots.txt Disallow so crawlers can still fetch the
page and read both the tag and the canonical, which already points at
the production origin.
* fix(landing): make staging noindex actually take effect
The previous commit read `process.env.OD_LANDING_NOINDEX` directly in
`seo-head.astro`, but `.astro` frontmatter is transformed by Vite and
does not see process.env, so the meta never rendered. Two fixes:
- Inject the flag as the compile-time constant `__OD_LANDING_NOINDEX__`
via `vite.define` in astro.config.ts (config runs in Node and can read
process.env); SeoHead consumes that constant.
- The homepage (`index.astro`) and `og.astro` build their own <head> and
never use SeoHead, so a per-component meta can miss pages. Add an
`astro:build:done` integration that appends a catch-all
`/* X-Robots-Tag: noindex, nofollow` to the Cloudflare Pages `_headers`
on staging/preview builds, covering every response (homepage, assets,
any custom-head page) at the HTTP layer. Production builds leave
`_headers` untouched.
Verified: build with OD_LANDING_NOINDEX=1 emits the _headers block and
the SeoHead <meta>; build without the flag emits neither; astro check
clean.
* fix(landing): address review — pin prod checkout to main, defer index pointer
Two blockers from review:
- landing-page-production: workflow_dispatch can be launched from any ref
via the Actions "Use workflow from" dropdown, so an operator could ship
an arbitrary branch to open-design.ai. Pin the checkout to `ref: main`
so the deployed artifact always equals reviewed main.
- blog-indexing-on-deploy: the `blog-indexed-prod` pointer was advanced
right after sitemap submission, before Inspect / Search Analytics /
Render status / Open status PR. A failure in any of those still moved
the pointer, so the next production run skipped those posts. Move the
advance to the very end, gated on `success()`, so a failure leaves the
tag in place and the range is re-processed next run (submissions are
idempotent).
* fix(landing): gate production promotion to the main ref only
Follow-up to the production-path review note: pinning checkout to main
fixed the deployed content, but the workflow was still dispatchable from
any ref, which records a non-main production run and would dodge
blog-indexing's `workflow_run` `branches: [main]` filter. Gate the whole
job on `github.ref == 'refs/heads/main'` so a dispatch from any other
branch/tag is skipped outright.
* feat(landing-page): plugin detail page interactive preview + share dialog
The new `/plugins/<manifest-id>/` detail page that shipped in #2926
landed without the two affordances PR #2679 added to the legacy
`/skills/<slug>/` and `/templates/<slug>/` pages: a click-to-expand
iframe of the live artifact, and a share dialog with brand-keyword
copy plus four-channel jump buttons (X / LinkedIn / Reddit /
Facebook). This restores both, sourced from the bundled-plugin
manifest under `plugins/_official/<bucket>/<slug>/open-design.json`.
## Interactive preview
Three preview-type behaviours, gated on `od.preview.type`:
- `video` (Cloudflare Stream URLs already in the manifest) —
inline `<video controls poster=...>` with the playable MP4 as
`<source>`. Detail-page row is unchanged from #2926; controls
double as the open-full affordance.
- `html` (a local `example.html` referenced by `od.preview.entry`,
only the `examples/` bucket today) — `<details>` toggle wraps the
poster image as the summary; clicking opens a sandboxed
`<iframe>` that loads the entry HTML lazily, with an
"Open in new tab ↗" pill in the frame's top-right corner so the
artifact can be inspected at full screen.
- `image` or no entry — static `<img>` (existing behaviour).
`copy-example-html.ts` is extended to mirror the local entry and any
`./assets/...` siblings to `out/plugins/<manifest-id>/<entry>` so the
iframe URL resolves on Cloudflare Pages instead of SPA-falling-back to
the homepage. The four examples carrying sibling-asset references
(flowai-live-dashboard-template, trading-analysis-dashboard-template,
open-design-landing, open-design-landing-deck) all render in-place.
## Share dialog
Same `<dialog data-share-dialog>` markup the legacy detail pages use,
so the global click handlers in `header-enhancer.astro`
(`data-share-open` / `data-share-copy` / `data-copy-link`) wire up
the open / copy actions automatically — no extra client bundle. Four
platform jumps (X / LinkedIn / Reddit / Facebook) plus a Copy-text /
Copy-link pair, with a single English template for now (the new
`/plugins/...` routes only generate English pages; localisation can
land alongside the i18n catch-all follow-up).
## Bundled in
- The `copy-example-html.ts` sibling-assets fix from open PR #2880.
Without it the existing `/skills/<slug>/` iframe still 404s on
Cloudflare Pages for after-hours-editorial-template and the four
others; bundling it here means the same script handles both
sources in one pass and sidesteps two PRs touching identical
helper code.
* fix(plugins): remove dangling preview.entry from example-hyperframes
The hyperframes example folder ships a SKILL.md (it's an instruction
manual for using the HyperFrames HTML format) but no runnable
`example.html`. The manifest still claimed `preview.type: html` /
`preview.entry: ./example.html`, which made the marketing site try
to iframe a non-existent file and forced the preview pipeline into
its `Path 3` fallback card — leaving the catalog row visually
inconsistent with the eleven sibling `video-template-hyperframes-*`
plugins that have real Cloudflare-Stream poster URLs.
Drop the preview block entirely so the manifest stops promising a
demo it can't deliver. The landing-page detail row continues to
render the typographic fallback card (sourced from title /
description / mode), which is now the honest representation:
"this is an instruction skill, not a renderable template".
* fix(landing-page): address PR #2958 review feedback on plugin preview pipeline
Two blocking issues called out in code review:
1) `bundled-plugins.ts` exposed `previewEntryUrl` for every manifest
that declared `preview.type: "html"`, even when the entry file
wasn't shipped. Several first-party manifests fall in this state
(example-design-brief's `./brief-preview.html`, example-x-research,
example-pptx-html-fidelity-audit, example-hatch-pet,
example-last30days, example-guizang-ppt, example-replit-deck,
example-live-artifact, example-html-ppt, example-dcf-valuation).
The detail page then rendered a click-to-expand iframe and popout
link to a file that copy-example-html.ts had skipped, so the
iframe URL SPA-fell-back to the homepage on Cloudflare Pages.
`entryRelativeUrl()` now `existsSync()`-checks the resolved local
path before returning a URL. When the file's missing the detail
page falls through to the static thumbnail branch, exactly like
plugins that ship no preview entry at all.
2) `copy-example-html.ts` recognised only `(src|href|poster)="./assets/..."`
and then bulk-copied the entry's sibling `assets/` folder, so it
missed two real ref shapes: bare-relative (`href="assets/styles.css"`,
`src="assets/deck-stage.js"` under example-html-ppt-zhangzara-pin-and-paper)
and cross-folder (`src="../open-design-landing/assets/hero.png"`
under example-open-design-landing-deck).
Replaced the heuristic with a generic walker that:
- Parses every relative ref in the entry HTML
(`(src|href|poster|srcset|data-src)=` plus `url(...)`), splitting
srcset on whitespace/commas so multi-URL attrs are honoured.
- Resolves each ref against `dirname(entrypointSrc)` for the source
and against `dirname(iframeAbsPath)` for the destination —
identical to how a browser resolves the same ref against the
iframe URL. Files outside the source root or the iframe root
are dropped.
- Recurses into copied HTML / CSS / JS / SVG so multi-step chains
(entry → assets/template.html → assets/fonts/foo.woff) don't
strand intermediate files.
- Tracks visited *destinations* rather than sources, so a single
source that legitimately needs to land at two different out-paths
(same-folder copy at /plugins/example-X/assets/foo.png AND a
cross-folder copy at /plugins/open-design-landing/assets/foo.png
for sibling decks that use `../open-design-landing/assets/foo.png`)
gets both copies.
Verified manually:
- /plugins/example-html-ppt-zhangzara-pin-and-paper/assets/styles.css
and assets/deck-stage.js → 200 (bare-relative)
- /plugins/open-design-landing/assets/hero.png and assets/about.png
→ 200 (cross-folder destination, no manifest-id prefix because
iframe URL `..` collapses the prefix)
- /plugins/example-design-brief/ renders the static thumbnail only,
no click-to-expand iframe (broken entry guard)
- /plugins/example-flowai-live-dashboard-template/assets/template.html
→ 200 (existing same-folder behaviour preserved)
Build now reports `copied 266 entry files + 65 referenced files`,
where the 65 includes both the same-folder `./assets/...` payloads
the previous heuristic captured and the bare-relative + cross-folder
shapes it didn't.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* feat(landing): add /share-out redirect for X share button click tracking
Adds a Cloudflare Pages Function at /share-out/:eventId that records each
click of the "Share on X" button surfaced in the contributor card comments
on GitHub, then 302-redirects to the original twitter.com / x.com intent
URL (passed via ?to=, host-allowlisted).
Together with the existing /share/:eventId function this gives us both
sides of the X funnel without an X API key:
- /share-out/:eventId -> GitHub user clicked the X button (funnel step 1)
- /share/:eventId -> someone on X clicked the posted tweet (funnel step 2)
Per-event KV storage is optional (SHARE_OUT_CLICK_EVENTS). When no KV is
bound the function falls back to console.log; aggregate counts are visible
in Cloudflare Pages analytics with no extra setup.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: retrigger CI
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Adds a new `--to zip` (and `--to all`) tools-pack Windows build target that
produces a portable `.zip` from the cached `win-unpacked` tree using the
bundled 7z. The zip lays files at the archive root so users can extract it
anywhere and launch `Open Design.exe` without going through the NSIS
installer, addressing the no-install download request.
Release plumbing is updated to publish the portable zip and its sha256
beside the existing installer on R2 for beta, preview, and stable channels
(default on, gated by `WINDOWS_INCLUDE_ZIP`/`WIN_INCLUDE_ZIP`). The
electron-updater `latest.yml` feed continues to point only at the
installer; the zip is a manual-download convenience and is intentionally
excluded from the in-app updater.
Closes#1121
Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)
Co-authored-by: libertecode <libertecode@proton.me>
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.
When creating a new conversation, the route-sync effect could fight the
conversation switch if the URL was not updated synchronously. This caused
users to be unable to switch back to older conversations after creating
a new one.
The fix mirrors the approach already used in handleSelectConversation
(added in PR #1710): push the new conversation ID into the URL
synchronously so the route-sync effect sees a matching routeConversationId
before it can revert activeConversationId.
Without this synchronous URL update:
1. handleNewConversation sets activeConversationId to fresh.id
2. The URL-sync effect (L1336) eventually updates the URL
3. But the route-sync effect (L800-819) may see the stale routeConversationId
and pull activeConversationId back to the old conversation
4. This prevents users from switching to other conversations
Fixes#2930
When switching from Edit to Draw mode, the preview could go blank because:
1. exitManualEditModeAfterFlush() clears manualEditFrozenSource
2. previewSource switches back to livePreviewSource
3. But activateSrcDocTransport() was not triggered
This fix adds a useEffect that detects when manualEditMode transitions
from true to false, and explicitly calls activateSrcDocTransport() to
ensure the iframe content is refreshed.
Fixes#2912
Some skill and design-template `example.html` files are thin shells that
iframe a neighbouring `./assets/<file>` (template HTML, mp4 showcases,
hero PNGs, etc.). The post-build copier only mirrored the entrypoint
file itself, so on Cloudflare Pages the asset path 404'd and the SPA
fallback served the OD homepage — users clicked "Click for live
preview" on /skills/after-hours-editorial-template/ and saw the landing
page rendered inside the iframe instead of the actual template.
Walk the entrypoint HTML for `(src|href|poster)="\./assets/..."` refs
and recursively mirror the sibling `assets/` directory only when one is
found. Most skills carry an `assets/` folder of reference PNGs the demo
never loads (open-design-landing alone is 22MB of unused mocks); a
relevance gate keeps the deploy from absorbing tens of MB per skill
that doesn't actually need them.
Six skills/templates currently match (after-hours-editorial-template,
8-bit-orbit-video-template, weread-year-in-review-video-template,
flowai-live-dashboard-template, trading-analysis-dashboard-template,
open-design-landing).
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
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>
* fix(daemon): widen HTTP keep-alive so SSE survives idle gaps
The daemon's `/api/runs/:id/events` SSE stream emits an in-band
`: keepalive` comment every 25s (`SSE_KEEPALIVE_INTERVAL_MS`), but
Node's default `server.keepAliveTimeout` is 5_000ms. When a run is
quiet for more than five seconds — e.g. the agent is still composing,
or the user briefly walks away — Node closes the underlying TCP
connection from under the SSE writer, the next 25s ping lands on a
dead socket, and the browser surfaces it as a generic
"network error" mid-stream.
This is most visible behind any keep-alive-aware middlebox (the
nginx running in the desktop bundle, the socat/docker bridges users
set up for remote access, EC2 security-group idle timers): the
default 5s window is shorter than every reasonable in-band keepalive
cadence, so the connection dies before the application gets a chance
to assert it's still alive.
Set the listener to:
- `keepAliveTimeout = 120_000` — 4.8× the in-band keepalive, plenty
of slack for clock skew and slow flushes.
- `headersTimeout = 125_000` — must exceed `keepAliveTimeout` per the
Node docs, otherwise a misbehaving client can stall request parsing
indefinitely.
- `requestTimeout = 0` — disable the per-request timeout entirely;
an SSE response intentionally runs for as long as the agent runs.
Verified by curling
`/api/runs/<id>/events` from inside the daemon container and
watching the connection stay open through three full 25s keepalive
cycles where it previously RST'd at ~5s.
* fix(daemon): address PR #2557 review — drop requestTimeout, add regression test
Three changes responding to @PerishCode's review (#2557):
1. Drop `server.requestTimeout = 0`. The reviewer is correct: that knob
bounds how long the server waits to *receive* a complete request
(headers + body) and is cleared the moment the request is fully
parsed — it does not gate the duration of an SSE response. Setting
it to 0 only removes Node 18+'s default 300s slow-loris guard, which
is a real regression on a daemon that binds to 0.0.0.0 / Tailscale.
2. Rewrite the comment block. The previous comment claimed
`keepAliveTimeout` "closes any idle SSE connection." Per the Node
docs, `keepAliveTimeout` arms *after* a response finishes writing —
it bounds the between-request idle gap on a kept-alive socket, not
an in-flight streaming response. SSE drops mid-stream are almost
always middlebox idle timers (nginx, socat/docker, EC2 NAT), not
Node's own socket timeout, and this listener-side change cannot
extend a connection past those middleboxes.
What this PR actually fixes: routine kept-alive sockets used around
an SSE stream (status polls, run-status fetches, the initial GET
before the SSE upgrade) surviving normal client pauses. 120s gives
comfortable headroom over the 25s in-band cadence so chat clients
stop reconnect-storming between bursts.
3. Add `apps/daemon/tests/server-keepalive.test.ts` so a future
refactor cannot silently restore the Node defaults. The test uses
the existing `startServer({ port: 0, returnServer: true })` fixture
(mirroring version-route.test.ts) and asserts the listener's
`keepAliveTimeout` and `headersTimeout` invariants.
Verified:
- pnpm --filter @open-design/daemon run typecheck passes
- pnpm vitest run tests/server-keepalive.test.ts → 2 passed
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>
Wire Aider (https://aider.chat) into the daemon agent registry alongside
the existing 16 CLIs. Aider is one of the most-used open-source coding
CLIs and routes through LiteLLM, so users can drive any provider they
already have a key for (OpenAI, Anthropic, DeepSeek, Gemini, OpenRouter,
local Ollama, etc.) without an extra adapter per provider.
Implementation follows the DeepSeek TUI pattern: prompt-via-argv with a
30 KB byte budget guard, plain stdout streaming, and the suppression
flags needed to keep aider runnable without a TTY (--yes-always,
--no-pretty, --no-git, --no-auto-commits, --no-suggest-shell-commands,
--no-show-model-warnings). `--message-file -` is not used because aider
treats `-` as a literal filename rather than a stdin sentinel.
Touchpoints mirror the other one-shot adapters:
- runtimes/defs/aider.ts new RuntimeAgentDef
- runtimes/registry.ts register in AGENT_DEFS
- runtimes/executables.ts AIDER_BIN override
- app-config.ts AIDER_BIN in agent env set
- web/utils/agentLabels.ts 'Aider' display label + aliases
- tests/runtimes/agent-args.test.ts buildArgs shape coverage
- tests/runtimes/env-and-detection bin override coverage
- tests/runtimes/helpers shared `aider` test helper
Validated with `pnpm guard`, `pnpm typecheck`, and
`pnpm --filter @open-design/daemon test tests/runtimes` (123 passing).
End-to-end probe against the live aider binary against DeepSeek via the
exact argv the adapter produces returned the expected output.
* feat(daemon): attach structured diagnostics to agent connection test results
Local agent connection-test failures currently flatten everything into
a single free-form `detail` string (e.g. "exit 1"). Settings UI and CLI
consumers can't tell what phase failed, which binary the daemon picked,
or what the child's exit metadata looked like — they have to scrape the
human-readable text.
Add an optional `diagnostics` block on the connection-test response so
callers can read structured fields instead. The existing `kind` and
`detail` strings are kept bit-for-bit identical, so older UIs keep
rendering unchanged.
- packages/contracts: add `ConnectionTestPhase`
(binary_resolution / version_probe / model_list / spawn /
connection_smoke_test / output_parse) and a `ConnectionTestDiagnostics`
interface with optional `binaryPath`, `binaryVersion`, `exitCode`,
`signal`, `stdoutTail`, `stderrTail`; extend
`ConnectionTestResponse.diagnostics?` to carry it.
- apps/daemon/connectionTest.ts: thread a `phase` tracker through
testAgentConnectionInternal, flip it at the meaningful boundaries
(binary_resolution → spawn → connection_smoke_test / output_parse),
and stamp diagnostics into every result return point — the four
result helpers plus both early returns. Tail data already buffered
by `createAgentSink` is reused; nothing new is captured.
- tests: three regressions per #2248 — success path attaches
phase='connection_smoke_test' + exitCode 0, exit-failed path
attaches phase='spawn' + the failing exitCode + the stderr tail,
and a missing-CLI path attaches an early-phase diagnostics block.
This is PR 1 of the #2248 plan (contracts + minimum daemon fill);
follow-ups will introduce a normalized failure classifier
(binary_not_found, unsupported_version, auth_failed, quota_exceeded,
network_failed, unsupported_flags, no_text_output, output_parse_failed,
spawn_failed), candidate-alternative reporting via
inspectAgentExecutableResolution, and the Settings "View details"
disclosure.
Refs #2248.
* fix(connectionTest): honor diagnostics contract on all local return paths
Two follow-ups from review of #2419:
- packages/contracts/src/api/connectionTest.ts advertises diagnostics
as 'Always set on local agent test responses', but three local
returns still bypassed buildDiagnostics(): the buildArgs failure
around 1295, the preflight probeAgentAuthStatus().status === 'missing'
branch around 1317, and the outer catch around 1566. Thread
buildDiagnostics() through all three; phase is still 'binary_resolution'
at the first two and whatever the runtime advanced to at the catch.
- resultFromAgentText() hard-coded exitCode: 0 even though
resultFromChildExit() routes ACP clean-SIGTERM completion through
this success helper (winner.code === null, winner.signal ===
'SIGTERM' with acpCleanCompletion). Add an optional exit argument
threaded from both call sites so the diagnostics reflect the actual
child code/signal pair instead of a synthesized 0 that masks the
SIGTERM teardown. Only synthesize 0 when no exit context is
available (theoretical text-without-exit path).
Tests:
- regression locking the diagnostics contract for the preflight auth
path on Cursor Agent (phase: binary_resolution, binaryPath set)
* docs(contracts): widen diagnostics contract to match early-failure paths
Reviewer flagged that the JSDoc-style comment on
ConnectionTestResponse.diagnostics still said 'Populated only when the
test actually spawned an agent CLI', but the previous follow-up made
the daemon stamp diagnostics on three pre-spawn local-agent failures
too: the unknown-agent and unresolved-binary branches around
connectionTest.ts:1123-1148 and the preflight auth return around
1338-1353. Reword the contract so Settings/CLI consumers do not
incorrectly special-case those early local failures as
diagnostics === undefined.
* fix(connectionTest): keep contracts browser-safe and fold probe output into preflight diagnostics
Two follow-ups from review of #2419:
- ConnectionTestDiagnostics.signal was typed as
`NodeJS.Signals | string | null`, which made the generated .d.ts of
the shared @open-design/contracts surface depend on ambient Node
types. Downstream consumers reading a plain HTTP response shape
should not need @types/node. Narrow to `string | null` (NodeJS.Signals
literals are strings, so the daemon write site is unchanged) and
document the boundary in the field comment.
- The Cursor-style preflight auth path stamped diagnostics built from
the smoke-test sink, which is always empty at that point because the
smoke spawn never happened. As a result the diagnostics block
silently dropped `cursor-agent status`'s own stderr/stdout/exit
context — the only structured failure information available on that
path. Thread the probe output back out of probeAgentAuthStatus()
via new optional stdoutTail/stderrTail/exitCode/signal fields, then
merge them into the diagnostics overrides in connectionTest.ts so
Settings/CLI consumers can render the auth-failure context instead
of just the guidance string.
Tests:
- extended the Cursor preflight regression to assert that diagnostics
carries the probe's stderr ("Not logged in") and exit code (1).
* chore(deps): upgrade express 4.22.1 -> 5.2.1 and @types/express
Breaking changes addressed:
- Renamed all bare wildcard route segments from * to *splat across
src/server.ts, src/static-resource-routes.ts, src/project-routes.ts,
src/import-export-routes.ts, and all three test stubs that define
app.get/options/delete routes using /raw/* or /raw/* patterns
- Updated wildcard param access from (req.params as any)[0] / req.params[0]
to Array.isArray(req.params.splat) ? req.params.splat.join('/') : String(...)
to handle the Express 5 / path-to-regexp v8 change where wildcard params
are now string[] instead of string
- Updated app.get('*') SPA fallback to app.get('/*splat') in server.ts
- Annotated five connector route handlers with Request<{ connectorId: string }>
so the typed param resolves as string, not string | string[], fixing the
10 TS2345 / TS2322 errors that surfaced when @types/express moved to 5.0.6
- Fixed two app.listen() beforeAll callbacks in origin-validation.test.ts to
accept and propagate the optional Error argument Express 5 now passes to
the listen callback, resolving TS2769 overload mismatch
* chore(nix): refresh daemonHash for rebased lockfile
* fix(daemon): await res.sendFile() in async route handlers for Express 5 compatibility
Express 5 res.sendFile() returns a Promise. Without await, async route
handlers return before the response is sent, causing Express to call
next() and fall through to a 404. Add await to all res.sendFile() calls
in async handlers in static-resource-routes.ts and server.ts.
* fix(daemon): use readFile+send for spritesheet route instead of sendFile
Express 5 res.sendFile() returns undefined (not a Promise). ENOENT errors
call next() asynchronously after the route handler's try/catch has returned,
causing unhandled 404 responses. Replacing with fs.promises.readFile + res.send
keeps the error path fully within the handler's try/catch.
---------
Co-authored-by: Patrick A <259201958+eefynet@users.noreply.github.com>
* fix(tabs): treat Home tab as a singleton and prevent duplication
* fix(tabs): address review comments on Home tab singleton navigation and diagnostics
* feat(landing-page): synthesize fallback preview cards for instruction skills
The skill catalog renders a diagonal-stripe placeholder for any skill
without a runnable example.html, which leaves ~70% of /skills/ as a
field of bare grey thumbs (instruction skills like copywriting,
creative-director, color-expert, brainstorming have no static demo
because their output depends on the agent's input).
Synthesize a typographic editorial card from each SKILL.md frontmatter
and screenshot it through the same Playwright pipeline that handles
real demos, so every catalog row carries a thumbnail. Cards include:
- OPEN DESIGN · SKILL top label + Nº NNN index (1..96 over the
instruction subset, sorted by od.featured then alphabetical)
- Big Playfair Display slug with a coral dot accent
- Italic serif description clamped to 3 lines
- mode/category chips + "Curated from <author>" attribution
- Warm-paper background with a subtle 135° stripe to thread the
landing's existing visual language
Bundle a few related improvements caught while building this:
- SkillRecord gains a `kind: 'instruction' | 'template'` field so
the detail page can render differently per kind (instruction
skills now render the SKILL.md body inline as "About this skill",
template skills keep the click-to-expand iframe demo).
- Catalog row thumbnails switch from the bespoke IntersectionObserver
pipeline to native `loading="lazy"` (with eager + fetchpriority=high
on the first 3). The observer's swap latency stranded mid-list
rows on the SVG placeholder during fast scrolls; native lazy uses
the browser's 1250-3000px lookahead so the placeholder flash is
gone.
- precise-lazyload rootMargin bumped to 1500px for any remaining
data-precise-src callers.
- CI cache key for generated previews now folds in
fallback-preview-card.ts so a template tweak invalidates the cache.
* feat(landing-page): rebuild plugins library to mirror in-app taxonomy
The marketing site's `/skills/`, `/templates/`, `/systems/`, `/craft/`
top-level entries were organized around author-supplied `od.mode` /
`od.scenario` taxonomies that visitors never see inside Open Design
itself. The in-app Plugins home (`apps/web/src/components/plugins-home/`)
groups every bundled plugin by the artifact it produces — Prototype,
Live Artifact, Slides, Image, Video, HyperFrames, Audio — and that's
the language users encounter the moment they open the product.
This PR rebuilds the public library around the same taxonomy and the
same data source so a visitor reading "Templates · 231" on the
marketing site sees the same 231 inside the app.
## What changes
- New top-level `/plugins/` hub: four tiles (Templates, Skills,
Systems, Craft) with live counts pulled straight from
`plugins/_official/<bucket>/<slug>/open-design.json` — the daemon's
bundled-plugin registry.
- `/plugins/templates/` lists every bundled plugin that lands in one
of the seven artifact kinds. Seven sub-routes
(`/plugins/templates/prototype/`, `/deck/`, `/image/`, `/video/`,
`/hyperframes/`, `/audio/`, `/live-artifact/`) carry the same chip
rail with an active state, so visitors can switch artifact kinds
with one click without losing the rail.
- Each artifact-kind sub-route shows a Scene chip rail when the kind
has scene buckets (Prototype / Slides / Image / Video each get
five-six). The Scene filter runs client-side via inline `style.display`
toggles; URLs stay one-per-kind so we don't multiply 25 × 18 locales
worth of static pages just for filter combinations.
- `/plugins/skills/` collects the instruction-only entries (mode
doesn't fit any of the seven kinds) — copywriting, color theory,
creative direction, brainstorming, etc.
- `/plugins/systems/` lists the 150 bundled design systems via the
legacy SystemCard renderer (palette swatches, tagline) so the
visual treatment matches the in-product library.
- `/plugins/craft/` keeps the existing craft principles list.
- `/plugins/<manifest-id>/` detail pages built from manifest metadata:
hero (poster image or playable Cloudflare Stream MP4 for video
templates), author / mode / scenario / tags, GitHub source link.
Author URLs pointing at the `nexu-io` org redirect to the
`nexu-io/open-design` repo so the attribution is actionable.
- Header dropdown labelled "Plugins" with the four sub-routes; footer
Library column updated to match.
- Old marketplace registry pages under `/plugins/` and
`/[locale]/plugins/` removed (they were a dormant placeholder UI;
the actual manifests it tried to load lived nowhere). The rest of
the legacy plugin-registry loader stays intact for any other
consumer.
## Preview generation
Bundled plugins ship `od.preview.poster` URLs on R2 for image and
video templates; those are used directly. The other 293 entries
(html-mode examples, design-systems, scenarios) had no poster, so
`generate-previews.ts` was extended to:
1. Screenshot a local `example.html` referenced by `od.preview.entry`
when present (134 examples).
2. Synthesize the same typographic editorial card the SKILL.md
fallback uses, sourced from manifest title / description / mode /
author (159 systems / scenarios / misc).
Output lands at `public/previews/plugins/<manifest-id>.png`. The
catalog loader checks for the local file when the manifest carries no
poster URL, so the row's `<img src>` always has something to point at.
Result: every catalog row and every detail page has a thumbnail;
visiting `/plugins/templates/video/` shows the same 48 entries the
in-app Plugins home shows, hyperframes the same 13, etc.
## Counts
- Templates: 231 (Prototype 59 + Slides 59 + Image 46 + Video 48 +
HyperFrames 13 + Audio 1 + Live Artifact 5)
- Skills: 15
- Systems: 150
- Craft: 11
Atoms (13 infrastructure plugins, `od.kind === 'atom'`) are filtered
to mirror the in-app behaviour.
* fix(landing-page): use Astro 6 render() helper for SKILL.md body
Astro 6 dropped `entry.render()` in favour of a top-level `render(entry)`
helper imported from `astro:content`. The instruction-kind skill detail
page was still using the legacy method, which compiled locally on Astro
6 only because tsx ignored the missing prototype method, but `astro
check` (run in CI) flagged it as ts(2551) and broke the workflow.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
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
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>
* docs: update README skill and design system counts to 132/150
Fixes remaining 7 stale count references identified in #2186:
- Line 3: intro paragraph (31→132, 72→150)
- Line 33-34: shields.io badges (131→132, 149→150)
- Line 68: feature comparison table Skills row (31→132)
- Line 114-115: screenshot caption (72→150, ×2)
- Line 876: user templates note (31→132)
- Line 902: comparison table Skills row (31→132)
Actual counts verified:
- skills/: 132 (134 entries - AGENTS.md - README.md)
- design-systems/: 150 (152 entries - _schema - AGENTS.md)
* fix: deduplicate example cards by skill ID to prevent duplicate rendering
Fixes#2889
The ExamplesTab component was rendering duplicate task-selection cards
when rawSkills contained multiple entries with the same skill.id. This
was particularly visible in the official xhs-white-editorial example flow.
Solution: Add deduplication logic in the skills useMemo hook. After
filtering out aggregatesExamples skills, we now use a Map to ensure
each skill.id appears only once, keeping the first occurrence.
This prevents duplicate cards from being rendered while preserving the
existing filter and sort behavior downstream.
* fix: prevent cursor jumping during IME composition after @ mention
Fixes#2851
When using Chinese input method (IME) after typing @ in the chat
composer, the cursor would jump to the previous character position,
making it confusing and difficult to type Chinese text.
Root cause: The handleChange function was detecting @ mentions and
updating the mention state during IME composition events. This caused
React to re-render and reset the cursor position while the user was
still composing characters.
Solution: Skip mention and slash-command detection when composingRef
indicates an active IME composition session. The existing
onCompositionStart/End handlers already track this state; we now
respect it in handleChange.
This fix applies to all IME-based input methods (Chinese, Japanese,
Korean, etc.) and prevents cursor jumping when typing after @ or /
triggers.
* update d3 visualization skill
* update d3 skill info
* fix(skills/d3): align seed triggers and clone path with SKILL.md
- Add 'd3 scroll' to the d3-visualization triggers array in
seed-curated-design-skills.ts so it matches the 16 triggers
already present in skills/d3-visualization/SKILL.md.
- Change `git clone` target from `.` to `skills/snow-d3` so
the install command produces the path described by the prose.
* 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>
The hover state on the primary button used brightness(0.98) which was
nearly invisible, making the button look unresponsive. Changed to
brightness(0.85) for a clear, perceptible hover feedback.
Fixes#2875
Co-authored-by: 贺闯 <hechuang@xiaomi.com>
The Recent projects card preview iframe rendered the page at the card's own
narrow width (the grid cell is as small as 180px), so a desktop-designed
gallery collapsed — overlapping its sticky top bar and clipping text. The
plugin-home HTML tiles already avoid this by rendering at a fixed design
width and scaling down with transform.
Mirror that here: make the thumb a container (container-type: inline-size)
and render the iframe at 1280x720 (matching the 16/9 thumb), scaled by
`scale(calc(100cqw / 1280px))` so it shrinks to the card's actual width.
Any non-responsive gallery now reads as a faithful thumbnail.
Co-authored-by: nicejames <nicejames@gmail.com>
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.
A design system imported from a SwiftUI repo could never be published. The
import's file scorer was web-only, so every .swift file scored 0 while the
repo's config dotfiles (.zenflow, .vscode, .zed) scored on the generic text
bonus and won the top-N selection. Even when a source file was selected, the
snapshot writer's text-file allowlist didn't include .swift, so it was dropped.
The result: snapshots were all config dotfiles, which the project files API
hides, so the publish gate saw zero evidence snapshots and stayed blocked no
matter how many times the intake ran.
Three layers fixed in tools-connectors-cli.ts:
- scoreDesignFile now scores native/design-token source (Swift, Kotlin, Go,
Rust, etc.), with a high boost for token files like ColorSystem.swift,
Typography.swift, Spacing.swift.
- shouldSkipRepoPath skips editor/CI/agent tooling dirs (.vscode, .zed, .idea,
.zenflow, .github, .husky, ...) so their files stop crowding out real source.
- isTextSnapshotPath recognizes native source extensions so selected files are
actually written.
Also teach the design-system swatch extraction to read SwiftUI colors:
swift-colors.ts parses Color(red:green:blue:), Color(hue:saturation:brightness:)
(HSB), and Color(white:), evaluating decimal, hex-byte, and division component
expressions (0xF4 / 255, 220 / 360), and design-systems.ts uses it as a new
swatch form. scoreDesignFile, shouldSkipRepoPath, and isTextSnapshotPath are
exported for unit tests.
Verified against a real SwiftUI repo: the intake now captures ColorSystem,
TypographySystem, SpacingSystem and the view/model source instead of config
dotfiles.
* 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.
Fork PR workflow runs can arrive without GitHub PR associations, which left ci and visual checks stuck awaiting manual approval even for allowlisted changes. Fall back to a single open PR match on head repo/ref/SHA so the approver stays conservative while still unblocking low-risk fork PR workflows.
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.
* 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.
* introduce MVP Helm chart
* address review feedback and add config checksums
* implement zero-trust sidecar proxy and fix health probes
* address PR feedback on publicBaseUrl and sidecar
* pass ingress headers and consolidate proxy port
* fallback allowed origins for localhost when ingress is disabled to prevent CORS errors during port-forwarding
* enforce proxy security and auto-derive base URL
* hardcode service targetPort to proxy
* require explicit base url for multi-host ingress
* prevent silent CORS failures for LoadBalancer and NodePort and README updates
* Added to the deployment pod annotations. This ensures that changes made to during a helm upgrade will properly trigger a rolling update of the pods instead of serving stale proxy configurations
* dynamically derive scheme per host in allowed origins
* dynamically derive scheme for single-host publicBaseUrl
* add default apiToken placeholder and wrap ingress path guardrail
* add recent changes to readme
* explicitly disallow non-root ingress path prefixes
* update README to reflect path and origin guardrails
---------
Co-authored-by: shaarron <sharon.kroch@gmail.com>
* 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>
The composed chat prompt prepends a '# Instructions (read first)'
block in front of '# User request' so a single user message carries
both the system rules and the actual request — the shape every agent
CLI (Claude, Codex, OpenCode, Gemini) expects on stdin.
In practice claude-opus-4-7 (and a few other instruction-tuned
models, particularly with --include-partial-messages on the stream)
start their reply by echoing the top of that user message verbatim.
The chat UI then shows the system prompt as a literal block leading
the visible answer, e.g.:
Instructions
Always respond in Korean. Use Korean for all explanations…
…Maintain full orthographic correctness…
).네, 완료했습니다. 전달하신 4가지 보강 포인트를 …
(The closing token of the instructions block runs straight into the
real answer without a newline — the telltale of a model-side echo
rather than a UI render bug.)
Close every Instructions block with one trailing line:
(Do not quote, restate, or echo the # Instructions block above in
your reply. Begin your response with the answer to the # User
request below.)
This kills the regression in practice without changing the turn
shape (still one user message), so no agent CLI plumbing has to move.
Tested via tests/chat-route.test.ts — pins the literal guard string
so a future refactor cannot silently drop it.
Co-authored-by: nicejames <nicejames@gmail.com>
* 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.
* 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.
* 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).
* fix(packaged): honor OD_DATA_DIR in desktop runtime
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): scope OD_DATA_DIR by namespace
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): reject relative OD_DATA_DIR overrides
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): preserve scoped OD_DATA_DIR overrides
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): surface OD_DATA_DIR validation as PackagedPathAccessError
Relative OD_DATA_DIR in packaged mode now throws PackagedPathAccessError
instead of a plain Error. apps/packaged/src/index.ts main() only routes
PackagedPathAccessError to dialog.showErrorBox, so the prior plain Error
made the app exit silently for GUI launches with an invalid override.
Extract PackagedPathAccessError into apps/packaged/src/errors.ts so
paths.ts can throw it without an inter-module value cycle with launch.ts.
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): make OD_DATA_DIR absolute-path guard platform-aware
The previous guard ran `win32.isAbsolute(expanded)` unconditionally on
every platform, so on macOS/Linux a value like `C:\Users\Fred\OD` passed
the check (win32 considers it absolute) and silently flowed into
`join(expanded, "namespaces", namespace, "data")`, producing a
cwd-relative POSIX path instead of throwing.
Branch the check on `process.platform === "win32"` so Windows paths are
only accepted on Windows. Update the existing Windows-themed test
fixtures to stub `process.platform = "win32"` (the omission was what
masked this bug) and add a regression that stubs `linux` and asserts
`C:\foo` and `\\server\share` are rejected as PackagedPathAccessError.
Co-authored-by: multica-agent <github@multica.ai>
* fix(packaged): reject mismatched scoped OD_DATA_DIR
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>
* 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.
In RTL the settings cog moves to the left side of the header, but
.avatar-popover still used right: 0 from the LTR rule, pushing the
280px popup off-screen to the left. Mirror the existing
entry-help-popover RTL pattern with right: auto; left: 0 so the
popover stays in view.
Fixes#2719
Fixes#2257
The Windows installer hardcoded calls to powershell.exe for running
instance detection, which fails on systems that only have PowerShell 7
(pwsh.exe) installed. This caused a false positive 'still running'
error during first-time installation.
Changes:
- DetectRunningInstances: try pwsh.exe first, fall back to powershell.exe
- CloseRunningInstances: same fallback logic with detailed logging
- Maintains backward compatibility with Windows PowerShell 5.1
- Provides clear error messages when neither PowerShell is available
The installer now works correctly on:
- Systems with only PowerShell 5.1 (powershell.exe)
- Systems with only PowerShell 7 (pwsh.exe)
- Systems with both installed
- Logs detailed errors when neither is available
Signed-off-by: xxiaoxiong <2482929840@qq.com>
The dev server runs on Next.js webpack by default. On a sizeable
monorepo like this one (19 locale files, many components, the i18n
content surface) webpack dev mode pushes the Node heap past the
default 4 GB ceiling and the process dies with 'Ineffective
mark-compacts near heap limit' after a few hot reloads, leaving the
desktop window pointing at a dead URL.
Switch the dev script to '--turbopack'. Next.js 16 ships Turbopack
as stable for dev, and apps/web/next.config.ts already declares
turbopack.root so the workspace resolution is consistent with the
webpack path. The build script is unchanged on purpose — this PR is
scoped to the dev server, where the OOM repro is.
In practice Turbopack runs the same dev workload with materially
lower steady-state heap usage (Rust-side bundling instead of
JavaScript-side webpack) and recovers faster on HMR, so a long dev
session no longer drifts toward the OOM ceiling.
Co-authored-by: nicejames <nicejames@gmail.com>
The detail-page interactive preview iframe pointed at
`/skills/<slug>/example.html/` and `/templates/<slug>/preview.html/`
with trailing slashes. Cloudflare Pages 308-redirects those URLs to
the extension-stripped form, but with the trailing slash present it
fails to map back to the published `out/skills/<slug>/example.html`
file and SPA-falls-back to the homepage. Result: every preview iframe
in production rendered the homepage instead of the skill or
live-artifact preview.
Verified against the deployed site after the #2679 release:
- /skills/deck-guizang-editorial/example.html → 4942 bytes (real preview)
- /skills/deck-guizang-editorial/example.html/ → 163377 bytes (homepage SPA fallback)
- /skills/deck-guizang-editorial/example → 4942 bytes (real preview)
- /skills/deck-guizang-editorial/example/ → 4942 bytes (real preview)
Drop the trailing slash from all six iframe `src` and "Open in new
tab" `href` attributes in `pages/skills/[slug]/index.astro` and
`pages/templates/[slug]/index.astro`, plus the inline comment that
documented the URL shape.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* fix(web): ensure sketch save button shows saving state
The save handler completes so quickly that the saving UI state flashes
for less than a frame, making it impossible for users to perceive that
a save is actually in progress. Add a 500ms minimum delay on the save
path so the button's saving state is visible, giving users clear
feedback that their action was registered.
Fixes#109
* fix(web): show saved confirmation after sketch save completes
After a sketch save succeeds, show a checkmark icon on the save button
for 2 seconds so users get clear confirmation that the save completed,
addressing the missing success feedback reported in the issue.
Fixes#109
* test(web): add save feedback tests for SketchEditor
Cover the Save button's default, saving, disabled, and post-save
checkmark states, including the 2-second saved confirmation timeout.
Refs #109
* fix(web): gate saved checkmark on explicit save success
saveSketch now returns false on write failure (daemon down, 4xx/5xx,
disk error, dropped connection) instead of resolving normally.
handleSave checks the return value and skips the green checkmark when
the save did not persist.
Refs #109
* fix(web): make sketch save delay a minimum floor not additive
saveSketch previously added a hardcoded 500ms after writeProjectTextFile completed, making the total perceived save time writeTime + 500ms regardless of how long the real write took.
Now it measures elapsed write time and only sleeps for the remainder of 500ms — fast saves still get the minimum visible duration, but a save that takes 600ms skips the sleep entirely.
Refs #109
* fix(web): remove showSaved from sketch save button disable guard
saveSketch's handleSave already skips the save when onSave returns false, so showSaved was doubling as an accidental debounce that blocked the user from saving again for 2 seconds after each save — even when they had continued drawing during the save animation.
The saving prop from the parent and the dirty/canSave check remain in place, so the button is still disabled while the async write is in flight and when there is nothing new to persist.
Refs #109
* fix(web): clear savedTimerRef on unmount
savedTimerRef schedules a setTimeout that calls setShowSaved(false)
after the save confirmation period. When the component unmounts before
the timer fires (e.g., closing the sketch tab), the pending callback
triggers a state update on an unmounted component. Add a useEffect
cleanup to clear the timer, consistent with the existing ResizeObserver
cleanup in this component.
Refs #109
* fix(web): strip trailing whitespace on save button className line
The className="primary" attribute on the save button accumulated
trailing whitespace in the recent save feedback changes. Remove it.
Refs #109
* test(web): add sketch save minimum visibility test for FileWorkspace
Add a test that verifies the sketch save button keeps the "Saving…"
state visible for at least 500ms before transitioning to the saved
checkmark. Mocks fetchProjectFileText, writeProjectTextFile, and a
stub ResizeObserver to make the SketchEditor render cleanly.
Refs #109
* test(web): update SketchEditor save disable assertion after removing showSaved guard
a6aa82a removed showSaved from the save button disable guard, so the button no longer stays disabled for 2 seconds after each save completes. This test expected the save button to remain disabled after save resolution, which no longer matches the component behavior — the saving prop (false) and dirty/canSave check (true) leave the button enabled immediately after save.
Refs #109
* fix(web): clear saved indicator when sketch save fails
When a save succeeds (shows checkmark), then the user saves again
and the second save fails, the stale checkmark remained visible
until the original 2-second timeout expired. The handler now clears
the timer and hides the indicator immediately on failure.
Refs #109
* fix(web): clear saved checkmark when sketch goes dirty again
After a successful save, the checkmark remained visible for the full
2-second window even if the user resumed drawing during that time.
dirty would become true again while the button still showed "Saved",
contradicting the actual unsaved state.
A new useEffect watches the dirty prop and immediately clears the
timer and hides the indicator when the sketch becomes dirty again.
Refs #109
* refactor(web): fix useEffect dependency array indentation
The closing bracket of the useEffect dependency array was indented
one space too far, breaking the code style consistency in
SketchEditor.tsx.
Refs #109
* refactor(web): remove trailing blank line in FileWorkspace
A blank line with trailing whitespace was left between the useEffect
closing brace and the following if-statement, violating the file's
whitespace conventions.
Refs #109
* fix(web): give sketch save button a stable accessible name
Refs #109
* test(web): add a11y tests for sketch save button aria-label
Refs #109
When the chat panel is widened, the workspace panel shrinks and
the preview toolbar action buttons can overflow. This changes the
fixed toolbar height to a min-height and enables flex-wrap on the
toolbar actions container so controls wrap instead of clipping.
Fixes#2166
Co-authored-by: algojogacor <algojogacor@users.noreply.github.com>
Two CI failures from PR #2461 root-caused to wrong picks in the
merge:
* apps/web/src/components/plugins-home/PluginCard.tsx — reverted to
release-side. The release-side version uses `localizePluginTitle
(locale, record)` / `localizePluginDescription(locale, record)`
to read each plugin's `titleI18n` / `descriptionI18n` fields,
driving the 'localizes plugin card titles' test in
plugins-home-section.test.tsx (which asserts '瑞士国际主义 Deck'
appears under zh-CN). The main-side version replaced that
record-level i18n with hardcoded English + `t()` aria-label
keys — a finer-grained i18n migration but a fundamental loss of
the record-level localization the test exercises. Taking
release-side keeps the test functionality; the aria-label i18n
keys are micro-optimisation we can re-port in a follow-up.
* e2e/ui/settings-local-cli-codex-fallback.test.ts — added the
`SETTINGS_MENU_LABEL` constant declaration that the menu-
dismissal helper (kept from main in c14baf07) references at
line 161. main's diff added the const at the top of the file but
it didn't carry through auto-merge alongside the helper block;
this restores it.
Both fixes verified locally:
- PluginCard now grep-finds locale + localizePluginTitle usage.
- fallback test grep-finds SETTINGS_MENU_LABEL declaration.
PR #2461 sync prep — resolves 14 conflicts merging 84 main-side commits
on top of 58 release-side commits accumulated during the 0.8.0 cycle.
Resolution summary:
Take main (theirs) where main carried deliberate forward progress:
- apps/web/src/components/PluginCard.tsx — 7 hunks, i18n migration:
hardcoded English aria-labels/titles replaced with t() calls keyed
on pluginCard.* (all 8 keys verified present in en.ts).
- apps/web/src/components/TasksView.tsx — 1 hunk, source-ingestion
feature: sortedRoutines (newest-first), sourceIngestionTemplates,
patchSourceForm, submitSourceIngestion. activeCount/pausedCount
semantics preserved (now keyed on sortedRoutines, count unchanged).
- e2e/ui/app.test.ts — new node:fs/promises + tmpdir + path + @/timeouts
imports needed by main-side test helpers.
- e2e/ui/settings-local-cli-codex-fallback.test.ts — menu-dismissal
helper block added by main.
Keep both sides where each added a different field to the same object
literal:
- apps/web/src/components/ProjectView.tsx (locale + analyticsHints
spread).
- apps/web/src/components/DesignSystemFlow.tsx (locale + analyticsHints).
Take release (ours) where release carried deliberate work that ships
0.8.0:
- CHANGELOG.md — release-side 0.8.0 entry + PR link refs; main's
Unreleased section was the same body of work, now finalized.
- apps/landing-page/public/{apple-touch-icon,favicon}.png +
apps/web/public/app-icon.svg — release-side visual refresh assets
consistent with 0.8.0 stable ship.
- tools/pack/src/linux.ts — packageVersion const required by line 466;
taking main's empty line would build-error.
- e2e/ui/project-management-flows.test.ts +
e2e/ui/settings-api-protocol.test.ts +
e2e/ui/settings-memory-routines.test.ts — release-side release-smoke
hardening (shangxinyu1 + PerishFire) takes precedence on overlap.
Closes-issue / unblocks: PR #2461 sync release/v0.8.0 → main.
#2160 removed `cp` from the node:fs/promises imports at the top of
tools/pack/src/mac/app.ts, but the `await cp(...)` call at line 269
that copies apps/desktop/dist/main/preload.cjs into the assembled app
root was not removed alongside it.
Result: TS2304 "Cannot find name 'cp'" at workspace install (postinstall
builds @open-design/tools-pack), which fails Preflight, which gates
every downstream PR job on main — Validate workspace, Daemon workspace
tests, Web workspace tests, Browser tests, Workspace unit tests, and
Build workspaces all fail at ~43-48s with the same install crash.
main HEAD's `ci` workflow is also red on this same failure
(commit 650f7a5d, run 26322737524). Re-add `cp` to the imports;
no other changes needed.
* feat(web): point .jsx module previews at their HTML entry
Multi-file React prototypes load .jsx modules from an HTML entry via
<script type="text/babel" src>. A module previewed on its own has no
standalone component, so it dead-ended on the React runtime error
"No React component export found".
Modules are now detected (a .jsx/.tsx referenced by a sibling HTML
entry's babel script src) and handled:
- The Preview shows a pointer to the HTML entry(ies) that render the
module; clicking one opens that page and closes the module tab.
- The Code tab still renders the raw source.
- Such modules no longer auto-open as preview tabs after a write.
* fix(web): bound module-detection cache, ignore commented-out scripts
Review follow-up on the .jsx module preview pointer:
- ProjectView: key the HTML content cache by file name with mtime stored
alongside, so a rewrite replaces the file's single entry instead of
leaking a new name@mtime key per revision.
- extractBabelScriptSrcs: strip HTML comments before scanning so a
commented-out babel script is not collected as a live reference.
- i18n: normalize the three new jsxModule values to single quotes across
all 19 locales to match each file's existing style.
* fix(prompt): instruct discovery form to follow user's chat language
The discovery form was reaching users in English even when their UI
language was Chinese (#1416). The form is generated by the LLM under
guidance from packages/contracts/src/prompts/discovery.ts, but the
prompt only mentioned that option labels MAY follow the user's
language. The example form embedded English text for title,
description, per-question labels, and placeholders, and the LLM
copied that text verbatim instead of localizing.
Two minimal changes to the prompt:
1. Add a sentence under RULE 1 making the language-match expectation
explicit before the example forms.
2. Expand the Form authoring rules bullet so it covers every
user-facing string (title, description, label, placeholder, option
label) and pins the unlocalized identifiers (id, type, option
value, branch values) for the runtime branch logic.
Fixes#1416
* fix(prompts): mirror discovery localization rule to daemon prompt copy
Apply the same 'Match the user's chat language' paragraph and the
expanded 'Localize every user-facing string' bullet to
apps/daemon/src/prompts/discovery.ts, which the daemon-backed chat
path uses (it imports ./discovery.js, not the contracts copy).
Also add apps/daemon/tests/prompts/discovery-localization-drift.test.ts,
which reads both prompt copies and asserts each one contains both rules,
so the contracts and daemon files cannot silently drift on this behavior.
Apply-anyway reason: pnpm install / pnpm vitest could not run locally
(registry DNS blocked in sandbox + node v26 vs required v24). Direct
Node content assertion over both files passes. CI will run vitest.
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix(web): route chat file links to workspace preview instead of new window (#1239)
Chat-emitted markdown links like `[template.html](template.html)` rendered
as `<a target="_blank">` with no click handler. In Electron that hits
`setWindowOpenHandler` and creates a new `od://` BrowserWindow; relative
hrefs have no base so the new window can't resolve them and the user
lands on the home screen — the file they wanted to preview is never
shown.
Detect in-project file paths in chat markdown via a new
`asInProjectFilePath` helper and route them through the existing
`requestOpenFile` workspace tab opener. External URLs, `mailto:`,
`#anchors`, absolute paths and `..` traversal keep their default
browser-link behavior. The `renderMarkdown(options)` extension is
backwards-compatible: existing callers (file viewer, system reminders)
keep their default `target="_blank"` behavior when the option is
omitted.
Closes#1239.
* fix(web): decode percent-encoded chat file links before workspace open (#1239)
Chat markdown frequently emits links as URL-encoded text — `Mock%20Page.html`
for a file named `Mock Page.html`, multi-byte sequences for non-ASCII
filenames. The workspace tab opener (`requestOpenFile` →
`FileWorkspace`) matches by literal on-disk file name, so handing it
the raw `%20`-encoded form silently misses the existing tab and the
user sees nothing happen on click — the exact regression #1239
reopened against.
Decode after the literal `..` check and re-check `..` on the
decoded form so a `%2E%2E` smuggling attempt cannot bypass the
traversal guard. Malformed encodings fall through to `null` (default
browser link behavior) instead of letting URIError crash the
renderer.
The same gap was flagged on the earlier draft PR #1255 by mrcfps and
lefarcen (P2) but never landed there; this PR now covers it with
five new regression tests (ASCII spaces, nested subdirs, UTF-8 byte
sequences, malformed `%`, percent-encoded traversal).
* fix: prevent plugin info collapse button from scrolling with content
Changed .ds-modal-stage-handle from position: absolute to position: sticky
so the collapse/expand button stays anchored at the vertical center of the
viewport instead of scrolling with the sidebar content.
Before: button moved with scrolling content and could overlap text
After: button remains fixed at 50% viewport height, always accessible
Closes#2209
* fix: use sticky positioning only for collapse button
The original approach broke the expand button because changing the base
class to position: sticky made the expand button's right: 0 ineffective.
Correct fix:
- Base class stays position: absolute (for expand button at stage edge)
- Only .is-collapse overrides to position: sticky (for sidebar scroll)
This ensures:
- Expand button anchors correctly at the right edge when sidebar closed
- Collapse button stays at viewport center when sidebar content scrolls
- No regression in either collapsed or expanded state
* fix(daemon): isolate per-agent detection failures so one bad probe cannot blank the picker
`detectAgents` ran every adapter probe in a bare `Promise.all`, so a
synchronous throw from any single probe (e.g. a filesystem error
during PATH walking on a packaged Windows daemon, or an unhandled
async rejection from one of the post-launch probes) rejected the
whole batch. The `/api/agents` route's `catch(() => [])` then handed
the UI an empty list and the model picker collapsed to BYOK / Cloud
only, losing every installed CLI option — which matches what users
in issue #2297 observed after one or two app restarts on Windows.
Wrap each probe in `safeProbe` so a single failure degrades just that
adapter to `unavailableAgent(def)` while the rest of the registry
keeps its real availability. The new
`apps/daemon/tests/runtimes/detection-resilience.test.ts` pins both
synchronous failure sites that previously sat outside the existing
inner try/catch blocks (`resolveAgentLaunch` and
`applyAgentLaunchEnv`) so a future code change cannot regress the
isolation contract.
This is a defensive guard rather than a Windows-only diagnosis: it
fixes any scenario where a single probe blows up, including ones we
have not reproduced yet. If a user still hits #2297 after this lands,
the daemon log will identify which adapter failed instead of silently
returning an empty list.
Fixes#2297
* ci: re-run checks (unrelated e2e baseline flake on previous run)
* docs(README): refresh stale skills and design-systems counts
The English README still reported 31 skills / 72 design systems and a
prototype(27)+deck(4) split last accurate around v0.4.1. Counted from
SKILL.md / DESIGN.md entry files at the current HEAD: 132 skills
(32 prototype, 9 deck, plus image/video/audio/template/design-system/
utility modes) and 150 design systems.
Updates the eight inline references in README.md plus the two ASCII
tree comments under `skills/` and `design-systems/`. Adds a one-line
note that the mode taxonomy now spans more than just prototype + deck
so the description stops contradicting the visible mode chips in the
picker.
Translations (README.de/fr/es/ko/ja-JP/zh-CN/zh-TW/pt-BR/ar/ru/uk/tr)
still carry the old numbers and need follow-up PRs from their original
translators; intentionally out of scope here to keep this PR easy to
review.
Refs #2186.
* docs(README): include utility mode in catalog summary
The mode summary listed image/video/audio/template/design-system but
dropped utility. The tree comment later in the same file already
mentions utility, and one skill (pptx-html-fidelity-audit) is registered
with mode: utility. Add utility to the inline list so the README stays
internally consistent with the registry the surrounding counts cite.
The Open Design wordmark sits next to the circular app icon in the
macOS title bar (`EntryNavRail` home button), and the inner mark inside
that circle reads as slightly off-center because its bounding box sits
about 16px right and 9px below the circle's geometric center.
Shift the inner mark by (-15.635, -9.135) so its bbox center matches
the circle center at (266.503, 266.503). The change rewrites the two
sub-paths that draw the mark and its interior cut-out; the outer ring
keeps the original coordinates so the gradient mask, blur filters, and
ring weight all render identically. Dock and Launchpad icons live in
`tools/pack/resources/{mac,win,linux}/icon.*` and are not touched, so
this fix is scoped to the title bar / favicon / mask-icon surfaces as
described in #2401.
PR #2724 (commit dbc86777) added a useI18n mock to fix the runtime
`No "useI18n" export defined` error in ProjectView.reattach-restore.test.tsx,
but ended up with TWO useI18n keys in the same object literal — the
intended new entry at the top of the mock, plus an apparently-existing
second entry at the bottom. main is now red at typecheck:
apps/web typecheck: tests/components/ProjectView.reattach-restore.test.tsx(41,3):
error TS1117: An object literal cannot have multiple properties with
the same name.
Drop the duplicate (the second, less-complete entry), keeping the
first one that matches the pattern used by the 9 sibling ProjectView
test files (useI18n returning { locale, setLocale, t }).
Verification: pnpm --filter @open-design/web typecheck → exit 0,
pnpm --filter @open-design/web test ProjectView.reattach-restore →
10/10 pass.
ProjectView calls useI18n() for locale/t, but this suite's `../../src/i18n`
mock only returned useT — so every render threw `No "useI18n" export is
defined on the i18n mock` and 5 of 10 cases failed. Mirror the i18n mock the
other ProjectView suites already use (useI18n + useT) so the suite renders.
Co-authored-by: nicejames <nicejames@gmail.com>
* feat(mcp): add write_file, delete_file, delete_project tools
External coding agents driving Open Design through MCP can create new
artifacts (create_artifact) but cannot iterate on a file once written
(create_artifact rejects existing targets), cannot remove a stale
file, and cannot tear down a throwaway project they just spun up via
create_project. Close that loop so the same agent can drive the full
file/project lifecycle end-to-end through MCP.
- write_file(path, content, encoding?): POSTs to /api/projects/:id/files
without `artifact: true`, which the daemon route writes as a plain
overwrite. Supports nested paths and base64 binaries.
- delete_file(path): DELETEs /api/projects/:id/raw/<path> so nested
paths work just like create_artifact's nested name argument.
- delete_project(project, confirm:true): DELETEs /api/projects/:id but
refuses to fall back to the active project and requires confirm:true,
since the operation purges the SQLite row and on-disk project dir
irreversibly. Marked destructiveHint:true on the annotation.
Tests cover each tool's success path, the active-context fallback for
write/delete_file, missing-argument rejection before any network call,
the daemon-error mapper, and the two delete_project guards.
* fix(mcp): echo resolvedProject from delete_project and cover the daemon error path
Two follow-ups from review of #2416:
- delete_project accepts a name substring per its inputSchema and the
server instructions block tells callers to verify which row was
matched via resolvedProject. write_file/delete_file already honor
that contract via withActiveEcho(json, active, resolved), but
deleteProject destructured only `id` and dropped the echo on the
one irreversible tool. Capture `resolved` and pass it through;
active is always null here because the active-context fallback is
intentionally disabled.
- formatDaemonError and the !resp.ok branches in writeFile/deleteFile/
deleteProject had zero coverage — all nine tests stubbed status: 200.
Add three regressions covering the structured-error reformat, the
raw-text fallthrough for non-JSON bodies, and the irreversible
delete_project surface, so a regression in the parse/fallthrough
logic will fail in CI instead of reaching agents.
* fix(tools-dev): preserve web origin trust on web start
Restart daemon/web when the trusted web port is missing, and reuse the active web port during repeated starts so run web and start web keep app-config origin checks aligned.
Generated-By: looper 0.0.0-dev (runner=worker, agent=opencode)
* fix(plugins): refresh official registry bundled count
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(tools-dev): preserve daemon/web reserved ports
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(tools-dev): preserve daemon reuse on web start
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(tools-dev): preserve running daemon port on web reuse
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(tools-dev): reserve explicit web port before daemon allocation
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* test(web): stabilize media provider reload flash timing
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(web): restore merged reattach workspace coverage
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* fix(tools-dev): reserve allocated daemon port
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* test(e2e): wait for artifact manifest persistence
Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
* chore(e2e): improve test framework quality
- Add lib/timeouts.ts with CI-scaled short/medium/long/xlong constants
- Add lib/playwright/mock-factory.ts to centralise standard localStorage,
/api/agents, and /api/app-config mock setup; migrate critical-smoke and
workspace-keyboard-flows to use applyStandardMocks()
- Delete empty lib/shared.ts placeholder
- Replace waitFor({ state: 'detached' }).catch(() => {}) with
waitFor({ state: 'hidden' }) in all UI tests; 'hidden' resolves
immediately when the element was never in the DOM, eliminating the
silent error-swallowing catch
- Remove redundant .catch(() => false) from all isVisible() call sites
since isVisible() never throws in Playwright
- Convert .waitFor().then(() => true).catch(() => false) guards in
openDesignFile() to explicit try/catch blocks for clarity
- Simplify sendPrompt() in app.test.ts: replace the 3-attempt manual
retry loop with a single fill + pressSequentially fallback; the core
workaround for contenteditable unreliability is preserved but the
loop structure is gone
* fix(e2e): guard routeMockAgents to GET only
routeMockAgents was intercepting all HTTP methods and returning the mock
fixture, silently swallowing any agent mutation requests. Mirror the
GET-only guard from routeAppConfig so writes fall through to the daemon.
* fix(e2e): address code review findings
- sendPrompt() in app.test.ts, workspace-keyboard-flows.test.ts,
app-restoration.test.ts: drop fill() (unreliable on contenteditable,
inputValue() always returns '' for them) and go straight to
pressSequentially(), which types key-by-key and is authoritative
- Import T from timeouts.ts in app.test.ts and use T.short for the
input/button waits, making the timeouts module non-dead
* fix(e2e): resolve adversarial review findings
- Revert sendPrompt to fill(): chat-composer-input is a textarea, not
contenteditable; fill() is atomic and ~60x faster than pressSequentially
- Use T.medium in all waitForLoadingToClear calls: CI workers scale this
to 20s automatically via the CI env var, eliminating cold-runner flakes
- Add T import to 6 files that needed it for T.medium
- Fix openDesignFile try/catch scope in app-manual-edit: previously the
catch block only caught waitFor but click/expect errors were also swallowed;
now only waitFor is inside try, real interaction failures propagate
- Fix regex escaping: .replace('.', '\\.') -> .replace(/\./g, '\\.') in
app-manual-edit and app-design-files to handle multi-dot filenames
- Migrate entry-chrome-flows.test.ts to applyStandardMocks: it had the
identical 3-call setup pattern as the factory but was not migrated
- Add GET method guard to project-management-flows app-config route handler,
matching the pattern used by every other route handler in the suite
- Remove no-op 'as const' from timeouts.ts: Math.ceil returns number,
not a literal, so the assertion had no effect
- Update e2e/AGENTS.md: remove deleted lib/shared.ts entry, document
lib/timeouts.ts and lib/playwright/mock-factory.ts
* fix(e2e): scope openDesignFile try/catch to waitFor only
Move click and expect(preview).toBeVisible() outside the catch block so
that a regression in either open path (tab-click or file-list fallback)
fails loudly instead of being silently absorbed. The try now wraps only
the fileTabButton.waitFor existence probe; the subsequent click and final
assertion are unconditional.
---------
Co-authored-by: Patrick A <186436799+eefynet@users.noreply.github.com>
Co-authored-by: Patrick A <259201958+eefynet@users.noreply.github.com>
* fix(web): chat pane preserves scroll position when todo card grows
The PinnedTodoSlot renders outside the chat-log scroll container. When
the todo card grows (new tasks added via TodoWrite), the scroll container's
clientHeight shrinks in the flex layout, drifting the user away from the
bottom. The existing ResizeObserver only observed children of the chat-log
div, so pinned-todo growth was invisible to followLatestIfPinned.
Fix: pass a containerRef to PinnedTodoSlot and observe that element in the
same ResizeObserver. syncPinnedTodo() is called on effect setup and from
the MutationObserver callback so observation stays current as the slot
appears and disappears across TodoWrite snapshots.
Red spec: apps/web/tests/components/chat-todo-autoscroll.test.tsx
* fixup! fix(web): chat pane preserves scroll position when todo card grows
Clarify test comment: the second test confirms followLatestIfPinned
snaps scroll to bottom when fired. The structural guarantee (pinned-todo
element is observed) is separately asserted in test 1, which is the
check that goes red on main without the fix.
* fix(web): correctness extend MutationObserver to pane ancestor for PinnedTodoSlot mount detection
The MutationObserver was only watching the .chat-log element. PinnedTodoSlot
(.chat-pinned-todo) is a sibling of .chat-log-wrap inside .pane, outside the
observed subtree. syncPinnedTodo inside the MutationObserver callback was
therefore dead code for mount/unmount transitions of the slot.
Add a second observation on paneEl (el.parentElement?.parentElement) with
childList-only so the MutationObserver fires when PinnedTodoSlot mounts or
unmounts and syncPinnedTodo can register/deregister the element with the
ResizeObserver.
* test(e2e): chat pane auto-scroll on todo card growth
Add Playwright spec that goes red on origin/main and green on this fix
branch. Scenario A asserts that a chat-log pinned to the bottom snaps
back after the PinnedTodoCard grows (the ResizeObserver-on-pinned-todo
path). Scenario B asserts that a deliberate scroll-up is not overridden.
Also allow OD_WORKSPACE_ROOT env override in next.config.ts so Turbopack
resolves node_modules correctly when the web app is booted from a worktree
whose node_modules symlinks resolve outside the default workspace root.
* docs(agents): note pinned-todo observer coverage in chat UI conventions
PinnedTodoSlot sits outside the .chat-log scroll container, so the
ResizeObserver and MutationObserver coverage that keeps auto-scroll
working when the todo card grows is non-obvious to future implementers.
Document the invariant in the Chat UI conventions section.
* fix(web): validate OD_WORKSPACE_ROOT, harden autoscroll test precondition
* fix(web): validate OD_WORKSPACE_ROOT existence, make autoscroll precondition unconditional
* fix(web): throw on invalid OD_WORKSPACE_ROOT instead of warn-and-fallback
* fix(web): require pnpm-workspace.yaml at OD_WORKSPACE_ROOT, drop dead test branch
Three follow-ups to nettee's review feedback:
1. apps/web/next.config.ts gains a pnpm-workspace.yaml existence check
after the relative-path validation. Without it, an override like
'<repo>/apps' or '<repo>/apps/web' passes the relative(resolved, WEB_ROOT)
check but the resolved path is missing the sibling packages/* directory
that apps/web imports from (for example @open-design/contracts). Next
would later fail deep inside file tracing / Turbopack with a much
harder-to-diagnose error. Now we throw at config load with a clear message.
2. e2e/ui/chat-todo-autoscroll.test.ts drops the redundant
'if (scrollUpOccurred)' branch. The hard precondition above it already
guarantees distanceAfterScroll > 80, so the if was dead code that read
as a false-green path. The body now runs unconditionally.
3. Same test tightens the post-grow assertion. The previous
toBeGreaterThan(60) would pass even if a regression dragged the log
most of the way back to the bottom (e.g. before=150, after=61).
Replaced with Math.abs(distanceAfterGrow - distanceAfterScroll) less than
SCROLL_PRESERVATION_TOLERANCE_PX (20) — a delta check that actually
verifies the comment's claim of 'within ~20px of where the user left it'.
* fix(web): canonicalize workspace root with realpathSync and tighten scenario B assertion
- Use realpathSync on both resolved and WEB_ROOT before the ancestor check so
that symlinked paths (macOS /tmp vs /private/tmp, worktree checkouts) compare
correctly instead of false-throwing on a physically valid override.
- Add isAbsolute(rel) guard for the Windows cross-drive case where path.relative()
returns an absolute path instead of a ..-prefixed string.
- Scenario B: replace distance-to-bottom delta assertion with scrollTop preservation
check. Growing the pinned todo naturally increases distance-to-bottom by ~extraPx
(clientHeight shrinks while scrollTop is held fixed), so the old Math.abs(after -
before) < 20 check would fail on correct behavior. asserting scrollTop directly
catches the real regression: followLatestIfPinned incorrectly snapping a non-pinned
user back to the bottom.
- Add hard precondition that clientHeight actually changed so the test fails fast
if the layout stops exercising the non-pinned path.
* test(e2e,web): add clientHeight guard to scenario A and mount-wiring unit test
---------
Co-authored-by: Patrick A <eefynet@users.noreply.github.com>
Co-authored-by: Patrick A <259201958+eefynet@users.noreply.github.com>
* blog(landing-page): announce Open Design 0.8.0 plugin engine rebuild
Adds the Product (announcement) post for the open-design-v0.8.0 release
(305 PRs · 75 contributors · 7 days; tag c20d156). Covers the three
architectural shifts that ship in 0.8.0 — plugin engine, headless-by-
default CLI, plugins create plugins — plus the design-system /
critique-theater / media-provider / packaged-auto-update plate. CTA
links to the GitHub release.
Also:
- Hero plate plate-15-plugin-engine-modular.png wired into postImages
map in app/pages/blog/index.astro (editorial study plate, warm paper
+ red circle, per the open-design-blog-factory hero policy)
- _topics.md backlog updated: new row in Shipped, supersedes the queued
#9 "auto-update on Windows + Linux" row (folded into this release
announcement), Last reviewed bumped to 2026-05-22
Verified locally:
- pnpm typecheck — 0 errors, 0 warnings
- tsx scripts/blog-indexing/lint-blog-seo.ts — 0 errors, 0 warnings
Co-authored-by: Cursor <cursoragent@cursor.com>
* blog(landing-page): rework plate-15 to match editorial study-plate family
The first generation of plate-15 read as a marketing cover (16:9 frame
with baked-in "Plugin Engine Rebuild 0.8.0" headline + OPEN DESIGN
wordmark), which broke the visual register of plates 09–14 — all square
1024×1024 editorial study plates with a single dominant red circle and
no on-image typography.
Regenerated the plate to match the family: 1024×1024, warm cream paper
with deckle edges, a bold red circle in the upper-right, schematic
plugin tiles around a central black cube engine core, olive branch +
ink arc at the bottom, no headline / wordmark text. Updated the alt
text to describe the actual artefact.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: ashley li <ashleyli@ashleydeMacBook-Air-2.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(landing-page): detail pages — interactive preview, share row, dual CTAs
Joey requested three additions to every `/skills/<slug>/` and
`/templates/<slug>/` detail page, with opendesigner.io's skills
catalog and youmind.com's seedance prompt page as references.
What
- **Interactive preview**: a `<details>` toggle below the static thumb
reveals an `<iframe sandbox>` rendering the canonical artifact
(`/skills/<slug>/example.html` for skill-template origins,
`/templates/<slug>/template.html` for live-artifact origins). The
iframe loads lazily — only on first toggle — so the page stays fast.
An "Open in new tab ↗" pill on top-right of the frame links to the
same URL standalone.
- **Six-channel share row**: Reddit, X, LinkedIn, Facebook, Email,
Copy-link. Each anchor is a vendor "intent" URL (no tracker SDKs);
the copy-link button uses the Clipboard API with a `prompt()`
fallback for older Safari / embedded webviews. Wired by a small
handler appended to `header-enhancer.astro`.
- **Two primary CTAs** in the detail-actions row:
- "Use this skill →" / "Use this template →" routes to
`/quickstart/?skill=<slug>` (or `?template=<slug>`). The OD
desktop client has no public protocol handler yet, so a
`od://skill/<slug>` deep link would 404. Quickstart is the v1
pivot; once the client registers a scheme, the anchor flips to
a JS try-`od://`-then-fallback without changing the page surface.
- "Find on GitHub →" deep-links into the source folder.
Share copy keeps "open-source Claude Design alternative" front and
center across every channel — same brand keyword Google associates
with the homepage and `/alternatives/claude-design/`, so each social
click reinforces the same entity claim. Per-skill name + summary
follow so a reader who lands on a friend's tweet has a concrete
reason to click.
- X intent: "I'm using <skill> from @opendesignai — the open-source
Claude Design alternative.\n\n<description>"
- Reddit submit title: "<skill> — open-source Claude Design alternative"
- Email subject: same as Reddit; body: "I thought you'd like this —
<skill>, an open-source Claude Design alternative skill from Open
Design.\n\n<description>\n\n<url>"
- LinkedIn / Facebook: URL-only (those vendors auto-fetch OG meta,
so they read the existing canonical title + image).
Surface area
- Marketing site only. `apps/landing-page/app/pages/skills/[slug].astro`,
`pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
`sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies. WeChat QR was dropped from the v1
scoping in favor of Joey's revised channel set; brings Reddit and
Facebook in instead.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-swiss-international/` shows all six share
buttons, both CTAs, and the iframe `<details>` toggle. Same on
`/templates/magazine-poster/`.
- Local dev: Reddit submit URL contains the SEO keyword in the title
param; X intent URL contains the @opendesignai mention + keyword
in the tweet body; Email mailto: subject + body wired correctly.
Followups
- Once OD desktop client registers a `od://` scheme, flip the "Use
this skill" anchor to JS-driven try + fallback so installed users
bypass /quickstart/.
- Translate the share copy + CTA labels across the 18 landing
locales (currently English-only).
- `i18n.ts` `ui.catalog.skills` keys could absorb the share-copy
template if we want per-locale share text in the future.
* fix(landing-page): preview clicks the thumb; CTA goes to releases
Two follow-ups to #2679 against Joey's review.
1. Preview UX: the thumb is the trigger
The previous shape rendered a static thumb followed by a separate
"View interactive preview ▸" disclosure row underneath. Joey wanted
one composed unit: click the thumb itself to open the live frame.
Wraps the existing `<details>` so that `<summary>` IS the thumb
image (with a hover overlay revealing "Click for live preview ↗"),
and once open the summary hides so the iframe lands in the same
visual slot. The figcaption moves below the open/closed unit so it
labels both states identically.
2. "Use this skill" / "Use this template" → /releases
Sends users straight to the desktop-app release page rather than
pivoting through /quickstart/. The flow is now concrete (download
the binary now) instead of asking users to read an install doc as
step 0. Once the desktop client registers a `od://skill/<slug>`
protocol handler, this anchor flips to a JS try-deep-link-then-
fallback without changing the page surface.
Note on the other two issues Joey raised:
- example.html 404: production has all 4 example files at HTTP 200
(verified with curl). The 404 in his screenshot was production
serving the previous deploy that pre-dates this PR; the fix is in
flight, not a missing route. Once #2679 deploys, the iframe will
resolve cleanly.
- Empty share copy: same root cause. Production HTML still rendered
the pre-#2679 share row (no copy at all). Local dev confirms the
X intent URL contains the full "I'm using <skill> from
@opendesignai — the open-source Claude Design alternative…"
string in the `text` param; Reddit submit URL contains the
"<skill> — open-source Claude Design alternative" title; Email
mailto: subject and body are wired. LinkedIn and Facebook are
URL-only by their vendor design — those platforms read the OG
meta tags from the destination page itself.
Surface area
- Marketing site only. `pages/skills/[slug].astro`,
`pages/templates/[slug].astro`, `sub-pages.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: skill detail thumb shows the live-preview overlay on
hover; click opens the iframe in the same frame. Use this skill
→ opens https://github.com/nexu-io/open-design/releases. Same on
the templates detail page.
* fix(landing-page): example.html copy step + share dialog with copy-then-paste flow
Two follow-ups against Joey's review of #2679.
1. example.html 404 — production was SPA-falling back to the homepage
The "404" Joey screenshotted on
`/skills/deck-guizang-editorial/example.html` was a Cloudflare Pages
SPA fallback: the URL returned HTTP 200 but the body was the
homepage HTML, so the iframe loaded "the homepage inside the iframe"
which the browser displays as broken-page. Root cause: the build
artifact never contained `out/skills/<slug>/example.html`. Astro
generates `<slug>/index.html` for the detail page from `[slug].astro`,
but the canonical `example.html` next to the SKILL.md file in the
repo root never gets copied into `out/`.
Adds `scripts/copy-example-html.ts` and chains it into the
`build` script. After `astro build`, the script walks:
- `skills/<slug>/example.html` → `out/skills/<slug>/example.html`
- `design-templates/<slug>/example.html` → `out/skills/<slug>/example.html`
(design-templates surface as skill-template-origin records in the
catalog and the iframe targets the `/skills/<slug>/example.html`
path for those.)
- `templates/live-artifacts/<slug>/template.html` → `out/templates/<slug>/template.html`
(live-artifact-origin records — the iframe targets template.html.)
Source files that don't exist are silently skipped. The script
prints a summary line so the build log makes the count visible.
2. Share UX — modal with copy-then-paste flow
The previous inline 6-button row had two problems Joey called out:
- Position was below the meta block, not prominent enough.
- LinkedIn and Facebook ignore `text` pre-fill params, so users
landing on those platforms saw an empty composer with no idea
what to write. X / Reddit pre-fill works but truncates Chinese
unpredictably.
Replaces the row with a `<dialog>` modal:
- A `Share ↗` button sits inside `.detail-actions` next to the
primary CTAs, so it has equal visual weight.
- Clicking opens the dialog with the canonical share copy
(containing the brand SEO keyword "open-source Claude Design
alternative") in a readonly `<textarea>`.
- `Copy text` button writes the textarea contents to the clipboard
(with a `prompt()` fallback for older browsers) and flashes the
coral confirmation state.
- `Copy link only` writes just the URL.
- Below: a row of platform jump buttons (X · LinkedIn · Reddit ·
Facebook · Email). Each opens the vendor's compose URL in a new
tab. The user pastes the already-copied text — uniformly
reliable across every platform.
- Modal closes via the × button (form method="dialog") or Escape.
Native `<dialog>` element + `showModal()` API. No new dependencies;
the JS handler lives in the existing `header-enhancer.astro`
inline script alongside the headroom + stars + hamburger handlers.
Surface area
- Marketing site only. `pages/skills/[slug].astro`,
`pages/templates/[slug].astro`, `_components/header-enhancer.astro`,
`sub-pages.css`, plus the new `scripts/copy-example-html.ts` and
one-line `package.json` build script change.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/<slug>/` shows the `Share ↗` trigger inside
detail-actions; clicking opens the modal with the readonly
textarea pre-filled with the canonical share copy. Copy text /
Copy link only both flash coral on click and write to clipboard.
Platform buttons open compose pages in new tabs.
- After deploy: `/skills/<slug>/example.html` will resolve to the
actual canonical example output rather than SPA-falling back to
the homepage. Same for templates.
* fix(landing-page): example.html endpoint routes + locale-aware share + brand logos
Three follow-ups against Joey's review of #2679 round 2.
1. example.html 404 — root cause + proper fix
The 404 Joey kept seeing was real, not a deploy lag: nothing in
the build pipeline copied `skills/<slug>/example.html` from the
repo root into the landing-page output. Astro generated only the
detail-page `index.html`; Cloudflare Pages SPA-fell-back to the
homepage on requests for `example.html`, which the browser
rendered as "wrong page in iframe" and Joey read as 404.
Replaces the post-build copy script (`scripts/copy-example-html.ts`,
removed) with two Astro endpoint routes:
- `pages/skills/[slug]/example.html.ts` — streams the canonical
example for skill-template-origin records, including the
design-templates passthrough
(`design-templates/<slug>/example.html` → same URL).
- `pages/templates/[slug]/template.html.ts` — streams the canonical
artifact for live-artifact-origin templates.
Both use `getStaticPaths` so Astro pre-renders into the static
build artifact under `out/`. Works in dev (Astro dev server runs
the endpoint live) and prod (file is on disk after `astro build`).
Required moving `pages/skills/[slug].astro` →
`pages/skills/[slug]/index.astro` (and same for templates) because
Astro can't have BOTH a `[slug].astro` file AND a `[slug]/`
directory with dynamic param children at the same level. The
`[locale]/skills/[slug].astro` re-exporters were updated to point
at the new index files.
`trailingSlash: 'always'` rewrites endpoint URLs to `path/`, so the
iframe `src` and "Open in new tab" anchor now use
`example.html/` and `template.html/` (with trailing slash). Tested
locally: HTTP 200 + real example HTML in the body.
2. Share copy now per-locale; description dropped
The previous template hardcoded the framing in English ("I'm using
X from @opendesignai…") with the description following from
`skill.description`. Joey's catch: when the SKILL.md description is
in one language and the page locale is another, the share text
reads as a forced bilingual mash-up.
Adds an inline `SHARE_COPY` table per landing locale (18 entries,
one per locale). Drops the description from the share template
entirely — the framing + URL is enough to prompt a click, and
removes any chance of a bilingual mismatch when SKILL.md
frontmatter happens to be in a non-matching language.
The brand keyword "open-source Claude Design alternative" stays
English because that's the canonical search query Google
associates with the domain — translating it would split the
entity claim. Surrounding sentence translates per locale so the
message reads as one voice.
Same template added for templates/[slug]/index.astro.
3. Share dialog UI: brand logos for the 4 platform jump buttons; Email dropped
Replaces the previous text labels (`X` / `LinkedIn` / `Reddit` /
`Facebook` / `Email`) with inline-SVG brand logos. Per Joey's
revision the Email channel was dropped — Gmail / Outlook
pre-fill is reliable but the audience reach is much smaller than
the four social platforms, and removing it tightens the row.
Logos are SimpleIcons-style SVG paths inlined directly (no font
dependency, no external icon library). Each button keeps an
`aria-label` plus a visually-hidden `<span class="sr-only">`
for screen readers.
Surface area
- Marketing site only. `pages/skills/[slug]/index.astro`,
`pages/skills/[slug]/example.html.ts`,
`pages/templates/[slug]/index.astro`,
`pages/templates/[slug]/template.html.ts`,
`_components/header-enhancer.astro`, `sub-pages.css`,
`package.json` (build script revert), and the two `[locale]/...`
re-exporters.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new top-level dependencies.
- The two restructured detail pages keep their existing route URLs
and existing static-paths logic — only the file location changed.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/skills/deck-guizang-editorial/example.html/` returns
HTTP 200 with a 4942-byte body that's the actual canonical
example output (not the homepage SPA fallback).
- Local dev: `/skills/deck-swiss-international/` share dialog shows
4 brand-logo platform buttons (no Email); textarea contains the
English-only framing + URL. `/zh/skills/...` shows the Chinese
framing + URL with no English bleed-through.
* fix(landing-page): punchier share copy with emojis across 18 locales
The previous share template ("I'm using <name> from @opendesignai —
the open-source Claude Design alternative.\n\n<url>") was too flat to
spark a click — Joey called it out as 平淡 with the keyword
front-and-center but no hook.
New shape: three-line punchy block with emojis as visual anchors.
Skills surface (`/skills/<slug>/`):
🎨 Just discovered <name> on @opendesignai — the open-source
Claude Design alternative.
✨ Local-first · BYOK · your agent does the design.
→ <url>
Templates surface (`/templates/<slug>/`):
🎨 Just forked <name> from @opendesignai — the open-source
Claude Design alternative.
✨ Templates as files, not vendor docs. Fork → swap → ship.
→ <url>
Pattern per locale:
- Line 1: action verb hook (`Just discovered` / `Just forked` /
locale equivalent like `安利一个` / `推薦一個` / `Gerade entdeckt` /
`Découvert` / etc) + skill name + brand keyword.
- Line 2: tight value-prop with `·` separators — Local-first ·
BYOK · agent does the design (skills) or Templates as files,
not vendor docs (templates).
- Line 3: → URL.
Both lines lead with an emoji (🎨 then ✨) so the post visually pops
in a feed. The brand keyword "open-source Claude Design alternative"
stays English in every locale (canonical search query for the
domain); surrounding sentence translates per locale.
All 18 landing locales rewritten — ar, de, en, es, fr, id, it, ja,
ko, nl, pl, pt-br, ru, tr, uk, vi, zh, zh-tw. Skills and templates
each have their own `SHARE_COPY` table; the templates variant has
fork-flavored framing because the user action there is fork-and-ship,
not run-once.
Surface area
- Marketing site only. `pages/skills/[slug]/index.astro` and
`pages/templates/[slug]/index.astro`.
- No other files touched. No new dependencies.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: en / zh / ja all render with emojis intact and
language-specific framing; X intent URL preserves the multiline
breaks via `\n` in the `text` query param.
* fix(landing-page): restore post-build copy step for preview iframes
The detail-page interactive preview iframe pointed at endpoint routes
(`pages/skills/[slug]/example.html.ts`,
`pages/templates/[slug]/template.html.ts`) introduced in 0d9c9a5, but
Astro 6 silently drops `pages/<dir>/[slug]/<file>.<ext>.ts` routes
under dynamic segments at build time — even with `export const
prerender = true` — so the URLs returned 404 in both `pnpm dev` and
the production build.
Verified locally: dev server `curl /skills/<slug>/example.html` → 404,
`find apps/landing-page/out -name 'example.html'` → 0 files after a
clean `pnpm build`.
Restore the post-build copy step that 138cbd2 had: an `astro build`
postscript that mirrors `skills/<slug>/example.html` and
`design-templates/<slug>/example.html` into the static output. While
re-introducing the script, also address the live-artifact preview
mismatch flagged by review:
- Live-artifact records carry a `live-` slug prefix from
`shapeLiveArtifactTemplate()` in `_lib/catalog.ts`, so the iframe
URL is `/templates/live-<slug>/preview.html` — copy the source
file into `out/templates/live-<slug>/preview.html` to match.
- Serve `index.html` (the rendered preview) rather than
`template.html` (which still contains `{{data.*}}` placeholders).
The iframe is for visitors and reviewers, not the template
runtime.
Detail-page iframe `src` and "Open in new tab" link in
`pages/templates/[slug]/index.astro` already use `/preview.html`;
sub-pages.css comment kept aligned.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Mirror the issue #398 fix the claude adapter already has: when
spawning Codex CLI without a custom OPENAI_BASE_URL, strip both
OPENAI_API_KEY and CODEX_API_KEY from the child env so Codex CLI's
own `~/.codex/auth.json` (codex login) wins.
Without this guard, a stale BYOK key left behind in
`agentCliEnv.codex.OPENAI_API_KEY` (e.g. after the user clears the
BYOK dialog and switches execution mode back to Local CLI) silently
flows through `spawnEnvForAgent` and trips 401 invalid_api_key.
The stripping is gated on OPENAI_BASE_URL so users who intentionally
route Codex CLI through a third-party OpenAI-compatible gateway keep
the credential that authenticates against it. Comparison is
case-insensitive to close the Windows mixed-case env name hole that
issue #398 already documents for ANTHROPIC_API_KEY.
Fixes#2420
* fix(daemon): finish live-artifact chat runs via watchdog quiet-period handoff (#1451)
Live-artifact runs were staying in `Working` for the full 10-minute
inactivity window even after the deliverable had been registered, and
sometimes finishing as `failed` with `Agent stalled without emitting
any new output for 600s`. The agent process kept its stdin/stdout
alive (claude-code stream-json idle stdin, post-write reasoning that
never reaches the chat) so the existing watchdog could not tell the
deliverable was already in the user's hands.
Wire `/api/tools/live-artifacts/create` back into the chat run via a
small per-run handle registry: on the first `created` event, the run
flips a local `artifactRegistered` flag and rearms the watchdog with
the shorter `OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS` (default 60s)
instead of the 10-minute pre-artifact ceiling. When that quiet timer
trips, the watchdog no longer emits a stalled-error / `failed` finish;
it SIGTERMs the child and lets the existing child-exit handler do
final classification — the close handler now treats a SIGTERM exit
after a registered artifact as `succeeded`, matching what the user
actually got (a delivered artifact, not a failed run).
The handoff stays with the existing child-exit lifecycle, so tool
token revocation, cancel semantics, and exit-status classification
keep their current owner — addressing the PR #1543 review history
where finishing the run from the tool route bypassed those guarantees.
Closes#1451.
* fix(daemon): gate artifact quiet-period close on daemon-initiated flag (#1451 review follow-up)
Reviewer (#2585) found that the close-handler branch reclassifying
SIGTERM/SIGKILL as `succeeded` only checked `artifactRegistered`, so an
unrelated later termination (external `kill`, OOM, container shutdown)
after a successful artifact write would silently flip the run from
`failed` to `succeeded` — the exact "completed without producing
anything visible" failure mode the existing close handler is trying
to prevent.
Track the watchdog-initiated shutdown explicitly: set
`artifactQuietShutdownRequested = true` immediately before
`failForInactivity()` sends SIGTERM (covering the kill-grace SIGKILL
escalation under the same flag), and require that flag in the close
handler's quiet-period branch.
Extract the final-status decision into a pure
`classifyChatRunCloseStatus` so the daemon-initiated vs external
signal cases can be pinned with focused unit tests instead of
asserting closure-internal state via end-to-end timing.
* fix(daemon): treat OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS=0 as disabled (#1451 review follow-up)
Reviewer (#2585 non-blocking) found that an operator override of
`OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS=0` no longer behaved as a
"disable the quiet period" knob: once the artifact was registered,
`activeInactivityTimeoutMs()` dropped to 0, `noteAgentActivity()`
early-returned without clearing the prior timer, and the pre-artifact
10-minute timer kept running while further agent activity stopped
refreshing it.
Make the quiet-period switch conditional on a positive value. A 0
override now means "do not shorten after artifact registration" — the
pre-artifact ceiling stays active, subsequent activity continues to
reschedule it, and the existing pre-artifact stalled-error path still
fires when the agent genuinely hangs. Pin the resolver as a pure
`resolveActiveInactivityTimeoutMs` helper so the four quiet-vs-pre
matrix cases are unit-tested directly.
* fix(daemon): arm the quiet-period watchdog when pre-artifact timeout is disabled (#1451 review follow-up)
Reviewer (#2585 non-blocking, round 3) found that
`OD_CHAT_RUN_INACTIVITY_TIMEOUT_MS=0` paired with
`OD_CHAT_RUN_ARTIFACT_QUIET_PERIOD_MS>0` left the watchdog disarmed
forever. The `noteAgentActivity()` call at run start exited early
because the pre-artifact delay was 0, so `inactivityTimer` was still
`null` when the artifact was registered, and the prior
`if (inactivityTimer) noteAgentActivity()` guard inside
`noteArtifactRegistered()` then skipped the re-arm. The
newly-positive quiet-period delay never armed a timer at all — a
chat run that went silent right after artifact creation would stay
`running` indefinitely.
Drop the guard. `noteAgentActivity()` is already the function that
decides whether to schedule (it bails when the active delay is 0),
so calling it unconditionally keeps the behavior coherent across the
four pre/quiet combinations: both non-zero (was already fine), pre=0
+ quiet>0 (now arms the quiet timer), pre>0 + quiet=0 (still falls
back to the pre-artifact ceiling via the existing resolver), both
zero (still no watchdog at all — operator opted out).
Pure-function coverage of the ceiling decision stays in
`resolveActiveInactivityTimeoutMs` — exercised across the same four
combinations in the existing unit suite.
* fix(daemon): restore full assistant turn after mid-flight reload reattach
When a daemon run is in progress and the browser reloads, the client
reattaches and the artifact recovers, but the restored chat turn drops
assistant text, thinking events, and producedFiles. Three independent
defects combine to cause this:
1. The reattach onDone never populated producedFiles. The pre-turn file
snapshot used as the diff baseline lived only in a closure. Now it is
persisted on the assistant message as preTurnFileNames so the reattach
path can rebuild the diff after reload.
2. The SSE replay used a strict `>` cursor compare. A client that had
already persisted lastRunEventId equal to the final event id received
zero replay events on terminal-run reattach, fell into the status-only
REST fallback, and never fired a clean onDone. The server now replays
the final buffered event on terminal-run reattach when the cursor is
at or past the end, so the client always sees a terminal signal.
3. The text buffer flushed on visibilitychange but not on pagehide.
Hard reloads on browsers where visibilitychange does not fire before
teardown could lose the last ~250ms of streamed text from the
persisted message. A pagehide listener now flushes synchronously.
Refactor: extracted computeProducedFiles helper so the send and reattach
flows share the diff logic and cannot drift apart again.
Tests:
- apps/web/tests/components/ProjectView.reattach-restore.test.tsx
covers: reattach onDone populates producedFiles from preTurnFileNames;
reattach reaches succeeded via SSE even when only the end event replays;
computeProducedFiles unit cases.
- apps/daemon/tests/runs.test.ts adds replay-cursor coverage for both
the terminal-replay safety branch and the no-duplicate normal branch.
* fix(daemon): persist preTurnFileNames end-to-end on the messages table
Review on #2383 caught that `ChatMessage.preTurnFileNames` (added in
packages/contracts) had no daemon-side persistence: the messages
schema, upsertMessage, and normalizeMessage all ignored the field.
saveMessage() would PUT the field, the daemon would silently drop it,
and a real page reload would read a row without `preTurnFileNames`, so
the reattach onDone fell back to `new Set(nextFiles.map(...))` and
still missed files produced earlier in the turn.
This commit closes the round trip:
- New `pre_turn_file_names_json TEXT` column on the messages table,
with a forward-compatible ALTER for existing databases (same pattern
as agent_id / feedback_json / run_status).
- Both upsertMessage branches (UPDATE and INSERT) now serialize
m.preTurnFileNames into the new column.
- listMessages, the post-upsert readback SELECT, and normalizeMessage
surface the column back to callers.
Round-trip tests in apps/daemon/tests/db-pre-turn-file-names.test.ts
cover: write+listMessages, the UPDATE upsert path preserving the
baseline, and a legacy-row case returning undefined.
* fix(web): preserve terminal status + full multi-file diff on reattach
Two correctness issues caught in review of the prior reattach commits:
1. The reattach onDone path hard-coded `runStatus: 'succeeded'`, which
overwrote a 'failed' or 'canceled' status that the replayed terminal
event had already recorded via onRunStatus. Restored messages would
come back as success even when the run had actually failed or been
canceled. Now derives the final status from `prev.runStatus` via the
existing `resolveSucceededRunStatus` helper, mirroring the send path
at line 2333.
2. When `findExistingArtifactProjectFile()` recovered an existing
on-disk artifact, the produced-files list was replaced with that
single file, dropping any other files the turn had created earlier.
Now always computes the full diff against `preTurnFileNames`, then
appends the recovered artifact only if it isn't already in that
set. Extracted as `mergeRecoveredArtifact(diff, recovered)` so the
logic is a unit-testable invariant.
Tests in ProjectView.reattach-restore.test.tsx:
- mergeRecoveredArtifact: three cases (recovered appended to pre-files,
no duplication when already in the diff, passthrough on no recovery).
- reattach failed-status: onRunStatus('failed') → onDone → final
saveMessage has runStatus 'failed', not 'succeeded'.
- reattach canceled-status: same shape for cancellation.
* fix(web): force keepalive PUT on pagehide so the last buffered chunk survives reload
Review on #2383 caught that onPageHide() only called flush(), which
updates React state then schedules persistSoon() — a 500ms debounce.
On a hard reload the page tears down before that timer fires, so the
final ~250ms of streamed text never reaches the daemon.
Threaded a new flushAndPersistNow() callback through
createBufferedTextUpdates(). Both buffer call sites (send-path +
reattach-path) supply it backed by persistMessageById(id, { keepalive:
true }). saveMessage in state/projects.ts forwards the new
SaveMessageOptions.keepalive flag onto fetch's keepalive option, which
the browser honors specifically for unload-time requests.
onPageHide now calls flush() followed by flushAndPersistNow?.(), so:
- flush() pushes the buffered delta into React state synchronously
- the immediate persistMessageById then PUTs the updated message with
keepalive:true, surviving document teardown
Regression test in ProjectView.reattach-restore.test.tsx: stream a
delta, dispatch pagehide, assert saveMessage was called with the
flushed content AND { keepalive: true } before the 500ms debounce
would otherwise have fired.
* feat(design-files): add directory support and localization for folders in design files panel
* feat(directory-navigation): implement directory navigation and selection reset in DesignFilesPanel
* feat(rename): improve draft handling for file renaming in DesignFilesPanel
The `/html-anything/` landing page was loading 11 screenshots directly
from `raw.githubusercontent.com/nexu-io/html-anything/main/docs/…` —
that's 6.5 MB of raw PNGs per first visit, no AVIF/WebP transcoding,
and a separate cross-origin TLS handshake the rest of the site doesn't
pay. The TODO comment at line 70 of the page has been calling this out
since the page landed.
This PR routes those images through the same pipeline as `hero.png`:
- Source PNGs upload to `static.open-design.ai/landing/assets/html-anything/`
on the existing `open-design-static` R2 bucket. Upload package lives
at `~/Downloads/r2-html-anything-orig/` (README + upload.sh ready);
merge AFTER running that script.
- Page uses `imageAsset('html-anything/<name>.png', { width, quality })`
so CF Image Resizing emits `format=auto` AVIF/WebP variants — same
contract every other landing image already uses.
- Banner is the LCP element; gets `srcset` across 768/1280/1920/2400
(mirroring `heroImageSrcset`) plus its existing `fetchpriority="high"`.
- 10 below-the-fold screenshots become `<LazyImg>` so they pick up the
precise IntersectionObserver (rootMargin 300px) instead of Chrome's
native lazy load that over-prefetches up to 3000px.
Expected on first visit (mobile, AVIF-capable browser):
banner: ~150 KB AVIF (was 2.05 MB PNG)
10 screenshots ~80-120 KB each (was 500-900 KB PNG each)
total ~1.2-1.5 MB (was 6.5 MB)
LCP estimated -800ms to -1.5s
* feat(daemon): add CTA hierarchy static QA pass
Introduce apps/daemon/src/qa/cta-hierarchy.ts exporting a pure
analyseCtaHierarchy(html) that parses generated prototypes with cheerio
and flags three precision-biased findings: multiple-primary CTAs in the
same section, ambiguous-weight (all CTAs share identical class + inline
style), and misleading-prominence (secondary-coded copy like "Learn
more" / "了解更多" styled with primary weight).
CTA candidates come from <button>, <a>, role="button" with btn/button/cta
class markers plus CTA copy keywords covering both English (Get started,
Sign up, Buy, Subscribe, Learn more, ...) and Chinese (立即购买,
立即下单, 了解更多, ...). Weight is inferred from class tokens
(primary/solid/filled/accent/cta) and from non-transparent inline
background-color, matching the inverse of the issue #2251 sample where
the header CTA was rendered with the neutral .btn style.
This PR only ships the pure function plus its tests. HTTP route, CLI
subcommand, and any auto-repair feedback loop are deliberate follow-ups
so the first cut can land without touching the daemon HTTP surface.
Refs #2251
* fix(qa): respect container boundaries in CTA hierarchy heuristics
Two precision fixes from review of #2427:
- computeContainerKey()'s parent fallback keyed by tag name alone, so
flat layouts like <div><a class=btn-primary>...</a></div> repeated
for sibling cards all landed in 'parent:div' and
detectMultiplePrimary() reported a fake shared-section conflict on
what is in fact one CTA per card. Switch to parent-node identity
(positional index of the matched parent within its tag group, same
trick the landmark branch already uses), so each sibling wrapper
gets its own bucket.
- detectAmbiguousWeight() compared signatures across the entire
document, so two unrelated sections each containing one '.btn' CTA
with matching style would trigger 'ambiguous-weight' despite neither
container having 2+ CTAs. The PR body's rule is narrower — 'every
CTA in a container shares the same class + inline style' — so bucket
by containerKey first and only emit the finding for containers with
2+ CTAs whose signatures are identical.
Tests lock both behaviors down:
- sibling <div> card-grid without a landmark ancestor stays under the
multiple-primary threshold;
- one-CTA-per-section pairs stay under the ambiguous-weight threshold.
Fix electron-builder failure in `pnpm exec tools-pack linux build --to appimage`
by adding required description, author, and repository fields to generated
package.json in tools/pack/src/linux.ts.
* fix(daemon): remove 10-item cap from discovery TodoWrite plan prompt
The RULE 3 sentence in DISCOVERY_AND_PHILOSOPHY told the model to write
'a plan of 5–10 short imperative items'. That upper bound caused the agent
to cap every plan at exactly ten steps even when the task genuinely needed
more. The TodoWrite JSON schema imposes no maxItems constraint, so the cap
was entirely prompt-driven.
Replace '5–10 short imperative items' with 'short imperative items covering
the work'. TodoWrite intent, RULE 3 label, and planning-before-building
requirement all survive unchanged.
Red spec: apps/daemon/tests/prompts/discovery-todo-cap.test.ts
* fix(prompts): remove 10-item cap from contracts discovery copy and harden tests
[pass-6,7 BLOCKER] packages/contracts/src/prompts/discovery.ts still had
the old '5-10 short imperative items' wording. apps/web imports
composeSystemPrompt from @open-design/contracts (ProjectView.tsx:43),
so web-originated chat runs were still subject to the cap.
[pass-8 WARNING] discovery-todo-cap.test.ts did not cover the contracts
copy, leaving that path unguarded. Also no guard against semantically
equivalent re-introduction via 'at most / maximum / no more than'.
Changes:
- packages/contracts/src/prompts/discovery.ts: apply same wording fix as
apps/daemon; add inline rationale comment
- apps/daemon/src/prompts/discovery.ts: add inline rationale comment
- apps/daemon/tests/prompts/discovery-todo-cap.test.ts: add 4th assertion
blocking 'at most|maximum|no more than N item' re-introduction
- packages/contracts/tests/system-prompt.test.ts: add 5-assertion suite
guarding the contracts copy and composed prompt output
* refactor(daemon): introduce HTTP Request Adapter + typed Deps (proof on active-context-routes)
Adds a typed HTTP boundary Adapter under apps/daemon/src/http/ that replaces
the untyped ServerContext service-locator pattern (30+ fields, mostly any)
for route handlers. Routes become pure (input, deps) -> Result<output>
functions, unit-testable without Express or supertest.
Six new modules under apps/daemon/src/http/:
- types.ts Result<T,E>, ok(), err(), JsonRouteSpec, Handler,
RouteInputContext, HttpMethod, InputParser
- parse.ts rawInput(req), validationError(message, issues?)
- response.ts sendJson(), sendApiError(), statusForError() +
ERROR_STATUS_BY_CODE map
- origin-guard.ts guardSameOrigin(req, origin) wrapping isLocalSameOrigin
as a Result
- adapter.ts defineJsonRoute(), mountJsonRoute() (only place that
knows about req/res)
- index.ts barrel
active-context-routes.ts migrated as proof of pattern. parsePostActive(),
handlePostActive(), handleGetActive() are now pure functions; postActiveRoute
and getActiveRoute are exported route specs. The wire signature
registerActiveContextRoutes(app, ctx) is preserved so server.ts is untouched.
Spec at specs/current/daemon-http-adapter.md captures the strangler migration
order for the remaining route files (mcp-routes, chat-routes, artifact
routes, etc.) and a StreamRoute follow-up where the Run Orchestrator lands.
Wire-format note: cross-origin response moves from the legacy
{ error: 'cross-origin request rejected' } shape to the structured
{ error: { code: 'FORBIDDEN', message: ... } } shape. Backwards-compatible
via the existing CompatibleErrorResponse = ApiErrorResponse | LegacyErrorResponse
union in @open-design/contracts.
Validation:
- pnpm install (post-rebase, exit 0)
- pnpm --filter @open-design/daemon typecheck (both tsconfig.json and
tsconfig.tests.json silent => pass)
- pnpm --filter @open-design/daemon test: 15 new tests pass
(tests/http/adapter.test.ts + tests/active-context-routes.test.ts).
84 pre-existing failures across 23 files are unchanged and unrelated
to this PR (Windows symlink / short-name / colon-in-filename, upstream
behavior drift, missing plugin marketplace fixtures, and a freshly-
added tools-connectors-cli suite of 38 failures that landed during
the rebase).
Sharpens W4/W5 of specs/current/maintainability-roadmap.md and unlocks
W6 (Run Orchestrator).
* chore: add core-js, electron-winstaller, protobufjs, sharp to pnpm.onlyBuiltDependencies
Two PSI-targeted wins (split from #2599 follow-up).
1. New `resource-hints.astro` mounted in every page's <head> declares
`<link rel="preconnect" href="https://api.github.com" crossorigin>`.
The inline enhancer script on /` issues 3 fetch() calls to
api.github.com right after DOMContentLoaded (stars, latest release,
contributors). Without preconnect each pays a full DNS + TCP + TLS
handshake (~150-300ms) inline with the fetch. With preconnect those
handshakes happen in parallel with HTML parse and all three share one
warmed HTTP/2 connection.
2. Wrap the scroll listener's read + classList write in
requestAnimationFrame. Trackpads and high-rate wheels fire scroll
faster than display refresh, and every callback that hits classList
triggers layout recalc. PSI was attributing ~700ms of "forced reflow"
to the un-throttled version. The rAF gate collapses each burst to one
DOM mutation per frame; `{ passive: true }` is preserved so the
listener still doesn't block the scroll thread.
Same throttling pattern mirrored to `header-enhancer.astro` (used by
every sub-page) and `home-enhancer.astro` (kept in lockstep even
though /` currently uses its own inline copy).
Expected PSI delta:
- "Preconnect to required origins" hint: cleared
- "Forced reflow" diagnostic 700ms → near zero
- LCP: small bonus from earlier GH fetch warm-up (~100-300ms)
The Settings → Automations/Routines section (RoutinesSection.tsx) used
hardcoded English for every label, action, status pill, schedule
description, time suffix, and dialog. With the app language set to
Chinese the whole section stayed in English, breaking the localized
experience.
Introduce a dedicated `routines.*` i18n namespace, wire RoutinesSection
and the SettingsDialog routines header through `useT()`, and add
Simplified + Traditional Chinese translations. Schedule summaries are
now built from interpolated keys instead of concatenated English
literals, and the 12-hour AM/PM suffix is localized too. The remaining
locales fall back to English via the existing `...en` spread, matching
the repo's incremental per-locale translation pattern.
A regression guard in locales.test.ts asserts the representative
`routines.*` keys are actually translated (not English fallback) in
zh-CN and zh-TW.
* feat(deploy): add one-click Docker/Podman Compose installer for Linux and macOS
- Add install.sh with interactive wizard, Podman/Docker runtime detection,
port conflict check, health verification, and systemd user unit creation
- Add update.sh for image pull and restart with health check
- Add uninstall.sh with interactive user data backup before removal
- Unify CLI output styling with step/ok/warn/error/info helpers
- Add install-guide.md documentation
- Add install.test.ts integration test suite
* feat(deploy): add one-click Docker/Podman Compose installer
- interactive setup wizard with port, image, CORS, memory prompts
- automatic Docker/Podman detection with install guidance
- systemd user unit for Linux, health check polling
- update.sh (pull + restart + prune) and uninstall.sh (backup + cleanup)
- node:test integration suite and install-guide.md
* style(deploy): improve POSIX sh compatibility and systemd unit handling
- unify shell shebangs to #!/usr/bin/env bash
- add pipefail option for better error handling
- fix systemd unit for Podman: remove After/Requires when no service
- correct documentation to match actual uninstall behavior
* fix(deploy): address review feedback for installer scripts
- remove curl | sh path, document clone-first only
- isolate tests via docker-compose.override.yml with unique names
- support both --image <ref> and --image=<ref> in update.sh
- add running container detection before install
* docs(install): remove demo scripts and add MCP note
* perf(landing): self-host fonts + inline critical CSS
PageSpeed Insights flagged ~2.3s of render-blocking on /:
globals.css 12.9 KB external link, 160ms
fonts CSS 2.2 KB fonts.googleapis.com, 750ms
+ 4 woff2 ~1200ms each from fonts.gstatic.com
Two changes drop that whole chain:
1. Self-host fonts via @fontsource-variable/{inter,inter-tight,
playfair-display,jetbrains-mono}. Each family ships a single variable
woff2 (covers all weights we use) that Astro bundles into /_astro/*
alongside the rest of the build, served same-origin through CF Pages —
no separate TLS handshake, no Google Fonts CSS round-trip. The CSS
variable names get an extra alias in front (`'Inter Tight Variable',
'Inter Tight', ...`) so a system fallback still works if the package
ever ships under a different family name.
2. `astro.config.ts: build.inlineStylesheets: 'always'` inlines every
emitted <style> into the HTML <head> instead of emitting a separate
/_astro/*.css link. The HTML grows from ~13KB to ~28KB (gzip) but
loses one stylesheet round-trip + the entire @font-face chain that
used to gate text rendering.
Component cleanup: the `<FontStylesheet>` component (preconnect + link to
fonts.googleapis.com) is no longer needed and is deleted, removed from
all 7 places that mounted it. og.astro keeps its own font setup since
it renders to a screenshot.
Expected effect (from PageSpeed Insights "Render-blocking requests"
diagnostic on the previous build):
FCP 1.9s → ~1.2s
LCP 2.2s → ~1.5s
Verified: pnpm typecheck 0 errors, pnpm build 1853 pages 78s, preview
serves /_astro/*.woff2 as font/woff2 same-origin, 0 fonts.googleapis or
fonts.gstatic references in the built HTML.
* perf(landing): include Playfair italic + bump nix pnpm-deps hash
Two follow-ups on the self-host fonts PR:
1. globals.css imported only `@fontsource-variable/playfair-display`,
which ships @font-face for font-style: normal only. The previous
Google Fonts URL included the italic axis (`ital,wght@0,500;1,400;
...`) and several rules (.roman, .work-rule .roman, .sec-rule .roman,
plus 8 other italics across globals.css + sub-pages.css) render
Playfair italics via `font-family: var(--serif); font-style: italic`.
Without the italic face self-hosted, those would fall through to
Times New Roman italic or browser synthesis. Adding
`wght-italic.css` keeps the typography visually equivalent.
2. nix/pnpm-deps.nix uses a fixed-output derivation hash that has to
match the pnpm vendored store; adding the four fontsource packages
changed pnpm-lock.yaml so the hash has to be bumped to the value Nix
reported in CI.
Codex (Looper reviewer) flagged #1 as non-blocking.
* perf(landing): pin fontsource versions exactly per repo guard
`pnpm add` defaulted to caret ranges (`^5.2.8`) but repo guard rejects
non-exact specs ("dependency specs must be exact versions like 1.2.3 or
workspace:*"). That was the actual cause of the Preflight + Validate
workspace failures — pinning to the locked versions Codex reviewer
called out:
@fontsource-variable/inter 5.2.8
@fontsource-variable/inter-tight 5.2.7
@fontsource-variable/jetbrains-mono 5.2.8
@fontsource-variable/playfair-display 5.2.8
`pnpm guard` now passes locally (6/6 tests).
Replaces the HA page `<title>` from the previous descriptive form
("HTML Anything — your local AI agent writes the HTML, you ship
it | Open Design") with a noun-stack that captures three high-value
brand-defense queries:
- "open source html anything" — researching alternatives
- "html anything official" — verifying the canonical source
- "open source html anything official" — precise long tail
Why
- The descriptive title before this change told the story but didn't
match what users actually type when they land on this page from
search. The brand has been growing fast (4.1K+ stars on the repo);
visitors increasingly arrive with intent to verify ("is this the
real one?"), to compare alternatives, or to find the canonical
install path. The new title format leads with the tokens those
queries hit.
- Joey called the change explicitly. The brand has reached a stage
where SEO defense beats descriptive copy on a one-line surface.
What
- New `OPEN_SOURCE_OFFICIAL_TITLE` constant maps every landing-page
locale to its translated title. Pattern: `{open-source} HTML Anything
{official}` with no separator. Word order follows what's natural in
each locale (German: Offiziell at end; French/Romance: adjectives
after the noun; CJK: adjectives before).
- The three explicit copy blocks (`HTML_ANYTHING_COPY_EN`,
`_ZH`, `_ZH_TW`) all reference the constant rather than holding
their own literal title strings, so future title changes update
in one place.
- `derivedHtmlAnythingCopy()` (which builds copy for the 15
non-explicit locales by spreading EN over locale UI strings) now
pulls the title from the same constant via `[locale]` lookup with
EN as a defensive fallback.
- Adds three new `SoftwareApplication.alternateName` entries for the
same query stack. Schema-level signal complements the title-level
signal: the structured-data entity now matches whichever shape the
user typed regardless of UI locale, since these are proper-noun
query variants that don't translate.
Surface area
- Marketing site only. Single file: `app/pages/html-anything/index.astro`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI.
- No new dependencies.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors.
- Local dev: every locale's HA page renders the new title.
- en → `<title>Open Source HTML Anything Official</title>`
- zh → `<title>开源 HTML Anything 官方</title>`
- ja → `<title>オープンソース HTML Anything 公式</title>`
- de → `<title>Open Source HTML Anything Offiziell</title>`
- ar → `<title>HTML Anything مفتوح المصدر الرسمي</title>`
- …same shape across all 18 landing locales.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* fix(landing-page): translate Library/Tutorials, drop dropdown numbers, dedupe Product menu
Three small UX issues surfaced after #2605 landed Tom's full i18n
bundle. None of them block deploy; all of them affect every visitor
who lands on the site, especially the non-English ones.
Issues
1. The new Library / Tutorials nav labels in #2605's header
restructure were hardcoded English. On `/zh/`, `/ja/`,
`/de/`, etc. the rest of the nav was localized but those two
labels stayed "Library" / "Tutorials" inside otherwise-translated
chrome.
2. The four catalog facets inside the Library dropdown carried the
live count badges from the previous top-row treatment ("Skills132",
"设计系统150", etc.). Inside a dropdown they read as a glued-on
suffix rather than a tag, and Joey called them out as "weird —
drop them entirely".
3. The Product dropdown still listed Tutorials as a child item
alongside Open Design and HTML Anything. After #2605 made
Tutorials a standalone top-level link, the duplicate was
confusing — same URL appearing in two places in the same nav row.
4. Chinese (zh and zh-tw) had `skills: 'Skill'` — the singular
English word, untranslated in an otherwise fully Chinese nav.
Fixes
- Extends `HeaderCopy.nav` interface with `library` and `tutorials`
keys; populates both across all 18 landing locales (en, zh, zh-tw,
ja, ko, de, fr, ru, es, pt-br, it, vi, pl, id, nl, ar, tr, uk).
- Updates `header.tsx` to read `headerCopy.nav.library` and
`headerCopy.nav.tutorials` instead of literal strings.
- Removes the four `<span class="dropdown-num">{count}</span>` badges
from inside the Library dropdown items. The numbers still sit on
the catalog index pages themselves (header counts, hero) and on
the homepage; the dropdown is for navigation, not status.
- Removes the Tutorials `<li>` from the Product dropdown — Tutorials
lives at the top-row level only. Updates the Product trigger's
`is-active` set so it no longer highlights when on `/tutorials/`.
- zh/zh-tw `nav.skills` switches to `技能` (the actual translation
every other Chinese localized site uses for "skills" in this
context).
Bonus: catalog-row horizontal padding unified at 24px to match the
fix already applied across `featured-card` / `template-card` /
`system-card` / `source-card` (see PR #2600). The skill list grid
on `/skills/` was the last family still on `padding: 22px 0`.
Surface area
- Marketing site only. `apps/landing-page/app/_components/header.tsx`,
`app/i18n.ts`, `app/sub-pages.css`, `app/globals.css`.
- No `apps/web`, no `apps/daemon`, no contracts, no CLI surfaces.
- No new dependencies.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev: `/zh/` shows `资源库 / 教程 / 博客`; `/zh/` Library
dropdown shows `技能 / 设计系统 / 模板 / 工艺` without count badges.
- Local dev: `/` Product dropdown shows only Open Design + HTML
Anything (no Tutorials child); top-row Tutorials link is the
single canonical entry to that page.
* fix(landing-page): keep catalog-row horizontal padding at narrow widths
#2610's catalog-row 24 px fix only addressed the desktop rule.
The `@media (max-width: 720px)` override block still set
`.catalog-row a { padding: 18px 0 }` — zero horizontal — which is
what Joey's screenshot was actually showing on narrow viewports.
Switches the narrow padding to `18px 20px`. Matches the same
narrow inset `.source-card` already uses (line 1058 of this file)
so the catalog-row family stays in lockstep with the rest of the
catalog tile families at narrow widths.
Also drops the now-stale hover override (`padding-left: 8px;
padding-right: 8px`) — that rule existed only to compensate for
the zero-horizontal default and now produces a visible jolt
against the new 20 px base.
* fix(landing-page): featured-card description honors the 24px gutter
Same shorthand fragility pattern that #2600 fixed for template-card,
just one card family further down the file.
`.featured-card p` (the description line on each featured strip
tile, e.g. "Nº 0.001 deck-swiss-international" with the 16-列网格
blurb under it) had `margin: 0 0 12px` — three-arg shorthand,
which expands to TOP RIGHT BOTTOM = LEFT, so margin-left and
margin-right both got reset to 0.
The group rule `.featured-card a > * + *` had set the horizontal
margin to 24px so every text child sits inside a clean 24px gutter
from the card border. The shorthand on `<p>` overrode the group
rule and the description text bled right up against the card's
right edge.
Switches to `margin-bottom: 12px` only. Horizontal margin stays
owned by the group rule, matching the fix template-summary already
got in #2600.
Surfaced by Joey on `/skills/` featured strip — the exact symptom
he's been calling out for the last few rounds. Same shape, different
file.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* chore(landing-page): bring PR #2469 content wholesale onto post-revert main
Step 1 of replicating @pftom's #2469 work without the deploy-blocking
issues that forced #2603. This commit copies the full \`apps/landing-page/\`
diff from #2469's HEAD (`9d2a4f1`) onto current main verbatim — every
i18n bundle, every page rewrite, every \`[locale]/\` wrapper. Subsequent
commits on this branch then surgically restore the SEO fixes that
#2469 silently regressed and configure the sitemap to survive the
Cloudflare Pages 25 MiB limit, so deploy is healthy when this lands.
What's in this commit
- Tom's i18n bundle: \`i18n.ts\` (5377 lines), \`home-page-i18n.ts\`,
\`info-page-i18n.ts\`, \`landing-ui-i18n.ts\`, \`content-i18n.ts\`
(~10K lines total of locale data)
- 18 landing-page locales: en, zh, zh-tw, ja, ko, de, fr, ru, es,
pt-br, it, vi, pl, id, nl, ar, tr, uk
- All existing pages rewritten to consume the new i18n bundle
- Full \`[locale]/<route>/\` wrapper tree for every catalog page
- \`plugin-registry.ts\` rewrite, \`catalog.ts\` adjustments
- \`astro.config.ts\` route + sitemap reconfiguration
- \`public/_headers\`, \`public/_redirects\`, \`public/favicon.svg\` adds
- \`_components/locale-switcher-script.astro\` add
What's intentionally NOT done in this commit (handled in follow-ups
on this same branch):
- Restore brand mark 44px + rounded corners (was lost from #2588)
- Restore HA SoftwareApplication \`alternateName\` array (was lost from #2566)
- Restore HA \`url\` canonical pointing at the landing page (was lost from #2586)
- Restore Product/Library/Tutorials/Blog nav grouping (was lost from #2588)
- Restore catalog-card padding 24px (was lost from #2600)
- Configure sitemap to filter \`[locale]/\` routes so the generated XML
stays under 25 MiB and Cloudflare Pages accepts the deploy
- Add \`/zh-CN/* → /zh/*\` redirects for backwards-compatibility with
any externally-linked OD-canonical locale URLs
Validation so far
- \`pnpm --filter @open-design/landing-page typecheck\` — 0 errors
* fix(landing-page): unblock deploy + restore SEO regressions on top of #2469
Step 2 of replicating @pftom's #2469. The previous commit on this
branch brings #2469's content wholesale; this commit applies the
surgical fixes that make the result actually deploy and preserves
the SEO improvements that #2469 silently regressed.
Fix 1 — sitemap stays under Cloudflare Pages 25 MiB upload limit
- `astro.config.ts` `filter` now drops every `/{locale}/...` route
so the sitemap only emits canonical English URLs.
- Locale variants are still discoverable via the
`<xhtml:link rel="alternate" hreflang="...">` annotations the
`namespaces.xhtml: true` option emits inside each canonical entry.
This is Google's recommended pattern for a multi-language site.
- Verified: post-fix `out/sitemap-0.xml` = 179 KB (was 38.4 MiB
on the prior attempt that forced #2603's revert).
Fix 2 — header brand block restored to the polished version
- Logo `width/height` 36 → 44 (matches PR #2588's brand-mark refresh
for visual weight against the new black speech-bubble glyph)
- `.brand-meta` block ("Studio Nº 01 · Berlin / Open / Earth") removed
from the header bar; the same editorial flourish still lives on the
rotated `.side-rail .rail-text` pseudo-elements at page edges.
Fix 3 — header nav grouped into Library + standalone Tutorials/Blog
- Skills / Systems / Templates / Craft are now children of a Library
dropdown (matches PR #2588's grouping). Each row keeps its count
badge inline; the trigger highlights when any of the four facet
pages is active.
- Tutorials and Blog stay as standalone top-row items (PR #2588's
original decision after Joey's review on the Learn dropdown).
- Contact removed from the header — it was a same-page anchor that
the footer already surfaces.
- Hardcoded "Library" / "Tutorials" labels match the brand-name
pattern: unlocalized across all 18 landing-page locales.
Fix 4 — HA SoftwareApplication entity canonicalized on the LP again
- `alternateName` is back to an explicit array of real query
variants `["html anything", "html-anything", "htmlanything",
"HTML Anything Editor", "The agentic HTML editor"]`. #2469
re-routed it through `copy.schemaAlternateName` which dropped
the literal alias declarations Google needs for spaced-vs-
hyphenated-vs-joined matching. (Restores PR #2566.)
- `url` flips back from `HA_URL` (the GitHub repo) to the LP URL
itself, matching the `BreadcrumbList` block on the same page.
GitHub repo lives in `sameAs` as a peer surface. (Restores PR
#2586. Without this, Google credits the GitHub repo as canonical
for the entity, which is the opposite of what this surface
exists for.)
Fix 5 — catalog-card horizontal padding unified at 24 px
- featured-card 22 → 24, template-card 20 → 24,
system-card 18 → 24, source-card 28 → 24.
- For template-card, also moved horizontal padding into the group
rule exclusively so future siblings join without re-asserting
margin shorthands. (Restores PR #2600.)
Fix 6 — `_redirects` for the locale-code rename
- This bundle uses `zh` / `zh-tw` / `pt-br` / `es` (the codes Tom's
i18n.ts ships). The previous OD landing-page used `zh-CN` /
`zh-TW` / `pt-BR` / `es-ES`. Externally-indexed and inbound-linked
URLs against the old prefixes now 301 to the new canonical.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- `pnpm --filter @open-design/landing-page build` — completed
successfully; 18,204 pages built; sitemap-0.xml is 179 KB
(well under the 25 MiB Cloudflare Pages limit).
* docs: promote 'open-source alternative to Claude Design' to README H1
Brings the missing README and .gitignore changes from #2469 that the
first wholesale-checkout in this branch missed (the auto-pulled diff
scope was filtered to apps/landing-page/ initially).
What
- Every README.*.md (13 locale variants) now leads with the
"open-source alternative to Claude Design" tagline as a subtitle to
the project name in the H1 / first paragraph. This was @pftom's
brand-positioning commit (`ee851dc`) on the original #2469 branch.
- `.gitignore` adds `growth/**` to keep growth-research scratch out of
the repo.
Why
- The README is one of the highest-PageRank surfaces a GitHub project
exposes to Google. Promoting the "alternative to Claude Design"
framing into the H1/subtitle position makes the project surface for
exactly the query the SEO work in this PR is trying to capture.
- Without this commit, the replicated #2469 in this branch would still
rank against the previous H1 ("Open Design") on GitHub crawls,
letting the SEO win at the LP fall short on the GitHub surface.
This is a strict subset of #2469's content — pure docs, no code,
no behavior change beyond what GitHub renders on the repo overview.
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* Enhance landing page with SEO-focused content and FAQ section
- Updated `.gitignore` to include growth directory.
- Modified `astro.config.ts` to prioritize high-intent landing pages for SEO.
- Added new FAQ styles and layout in `globals.css` for better user experience.
- Implemented FAQ section in `page.tsx`, ensuring it aligns with structured data requirements.
- Created dedicated pages for agents and alternatives to Claude Design, enhancing SEO and user navigation.
- Introduced comparison page for evaluating Open Design against competitors.
- Added favicon links component for consistent branding across all pages.
* Add SVG favicon and update favicon links for improved branding
* Enhance landing page with official source pillars for improved branding and navigation
- Added five canonical "official source" pillars to the homepage, reinforcing key links: official site, GitHub repository, releases, documentation, and Discord community.
- Updated URLs for releases, issues, documentation, and license to streamline access and improve user experience.
* Add locale support and enhance landing page with language switcher
- Introduced locale management with a new i18n module, defining multiple languages for the landing page.
- Implemented a locale switcher in the topbar and header, allowing users to select their preferred language.
- Updated global styles for the locale selector and adjusted layout for better responsiveness.
- Enhanced SEO by ensuring localized content is served based on user selection.
- Added a script for automatic locale detection and persistence in local storage.
* Implement localized routing and enhance navigation with href utility
- Added `stripLocaleFromPath` and `localizedHref` functions to manage locale-based URL paths.
- Updated `localePath` to normalize paths based on the detected locale.
- Refactored links in the header, footer, and main page components to utilize the new `localizedHref` function for improved navigation.
- Introduced locale-aware routing for new pages, ensuring consistent user experience across different languages.
- Enhanced SEO with alternate links for localized content in the sub-page layout.
* Update header component to use new logo format
- Replaced favicon image with a new logo in WebP format for improved performance and quality.
- Ensured consistent branding across the landing page with the updated logo.
* Enhance landing page with localization and new UI components
- Introduced a comprehensive localization schema for content management, allowing for multilingual support across various sections.
- Updated the blog and collection schemas to include internationalization (i18n) fields for better content localization.
- Implemented a new official source strip for improved navigation and branding, linking to key resources like the official site and documentation.
- Enhanced the locale switcher functionality, allowing users to select their preferred language with improved UX.
- Updated styles for the locale switcher and added new components for better responsiveness and accessibility.
- Refactored existing components to utilize localized URLs, ensuring a consistent user experience across different languages.
* Implement comprehensive localization and enhance landing page UI
- Introduced new localization features, including `EXTRA_LOCALIZED_HOME_BODY_COPY` and `EXTRA_LOCALIZED_LANDING_UI_COPY`, to support multilingual content across the landing page.
- Updated `astro.config.ts` to integrate internationalization (i18n) settings for the sitemap, improving SEO for localized content.
- Created new files for home page and info page internationalization, defining structured content for various languages.
- Enhanced the locale switcher functionality with improved UX, allowing users to easily select their preferred language.
- Refactored existing components to utilize localized content and URLs, ensuring a consistent experience across different languages.
- Made CSS adjustments for better responsiveness and accessibility in the UI components.
* Enhance landing page with new design templates and localization improvements
- Added support for design templates in the content management system, allowing for better organization and access to design resources.
- Implemented comprehensive localization for blog topics, enhancing multilingual support across various sections of the landing page.
- Updated the header component to include new product menu items, improving navigation and user experience.
- Refactored CSS for improved responsiveness and accessibility, including a new sticky chrome bar for better navigation.
- Enhanced the locale switcher functionality, ensuring a seamless experience for users selecting their preferred language.
* docs(readme): promote 'open-source alternative to Claude Design' tagline to subtitle across locales
Pulls the high-value, low-conflict slices of @pftom's #2469 onto
current main. That PR's branch was based on `af63af3` (May 20) and
diverged from a fast-moving main, so its Tier 1 SEO content is shipped
here as a fresh PR rather than rebased.
What's included
- **5 new SEO landing pages**, English-only for v1:
- `/official/` — brand-authority hub naming every alias the project
is searched as (Open Design, OpenDesign, open-design, opendesign,
Open Design AI, OD); Organization JSON-LD with the same alternateName.
- `/quickstart/` — three-command install path with HowTo JSON-LD,
requirements, expected output, troubleshooting, next steps.
- `/agents/` — 17 BYOK adapters across three tiers, ItemList JSON-LD,
BYOK explainer.
- `/compare/` — evaluation-stage comparison hub vs Claude Design,
Figma Make, v0, Lovable/Bolt, Open CoDesign; mandatory honest-limits
block as FAQPage JSON-LD.
- `/alternatives/claude-design/` — primary commercial-intent page;
TL;DR, why-people-search, BYOK explainer, feature-comparison table,
who-should-pick-which, migration steps, FAQPage JSON-LD.
- **`[locale]/` wrappers** for each of the 5 new pages, generated via
`PREFIXED_LOCALES` from OD's existing `_lib/i18n.ts`. Each wrapper is
a thin re-export of the canonical English page; per-locale translations
are a follow-up.
- **`seo-head.astro` schema upgrades** — `inLanguage` on Article /
WebSite / Blog JSON-LD, `availableLanguage` on the WebSite block,
`og:locale:alternate` for non-current locales, hreflang `x-default`
link.
- **`favicon-links.astro` extracted as a shared component** so every
page emits the same icon set; new sizes (favicon-16x16, favicon-32x32,
android-chrome-192x192, android-chrome-512x512) generated from the
current brand mark; `site.webmanifest` published for PWA / Android
install affordances.
- **`llms.txt`** restructured to lead with brand-alias declaration and
list every new official entry point. The Sister Projects section
introduced in #2566 stays.
What's intentionally NOT included
- @pftom's parallel i18n bundle (`i18n.ts` / `home-page-i18n.ts` /
`info-page-i18n.ts` / `landing-ui-i18n.ts` / `content-i18n.ts`,
~10K lines of locale data). It duplicates and conflicts with OD's
existing `_lib/i18n.ts` + `_lib/home-copy.ts`. Adapting the new
pages to OD's existing system instead keeps the codebase on a
single source of truth; per-locale translations of the new pages
can land as a separate PR.
- Mass-rewrites of existing pages (homepage, HA, Skills/Systems
catalogs, blog detail). #2469's branch base predates a number of
in-flight PRs that touched those files heavily; cherry-picking the
rewrites would re-litigate already-merged work.
- The `[locale]/<existing-route>/` routing tree for Skills, Systems,
Templates, etc. Without translations, mirroring 26 routes per locale
produces duplicate-content signals; that lift belongs with a
per-locale copy push.
Surface area
- Marketing site only. No `apps/web`, no `apps/daemon`, no contracts,
no CLI surfaces. No new dependencies. No env vars.
- New favicon assets are static PNGs generated from the canonical
brand-mark source via `magick`; committed as files, no pipeline.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: every new English route returns 200; every
`[locale]/<route>/` variant (e.g. `/zh-CN/agents/`) returns 200;
`/favicon.ico` and `/site.webmanifest` resolve.
- Local dev server: `seo-head.astro` emits the new `inLanguage`,
`availableLanguage`, `og:locale:alternate`, and `hreflang=x-default`
signals; `<FaviconLinks />` renders the consolidated icon set
including the new manifest link.
Followups
- Translate the 5 new pages and replace the English-content wrappers
under `[locale]/`.
- Swap `sub-page-layout.astro` to use `<SeoHead />` so the new schema
upgrades reach all sub-pages, not just the homepage and any future
pages that opt into SeoHead directly.
- Mass-update the homepage and HA page along the lines of #2469's
rewrite, but rebased onto current main rather than off `af63af3`.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* feat(landing-page): group header nav into Product / Library / Learn
Reduces the top-level nav from 7 link slots (Product, Skills, Systems,
Templates, Craft, Tutorials, Blog, Contact — plus the locale switcher
and two CTAs) to 3 dropdown groups, eliminating the row-overflow
that forced the brand sub-meta to truncate at narrow desktop widths
and cramming the hamburger panel on tablet.
Why
- The four catalog pages — Skills, Systems, Templates, Craft — share
the same shape: a list page that links into per-record details.
They are facets of one library, not competing peer destinations.
Letting each occupy its own top-row slot was a policy of last
resort when there were only four; with Product newly added and
Tutorials newly added since #2452 + #2266, the row no longer fits.
- Tutorials and Blog are both editorial reading surfaces (videos
vs articles), so they pair naturally under one Learn group.
- Contact in the top nav was always misleading — it links to
`#contact`, a same-page anchor on the homepage CTA section. The
footer already lists it. Promoting it to top-row real estate while
the row was overflowing made no sense.
What
- New `Library` dropdown holds Skills (132) / Systems (150) /
Templates (111) / Craft (11). Each row inside the panel keeps its
count badge inline beside the label, plus a one-line blurb so the
panel reads as a directory rather than an undifferentiated list.
The trigger highlights `is-active` whenever any of the four facet
pages is the active route, so users still see "you are inside
Library" feedback.
- New `Learn` dropdown holds Tutorials and Blog. Same pattern: inline
blurbs, parent-highlight when either child page is active.
- `Contact` removed from the header. The footer surface still lists
it; the top-row capacity it occupied is reclaimed.
- `.dropdown-num` CSS class added for the inline count badge inside
dropdown rows, distinct from the existing `.num` (which floats
above top-row links as a superscript).
Active-state semantics are preserved — every sub-page that already
passes `active="skills"`, `active="systems"`, etc. keeps working
without changes; the new Library trigger reads those same values
to decide whether to highlight itself.
Surface area
- Marketing site only. No `apps/web`, no `apps/daemon`, no contracts,
no CLI surfaces. No new dependencies. No i18n keys (existing
`copy.navSkills` / `copy.navSystems` / `copy.navTemplates` /
`copy.navCraft` / `copy.navBlog` keep working; "Library" and
"Learn" are universal English labels that don't need translation
tables for this iteration).
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server at narrow breakpoints (≤1080px) — hamburger panel
flat-expands the new Library / Learn dropdowns; existing handler
closes the panel on link click.
- Local dev server at desktop widths (≥1180px) — top row now reads
Product · Library · Learn, matching the design intent.
* feat(landing-page): polish brand mark — bigger glyph, single-line wordmark
Three small refinements to the header brand block, requested after
the new logo + grouped-nav landed:
1. The "Open Design" wordmark was wrapping to two lines once the new
nav groups (Product · Library · Learn) sat alongside it on narrow
desktop widths. The hamburger fallback already hides the entire
nav at ≤1080px so the wordmark has plenty of horizontal room
there — wrapping was a layout glitch, not a width constraint.
Applies `white-space: nowrap` on `.brand` and adds an explicit
`.brand-name` span so the rule is intent-bearing rather than
relying on the parent inline-flex behaviour.
2. The brand mark felt undersized against the wordmark's optical
weight, especially with the new black mark where the negative
space inside the speech bubble visually shrinks the glyph. Bumps
the displayed size from 36×36 to 44×44 (+22%) in both the header
and the footer, so the proportion matches what users see in the
logo.webp source.
3. Removes the `<span class="brand-meta">` block ("STUDIO Nº 01 ·
BERLIN / OPEN / EARTH"). It was decorative metadata that earned a
third of the brand block's horizontal real estate without
carrying any user-facing function. Drops the JSX, drops the
styles, drops the now-stale `display:none` overrides at the 1180
and 880 breakpoints. The same rotated brand-string still lives on
the side-rail pseudo-elements at the page edges (`.side-rail
.rail-text`) — that surface is the canonical home for that
editorial flourish, not the nav.
Surface area
- Marketing site only. `_components/header.tsx`, `page.tsx` (footer
brand block), and `globals.css`. No `apps/web`, no `apps/daemon`,
no contracts, no CLI. No new dependencies, no i18n changes.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: header HTML no longer contains `brand-meta`;
the brand block on `/` and any sub-page renders the bigger 44px
glyph + single-line "Open Design" wordmark.
* refactor(landing-page): unbundle Learn dropdown and round the brand mark
Two small follow-ups after looking at the rendered nav:
1. Reverts the Learn dropdown back to two standalone top-row links
(Tutorials, Blog). Rolling exactly two items into a dropdown adds
a click without reclaiming any horizontal space — the Library
grouping is what genuinely cleared the row, and Tutorials + Blog
can live side by side. Active-state semantics are preserved
automatically since both pages already pass `active="tutorials"`
and `active="blog"`.
2. Adds `border-radius: 10px` to the brand-mark image. The source
PNG is a solid-fill square; clipping the corners gives the mark
the modern app-icon silhouette (~22% of side length, matching
the iOS / macOS convention) so it reads as a brand glyph rather
than a raw screenshot next to the wordmark. Applies to both the
header and footer brand blocks via the shared `.brand-mark img`
selector.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- Local dev server: top row reads `Product ▾ · Library ▾ · Tutorials · Blog`
- Local dev server: brand mark renders with rounded corners on `/`
and any sub-page (header + footer reuse the same `.brand-mark`
styling)
---------
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Follow-up to #2566. That PR introduced `url: HA_URL` plus a
two-element `sameAs` on the SoftwareApplication block, which declared
the GitHub repo as the entity's canonical surface — Google uses `url`
to decide which page to attribute structured-data entities to. The
BreadcrumbList block right below uses the landing page URL, so the
two blocks contradicted each other on what the canonical page was.
Since the goal of #2566 was making "html anything" surface the landing
page in search, canonicalizing on GitHub was an inversion of intent.
This commit switches `url` to the landing page URL and keeps
`HA_URL` only in `sameAs` so the GitHub repo is registered as a peer
reference surface, not the canonical one. The two structured-data
blocks now agree.
Surfaced by Looper (PerishCode) on the #2566 review thread; the fix
landed too late to ride the original PR, hence this small follow-up.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Three small additions that close the gap between users searching
"html anything" (with a space) and finding the existing
`/html-anything/` page:
1. JSON-LD `SoftwareApplication.alternateName` is now an array of
real query variants — `html anything`, `html-anything`,
`htmlanything`, `HTML Anything Editor`, plus the existing tagline.
Each entry maps to a query users actually type. Feeding them as
declared alternates is a stronger signal than relying on Google's
tokenizer to figure out hyphen ↔ space equivalence on its own.
Also adds a `sameAs` array linking the canonical landing page and
the GitHub repo so structured-data crawlers see the entity span
both surfaces.
2. `llms.txt` now lists the HA landing page in its canonical entry
points and adds a dedicated "Sister Projects" paragraph that
spells out the name variants and the export targets. LLM crawlers
(Claude, Perplexity, ChatGPT browsing) read this file to decide
what to surface; the previous version made HA invisible to them.
3. The home page footer's Library column now lists "HTML Anything"
as a peer to Skills / Systems / Templates / Craft, which puts a
real body-anchor link to `/html-anything/` on `/`. Nav-dropdown
anchors (the Product menu) carry less SEO weight than an anchor
in a discoverable content area, so this makes the home actually
pass link equity to the sub-page. The label is hardcoded English
intentionally — "HTML Anything" stays unlocalized as a brand name
on every locale, so threading a new key through all 18 home-copy
translations would just churn translators.
## Why now
The HA landing page has been live since #2452 but a "html anything"
query (the variant most users type) doesn't surface us, while the
hyphenated form does. Tokenizer equivalence isn't enough on a young
domain — we need declared signals, an inbound link from a
content-area on the highest-PR page, and machine-readable hints for
LLM crawlers. This PR ships all three.
## Test plan
- [x] `pnpm --filter @open-design/landing-page typecheck` — 0 errors
- [ ] After deploy: `curl https://open-design.ai/html-anything/`
shows `"alternateName":[...]` array in JSON-LD
- [ ] After deploy: `curl https://open-design.ai/llms.txt` includes
a `## Sister Projects` section
- [ ] After deploy: home page footer Library column lists HTML Anything
## Surface area
- [x] `apps/landing-page` only
- [ ] no `apps/web`, no `apps/daemon`, no contracts, no CLI
- [ ] no new dependencies
- [ ] no skill / design-system / template / craft additions
## Followups (not in this PR)
- Mirror the HA hero banner to R2 + Image Resizing (Core Web Vitals)
- Generate a HA-specific Open Graph card so social previews stay on
brand (currently reuses the GitHub-hosted banner)
- Add a `SoftwareSourceCode` JSON-LD block to associate the landing
page with the GitHub repository entity
- Land a Chinese landing page at `/zh-CN/html-anything/` to capture
CN-locale queries
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
Browsers omit the Origin header on same-origin GET requests per the Fetch
spec, but the no-Origin host-only branch in isLocalSameOrigin only honors
IP-literal entries in OD_ALLOWED_ORIGINS (via ipOnlyExtraOrigins). When a
remote-access proxy (Tailscale Serve, Caddy, etc.) terminates TLS at a
non-loopback hostname listed in OD_ALLOWED_ORIGINS, those legitimate
same-origin GETs to /api/app-config, /api/mcp/servers, etc. get rejected
with "cross-origin request rejected" 403s while the matching POST/PUTs
(which always carry Origin) succeed.
Trust Sec-Fetch-Site: same-origin as a fallback when the Origin header is
absent. The Sec-* prefix is browser-set and cannot be modified by
JavaScript, so a value of "same-origin" attests that the request really
originated from the same origin as the target — a cross-site
<img>/<script> exploit would carry "cross-site" instead, leaving the
existing CSRF defenses intact.
Tests:
- pnpm --filter @open-design/daemon test -- tests/origin-validation.test.ts → 43 passed (5 new)
- Add useMemo to sort routines by createdAt in descending order
- Replace all references to routines with sortedRoutines in render
- Newly created automations now appear at the top of the list
- Makes new items immediately discoverable after creation
Fixes#2387
Replaces the four icon assets in `apps/landing-page/public/` with
renders of the new brand mark — black-fill speech bubble + white
pointer arrow — and adds a real multi-resolution `favicon.ico` at
the path SEO crawlers actually probe.
Why
- The brand mark was refreshed on 2026-05-21 (canonical source:
black 2988×2988 PNG of the speech-bubble + pointer logo). The
marketing site needed the matching favicon, apple-touch-icon, and
header brand mark refreshed in lockstep so the browser tab, iOS
home-screen tile, and the in-page nav glyph all line up with the
new identity.
- `/favicon.ico` did not exist on the published site. The Astro head
declares `<link rel="icon" href="/favicon.png">`, which modern
browsers honor, but a long tail of SEO crawlers, link-preview
services (Slack, Discord, third-party SEO tools), and older
clients hard-probe `/favicon.ico` regardless of the link tag. Hits
to that URL were falling through to the SPA fallback HTML
(200 with `content-type: text/html`), so those clients rendered
an empty/broken favicon. Several SEO surfaces showed an empty
black circle instead of the brand mark.
- Adding a real `favicon.ico` plus an explicit
`<link rel="icon" type="image/x-icon" href="/favicon.ico">` is
the smallest defensive fix that covers both well-behaved and
hard-probing clients.
What
- Regenerated icon assets from the new logo source:
- `favicon.ico` — multi-resolution ICO with 16/32/48/64 PNG-encoded
entries. The 16/32 entries are what browser tabs, bookmarks, and
most crawlers sample; 48/64 cover high-DPI tabs and Windows
pinned-tile sampling.
- `favicon.png` — 32×32 PNG (existing slot).
- `apple-touch-icon.png` — 180×180 PNG (existing slot, iOS
home-screen).
- `logo.webp` — 144×144 WebP, 4× the 36px logical size used by
the header brand mark for crisp retina rendering.
- Added `<link rel="icon" type="image/x-icon" href="/favicon.ico" sizes="any">`
to both `app/pages/index.astro` and the shared `sub-page-layout`
so every route under `open-design.ai` advertises the ICO. Existing
PNG and apple-touch links are preserved — modern browsers will
still pick the PNG, the ICO catches the hard-probing tail.
Surface area
- Marketing site only. No `apps/web`, `apps/daemon`, contracts, or
CLI surfaces touched.
- No new dependencies; assets generated locally from the canonical
source via `magick` + `cwebp` and committed as static files.
Validation
- `pnpm --filter @open-design/landing-page typecheck` — 0 errors.
- File integrity:
- `favicon.ico` — `MS Windows icon resource - 4 icons,
16x16, 32x32, 48x48, 64x64`
- `logo.webp` — `RIFF Web/P image, VP8 encoding, 144x144`
- Manual: `/favicon.ico` will return `image/x-icon` once deployed,
not the SPA fallback HTML it returns today.
Followup
- Once Cloudflare's edge cache rolls (or is purged), third-party
favicon caches (Google SERP, Slack link-preview) take days-to-weeks
to refresh on their own; that lag is expected and not a deploy
problem.
Co-authored-by: Joey-nexu <joeylee12629@gmail.com>
* fix(web): show feedback prompt on every successful assistant turn
Previously the thumbs-up/down widget only appeared when the turn produced
an `<artifact>`, wrote a file via Write/Edit, or emitted a live_artifact
event. That left whole categories of completed turns — image/video
generation via `generate_image` / `generate_video`, MCP tool runs, plain
text answers — without any way for the user to rate them.
Drop the `hasArtifactWork` gate from `isFeedbackEligible` and remove the
helper functions that fed it. The remaining filters (streaming in
progress, empty response, unfinished todos, runStatus !== "succeeded")
still suppress the widget for genuinely incomplete turns.
The PostHog `has_produced_files` field is still emitted so the analytics
side can keep slicing feedback by whether the turn produced artifacts.
* test(web): flip artifact-only feedback assertion in AssistantMessage.test.tsx
Companion test file to `chat-feedback.test.tsx` — the same artifact-only
visibility rule was duplicated here and asserted the text-only success
case stays hidden. Flip that case to assert the widget now appears, and
update the file-level comment so the new rule reads cleanly. The
streaming / failed-run / empty-response cases keep their existing
"hidden" assertions; those are still excluded under the new rule.
2026-05-21 15:23:59 +08:00
1428 changed files with 193458 additions and 43170 deletions
description: Open a first-contribution PR (or bug issue) on nexu-io/open-design — works for non-coders too.
argument-hint: "[skill | design-system | i18n | docs | bug — optional, otherwise the skill will ask]"
---
You are entering the **od-contribute** flow.
User input (may be empty): `$ARGUMENTS`
## What to do right now
1. **Load the skill** by invoking the `od-contribute` skill via the Skill tool. The skill owns the full execution playbook — do not reimplement it inline.
2. **Pass the user input forward**:
- If `$ARGUMENTS` is one of `skill`, `design-system`, `i18n`, `docs`, `bug` (or a recognizable Chinese / English equivalent), pre-select that branch and skip the type-picking `AskUserQuestion` in Step 2.
- Otherwise, the skill will ask the user via `AskUserQuestion`.
3. **Honor the interactive contract**:
- Always run the prerequisite check first (`gh` installed + authed). If it fails, surface the install/auth hint and stop — do not try workarounds.
- Always show the preview + require explicit confirmation before pushing or opening any PR/issue.
- At the end, print the PR or issue URL on its own line so the user can click through.
description: One-click contribution flow for Open Design (nexu-io/open-design) — even for non-coders. Pick one of four cards (ship a Skill or Design System you made with OD; translate docs; fix a typo / write a blog; report a bug), the agent validates and opens a PR (or issue) for you. Trigger words contribute to open design, ship my OD skill, ship my OD design system, translate OD docs, report an OD bug, od-contribute.
allowed-tools:
- Bash
- Read
- Write
- Edit
- AskUserQuestion
- TaskCreate
- TaskUpdate
- WebFetch
---
# od-contribute — first-contribution flow for Open Design
Locked to `nexu-io/open-design`. Branches by **contribution type**, not by issue. Replaces the dev-loop with type-specific no-code validators. Designed so a product user with zero coding background can ship a real PR.
## Language
Mirror the user's language in every user-facing message — `AskUserQuestion` labels and descriptions, status updates, error explanations. Detect from their first message; when uncertain, default to English.
**Generated artifacts (PR titles, commit messages, PR/issue body files, branch names) MUST be English** regardless of the user's chat language. GitHub conventions, maintainer review, and search all assume English. The templates under `templates/` are already English — keep them that way when rendering.
Scripts live under `scripts/`. Source the shared helpers from any script:
```bash
source "$(dirname "$0")/config.sh"
```
`SKILL_DIR` below = the directory that contains this `SKILL.md`.
---
## Step 1 — Prereq check (always first)
```bash
bash "$SKILL_DIR/scripts/check-prereqs.sh"
```
- Exit 0: capture `GH_USER=<login>` from stdout. Default `TARGET_FORK="${GH_USER}/open-design"`.
- Exit 2: surface the printed install / auth hint **verbatim** and stop. Do not attempt token workarounds.
If `gh repo view "$TARGET_FORK"` fails, ask the user (one `AskUserQuestion`) whether to fork now via `gh repo fork nexu-io/open-design --clone=false`. Default to yes.
## Step 2 — Pick contribution type
Single `AskUserQuestion` (header: "Contribution", multiSelect: false), four options. Translate option labels/descriptions into the user's chat language; the branch routing is unchanged.
1. **🎨 Ship something I made with OD** — _a Skill, Design System, HyperFrame, or template I want to contribute upstream_ → branch `3a`
2. **🌍 Translate OD docs** — _README / QUICKSTART / CONTRIBUTING into a new language_ → branch `3b`
3. **📝 Fix docs / write a blog / fix a typo** — _typo fix, dead link, use-case writeup_ → branch `3c`
4. **🐛 Report a bug** — _something broke; I'll help turn it into a high-quality issue_ → branch `3d` (issue path, no PR)
Each branch below is self-contained. Steps 7–8 (preview + push) are shared across branches `3a`/`3b`/`3c`. Branch `3d` skips them entirely.
**3a.1** Ask user: "What's the local path to the artifact you want to ship?" (single free-text, translated into the user's chat language). Common: a folder path (Skill) or a single `DESIGN.md` file (Design System).
**3a.2** Sniff type:
```bash
# Skill: folder containing SKILL.md with frontmatter.
- `status: "missing"` first (missing language is highest leverage)
- then `status: "stale"` ordered by `english_commits_since_translation` desc
- README family before QUICKSTART before CONTRIBUTING
**3b.3** Take the top 3–4 gaps and present via `AskUserQuestion` (header: "Translation target"). Each option label like: `README → 한국어 (Korean)` / `QUICKSTART (zh-CN) refresh — 12 commits behind`. Translate the header text into the user's chat language but keep the option labels descriptive (the language names belong in their native script).
**3b.4** Once user picks, **rename branch** to be specific:
(or pre-set the slug in step 3b.1 if the user confirmed earlier.)
**3b.5** Translate. Read the English source. Translate **structure-preserving**:
- Code blocks: leave untranslated
- Brand / product names: leave untranslated
- Filenames in inline code: leave untranslated
- Image / link targets: leave untranslated; if a localized version of a linked doc exists, swap the link to the localized file
- Headings: translate, keep the heading depth identical
- Tables: translate cell text only, keep alignment / pipes
Write the result to `$WORKDIR/<TRANSLATED_PATH>` (e.g. `QUICKSTART.es.md`). Show user a unified diff vs. the English source for visual sanity-check (line-count delta within ±15% is a healthy signal).
**3b.6** Validate the translated file against the English source. The `--reference` flag tells the validator to ignore relative refs that were already broken in the source — OD docs frequently link to website route slugs (e.g. `skills/blog-post/`) that aren't files on disk; we don't want a structure-preserving translation to fail because of pre-existing dead refs.
```bash
bash "$SKILL_DIR/scripts/validate-markdown.sh" \
"$WORKDIR/<TRANSLATED_PATH>" \
--reference "$WORKDIR/<ENGLISH_PATH>"
```
If FAIL → surface verbatim, fix, retry.
**3b.7** Render `templates/PR-BODY-i18n.md` with `{{DOC_NAME}}`, `{{LANG_DISPLAY_NAME}}`, `{{LANG_CODE}}`, `{{TRANSLATED_PATH}}`, `{{ENGLISH_PATH}}`, `{{STATUS}}`, `{{TRANSLATION_NOTES}}` (one paragraph from the agent: anything tricky, untranslated terms it kept, etc.), `{{DISCORD_INVITE}}`.
Group by `kind` (typo / deadlink / todo). Show the user up to 6 candidates via `AskUserQuestion`. Once picked, apply the fix in code (typo: replace word; deadlink: ask user for the new URL; todo: that's a proper task, ask user to write the missing prose).
**3c.4 (Specific-fix branch)** Read the file, apply user's described change. Confirm via diff.
**3c.5 (Blog branch)** First check whether OD has a blog directory:
```bash
ls "$WORKDIR/docs" 2>/dev/null
```
If a `docs/blog/` or similar exists, place the new post there. If not, ask the user where it should live, defaulting to `docs/<slug>.md`. Generate an outline → user fills in user-specific bits (their use case, screenshots, the prompt they used, the rendered output) → agent stitches into a final Markdown.
**3c.6** Validate every changed/added file. For files that already exist in the repo (typo fix, dead-link fix, doc edit), pass `--reference` pointing at HEAD's version so we only fail on relative refs the user *introduced*, not on pre-existing route slugs:
```bash
# For modifications to existing files:
git -C "$WORKDIR" show "HEAD:<path>" > "/tmp/od-contrib-orig-<basename>" 2>/dev/null
bash "$SKILL_DIR/scripts/validate-markdown.sh" \
"$WORKDIR/<changed-path>" \
--reference "/tmp/od-contrib-orig-<basename>"
# For brand-new files (e.g. a blog post the user is creating from scratch),
# omit --reference. The validator will skip the relative-ref check entirely
# (since it can't tell route slugs from real paths in isolation).
```
**3c.7** Render `templates/PR-BODY-docs.md` with `{{ONE_LINE_SUMMARY}}`, `{{DETAILS}}`, `{{FILES_LIST}}`, `{{DISCORD_INVITE}}`.
→ **Step 7**.
---
### Step 3d — Bug report (issue path, no PR)
**3d.1** Read OD's actual schema at runtime to make sure we mirror it:
```bash
gh api "repos/${TARGET_REPO}/contents/.github/ISSUE_TEMPLATE/bug-report.yml" --jq .content | base64 -d > /tmp/od-bug-report.yml
```
If the schema has drifted from the template (`templates/ISSUE-BODY-bug.md`), regenerate the body to match.
**3d.2** Ask the user via `AskUserQuestion`, one structured prompt per critical field. Use **plain language**, not the YAML field names:
| Bug-report field | Prompt to user |
|---|---|
| `description` | "What went wrong? One sentence is fine." |
| `steps` | "How can I reproduce it? Walk me through step by step." |
| `expected` | "What did you expect to happen?" |
| `version` | "Which OD version are you running? (About menu, or `od --version`)" |
| `platform` | dropdown: macOS (Apple Silicon) / macOS (Intel) / Windows / Linux / Other |
| `logs` | "Any error logs you can paste? Skip if you don't have them." |
| `screenshots` | "Path to a screenshot? Skip if you don't have one." |
Translate every prompt above into the user's chat language at runtime.
**3d.3** Auto-collect what we can (these don't need to ask the user):
- OS family from `uname`
- Node version from `node -v` if relevant
**3d.4** Dedupe: extract 3–5 keywords from the description, run:
If matches exist, present them to the user via `AskUserQuestion` (translate to user's language): "These existing issues look related. Do you want to: (a) comment on an existing one, (b) open a new issue anyway, (c) cancel?"
**3d.5** If proceeding with new issue, render `templates/ISSUE-BODY-bug.md` and submit:
- Never push to `main` / `master` / `develop`. The push scripts refuse.
- Never `--force` push. Just don't.
- All workspace activity stays under `$OD_WORK_ROOT` (default `$HOME/od-contrib-work`). `od::assert_in_workroot` enforces this.
- Bug-report path **always** runs the dedupe search before `gh issue create`.
- Honor user memory: skip GitHub user `xxiaoxiong` from any contributor lookup ([[feedback_no_outreach_xxiaoxiong]]).
## When NOT to use this skill
- The user wants to fix a daemon / web bug or add a feature with code changes → use `auto-github-contributor` instead (it has the TDD loop). This skill deliberately doesn't run lint/typecheck/tests because content paths don't need them.
- The user wants to *generate* a Skill / Design System from scratch → that's Open Design itself. Run OD first, get an artifact, then come back here to ship it.
Reference for the `od-contribute` skill's `validate-design-system.sh` step.
> **Authoritative source**: read 1–2 existing folders under `design-systems/` in `nexu-io/open-design` at runtime — the conventions evolve as new systems land.
## Minimum viable design system
```
design-systems/<brand-slug>/
└── DESIGN.md # required — the brand brief OD loads
```
A few systems include extras: `components.html`, `tokens.css`. These are optional, referenced from `DESIGN.md` if present.
## DESIGN.md structure (observed convention)
H1 with the brand name, then a blockquote with category + one-sentence pitch, then numbered H2 sections. Looking at established systems (`airbnb`, `apple`, etc.), the typical section list is:
```markdown
# Design System Inspired by <Brand>
> Category: <e.g.E-Commerce&Retail>
> <one-sentencepitch>
## 1. Visual Theme & Atmosphere
## 2. Color Palette & Roles
## 3. Typography
## 4. Layout & Spacing
## 5. Components
## 6. Motion & Interaction
## 7. Iconography & Imagery
## 8. Voice & Tone
## 9. Edge Cases & Variations
```
Section ordering and exact titles vary — the validator only checks **structural overlap with reference systems**, not exact heading text. ≥30% overlap with the union of headings from existing systems is enough to pass.
## What the validator actually enforces
1. File is non-empty and has at least one H1.
2. ≥30% heading overlap with reference DESIGN.md files (when references are passed in).
3. No `../` relative paths that would resolve outside `design-systems/<brand>/`.
That's deliberately loose — DESIGN.md is a creative brief, not a schema.
## Don'ts
- Don't reference assets outside the brand folder.
- Don't paste binary fonts; use a CSS `@font-face` reference and let OD resolve at runtime.
- Don't use real customer logos / proprietary brand assets you don't have rights to (the validator won't catch this — it's a maintainer-review concern).
Per user feedback ([[feedback_outreach_minimal]]), keep it minimal. The PR body is the **only** place we get to shape the maintainer's first impression of this contributor — make it warm, brief, and useful.
## Hard rules
1. **Always end the PR body with two things:**
- "👋 This is my first OD contribution." (or a similar one-line warmth)
- The OD Discord invite: <https://discord.gg/qhbcCH8Am4> (read from `OD_DISCORD_INVITE` env, never hardcode)
2. **Never claim more than the PR actually does.** A typo fix is a typo fix — don't dress it up as "improving documentation quality" or list 5 fake checkboxes.
3. **Plain language only.** No "ergonomic", "DX", "stakeholder", "stack rank". Talk like a friendly user, not a startup blog.
4. **No emojis except the opening 👋 and one optional 🎨 / 🌍 / 📝 / 🐛 in the title or first line.** OD is design-loving but the maintainers read a *lot* of PRs.
## Soft rules
- Lead with **what changed**, not why or how. Maintainers can read the diff for the how.
- "Why" gets at most 2–3 sentences. If it needs more, the work is too big for this skill — open an issue instead.
- One screenshot if the change is visible. Zero is fine.
- The "checklist" should reflect what the validator actually checked, not a generic ceremonial list.
## Anti-patterns (do not do these)
- **Don't** write an "ask" section. Don't say "please review when you have time" — the PR is the ask.
- **Don't** invite the maintainer to call / DM you. Discord is the channel.
- **Don't** apologize. ("Sorry if this isn't right" — the maintainer will tell you if it isn't.)
- **Don't** include a "TL;DR" — if the summary needs a TL;DR, the summary is too long.
## Title conventions (for `git commit` and `gh pr create --title`)
| i18n | `Translate <doc> to <Lang>` | `Translate QUICKSTART to Spanish` |
| i18n (refresh) | `Update <Lang> translation of <doc>` | `Update zh-CN translation of README` |
| Docs typo | `Fix typo in <file>` | `Fix typo in README.md` |
| Docs other | `<verb> <noun> in <where>` | `Clarify daemon setup in QUICKSTART` |
| Bug (issue title) | `<observed> on <surface>` | `Preview iframe is blank on Safari 17` |
## When to ask before writing
If the user wants to ship something whose tone is unusual (a manifesto blog post, a contentious refactor, naming a brand after a real company without rights), pause and ask the user. Better to skip the PR than ship something the maintainer will close politely.
Mirrors `nexu-io/open-design``CONTRIBUTING.md` so the skill doesn't need to re-fetch it on every run. **If this drifts from upstream CONTRIBUTING.md, upstream wins** — re-read the live file when in doubt.
## Three high-leverage contribution surfaces (per OD's CONTRIBUTING.md)
| If you want to… | You're really adding | Where it lives | Ship size |
|---|---|---|---|
| Make OD render a new kind of artifact | a **Skill** | `skills/<your-skill>/` | one folder, ~2 files |
| Make OD speak a new brand's visual language | a **Design System** | `design-systems/<brand>/DESIGN.md` | one Markdown file |
| Hook up a new coding-agent CLI | an **Agent adapter** | `apps/daemon/src/agents.ts` | ~10 lines (code — out of scope for this skill) |
| Improve docs, port a section to fr / de / zh-CN, fix typos | docs | `README.md`, `README.fr.md`, `README.de.md`, `README.zh-CN.md`, `docs/`, `QUICKSTART.md` | one PR |
## Localized doc files we know about
| Doc family | English source | Translations seen on disk (as of plan time) |
The skill `discover-i18n-gaps.sh` does NOT trust this table — it scans the workspace at runtime. Use this list only when you need to seed an `AskUserQuestion` card without a workspace.
Reference for the `od-contribute` skill's `validate-skill-submission.sh` step and for guiding a user through assembling a Skill submission.
> **Authoritative source**: read 1–2 existing folders under `skills/` in `nexu-io/open-design` at runtime — conventions evolve faster than this doc.
## Minimum viable skill
```
skills/<your-skill>/
└── SKILL.md # required, must have YAML frontmatter
```
That's it. Many of the simplest skills in OD are exactly that: one Markdown file in one folder.
## Frontmatter — what `validate-skill-submission.sh` requires
```yaml
---
name: <kebab-case-slug> # required; usually matches the folder name
description: | # required; one paragraph; what the skill does in user terms
Generate and iterate ad creative including headlines, descriptions, and primary text.
triggers: # optional but strongly recommended
- "ad creative"
- "ad headline"
od: # optional; OD-specific metadata
mode: design-system # or other modes; check existing skills
category: <category-slug>
upstream: "https://github.com/..." # if the skill was lifted from somewhere
---
```
**Required by validator**: `name`, `description`. Everything else is convention.
## Body conventions (after the frontmatter)
Looking at existing skills, the typical body has:
1. `# <skill-name>` H1.
2. A one-line "what it does" sentence.
3. Optional `## Source` block when adapted from upstream (with attribution).
4. `## How to use` with one or two example prompts the user might type.
## When a skill folder needs more than `SKILL.md`
- **Reference assets** — long prompt fragments, example outputs, image references — go alongside `SKILL.md` in the same folder, referenced via relative paths in `SKILL.md`.
- **Subfolders** are fine: the validator only requires that every relative reference inside `SKILL.md` resolves and that no path escapes the skill folder.
## Don'ts
- Don't put runtime code in here. Skills are *content* — Markdown + maybe assets. Code adapters live in `apps/daemon/src/`.
- Don't reference files outside `skills/<your-skill>/` — that breaks portability.
- Don't put binaries you don't need (the lighter the folder, the easier the review).
A new Design System — **{{BRAND_NAME}}** — at `design-systems/{{BRAND_SLUG}}/DESIGN.md`.
> {{PITCH}}
## What this design system covers
{{COVERAGE_NOTES}}
## How to try it
1. `cd open-design`
2. `pnpm tools-dev run web`
3. Start a new project and pick **{{BRAND_NAME}}** from the design system picker.
4. Ask the model: _"{{TRY_PROMPT}}"_
{{SCREENSHOT_BLOCK}}
## What's in this PR
- `design-systems/{{BRAND_SLUG}}/DESIGN.md` — the canonical design brief OD loads.
- Any supporting assets in `design-systems/{{BRAND_SLUG}}/` are referenced from `DESIGN.md`.
## Checklist
- [x] DESIGN.md has the conventional sections (compared against existing OD design systems)
- [x] No `../` path escapes outside the brand folder
- [ ] Maintainer review
---
👋 This is my first OD contribution. Hi! If anything looks off, tell me what to change and I'll happily push a fixup commit.
If you want to chat (or you're another newcomer reading this and want help shipping your first PR), come hang out in the OD Discord: {{DISCORD_INVITE}}
A new Skill — **{{SKILL_NAME}}** — at `skills/{{SKILL_SLUG}}/`.
> {{PITCH}}
## Why I made it
{{MOTIVATION}}
## How to try it
1. `cd open-design`
2. Run OD locally: `pnpm tools-dev run web`
3. Open a project, start a chat, and ask: _"{{TRY_PROMPT}}"_
{{SCREENSHOT_BLOCK}}
## What's in this PR
- `skills/{{SKILL_SLUG}}/SKILL.md` — the skill itself (frontmatter + instructions)
- everything else inside `skills/{{SKILL_SLUG}}/` is referenced from `SKILL.md`
## Checklist
- [x] `SKILL.md` has a `name` and `description` in the frontmatter
- [x] Every relative path in `SKILL.md` resolves
- [x] No path escapes the skill folder
- [ ] Maintainer review
---
👋 This is my first OD contribution. Hi! If anything looks off, tell me what to change and I'll happily push a fixup commit.
If you want to chat (or you're another newcomer reading this and want help shipping your first PR), come hang out in the OD Discord: {{DISCORD_INVITE}}
gh issue close "$existing" --repo nexu-io/open-design
fi
@ -262,7 +264,9 @@ jobs:
run:|
existing=$(gh issue list --repo nexu-io/open-design --state open --search 'in:title "Blog traffic — indexed posts with zero impressions"' --json number --jq '.[0].number // empty')
if [ -n "$existing" ]; then
gh issue comment "$existing" --repo nexu-io/open-design --body 'All previously low-traffic tracked URLs now have impressions or no longer match the escalation window. Closing automatically. — `blog-indexing-monitor`'
printf '%s\n' "All previously low-traffic tracked URLs now have impressions or no longer match the escalation window. Closing automatically. — \`blog-indexing-monitor\`" > "$body_file"
echo "Unable to resolve a unique open PR from trusted workflow metadata for $source_head_repository@$source_head_branch ($source_head_sha); found $match_count matches." >&2
exit 1
fi
if [ "$match_count" -eq 1 ]; then
pr_number="$matching_pr_numbers"
else
pr_number="$manifest_pr_number"
fi
fi
if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then
echo "Unable to derive PR number from trusted workflow metadata for workflow head $source_head_sha." >&2
exit 1
fi
if [ -n "$WORKFLOW_PR_NUMBER" ] && [ "$manifest_pr_number" != "$WORKFLOW_PR_NUMBER" ]; then
echo "Manifest pr_number ($manifest_pr_number) does not match workflow_run PR number ($WORKFLOW_PR_NUMBER)." >&2
@ -14,7 +14,7 @@ This file is the single source of truth for agents entering this repository. Rea
## Workspace directories
- Workspace packages come from `pnpm-workspace.yaml`: `apps/*`, `packages/*`, `tools/*`, and `e2e`.
- Top-level content directories: `skills/` (functional skills the agent invokes mid-task — utilities, briefs, packagers; see `skills/AGENTS.md`), `design-templates/` (rendering catalogue: decks, prototypes, image/video/audio templates; see `design-templates/AGENTS.md` and `specs/current/skills-and-design-templates.md`), `design-systems/` (brand `DESIGN.md` files), `craft/` (universal brand-agnostic craft rules a skill can opt into via `od.craft.requires`).
- Top-level content directories: `skills/` (functional skills the agent invokes mid-task — utilities, briefs, packagers; see `skills/AGENTS.md`), `design-templates/` (rendering catalogue: decks, prototypes, image/video/audio templates; see `design-templates/AGENTS.md` and `specs/current/skills-and-design-templates.md`), `design-systems/` (brand `DESIGN.md` files), `craft/` (universal brand-agnostic craft rules a skill can opt into via `od.craft.requires`), `mocks/` (replay-based mock CLIs for `opencode`/`claude`/`codex`/`gemini`/`cursor-agent`/`deepseek`/`qwen`/`grok`, the ACP family `devin`/`hermes`/`kilo`/`kimi`/`kiro`/`vibe`, and the AMR `vela` CLI (login + models + ACP), built from anonymized Langfuse traces — PATH-overlay drop-in for tests and self-validation; see `mocks/README.md`).
- `apps/web` is the Next.js 16 App Router + React 18 web runtime; do not restore `apps/nextjs`.
- `apps/daemon` is the local privileged daemon and `od` bin. It owns `/api/*`, agent spawning, skills, design systems, artifacts, and static serving.
- `apps/desktop` is the Electron shell; it discovers the web URL through sidecar IPC.
@ -23,7 +23,6 @@ This file is the single source of truth for agents entering this repository. Rea
- `packages/sidecar-proto` owns the Open Design sidecar business protocol; `packages/sidecar` owns the generic sidecar runtime; `packages/platform` owns generic OS process primitives.
- `tools/dev` is the local development lifecycle control plane.
- `tools/pack` is the local packaged build/start/stop/logs control plane, packaged updater harness, installer identity/registry validation surface, and mac beta release artifact preparation surface.
- `tools/pr` is the maintainer PR-duty control plane: a thin `gh` wrapper that encodes this repo's review-lane derivation, forbidden-surface flags, lane checklists, and validation-command suggestions.
- `tools/serve` is the local fixture-service control plane; first service is `tools-serve start updater` for deterministic updater metadata and artifacts.
- `e2e` owns user-level end-to-end smoke tests and Playwright UI automation; read `e2e/AGENTS.md` before editing its tests or commands.
@ -58,8 +57,8 @@ This file is the single source of truth for agents entering this repository. Rea
## Root command boundary
- Keep root scripts reserved for true repo-level checks and tools control-plane entrypoints: `pnpm guard`, `pnpm typecheck`, `pnpm tools-dev`, `pnpm tools-pack`,`pnpm tools-pr`, and `pnpm tools-serve`.
- Do not add root aggregate `pnpm build` or `pnpm test` aliases. Build/test commands must stay package-scoped (`pnpm --filter <package> ...`) or tool-scoped (`pnpm tools-pack ...` / `pnpm tools-pr ...`).
- Keep root scripts reserved for true repo-level checks and tools control-plane entrypoints: `pnpm guard`, `pnpm typecheck`, `pnpm tools-dev`, `pnpm tools-pack`, and `pnpm tools-serve`.
- Do not add root aggregate `pnpm build` or `pnpm test` aliases. Build/test commands must stay package-scoped (`pnpm --filter <package> ...`) or tool-scoped (`pnpm tools-pack ...`).
- Do not add root e2e aliases; e2e package commands and ownership rules live in `e2e/AGENTS.md`.
## Release channel model
@ -123,16 +122,11 @@ Every user-facing capability must be reachable through both the web UI **and** t
## PR-duty tooling
`pnpm tools-pr` is the maintainer-only control plane for PR-duty work on this repo. It is a thin `gh` wrapper that encodes repo-specific knowledge — review-lane derivation, forbidden-surface flags, per-lane checklists, validation-command suggestions, and a fixed dictionary of factual classify tags (`bot-only-approval`, `needs-rebase`, `stale-approval`, `unresolved-changes-requested`, `awaiting-*` timing, `org-member`, etc.). The tool is read-only on the PR surface: it never approves, merges, comments, or closes; those side effects stay in explicit `gh` invocations the maintainer runs.
Common subcommands:
- `pnpm tools-pr list` — triage the open queue by lane and review-state bucket.
- `pnpm tools-pr view <num>` — factual review brief for a single PR.
- `pnpm tools-pr classify --all` — script-level tag JSON for the whole open queue (entry point for cron / digest consumers); per-PR `classify <num>` for spot checks.
- `pnpm tools-pr assignment` — assigner-perspective ownership + idle-time / blocker view across the queue.
For the full tag dictionary, operational playbook (direct merge / duplicate-title / awaiting-author / org-member / agent-review flows), comment templates, language-detection rules, and tool-design constraints (precision boundaries, factual-output rule, retry + pagination strategy), see [`tools/pr/AGENTS.md`](tools/pr/AGENTS.md).
This repository no longer ships a maintainer PR-duty control plane. The former
`pnpm tools-pr` workflow has moved to the standalone `PerishCode/duty` project
so personal review-lane automation does not become product workspace
maintenance surface. Do not recreate `tools/pr`, `@open-design/tools-pr`, or a
root `pnpm tools-pr` script without a new explicit maintainer decision.
## Agent runtime conventions
@ -145,10 +139,19 @@ For the full tag dictionary, operational playbook (direct merge / duplicate-titl
## Chat UI conventions
- `apps/web/src/components/file-viewer-render-mode.ts` decides URL-load vs srcDoc for HTML previews. Bridges (deck, comment/inspect selection, palette, edit, tweaks) can ONLY inject through the srcDoc path. Add a new disqualifier to `UrlLoadDecision` whenever a feature needs a srcDoc-only bridge; pass it from `FileViewer.tsx` based on a source-content heuristic where appropriate (e.g. `hasTweaksTemplate`). The host keeps both iframes mounted simultaneously and swaps CSS visibility so toggling render mode does not cause an iframe reload flash; `iframeRef.current` stays aligned with the active iframe via `useEffect`. Receive filters use `isOurIframe(ev.source)` to accept messages from either iframe but signals that should ONLY come from the active iframe (e.g. `od:tweaks-available`) re-check `ev.source === iframeRef.current?.contentWindow`.
- TodoWrite UI pins one canonical task list above the chat composer via `PinnedTodoSlot` in `ChatPane.tsx`. The slot reads the latest TodoWrite snapshot across the conversation through `latestTodoWriteInputFromMessages` (`apps/web/src/runtime/todos.ts`). `AssistantMessage.stripTodoToolGroups` removes any TodoWrite tool groups from per message rendering so there is exactly one TodoCard on screen. The progress count includes both `completed` and `in_progress` items (1/4 reads "one underway" not "zero finished"). Dismissal via the Done button is keyed on the snapshot's JSON, so a fresh TodoWrite from the agent automatically re shows the card.
- TodoWrite UI pins one canonical task list above the chat composer via `PinnedTodoSlot` in `ChatPane.tsx`. The slot reads the latest TodoWrite snapshot across the conversation through `latestTodoWriteInputFromMessages` (`apps/web/src/runtime/todos.ts`). `AssistantMessage.stripTodoToolGroups` removes any TodoWrite tool groups from per message rendering so there is exactly one TodoCard on screen. The progress count includes both `completed` and `in_progress` items (1/4 reads "one underway" not "zero finished"). Dismissal via the Done button is keyed on the snapshot's JSON, so a fresh TodoWrite from the agent automatically re shows the card.`PinnedTodoSlot` sits OUTSIDE the `.chat-log` scroll container, so auto-scroll requires explicit coverage: `ChatPane`'s `ResizeObserver` accepts a `containerRef` from `PinnedTodoSlot` and observes that element directly, and a pane-level `MutationObserver` (`childList: true` on the chat pane ancestor) re-syncs that observation whenever the slot mounts or unmounts as new TodoWrite snapshots arrive.
- `AskUserQuestionCard` (in `ToolCard.tsx`) prefers the live `onAnswerToolUse(toolUseId, content)` route (POSTs to `/api/runs/:id/tool-result`) and falls back to the legacy `onSubmitForm(text)` path when the run has already terminated. Selected chips persist across reloads by parsing the stored `tool_result.content` back into the selections shape.
- Tool group rendering uses `dedupeSnapshotToolRetries` to collapse identical `AskUserQuestion` retries (one card per unique input, keeping the latest tool_use_id) and `TodoWrite` snapshots (only the most recent call, since each call is a state replace).
## Web CSS ownership
- `apps/web/src/index.css` is an import-only cascade entrypoint. Do not add selectors or declarations there; add imports only when a truly global stylesheet is needed, and keep import order intentional.
- Shared global styles belong in `apps/web/src/styles/`: design tokens, base/reset rules, primitives, app-shell layout, and legacy cross-component selectors that cannot safely be scoped yet. Keep domain-level global files grouped by owner (for example `styles/viewer/` and `styles/workspace/`) instead of adding more large files directly under `styles/`.
- New component-owned UI styles should default to CSS Modules next to the component (`Component.module.css`) instead of expanding global stylesheets. This is preferred for isolated components, panels, menus, drawers, toolbars, cards, and form sections.
- When touching an existing component with nearby global styles, prefer migrating that component's local selectors to a CSS Module as part of the change if it is small and testable. Do not mix a large mechanical move with behavior/styling changes in the same patch.
- Keep global class names only for deliberate shared contracts: reusable primitives, theme hooks, third-party/content styling, cross-component layout, or selectors that rely on global cascade/specificity. Document any new global selector group with its owning feature.
- CSS refactors must preserve cascade semantics. For mechanical splits, verify expanded import content/order matches the previous stylesheet; for CSS Module migrations, validate the affected UI path with `pnpm --filter @open-design/web typecheck` and a focused build/test or visual check when practical.
## i18n keys
- `apps/web/src/i18n/types.ts` is the typed `Dict`; every key must be defined in all 18 locale files under `apps/web/src/i18n/locales/*.ts` (`ar`, `de`, `en`, `es-ES`, `fa`, `fr`, `hu`, `id`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `zh-CN`, `zh-TW`). Add the key to `types.ts` first; missing translations produce a typecheck error.
@ -164,6 +167,8 @@ For the full tag dictionary, operational playbook (direct merge / duplicate-titl
## Validation strategy
- After package, workspace, or command-entry changes, run `pnpm install` so workspace links and generated dist entries stay fresh.
- For agent-stream / parser changes (`apps/daemon/src/claude-stream.ts`, `json-event-stream.ts`, `qoder-stream.ts`, etc.), replay a recorded session through the mock CLIs in `mocks/` to verify event shapes round-trip without burning provider budget. PATH-overlay activation: `export PATH="$PWD/mocks/bin:$PATH" OD_MOCKS_TRACE=<8-char-id> OD_MOCKS_NO_DELAY=1`. See `mocks/README.md` for the trace catalog and selection knobs.
- Treat every `pnpm-lock.yaml` change as requiring a Nix pnpm deps hash refresh check. `nix/pnpm-deps.nix` is a generated lock artifact; use `pnpm nix:update-hash` only when intentionally maintaining Nix packaging, then re-run `nix flake check --print-build-logs --keep-going`. Contributors without Nix can rely on the PR `Validate workspace` gate, which now uploads or auto-applies the generated hash-only fix when possible.
- Before marking regular work ready, run at least `pnpm guard` and `pnpm typecheck`, plus the package-scoped tests/builds that match the files changed. Do not use or add root `pnpm test`/`pnpm build` aliases.
- For local web runtime loops, prefer `pnpm tools-dev run web --daemon-port <port> --web-port <port>`.
- On a GUI-capable machine, validate desktop by running `pnpm tools-dev`, then `pnpm tools-dev inspect desktop status`.
@ -188,6 +193,7 @@ For a worked example of one full loop (red e2e spec → fix → green), see `e2e
```bash
pnpm install
pnpm nix:update-hash
pnpm tools-dev
pnpm tools-serve start updater
pnpm tools-dev start web
@ -205,14 +211,6 @@ pnpm guard
pnpm typecheck
```
```bash
pnpm tools-pr list
pnpm tools-pr list --bucket=merge-ready,approved-blocked
The default voice option shown when the Home Audio composer cannot load configured ElevenLabs voices. It keeps ElevenLabs speech runnable by selecting the same default voice id the daemon uses when no explicit voice is supplied.
- A **Home Composer Media Surface** maps user intent to an existing project kind and project metadata at submit time.
- The **Chip Rail** is the visible Home entry point for choosing a **Home Composer Media Surface**.
- **Essential Audio Generation** uses an **Audio Source Field** plus model options before creating an audio **Project**.
- **AMR Cloud** is the user-facing product choice; **AMR CLI** is the local execution adapter behind that capability.
- The **AMR CLI Distribution Contract** is owned separately from Open Design; Open Design release packaging consumes it instead of defining the native CLI release itself.
- The first **AMR CLI Distribution Slice** is mac arm64 only.
- **AMR Account Status** describes account readiness for **AMR Cloud**, not the environment profile or CLI installation state.
- An **AMR Environment Profile** is independent from release channel identity; a beta, preview, nightly, or stable package can target different AMR service environments when explicitly configured.
- **Onboarding Skip** bypasses setup completion requirements that belong to the normal onboarding continue path.
pnpm --filter @open-design/web build # Web-Paket bei Bedarf bauen
```
Node `~24` und pnpm `10.33.x` sind erforderlich. `nvm` / `fnm` sind optional; nutzen Sie `nvm install 24 && nvm use 24` oder `fnm install 24 && fnm use 24`, wenn Sie Node so verwalten. macOS, Linux und WSL2 sind die primären Pfade. Windows nativ sollte funktionieren, ist aber kein primäres Ziel.
Node `~24` und pnpm `10.33.x` sind erforderlich. `nvm` / `fnm` sind optional; nutzen Sie `nvm install 24 && nvm use 24` oder `fnm install 24 && fnm use 24`, wenn Sie Node so verwalten. macOS, Linux und WSL2 sind die primären Pfade. Windows nativ wird unterstützt; siehe [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) für die häufigsten Setup-Stolpersteine.
Sie brauchen keine Agent-CLI im `PATH`, um OD selbst zu entwickeln. Der daemon meldet dann "no agents found" und fällt auf den **Anthropic API · BYOK** Pfad zurück, der oft die schnellste Dev-Schleife ist.
@ -217,6 +217,7 @@ Außerdem:
- **Ein Anliegen pro PR.**
- **Titel ist imperativ + Scope.**`add dating-web skill`, `fix daemon SSE backpressure when CLI hangs`, `docs: clarify .od layout`.
- **Nutzen Sie das PR-Template.** Füllen Sie jeden Abschnitt von [`.github/pull_request_template.md`](.github/pull_request_template.md) aus — Why, What users will see, Surface area, Screenshots (bei UI), Bug fix verification (bei Bugfix), Validation. Leere Abschnitte ergeben einen "please fill in"-Kommentar.
- **Body erklärt das Warum.** Der Diff zeigt oft das Was, aber selten den Grund.
- **Issue referenzieren**, falls vorhanden. Bei nicht-trivialen PRs ohne Issue bitte zuerst eines öffnen.
- **Während Review nicht squashen.** Fixups pushen; wir squashen beim Merge.
pnpm --filter @open-design/web build # build do pacote web quando necessário
```
Node `~24` e pnpm `10.33.x` são obrigatórios. `nvm` / `fnm` são opcionais; use `nvm install 24 && nvm use 24` ou `fnm install 24 && fnm use 24` se preferir gerenciar Node assim. macOS, Linux e WSL2 são os caminhos principais. Windows nativo costuma funcionar, mas não é alvo principal — abra uma issue se quebrar.
Node `~24` e pnpm `10.33.x` são obrigatórios. `nvm` / `fnm` são opcionais; use `nvm install 24 && nvm use 24` ou `fnm install 24 && fnm use 24` se preferir gerenciar Node assim. macOS, Linux e WSL2 são os caminhos principais. Windows nativo é suportado; veja [`docs/windows-troubleshooting.md`](docs/windows-troubleshooting.md) para os tropeços de configuração mais comuns.
Você não precisa de nenhum CLI de agente no `PATH` para desenvolver o próprio OD — o daemon dirá "no agents found" e cairá no caminho **Anthropic API · BYOK**, que é o loop de dev mais rápido de qualquer jeito.
@ -243,6 +243,7 @@ Além disso:
- **Uma preocupação por PR.** Adicionar uma skill + refatorar o parser + bumpar uma dep são três PRs.
- **Título é imperativo + escopo.**`add dating-web skill`, `fix daemon SSE backpressure when CLI hangs`, `docs: clarify .od layout`.
- **Use o template de PR.** Preencha cada seção de [`.github/pull_request_template.md`](.github/pull_request_template.md) — Why, What users will see, Surface area, Screenshots (se UI), Bug fix verification (se correção de bug), Validation. Seções vazias ganham um comentário "please fill in".
- **Corpo explica o porquê.** "O que isso faz" geralmente é óbvio do diff; "por que isso precisa existir" raramente é.
- **Referencie uma issue** se houver. Se não houver e o PR for não-trivial, abra uma antes para combinarmos que a mudança é desejada antes de você gastar o tempo.
- **Sem squash durante review.** Empurre fixups; squash no merge.
# Open Design — البديل الرسمي مفتوح المصدر لـ [Claude Design][cd]
> **Open Design هو البديل مفتوح المصدر والمحلي أولاً لـ [Claude Design][cd].** قابل للنشر على Vercel، ويدعم BYOK في كل طبقة — **16 أداة CLI لوكلاء البرمجة** يكتشفها تلقائياً من `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) لتصبح هي محرّك التصميم، مدفوعةً بـ **31 Skill قابلة للتركيب** و**72 نظام تصميم بمستوى الهوية البصرية**. لا توجد لديك CLI؟ بروكسي BYOK متوافق مع OpenAI يقدّم نفس الحلقة بدون عملية الـ spawn.
> [!IMPORTANT]
> ### 🔥 وصلت النسخة `0.8.0-preview`. عالم التصميم القديم ينتهي هنا.
>
> بديل مفتوح المصدر و agent-native لـ Claude Design / Figma — 40k نجمة في أسبوعين أوصلتنا إلى هنا. **نحتاجك لدفعنا بقية الطريق.**
> بديل مفتوح المصدر لـ Claude Design / Figma — 40k نجمة في أسبوعين أوصلتنا إلى هنا. **نحتاجك لدفعنا بقية الطريق.**
>
> **تكرار سريع على `main`** — 0.8.0 هي المرحلة التالية من Open Design. أرسل PR، اطرح فكرة جامحة، أبلغ عن عُلّة — ما تجلبه أنت هو ما تصير إليه هذه الحركة.
>
> → [**اقرأ الإعلان · حمّل المثبّت · انضم إلى الحركة**](https://github.com/nexu-io/open-design/discussions/1727) · يعمل جنبًا إلى جنب مع نسخة 0.7 الحالية لديك.
> **البديل مفتوح المصدر لـ [Claude Design][cd].** يعمل محلياً أولاً، قابل للنشر على Vercel، ويدعم BYOK في كل طبقة — **16 أداة CLI لوكلاء البرمجة** يكتشفها تلقائياً من `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) لتصبح هي محرّك التصميم، مدفوعةً بـ **31 Skill قابلة للتركيب** و**72 نظام تصميم بمستوى الهوية البصرية**. لا توجد لديك CLI؟ بروكسي BYOK متوافق مع OpenAI يقدّم نفس الحلقة بدون عملية الـ spawn.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — غلاف افتتاحي: صمّم مع الوكيل على حاسوبك المحمول"width="100%"/>
</p>
@ -800,7 +800,7 @@ Issues و PRs و skills جديدة وأنظمة تصميم جديدة، كلّه
شكراً لكلّ من ساعد في دفع Open Design للأمام — بكود، بوثائق، بملاحظات، بـ skills جديدة، بأنظمة تصميم جديدة، أو حتى بـ issue حادّة. كلّ مساهمة حقيقية تهمّ، والجدار أدناه أسهل طريقة لقول ذلك علناً.
# Open Design — die offizielle Open-Source-Alternative zu [Claude Design][cd]
> **Open Design ist die Open-Source- und Local-first-Alternative zu [Claude Design][cd].** Web-deploybar, BYOK auf jeder Ebene: **16 coding-agent CLIs** werden automatisch in Ihrem `PATH` erkannt (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) und werden zur Design-Engine, gesteuert von **31 kombinierbaren Skills** und **72 brandreifen Design Systems**. Keine CLI? Ein OpenAI-kompatibler BYOK-Proxy ist dieselbe Schleife ohne Spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` ist da. Hier endet die alte Welt des Designs.
>
> Eine Open-Source-, agent-native Alternative zu Claude Design / Figma — 40k Sterne in zwei Wochen haben uns hierher gebracht. **Wir brauchen dich für den Rest des Weges.**
> Eine Open-Source-Alternative zu Claude Design / Figma — 40k Sterne in zwei Wochen haben uns hierher gebracht. **Wir brauchen dich für den Rest des Weges.**
>
> **Schnelle Iteration auf `main`** — 0.8.0 ist die nächste Phase von Open Design. Schick einen PR, wirf eine wilde Idee rein, melde einen Bug — was du mitbringst, dazu wird diese Bewegung.
>
> → [**Ankündigung lesen · Installer herunterladen · der Bewegung beitreten**](https://github.com/nexu-io/open-design/discussions/1727) · läuft parallel zu deinem aktuellen 0.7.
> **Die Open-Source-Alternative zu [Claude Design][cd].** Local-first, web-deploybar, BYOK auf jeder Ebene: **16 coding-agent CLIs** werden automatisch in Ihrem `PATH` erkannt (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) und werden zur Design-Engine, gesteuert von **31 kombinierbaren Skills** und **72 brandreifen Design Systems**. Keine CLI? Ein OpenAI-kompatibler BYOK-Proxy ist dieselbe Schleife ohne Spawn.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — editorial cover: design with the agent on your laptop"width="100%"/>
</p>
@ -726,7 +726,7 @@ Vollständiger Walkthrough, Merge-Messlatte, Code Style und was wir nicht annehm
Danke an alle, die Open Design vorangebracht haben: durch Code, Docs, Feedback, neue Skills, neue Design Systems oder auch ein scharfes Issue. Jeder echte Beitrag zählt, und die Wand unten ist die einfachste Art, das laut zu sagen.
Wenn Sie Ihren ersten PR gemergt haben: willkommen. Das Label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) ist der Einstiegspunkt.
@ -743,9 +743,9 @@ Das SVG oben wird täglich von [`.github/workflows/metrics.yml`](.github/workflo
# Open Design — la alternativa open source oficial a [Claude Design][cd]
> **Open Design es la alternativa open source y local-first a [Claude Design][cd].** Desplegable en web, BYOK en cada capa: **16 CLI de coding agents** detectadas automáticamente en tu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) se convierten en el motor de diseño, impulsadas por **31 Skills componibles** y **72 Design Systems de nivel marca**. ¿No tienes una CLI? Un proxy BYOK compatible con OpenAI ejecuta el mismo bucle sin el spawn local.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` ya está aquí. El viejo mundo del diseño termina aquí.
>
> Una alternativa open source y agent-native a Claude Design / Figma — 40k estrellas en dos semanas nos trajeron hasta aquí. **Te necesitamos para empujar el resto del camino.**
> Una alternativa open source a Claude Design / Figma — 40k estrellas en dos semanas nos trajeron hasta aquí. **Te necesitamos para empujar el resto del camino.**
>
> **Iterando rápido sobre `main`** — 0.8.0 es la próxima fase de Open Design. Envía un PR, lanza una idea loca, reporta un bug — lo que traes tú es en lo que este movimiento se convierte.
>
> → [**Lee el anuncio · descarga el instalador · únete al movimiento**](https://github.com/nexu-io/open-design/discussions/1727) · funciona en paralelo con tu 0.7 actual.
> **La alternativa open source a [Claude Design][cd].** Local-first, desplegable en web, BYOK en cada capa: **16 CLI de coding agents** detectadas automáticamente en tu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) se convierten en el motor de diseño, impulsadas por **31 Skills componibles** y **72 Design Systems de nivel marca**. ¿No tienes una CLI? Un proxy BYOK compatible con OpenAI ejecuta el mismo bucle sin el spawn local.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — editorial cover: design with the agent on your laptop"width="100%"/>
</p>
@ -787,7 +787,7 @@ Walkthrough completo, estándar de merge, code style y lo que no aceptamos → [
Gracias a todas las personas que han ayudado a mover Open Design hacia adelante: con código, docs, feedback, nuevas skills, nuevos design systems o incluso un issue preciso. Toda contribución real cuenta, y el muro de abajo es la forma más simple de decirlo en voz alta.
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-18" alt="Contribuidores de Open Design"/>
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-31" alt="Contribuidores de Open Design"/>
</a>
Si ya enviaste tu primer PR, bienvenido. La etiqueta [`good-first-issue`](https://github.com/nexu-io/open-design/labels/good-first-issue) es el punto de entrada.
@ -804,9 +804,9 @@ El SVG anterior se regenera diariamente mediante [`.github/workflows/metrics.yml
# Open Design — l’alternative open source officielle à [Claude Design][cd]
> **Open Design est l’alternative open source et local-first à [Claude Design][cd].** Déployable sur le web, BYOK à chaque couche : vos CLI de coding agents détectées automatiquement dans le `PATH` deviennent le design engine, piloté par les catalogues de **Skills** et de **Design Systems** du repo. Aucune CLI ? Le proxy BYOK multi-provider exécute la même boucle, sans spawn local.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` est là. L'ancien monde du design s'arrête ici.
>
> Une alternative open source et agent-native à Claude Design / Figma — 40k étoiles en deux semaines nous ont menés jusqu'ici. **Nous avons besoin de toi pour faire le reste du chemin.**
> Une alternative open source à Claude Design / Figma — 40k étoiles en deux semaines nous ont menés jusqu'ici. **Nous avons besoin de toi pour faire le reste du chemin.**
>
> **Itération rapide sur `main`** — 0.8.0 est la prochaine phase d'Open Design. Envoie une PR, balance une idée folle, signale un bug — ce que tu apportes, c'est ce que ce mouvement devient.
>
> → [**Lire l'annonce · télécharger l'installateur · rejoindre le mouvement**](https://github.com/nexu-io/open-design/discussions/1727) · s'installe à côté de votre 0.7 actuel.
> **L’alternative open source à [Claude Design][cd].** Local-first, déployable sur le web, BYOK à chaque couche : vos CLI de coding agents détectées automatiquement dans le `PATH` deviennent le design engine, piloté par les catalogues de **Skills** et de **Design Systems** du repo. Aucune CLI ? Le proxy BYOK multi-provider exécute la même boucle, sans spawn local.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design : couverture éditoriale, design avec l’agent sur votre laptop"width="100%"/>
</p>
@ -733,7 +733,7 @@ Guide complet, critères de merge, style de code et refus fréquents → [`CONTR
Merci à toutes les personnes qui font avancer Open Design : code, docs, retours, nouveaux Skills, nouveaux Design Systems ou issues bien ciblées. Chaque vraie contribution compte.
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-18" alt="Contributeurs Open Design"/>
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-31" alt="Contributeurs Open Design"/>
</a>
Si vous avez livré votre première PR, bienvenue. Le label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) est le point d’entrée.
@ -750,9 +750,9 @@ Le SVG ci-dessus est régénéré chaque jour par [`.github/workflows/metrics.ym
# Open Design — the open-source Claude Design alternative
> **Open Design is the open-source, local-first alternative to [Claude Design][cd].** Web-deployable, BYOK at every layer — **16 coding-agent CLIs** auto-detected on your `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) become the design engine, driven by **137 composable Skills** and **150 brand-grade Design Systems**. No CLI? An OpenAI-compatible BYOK proxy is the same loop minus the spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` is here. Design's old world ends here.
>
> An open, agent-native alternative to Claude Design / Figma — 40k stars in two weeks got us this far. **We need you to push the rest of the way.**
> The open-source alternative to Claude Design / Figma — 40k stars in two weeks got us this far. **We need you to push the rest of the way.**
>
> **Iterating fast on `main`** — 0.8.0 is the next phase of Open Design. Ship a PR, drop a wild idea, file a bug — what you bring is what this movement becomes.
>
> → [**Read the announcement, grab the installer, join the movement**](https://github.com/nexu-io/open-design/discussions/1727) · runs side-by-side with your current 0.7.
> **The open-source alternative to [Claude Design][cd].** Local-first, web-deployable, BYOK at every layer — **16 coding-agent CLIs** auto-detected on your `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) become the design engine, driven by **31 composable Skills** and **72 brand-grade Design Systems**. No CLI? An OpenAI-compatible BYOK proxy is the same loop minus the spawn.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — editorial cover: design with the agent on your laptop"width="100%"/>
<ahref="https://x.com/nexudotio"><imgalt="Follow @nexudotio on X"src="https://img.shields.io/badge/follow-%40nexudotio-1DA1F2?style=flat-square&logo=x&logoColor=white"/></a>
@ -64,9 +64,9 @@ OD stands on four open-source shoulders:
|---|---|
| **Coding-agent CLIs (16)** | Claude Code · Codex CLI · Devin for Terminal · Cursor Agent · Gemini CLI · OpenCode · Qwen Code · Qoder CLI · GitHub Copilot CLI · Hermes (ACP) · Kimi CLI (ACP) · Pi (RPC) · Kiro CLI (ACP) · Kilo (ACP) · Mistral Vibe CLI (ACP) · DeepSeek TUI — auto-detected on `PATH`, swap with one click |
| **BYOK fallback** | Protocol-specific API proxy at `/api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream` — paste `baseUrl` + `apiKey` + `model`, choose Anthropic / OpenAI / Azure OpenAI / Google Gemini / Ollama Cloud / SenseAudio, and the daemon normalizes SSE back to the same chat stream. SenseAudio chat additionally exposes `generate_image` and `generate_video` tools so the model can write rendered artifacts straight into the active project's folder. Internal-IP/SSRF blocked at the daemon edge. |
| **Design systems built-in** | **129** — 2 hand-authored starters + 70 product systems (Linear, Stripe, Vercel, Airbnb, Tesla, Notion, Anthropic, Apple, Cursor, Supabase, Figma, Xiaohongshu, …) from [`awesome-design-md`][acd2], plus 57 design skills from [`awesome-design-skills`][ads] added directly under `design-systems/` |
| **Media generation** | Image · video · audio surfaces ship alongside the design loop. **gpt-image-2** (Azure / OpenAI) for posters, avatars, infographics, illustrated maps · **Seedance 2.0** (ByteDance) for cinematic 15s text-to-video and image-to-video · **HyperFrames** ([heygen-com/hyperframes](https://github.com/heygen-com/hyperframes)) for HTML→MP4 motion graphics (product reveals, kinetic typography, data charts, social overlays, logo outros). Other image generators can already plug in through **Custom Image API** / **ImageRouter** when they expose an OpenAI-compatible image endpoint; workflow-first local runtimes such as **ComfyUI** are tracked separately as planned adapters.**93** ready-to-replicate prompts gallery — 43 gpt-image-2 + 39 Seedance + 11 HyperFrames — under [`prompt-templates/`](prompt-templates/), with preview thumbnails and source attribution. Same chat surface as code; outputs a real `.mp4` / `.png` chip into the project workspace. |
| **Visual directions** | 5 curated schools (Editorial Monocle · Modern Minimal · Warm Soft · Tech Utility · Brutalist Experimental) — each ships a deterministic OKLch palette + font stack ([`apps/daemon/src/prompts/directions.ts`](apps/daemon/src/prompts/directions.ts)) |
| **Device frames** | iPhone 15 Pro · Pixel · iPad Pro · MacBook · Browser Chrome — pixel-accurate, shared across skills under [`assets/frames/`](assets/frames/) |
| **Agent runtime** | Local daemon spawns the CLI in your project folder — agent gets real `Read`, `Write`, `Bash`, `WebFetch` against a real on-disk environment, with Windows `ENAMETOOLONG` fallbacks (stdin / prompt-file) on every adapter |
@ -111,8 +111,8 @@ Linux AppImage packaging is available through the optional release lane and is c
<sub><b>Sandboxed preview</b> — every <code><artifact></code> renders in a clean srcdoc iframe. Editable in place via the file workspace; downloadable as HTML, PDF, ZIP.</sub>
<sub><b>72-system library</b> — every product system shows its 4-color signature. Click for the full <code>DESIGN.md</code>, swatch grid, and live showcase.</sub>
<sub><b>150-system library</b> — every product system shows its 4-color signature. Click for the full <code>DESIGN.md</code>, swatch grid, and live showcase.</sub>
</td>
</tr>
<tr>
@ -129,9 +129,9 @@ Linux AppImage packaging is available through the optional release lane and is c
## Skills
**31 skills ship in the box.** Each is a folder under [`skills/`](skills/) following the Claude Code [`SKILL.md`][skill] convention with an extended `od:` frontmatter that the daemon parses verbatim — `mode`, `platform`, `scenario`, `preview.type`, `design_system.requires`, `default_for`, `featured`, `fidelity`, `speaker_notes`, `animations`, `example_prompt` ([`apps/daemon/src/skills.ts`](apps/daemon/src/skills.ts)).
**137 skills ship in the box.** Each is a folder under [`skills/`](skills/) following the Claude Code [`SKILL.md`][skill] convention with an extended `od:` frontmatter that the daemon parses verbatim — `mode`, `platform`, `scenario`, `preview.type`, `design_system.requires`, `default_for`, `featured`, `fidelity`, `speaker_notes`, `animations`, `example_prompt` ([`apps/daemon/src/skills.ts`](apps/daemon/src/skills.ts)).
Two top-level **modes** carry the catalog: **`prototype`** (27 skills — anything that renders as a single-page artifact, from a magazine landing to a phone screen to a PM spec doc) and **`deck`** (4 skills — horizontal-swipe presentations with deck-framework chrome). The **`scenario`** field is what the picker groups them by: `design` · `marketing` · `operation` · `engineering` · `product` · `finance` · `hr` · `sale` · `personal`.
Two **modes** anchor the interactive catalog: **`prototype`** (anything that renders as a single-page artifact, from a magazine landing to a phone screen to a PM spec doc) and **`deck`** (horizontal-swipe presentations with deck-framework chrome). The catalog also ships `image`, `video`, `audio`, `template`, `design-system`, and `utility` modes for media generation, catalog updaters, and post-export audit helpers. The **`scenario`** field is what the picker groups them by: `design` · `marketing` · `operation` · `engineering` · `product` · `finance` · `hr` · `sale` · `personal`.
### Showcase examples
@ -259,8 +259,8 @@ What you compose at send time isn't "system + user". It's:
3. Open `.env` in your editor, find `OD_API_TOKEN=`, and paste the generated token there.
Then start the service:
```bash
docker compose up -d
```
@ -392,7 +408,7 @@ For desktop/background startup, fixed-port restarts, and media generation dispat
The first load:
1. Detects which agent CLIs you have on `PATH` and picks one automatically.
2. Loads 31 skills + 72 design systems.
2. Loads 137 skills + 150 design systems.
3. Pops the welcome dialog so you can paste an Anthropic key (only needed for the BYOK fallback path).
4. **Auto-creates `./.od/`** — the local runtime folder for the SQLite project DB, per-project artifacts, and saved renders. There is no `od init` step; the daemon `mkdir`s everything it needs on boot.
<imgsrc="docs/assets/design-systems-library.png"alt="The 72 design systems library — style guide spread" width="100%"/>
<imgsrc="docs/assets/design-systems-library.png"alt="The 150 design systems library — style guide spread" width="100%"/>
</p>
72 systems out of the box, each as a single [`DESIGN.md`](design-systems/README.md):
150 systems out of the box, each as a single [`DESIGN.md`](design-systems/README.md):
<details>
<summary><b>Full catalog</b> (click to expand)</summary>
@ -798,7 +814,7 @@ Full spec → [`apps/daemon/src/prompts/directions.ts`](apps/daemon/src/prompts/
OD doesn't stop at code. The same chat surface that produces `<artifact>` HTML also drives **image**, **video**, and **audio** generation, with model adapters wired into the daemon's media pipeline ([`apps/daemon/src/media-models.ts`](apps/daemon/src/media-models.ts), [`apps/web/src/media/models.ts`](apps/web/src/media/models.ts)). Every render lands as a real file in the project workspace — `.png` for image, `.mp4` for video — and shows up as a download chip when the turn ends.
Three model families carry the load today:
Three flagship paths carry the load today:
| Surface | Model | Provider | What it's for |
|---|---|---|---|
@ -806,6 +822,12 @@ Three model families carry the load today:
| **Video** | `hyperframes-html` | [HeyGen / OSS](https://github.com/heygen-com/hyperframes) | HTML→MP4 motion graphics — product reveals, kinetic typography, data charts, social overlays, logo outros, TikTok-style verticals with karaoke captions |
Other generators are possible, but the path depends on the API shape:
- **Today:** use **Settings → Media providers → Custom Image API** for any local or hosted image generator that exposes an OpenAI-compatible `POST /v1/images/generations` endpoint. **ImageRouter** covers the same contract for routed image and video backends.
- **Not wired yet:** workflow-first local runtimes such as **ComfyUI**. OD now lists ComfyUI in the Media providers “Coming soon” drawer to make that gap explicit, but direct JSON-workflow execution still needs a dedicated adapter.
- **Still provider-specific:** arbitrary non-OpenAI-compatible video APIs. Those need a first-class daemon integration rather than just a base URL swap.
A growing **prompt gallery** at [`prompt-templates/`](prompt-templates/) ships **93 ready-to-replicate prompts** — 43 image (`prompt-templates/image/*.json`), 39 Seedance (`prompt-templates/video/*.json` excluding `hyperframes-*`), 11 HyperFrames (`prompt-templates/video/hyperframes-*.json`). Each carries a preview thumbnail, the prompt body verbatim, the target model, the aspect ratio, and a `source` block for license + attribution. The daemon serves them at `GET /api/prompt-templates`, the web app surfaces them as a card grid in the **Image templates** and **Video templates** tabs of the entry view; one click drops a prompt into the composer with the right model preselected.
### gpt-image-2 — image gallery (sample of 43)
@ -873,7 +895,7 @@ The chat / artifact loop gets the spotlight, but a handful of less-visible capab
- **Claude Design ZIP import.** Drop an export from claude.ai onto the welcome dialog. `POST /api/import/claude-design` extracts it into a real `.od/projects/<id>/`, opens the entry file as a tab, and stages a continue-where-Anthropic-left-off prompt for your local agent. No re-prompting, no "ask the model to re-create what we just had". ([`apps/daemon/src/server.ts`](apps/daemon/src/server.ts) — `/api/import/claude-design`)
- **Multi-provider BYOK proxy.**`POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream` takes `{ baseUrl, apiKey, model, messages }`, builds the provider-specific upstream request, normalizes SSE chunks into `delta/end/error`, and allows loopback local LLM providers while rejecting non-loopback private, link-local, CGNAT, multicast, reserved, and redirect targets to head off SSRF. OpenAI-compatible covers OpenAI, Azure AI Foundry `/openai/v1`, DeepSeek, Groq, MiMo, OpenRouter, Ollama, LM Studio, and self-hosted vLLM; Azure OpenAI adds deployment URL + `api-version`; Google uses Gemini `:streamGenerateContent`.
- **User-saved templates.** Once you like a render, `POST /api/templates` snapshots the HTML + metadata into the SQLite `templates` table. The next project picks it from a "your templates" row in the picker — same surface as the shipped 31, but yours.
- **User-saved templates.** Once you like a render, `POST /api/templates` snapshots the HTML + metadata into the SQLite `templates` table. The next project picks it from a "your templates" row in the picker — same surface as the shipped 137, but yours.
- **Tab persistence.** Every project remembers its open files and active tab in the `tabs` table. Reopen the project tomorrow and the workspace looks exactly the way you left it.
- **Artifact lint API.**`POST /api/artifacts/lint` runs structural checks on a generated artifact (broken `<artifact>` framing, missing required side files, stale palette tokens) and returns findings the agent can read back into its next turn. The five-dim self-critique uses this to ground its score in real evidence, not vibes.
- **Sidecar protocol + desktop automation.** Daemon, web, and desktop processes carry typed five-field stamps (`app · mode · namespace · ipc · source`) and expose a JSON-RPC IPC channel at `/tmp/open-design/ipc/<namespace>/<app>.sock`. `tools-dev inspect desktop status \| eval \| screenshot` drives that channel, so headless E2E works against a real Electron shell without bespoke harnesses ([`packages/sidecar-proto/`](packages/sidecar-proto/), [`apps/desktop/src/main/`](apps/desktop/src/main/)).
@ -899,8 +921,8 @@ The whole machinery below is the [`huashu-design`](https://github.com/alchaincyf
| Form factor | Web (claude.ai) | Desktop (Electron) | **Web app + local daemon** |
- [x] Multi-provider BYOK proxy (`/api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream`) with SSRF guard
- [x] Claude Design ZIP import (`/api/import/claude-design`)
@ -1018,7 +1040,7 @@ Full walkthrough, bar-for-merging, code style, and what we don't accept → [`CO
Thanks to everyone who has helped move Open Design forward — through code, docs, feedback, new skills, new design systems, or even a sharp issue. Every real contribution counts, and the wall below is the easiest way to say so out loud.
If you've shipped your first PR — welcome. The [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) label is the entry point.
@ -1035,9 +1057,9 @@ The SVG above is regenerated daily by [`.github/workflows/metrics.yml`](.github/
# Open Design — a alternativa open source oficial ao [Claude Design][cd]
> **Open Design é a alternativa open source e local-first ao [Claude Design][cd].** Deployável via web, BYOK em toda camada — **16 CLIs de agentes de código** detectados automaticamente no seu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) viram a engine de design, dirigidos por **31 Skills compositáveis** e **72 Design Systems de qualidade de marca**. Sem CLI? Um proxy BYOK compatível com OpenAI é o mesmo loop, só sem o spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` chegou. O velho mundo do design acaba aqui.
>
> Uma alternativa open source e agent-native ao Claude Design / Figma — 40k estrelas em duas semanas nos trouxeram até aqui. **Precisamos de você para nos levar o resto do caminho.**
> Uma alternativa open source ao Claude Design / Figma — 40k estrelas em duas semanas nos trouxeram até aqui. **Precisamos de você para nos levar o resto do caminho.**
>
> **Iterando rápido na `main`** — 0.8.0 é a próxima fase do Open Design. Mande um PR, jogue uma ideia maluca, abra um bug — o que você traz é no que este movimento se transforma.
>
> → [**Leia o anúncio · baixe o instalador · junte-se ao movimento**](https://github.com/nexu-io/open-design/discussions/1727) · roda em paralelo com seu 0.7 atual.
> **A alternativa open-source ao [Claude Design][cd].** Local-first, deployável via web, BYOK em toda camada — **16 CLIs de agentes de código** detectados automaticamente no seu `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) viram a engine de design, dirigidos por **31 Skills compositáveis** e **72 Design Systems de qualidade de marca**. Sem CLI? Um proxy BYOK compatível com OpenAI é o mesmo loop, só sem o spawn.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — capa editorial: design com o agente no seu laptop"width="100%"/>
</p>
@ -730,7 +730,7 @@ Walkthrough completo, barra para mergear, estilo de código e o que não aceitam
Obrigado a todas as pessoas que ajudaram a empurrar o Open Design pra frente — via código, docs, feedback, novas skills, novos design systems ou até uma issue afiada. Toda contribuição real conta, e a parede abaixo é a forma mais simples de dizer isso em voz alta.
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-18" alt="Contribuidoras e contribuidores do Open Design"/>
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-31" alt="Contribuidoras e contribuidores do Open Design"/>
</a>
Se você acabou de mandar seu primeiro PR — bem-vindo. A label [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) é o ponto de entrada.
@ -747,9 +747,9 @@ O SVG acima é regenerado diariamente por [`.github/workflows/metrics.yml`](.git
# Open Design — официальная open-source альтернатива [Claude Design][cd]
> **Open Design — открытая, локально-ориентированная альтернатива [Claude Design][cd].** Пригодна для web-деплоя, с BYOK на каждом уровне: **16 CLI coding-агентов** автоматически обнаруживаются в вашем `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) и превращаются в движок генерации дизайна, управляемый **31 комбинируемым навыком** и **72 дизайн-системами уровня бренда**. Нет CLI? OpenAI-совместимый BYOK-прокси даёт тот же цикл без локального запуска агента.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` уже здесь. Старый мир дизайна заканчивается здесь.
>
> Open-source, agent-native альтернатива Claude Design / Figma — 40k звёзд за две недели довели нас до этой точки. **Дальше — только с тобой.**
> Open-source альтернатива Claude Design / Figma — 40k звёзд за две недели довели нас до этой точки. **Дальше — только с тобой.**
>
> **Быстрая итерация на `main`** — 0.8.0 — следующая фаза Open Design. Кидай PR, бросай безумную идею, заводи баг — что приносишь ты, таким и становится это движение.
>
> → [**Прочитать анонс · скачать установщик · присоединиться к движению**](https://github.com/nexu-io/open-design/discussions/1727) · устанавливается параллельно с твоей текущей 0.7.
> **Открытая альтернатива [Claude Design][cd].** Локально-ориентированная, пригодная для web-деплоя, с BYOK на каждом уровне: **16 CLI coding-агентов** автоматически обнаруживаются в вашем `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) и превращаются в движок генерации дизайна, управляемый **31 комбинируемым навыком** и **72 дизайн-системами уровня бренда**. Нет CLI? OpenAI-совместимый BYOK-прокси даёт тот же цикл без локального запуска агента.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — редакционная обложка: дизайн вместе с агентом на вашем ноутбуке"width="100%"/>
</p>
@ -729,7 +729,7 @@ Issues, PR, новые skills и новые design systems приветству
Спасибо всем, кто помогает двигать Open Design вперёд — кодом, документацией, обратной связью, новыми skills, новыми design systems или просто точным issue. Вклад любого реального масштаба здесь важен, а стена ниже — самый простой способ сказать это вслух.
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-18" alt="Contributors Open Design"/>
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-31" alt="Contributors Open Design"/>
</a>
Если вы только что отправили свой первый PR — добро пожаловать. Метка [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) — хорошая точка входа.
@ -746,9 +746,9 @@ SVG выше ежедневно пересобирается workflow [`.github/
# Open Design — [Claude Design][cd]'in resmi açık kaynak alternatifi
> **Open Design, [Claude Design][cd]'in açık kaynak ve yerel öncelikli alternatifidir.** Web'e dağıtılabilir, her katmanda BYOK; `PATH` üzerinde otomatik algılanan **16 coding-agent CLI** (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) tasarım motoruna dönüşür. Hepsi **31 birleştirilebilir Skill** ve **72 marka kalitesinde Design System** tarafından yönlendirilir. CLI yok mu? OpenAI uyumlu BYOK proxy aynı döngünün agent spawn olmadan çalışan halidir.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` burada. Tasarımın eski dünyası burada bitiyor.
>
> Claude Design / Figma'ya açık kaynaklı, agent-native bir alternatif — iki haftada 40k stars bizi buraya getirdi. **Yolun geri kalanını birlikte yürüyelim.**
> Claude Design / Figma'ya açık kaynaklı bir alternatif — iki haftada 40k stars bizi buraya getirdi. **Yolun geri kalanını birlikte yürüyelim.**
>
> **`main` üzerinde hızlı iterasyon** — 0.8.0 Open Design'in bir sonraki aşaması. Bir PR gönder, çılgın bir fikir at, bir bug bildir — sen ne getirirsen bu hareket o olur.
>
> → [**Duyuruyu oku · kurulum dosyasını indir · harekete katıl**](https://github.com/nexu-io/open-design/discussions/1727) · mevcut 0.7'nin yanına paralel kurulur.
> **[Claude Design][cd] için açık kaynak alternatif.** Yerel öncelikli, web'e dağıtılabilir, her katmanda BYOK; `PATH` üzerinde otomatik algılanan **16 coding-agent CLI** (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) tasarım motoruna dönüşür. Hepsi **31 birleştirilebilir Skill** ve **72 marka kalitesinde Design System** tarafından yönlendirilir. CLI yok mu? OpenAI uyumlu BYOK proxy aynı döngünün agent spawn olmadan çalışan halidir.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — dizüstü bilgisayarındaki agent ile tasarım yapma editoryal kapağı"width="100%"/>
</p>
@ -887,7 +887,7 @@ Tam walkthrough, merge çıtası, code style ve kabul etmediklerimiz → [`CONTR
Open Design'ı kod, doküman, feedback, yeni skill, yeni design system veya keskin bir issue ile ileri taşıyan herkese teşekkürler. Her gerçek katkı önemlidir; aşağıdaki wall bunu yüksek sesle söylemenin en kolay yolu.
İlk PR'ını gönderdiysen hoş geldin. [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) label'ı giriş noktasıdır.
# Open Design — офіційна open-source альтернатива [Claude Design][cd]
> **Open Design — це альтернатива з відкритим кодом і локально-перший варіант [Claude Design][cd].** Розгортується в web, BYOK на кожному рівні — **16 CLI агентів для кодування** автоматично виявляються у вашому `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) стають механізмом дизайну, керуються **31 компонуваною навичкою** та **72 системами дизайну комерційного класу**. Немає CLI? OpenAI-сумісний BYOK проксі — це той же цикл без spawn.
> [!IMPORTANT]
> ### 🔥 `0.8.0-preview` уже тут. Старий світ дизайну закінчується тут.
>
> Open-source, agent-native альтернатива Claude Design / Figma — 40k зірок за два тижні привели нас сюди. **Далі — тільки з тобою.**
> Open-source альтернатива Claude Design / Figma — 40k зірок за два тижні привели нас сюди. **Далі — тільки з тобою.**
>
> **Швидка ітерація на `main`** — 0.8.0 — наступна фаза Open Design. Кидай PR, кидай шалену ідею, заводь баг — те, що приносиш ти, тим стає цей рух.
>
> → [**Прочитати анонс · завантажити інсталятор · приєднатися до руху**](https://github.com/nexu-io/open-design/discussions/1727) · встановлюється паралельно з твоєю поточною 0.7.
> **Альтернатива з відкритим кодом до [Claude Design][cd].** Локально-перший, розгортується в web, BYOK на кожному рівні — **16 CLI агентів для кодування** автоматично виявляються у вашому `PATH` (Claude Code, Codex, Devin for Terminal, Cursor Agent, Gemini CLI, OpenCode, Qwen, Qoder CLI, GitHub Copilot CLI, Hermes, Kimi, Pi, Kiro, Kilo, Mistral Vibe, DeepSeek TUI) стають механізмом дизайну, керуються **31 компонуваною навичкою** та **72 системами дизайну комерційного класу**. Немає CLI? OpenAI-сумісний BYOK проксі — це той же цикл без spawn.
<palign="center">
<imgsrc="docs/assets/banner.png"alt="Open Design — editorial cover: design with the agent on your laptop"width="100%"/>
</p>
@ -729,7 +729,7 @@ OD не зупиняється на коді. Та сама поверхня ч
Дякуємо всім, хто допоміг просувати Open Design — через код, документацію, зворотний зв'язок, нові навички, нові системи дизайну або навіть гостре питання. Кожен реальний внесок рахується, а стіна нижче — найпростіший спосіб сказати це вголос.
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-18" alt="Контриб'ютори Open Design"/>
<imgsrc="https://contrib.rocks/image?repo=nexu-io/open-design&cache_bust=2026-05-31" alt="Контриб'ютори Open Design"/>
</a>
Якщо ви злили свій перший PR — ласкаво просимо. Мітка [`good-first-issue`/`help-wanted`](https://github.com/nexu-io/open-design/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22%2C%22help+wanted%22) — це точка входу.
@ -746,9 +746,9 @@ SVG вище перегенерується щодня [`.github/workflows/metri
Translate ordinary UI terms, workflow labels, and non-identifier product copy
when a natural French equivalent exists:
| English source | French |
| -------------- | ------ |
| Settings | Paramètres |
| Save | Enregistrer |
| Cancel | Annuler |
| Delete | Supprimer |
| Folder | Dossier |
| File | Fichier |
| Download | Télécharger |
| Upload | Téléverser |
| Search | Rechercher |
| Preview | Aperçu |
| Project | Projet |
| Conversation | Conversation |
| Dashboard | Tableau de bord |
| Schedule | Planification |
| Automation | Automatisation |
| Artifact | Artefact |
| Live artifact | Artefact dynamique |
| Design files | Fichiers de design |
| Slide deck | Présentation |
| Engineering handoff | Transmission aux ingénieurs |
| Shipped (product/software status) | Livré |
#### Context-Sensitive Choices
- `Skill` stays `Skill` when it names the Open Design/Claude skill format.
Translate only generic prose such as "ability" or "capability" as
`capacité`.
- `Design System` may stay `Design System` when referring to the product
registry/object name. In explanatory prose, `système de design` is also
acceptable when it improves readability.
- `runtime` stays `runtime` as a noun. Labels like "execution mode" can still
use `mode d’exécution`.
- `source` can stay `source` for provenance labels, but translate ordinary
"data source" as `source de données`.
- Do not translate command output or examples that users should see exactly in
their terminal.
### zh-CN ↔ zh-TW Glossary
When converting between Simplified and Traditional Chinese, prefer Taiwan-specific phrasing in zh-TW rather than character-only conversion. This list grew out of [PR #194](https://github.com/nexu-io/open-design/pull/194) and is meant as a starting point, not a rulebook.
'Generate a text-to-speech voiceover using SenseAudio TTS. Returns a URL pointing to the rendered MP3. Use this whenever the user asks for narration, voiceover, speech, TTS, or spoken audio. After this tool succeeds, reply with a clickable markdown link to the MP3.',
parameters:{
type:'object',
properties:{
text:{
type:'string',
description:
'Exact script to speak. Include only the words that should be spoken, not production notes.',
},
voice_id:{
type:'string',
description:
`Optional SenseAudio voice id. Defaults to ${SENSEAUDIO_DEFAULT_VOICE_ID}.`,
// Per-request role-marker guard for BYOK proxy streams (#3247).
functioncreateDeltaGuard(sse: any){
constguard=createRoleMarkerGuard('proxy');
return{
sendDelta(text: string){
if(guard.contaminated||!text)return;
constsafe=guard.feedText(text);
if(safe.length>0){
sse.send('delta',{delta: safe});
}
if(guard.contaminated){
constwarn=guard.warningEvent();
constmarkerText=warn?.marker??'## user';
sse.send('delta',{
delta:`\n\n---\n⚠️ **Security warning:** The model attempted to emit a fabricated role marker (\`${markerText}\`). Response was truncated to prevent unauthorized instruction injection. See issue #3247.\n`,
?`Video generated successfully. URL: ${result.url}. Reply to the user with a clickable markdown link, e.g. [▶ Play video](${result.url}). Do NOT use markdown image syntax — the chat renderer does not embed <video> tags.`
:result.kind==='speech'
?`Speech generated successfully. URL: ${result.url}. Reply to the user with a clickable markdown link to the MP3, e.g. [▶ Play voiceover](${result.url}).`
:`Image generated successfully. URL: ${result.url}. Reply to the user with: `
:result.kind==='video'
?`Video generation failed: ${result.error}. Apologize briefly and suggest a retry with a more specific prompt or a shorter duration.`
:result.kind==='speech'
?`Speech generation failed: ${result.error}. Apologize briefly and suggest a retry with a shorter script or a valid voice id.`
:`Image generation failed: ${result.error}. Apologize briefly and suggest a retry with a more specific prompt.`;