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>
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>
* 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
* 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>
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.
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
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
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.
* 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>
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).