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>